Compare commits
6 Commits
image-2019
...
original_w
Author | SHA1 | Date | |
---|---|---|---|
4fb99c05e0 | |||
d84e672e6e | |||
a687d7f163 | |||
9f1c9a0a09 | |||
f531f3c464 | |||
d3161b6fc1 |
@ -1,5 +0,0 @@
|
||||
.git
|
||||
.gitignore
|
||||
.idea
|
||||
**/*.pyc
|
||||
**/*.swp
|
5
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
**/*.pyc
|
||||
**/*.swp
|
||||
*.pyc
|
||||
*.swp
|
||||
tags
|
||||
.idea
|
||||
|
165
README.md
@ -1,90 +1,64 @@
|
||||
OpenWebRX
|
||||
=========
|
||||
|
||||
[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/)
|
||||
# OpenWebRX
|
||||
|
||||
OpenWebRX is a multi-user SDR receiver software with a web interface.
|
||||
|
||||
----
|
||||
|
||||
### ⚠️ From 2019-12-29 OpenWebRX development is discontinued. ⚠️
|
||||
|
||||
I'm would like to say a big thanks to everyone who supported me during this project, including those who contributed either code or donations. It has been a very fruitful 6 years, but now it's time to move on to other projects. See also my [blog](https://blog.sdr.hu) about that.
|
||||
|
||||
(@simonyiszk, please keep this GitHub repo for historic purposes.)
|
||||
|
||||
Know limitations of the last version:
|
||||
|
||||
- Python 2.7, a main dependency of the project, will be not be officially maintained from 1 January 2020. By time, probably it will not be secure to use this version on public servers, unless someone still provides security patches for Python 2.
|
||||
- Some specific parts of the DSP code could be improved for better SNR.
|
||||
|
||||
Even though these limitations are probably acceptable in an amateur radio project, I would not build critical infrastructure on it.
|
||||
|
||||
For commercial inquiries (e.g. if someone wants me to develop an improved version without these limitations), I'm still open, [drop me an e-mail](mailto:randras@sdr.hu).
|
||||
|
||||
----
|
||||
|
||||
[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/)
|
||||
|
||||

|
||||
|
||||
It has the following features:
|
||||
|
||||
- [csdr](https://github.com/simonyiszk/csdr) based demodulators (AM/FM/SSB/CW/BPSK31),
|
||||
- <a href="https://github.com/simonyiszk/csdr">csdr</a> based demodulators (AM/FM/SSB/CW/BPSK31),
|
||||
- filter passband can be set from GUI,
|
||||
- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
|
||||
- it works in Google Chrome, Chromium and Mozilla Firefox
|
||||
- currently supports RTL-SDR, HackRF, SDRplay, AirSpy
|
||||
- Multiple SDR devices can be used simultaneously
|
||||
- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF)
|
||||
- [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN)
|
||||
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9)
|
||||
- waterfall display can be shifted back in time,
|
||||
- it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>,
|
||||
- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28),
|
||||
- currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the <a href="https://github.com/simonyiszk/openwebrx/wiki/">OpenWebRX Wiki</a>,
|
||||
- it has a 3D waterfall display:
|
||||
|
||||
**News (2019-11-24 by DD5JFK)**
|
||||
- There is now a new way to interface with SDR hardware, [owrx_connectors](https://github.com/jketterl/owrx_connector).
|
||||
They talk directly to the hardware (no rtl_sdr / rx_sdr necessary) and offer I/Q data on a socket, just like nmux
|
||||
did before. They additionally offer a control socket that allows openwebrx to control the SDR parameters directly,
|
||||
without the need for repeated restarts. This allows for quicker profile changes, and also reduces the risk of your
|
||||
SDR hardware from failing during the switchover. See `config_webrx.py` for further information and instructions.
|
||||
- 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.
|
||||
- As usual, plenty of fixes and improvements.
|
||||

|
||||
|
||||
**News (2019-10-27 by DD5JFK)**
|
||||
- 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). The Raspberry Pi image has been updated to include https due to the SecureContext requirement.
|
||||
- Mousewheel controls for the receiver sliders
|
||||
- Error handling for failed SDR devices
|
||||
**News (2015-08-18)**
|
||||
- My BSc. thesis written on OpenWebRX is <a href="https://sdr.hu/static/bsc-thesis.pdf">available here.</a>
|
||||
- Several bugs were fixed to improve reliability and stability.
|
||||
- OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.)
|
||||
- OpenWebRX now uses <a href="https://github.com/simonyiszk/csdr#sdrjs">sdr.js</a> (*libcsdr* compiled to JavaScript) for some client-side DSP tasks.
|
||||
- Receivers can now be listed on <a href="http://sdr.hu/">SDR.hu</a>.
|
||||
- License for OpenWebRX is now Affero GPL v3.
|
||||
|
||||
**News (2019-09-29 by DD5FJK)**
|
||||
- One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels). There's two kinds of bookmarks available:
|
||||
- Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples!
|
||||
- Clientside bookmarks which every user can store for themselves. They are stored in the browser's localStorage.
|
||||
- Some more bugs in the websocket handling have been fixed.
|
||||
**News (2016-02-14)**
|
||||
- The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before.
|
||||
- Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases.
|
||||
- OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb`
|
||||
- UI improvements were made, thanks to John Seamons and Gnoxter.
|
||||
|
||||
**News (2019-09-25 by DD5JFK)**
|
||||
- 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.
|
||||
**News (2017-04-04)**
|
||||
- *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package.
|
||||
- Most consumer SDR devices are supported via <a href="https://github.com/rxseger/rx_tools">rx_tools</a>, see the <a href="https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX">OpenWebRX Wiki</a> on that.
|
||||
|
||||
**News (2019-09-13 by DD5JFK)**
|
||||
- New set of APRS-related features
|
||||
- Decode Packet transmissions using [direwolf](https://github.com/wb2osz/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.
|
||||
**News (2017-07-12)**
|
||||
- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display.
|
||||
|
||||
**News (2019-07-21 by DD5JFK)**
|
||||
- Latest Features:
|
||||
- More WSJT-X modes have been added, including the new FT4 mode
|
||||
- I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the dial frequency for digital modes
|
||||
- fixed some bugs in the websocket communication which broke the map
|
||||
|
||||
**News (2019-07-13 by DD5JFK)**
|
||||
- Latest Features:
|
||||
- FT8 Integration (using wsjt-x demodulators)
|
||||
- New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice
|
||||
- New Feature report that will show what functionality is available
|
||||
- There's a new Raspbian SD Card image available (see below)
|
||||
|
||||
**News (2019-06-30 by DD5JFK)**
|
||||
- I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near future. Please check this place for updates.
|
||||
- My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official version.
|
||||
- I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there!
|
||||
- This version sports the following new and amazing features:
|
||||
- Support of multiple SDR devices simultaneously
|
||||
- Support for multiple profiles per SDR that allow the user to listen to different frequencies
|
||||
- Support for digital voice decoding
|
||||
- Feature detection that will disable functionality when dependencies are not available (if you're missing the digital buttons, this is probably why)
|
||||
- Raspbian SD Card Images and Docker builds available (see below)
|
||||
- I am currently working on the feature set for a stable release, but you are more than welcome to test development versions!
|
||||
|
||||
> When upgrading OpenWebRX, please make sure that you also upgrade *csdr* and *digiham*!
|
||||
> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*!
|
||||
|
||||
## OpenWebRX servers on SDR.hu
|
||||
|
||||
@ -94,58 +68,27 @@ It has the following features:
|
||||
|
||||
## Setup
|
||||
|
||||
### Raspberry Pi SD Card Images
|
||||
|
||||
Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-10-27-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+.
|
||||
|
||||
This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply.
|
||||
|
||||
Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!)
|
||||
|
||||
Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which should make it available as https://openwebrx:8073/ on most networks. This may vary depending on your specific setup.
|
||||
|
||||
For Digital voice, the minimum requirement right now seems to be a Rasbperry Pi 3B+. I would like to work on optimizing this for lower specs, but at this point I am not sure how much can be done.
|
||||
|
||||
### Docker Images
|
||||
|
||||
For those familiar with docker, I am providing [recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx). You can find a short introduction there.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
OpenWebRX currently requires Linux and python >= 3.6 to run.
|
||||
OpenWebRX currently requires Linux and python 2.7 to run.
|
||||
|
||||
First you will need to install the dependencies:
|
||||
|
||||
- [csdr](https://github.com/simonyiszk/csdr)
|
||||
- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
|
||||
|
||||
Optional dependency for improved hardware access (to become mandatory at some point):
|
||||
|
||||
- [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)
|
||||
- <a href="https://github.com/simonyiszk/csdr">libcsdr</a>
|
||||
- <a href="http://sdr.osmocom.org/trac/wiki/rtl-sdr">rtl-sdr</a>
|
||||
|
||||
After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
|
||||
|
||||
./openwebrx.py
|
||||
python openwebrx.py
|
||||
|
||||
You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>.
|
||||
|
||||
Please note that the server is also listening on the following ports (on localhost only):
|
||||
|
||||
- ports 4950 to 4960 for the multi-user I/Q servers.
|
||||
- port 4951 for the multi-user I/Q server.
|
||||
|
||||
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>*
|
||||
*Andras Retzler, HA7ILM <randras@sdr.hu>*
|
||||
|
||||
## Usage tips
|
||||
|
||||
@ -161,6 +104,8 @@ If you have any problems installing OpenWebRX, you should check out the <a href=
|
||||
|
||||
Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it.
|
||||
|
||||
If you want to run OpenWebRX on a remote server instead of *localhost*, do not forget to set *server_hostname* in `config_webrx.py`.
|
||||
|
||||
## Licensing
|
||||
|
||||
OpenWebRX is available under Affero GPL v3 license (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">summary</a>).
|
||||
|
190
bands.json
@ -1,190 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "160m",
|
||||
"lower_bound": 1810000,
|
||||
"upper_bound": 2000000,
|
||||
"frequencies": {
|
||||
"psk31": 1838000,
|
||||
"ft8": 1840000,
|
||||
"wspr": 1836600,
|
||||
"jt65": 1838000,
|
||||
"jt9": 1839000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "80m",
|
||||
"lower_bound": 3500000,
|
||||
"upper_bound": 3800000,
|
||||
"frequencies": {
|
||||
"psk31": 3580000,
|
||||
"ft8": 3573000,
|
||||
"wspr": 3592600,
|
||||
"jt65": 3570000,
|
||||
"jt9": 3572000,
|
||||
"ft4": [3568000, 3575000]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "60m",
|
||||
"lower_bound": 5351500,
|
||||
"upper_bound": 5366500,
|
||||
"frequencies": {
|
||||
"ft8": 5357000,
|
||||
"wspr": 5364700
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "40m",
|
||||
"lower_bound": 7000000,
|
||||
"upper_bound": 7200000,
|
||||
"frequencies": {
|
||||
"psk31": 7040000,
|
||||
"ft8": 7074000,
|
||||
"wspr": 7038600,
|
||||
"jt65": 7076000,
|
||||
"jt9": 7078000,
|
||||
"ft4": 7047500
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "30m",
|
||||
"lower_bound": 10100000,
|
||||
"upper_bound": 10150000,
|
||||
"frequencies": {
|
||||
"psk31": 10141000,
|
||||
"ft8": 10136000,
|
||||
"wspr": 10138700,
|
||||
"jt65": 10138000,
|
||||
"jt9": 10140000,
|
||||
"ft4": 10140000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "20m",
|
||||
"lower_bound": 14000000,
|
||||
"upper_bound": 14350000,
|
||||
"frequencies": {
|
||||
"psk31": 14070000,
|
||||
"ft8": 14074000,
|
||||
"wspr": 14095600,
|
||||
"jt65": 14076000,
|
||||
"jt9": 14078000,
|
||||
"ft4": 14080000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "17m",
|
||||
"lower_bound": 18068000,
|
||||
"upper_bound": 18168000,
|
||||
"frequencies": {
|
||||
"psk31": 18098000,
|
||||
"ft8": 18100000,
|
||||
"wspr": 18104600,
|
||||
"jt65": 18102000,
|
||||
"jt9": 18104000,
|
||||
"ft4": 18104000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "15m",
|
||||
"lower_bound": 21000000,
|
||||
"upper_bound": 21450000,
|
||||
"frequencies": {
|
||||
"psk31": 21070000,
|
||||
"ft8": 21074000,
|
||||
"wspr": 21094600,
|
||||
"jt65": 21076000,
|
||||
"jt9": 21078000,
|
||||
"ft4": 21140000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "12m",
|
||||
"lower_bound": 24890000,
|
||||
"upper_bound": 24990000,
|
||||
"frequencies": {
|
||||
"psk31": 24920000,
|
||||
"ft8": 24915000,
|
||||
"wspr": 24924600,
|
||||
"jt65": 24917000,
|
||||
"jt9": 24919000,
|
||||
"ft4": 24919000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "10m",
|
||||
"lower_bound": 28000000,
|
||||
"upper_bound": 29700000,
|
||||
"frequencies": {
|
||||
"psk31": [28070000, 28120000],
|
||||
"ft8": 28074000,
|
||||
"wspr": 28124600,
|
||||
"jt65": 28076000,
|
||||
"jt9": 28078000,
|
||||
"ft4": 28180000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "6m",
|
||||
"lower_bound": 50030000,
|
||||
"upper_bound": 51000000,
|
||||
"frequencies": {
|
||||
"psk31": 50305000,
|
||||
"ft8": 50313000,
|
||||
"wspr": 50293000,
|
||||
"jt65": 50310000,
|
||||
"jt9": 50312000,
|
||||
"ft4": 50318000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "4m",
|
||||
"lower_bound": 70150000,
|
||||
"upper_bound": 70200000,
|
||||
"frequencies": {
|
||||
"wspr": 70091000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "2m",
|
||||
"lower_bound": 144000000,
|
||||
"upper_bound": 146000000,
|
||||
"frequencies": {
|
||||
"wspr": 144489000,
|
||||
"ft8": 144174000,
|
||||
"ft4": 144170000,
|
||||
"jt65": 144120000,
|
||||
"packet": 144800000
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "70cm",
|
||||
"lower_bound": 430000000,
|
||||
"upper_bound": 440000000
|
||||
},
|
||||
{
|
||||
"name": "23cm",
|
||||
"lower_bound": 1240000000,
|
||||
"upper_bound": 1300000000
|
||||
},
|
||||
{
|
||||
"name": "13cm",
|
||||
"lower_bound": 2320000000,
|
||||
"upper_bound": 2450000000
|
||||
},
|
||||
{
|
||||
"name": "9cm",
|
||||
"lower_bound": 3400000000,
|
||||
"upper_bound": 3475000000
|
||||
},
|
||||
{
|
||||
"name": "6cm",
|
||||
"lower_bound": 5650000000,
|
||||
"upper_bound": 5850000000
|
||||
},
|
||||
{
|
||||
"name": "3cm",
|
||||
"lower_bound": 10000000000,
|
||||
"upper_bound": 10500000000
|
||||
}
|
||||
]
|
222
bookmarks.json
@ -1,222 +0,0 @@
|
||||
[
|
||||
{
|
||||
"name": "DB0ZU",
|
||||
"frequency": 145725000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ZM",
|
||||
"frequency": 145750000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DM0ULR",
|
||||
"frequency": 145787500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0EL",
|
||||
"frequency": 439275000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0NJ",
|
||||
"frequency": 438775000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0NJ",
|
||||
"frequency": 439437500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0UFO",
|
||||
"frequency": 438312500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0PV",
|
||||
"frequency": 438525000,
|
||||
"modulation": "ysf"
|
||||
},
|
||||
{
|
||||
"name": "DB0BZA",
|
||||
"frequency": 438412500,
|
||||
"modulation": "ysf"
|
||||
},
|
||||
{
|
||||
"name": "DB0OSH",
|
||||
"frequency": 438250000,
|
||||
"modulation": "ysf"
|
||||
},
|
||||
{
|
||||
"name": "DB0ULR",
|
||||
"frequency": 439325000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ZU",
|
||||
"frequency": 438850000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ISW",
|
||||
"frequency": 438650000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "Radio DARC",
|
||||
"frequency": 6070000,
|
||||
"modulation": "am"
|
||||
},
|
||||
{
|
||||
"name": "DB0TVM",
|
||||
"frequency": 439575000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DB0TVM",
|
||||
"frequency": 439800000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0TR",
|
||||
"frequency": 438700000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0PME",
|
||||
"frequency": 439825000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0HKN",
|
||||
"frequency": 438300000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "OE2XHM",
|
||||
"frequency": 438825000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DM0WW",
|
||||
"frequency": 438962500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "OE7XXR",
|
||||
"frequency": 438200000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "OE2XZR",
|
||||
"frequency": 439000000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DB0OAL",
|
||||
"frequency": 439912500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0AAT",
|
||||
"frequency": 439550000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0FSG",
|
||||
"frequency": 439937500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "Pocsag",
|
||||
"frequency": 439987500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ULR",
|
||||
"frequency": 145575000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0RDH",
|
||||
"frequency": 145737500,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DM0GAP",
|
||||
"frequency": 145612500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0XF",
|
||||
"frequency": 145600000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0TOL",
|
||||
"frequency": 145712500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0TTB",
|
||||
"frequency": 439587500,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0TRS",
|
||||
"frequency": 439125000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0OAL",
|
||||
"frequency": 438937500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DM0ULR",
|
||||
"frequency": 439337500,
|
||||
"modulation": "nxdn"
|
||||
},
|
||||
{
|
||||
"name": "DB0MIR",
|
||||
"frequency": 439300000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0PM",
|
||||
"frequency": 439075000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0CP",
|
||||
"frequency": 439025000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "OE7XGR",
|
||||
"frequency": 438925000,
|
||||
"modulation": "dmr"
|
||||
},
|
||||
{
|
||||
"name": "DB0TOL",
|
||||
"frequency": 438725000,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0OAL",
|
||||
"frequency": 438325000,
|
||||
"modulation": "dstar"
|
||||
},
|
||||
{
|
||||
"name": "DB0ROL",
|
||||
"frequency": 439237500,
|
||||
"modulation": "nfm"
|
||||
},
|
||||
{
|
||||
"name": "DB0ABX",
|
||||
"frequency": 439137500,
|
||||
"modulation": "nfm"
|
||||
}
|
||||
]
|
22
build.sh
@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
BASE_IMAGE=alpine
|
||||
;;
|
||||
armv*)
|
||||
BASE_IMAGE=arm32v6/alpine
|
||||
esac
|
||||
|
||||
TAGS=$ARCH
|
||||
|
||||
docker build --build-arg BASE_IMAGE=$BASE_IMAGE -t openwebrx-base:$ARCH -f docker/Dockerfiles/Dockerfile-base .
|
||||
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-rtlsdr:$ARCH -f docker/Dockerfiles/Dockerfile-rtlsdr .
|
||||
docker build --build-arg ARCH=$ARCH -t openwebrx-soapysdr-base:$ARCH -f docker/Dockerfiles/Dockerfile-soapysdr .
|
||||
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-sdrplay:$ARCH -f docker/Dockerfiles/Dockerfile-sdrplay .
|
||||
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-hackrf:$ARCH -f docker/Dockerfiles/Dockerfile-hackrf .
|
||||
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-airspy:$ARCH -f docker/Dockerfiles/Dockerfile-airspy .
|
||||
docker build --build-arg ARCH=$ARCH -t jketterl/openwebrx-full:$ARCH -t jketterl/openwebrx:$ARCH -f docker/Dockerfiles/Dockerfile-full .
|
249
config_webrx.py
@ -6,7 +6,6 @@ config_webrx: configuration options for OpenWebRX
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -37,14 +36,19 @@ config_webrx: configuration options for OpenWebRX
|
||||
|
||||
# ==== Server settings ====
|
||||
web_port=8073
|
||||
server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket)
|
||||
max_clients=20
|
||||
|
||||
# ==== Web GUI configuration ====
|
||||
receiver_name="[Callsign]"
|
||||
receiver_location="Budapest, Hungary"
|
||||
receiver_qra="JN97ML"
|
||||
receiver_asl=200
|
||||
receiver_ant="Longwire"
|
||||
receiver_device="RTL-SDR"
|
||||
receiver_admin="example@example.com"
|
||||
receiver_gps=(47.000000,19.000000)
|
||||
photo_height=350
|
||||
photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory"
|
||||
photo_desc="""
|
||||
You can add your own background photo and receiver information.<br />
|
||||
@ -61,14 +65,17 @@ Website: <a href="http://localhost" target="_blank">http://localhost</a>
|
||||
sdrhu_key = ""
|
||||
# 3. Set this setting to True to enable listing:
|
||||
sdrhu_public_listing = False
|
||||
server_hostname = "localhost"
|
||||
|
||||
# ==== DSP/RX settings ====
|
||||
fft_fps=9
|
||||
fft_size=4096 #Should be power of 2
|
||||
fft_voverlap_factor = (
|
||||
0.3 # If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
||||
)
|
||||
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
|
||||
|
||||
# samp_rate = 250000
|
||||
samp_rate = 2400000
|
||||
center_freq = 144250000
|
||||
rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode.
|
||||
ppm = 0
|
||||
|
||||
audio_compression="adpcm" #valid values: "adpcm", "none"
|
||||
fft_compression="adpcm" #valid values: "adpcm", "none"
|
||||
@ -76,11 +83,7 @@ fft_compression = "adpcm" # valid values: "adpcm", "none"
|
||||
digimodes_enable=True #Decoding digimodes come with higher CPU usage.
|
||||
digimodes_fft_size=1024
|
||||
|
||||
# determines the quality, and thus the cpu usage, for the ambe codec used by digital voice modes
|
||||
# if you're running on a Raspi (up to 3B+) you'll want to leave this on 1
|
||||
digital_voice_unvoiced_quality = 1
|
||||
# enables lookup of DMR ids using the radioid api
|
||||
digital_voice_dmr_id_lookup = True
|
||||
start_rtl_thread=True
|
||||
|
||||
"""
|
||||
Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
@ -98,114 +101,81 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
|
||||
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support #
|
||||
#################################################################################################
|
||||
|
||||
# Currently supported types of sdr receivers: "rtl_sdr", "sdrplay", "hackrf", "airspy"
|
||||
#
|
||||
# NEW: There is now custom connector software available, that is tailored for the use with
|
||||
# openwebrx. The connectors allow the SDR to be reprogrammed while running, which allows for
|
||||
# quicker profile changes. It also reduces the risk of a USB disconnect that can happen when the
|
||||
# SDR software is restarted, since the connector will run continuously.
|
||||
# Check out the connector repository here: https://github.com/jketterl/owrx_connector
|
||||
#
|
||||
# The following connectors are available (simply use them as the "type" in the config below):
|
||||
# "rtl_sdr_connector", "sdrplay_connector", "airspy_connector"
|
||||
#
|
||||
# NOTE: These connectors will become the default as soon as they have become mature; the existing
|
||||
# receiver types will then automatically be migrated to connectors. At that point, the old "nmux"
|
||||
# method will start to be phased out.
|
||||
# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR):
|
||||
|
||||
sdrs = {
|
||||
"rtlsdr": {
|
||||
"name": "RTL-SDR USB Stick",
|
||||
"type": "rtl_sdr_connector",
|
||||
"ppm": 0,
|
||||
# you can change this if you use an upconverter. formula is:
|
||||
# center_freq + lfo_offset = actual frequency on the sdr
|
||||
# "lfo_offset": 0,
|
||||
"profiles": {
|
||||
"70cm": {
|
||||
"name": "70cm Relais",
|
||||
"center_freq": 438800000,
|
||||
"rf_gain": 30,
|
||||
"samp_rate": 2400000,
|
||||
"start_freq": 439275000,
|
||||
"start_mod": "nfm",
|
||||
},
|
||||
"2m": {
|
||||
"name": "2m komplett",
|
||||
"center_freq": 145000000,
|
||||
"rf_gain": 30,
|
||||
"samp_rate": 2400000,
|
||||
"start_freq": 145725000,
|
||||
"start_mod": "nfm",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sdrplay": {
|
||||
"name": "SDRPlay RSP2",
|
||||
"type": "sdrplay_connector",
|
||||
"ppm": 0,
|
||||
"profiles": {
|
||||
"20m": {
|
||||
"name": "20m",
|
||||
"center_freq": 14150000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 14070000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A",
|
||||
},
|
||||
"30m": {
|
||||
"name": "30m",
|
||||
"center_freq": 10125000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 250000,
|
||||
"start_freq": 10142000,
|
||||
"start_mod": "usb",
|
||||
},
|
||||
"40m": {
|
||||
"name": "40m",
|
||||
"center_freq": 7100000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 7070000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A",
|
||||
},
|
||||
"80m": {
|
||||
"name": "80m",
|
||||
"center_freq": 3650000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 3570000,
|
||||
"start_mod": "usb",
|
||||
"antenna": "Antenna A",
|
||||
},
|
||||
"49m": {
|
||||
"name": "49m Broadcast",
|
||||
"center_freq": 6000000,
|
||||
"rf_gain": 0,
|
||||
"samp_rate": 500000,
|
||||
"start_freq": 6070000,
|
||||
"start_mod": "am",
|
||||
"antenna": "Antenna A",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
# >> RTL-SDR via rtl_sdr
|
||||
start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
|
||||
format_conversion="csdr convert_u8_f"
|
||||
|
||||
#lna_gain=8
|
||||
#rf_amp=1
|
||||
#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain)
|
||||
#format_conversion="csdr convert_s8_f"
|
||||
"""
|
||||
To use a HackRF, compile the HackRF host tools from its "stdout" branch:
|
||||
git clone https://github.com/mossmann/hackrf/
|
||||
cd hackrf
|
||||
git fetch
|
||||
git checkout origin/stdout
|
||||
cd host
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DINSTALL_UDEV_RULES=ON
|
||||
make
|
||||
sudo make install
|
||||
"""
|
||||
|
||||
# >> Sound card SDR (needs ALSA)
|
||||
# I did not have the chance to properly test it.
|
||||
#samp_rate = 96000
|
||||
#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate)
|
||||
#format_conversion="csdr convert_s16_f | csdr gain_ff 30"
|
||||
|
||||
# >> /dev/urandom test signal source
|
||||
# samp_rate = 2400000
|
||||
# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate)
|
||||
# format_conversion="csdr convert_u8_f"
|
||||
|
||||
# >> Pre-recorded raw I/Q file as signal source
|
||||
# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file.
|
||||
#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05)
|
||||
#format_conversion="csdr convert_u8_f"
|
||||
|
||||
#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc.
|
||||
# It will auto-detect your SDR hardware if the following tools are installed:
|
||||
# * the vendor provided driver and library,
|
||||
# * the vendor-specific SoapySDR wrapper library,
|
||||
# * and SoapySDR itself.
|
||||
# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/
|
||||
#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm)
|
||||
#format_conversion=""
|
||||
|
||||
# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source)
|
||||
#start_rtl_command="cat /tmp/osmocom_fifo"
|
||||
#format_conversion=""
|
||||
|
||||
# ==== Misc settings ====
|
||||
|
||||
iq_port_range = [
|
||||
4950,
|
||||
4960,
|
||||
] # TCP port for range ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
|
||||
shown_center_freq = center_freq #you can change this if you use an upconverter
|
||||
|
||||
client_audio_buffer_size = 5
|
||||
#increasing client_audio_buffer_size will:
|
||||
# - also increase the latency
|
||||
# - decrease the chance of audio underruns
|
||||
|
||||
start_freq = center_freq
|
||||
start_mod = "nfm" #nfm, am, lsb, usb, cw
|
||||
|
||||
iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default.
|
||||
|
||||
#access_log = "~/openwebrx_access.log"
|
||||
|
||||
# ==== Color themes ====
|
||||
|
||||
#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
|
||||
|
||||
### default theme by teejez:
|
||||
waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
|
||||
waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]"
|
||||
waterfall_min_level = -88 #in dB
|
||||
waterfall_max_level = -20
|
||||
waterfall_auto_level_margin = (5, 40)
|
||||
@ -227,16 +197,7 @@ waterfall_auto_level_margin = (5, 40)
|
||||
# 3D view settings
|
||||
mathbox_waterfall_frequency_resolution = 128 #bins
|
||||
mathbox_waterfall_history_length = 10 #seconds
|
||||
mathbox_waterfall_colors = [
|
||||
0x000000FF,
|
||||
0x2E6893FF,
|
||||
0x69A5D0FF,
|
||||
0x214B69FF,
|
||||
0x9DC4E0FF,
|
||||
0xFFF775FF,
|
||||
0xFF8A8AFF,
|
||||
0xB20000FF,
|
||||
]
|
||||
mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
|
||||
|
||||
# === Experimental settings ===
|
||||
#Warning! The settings below are very experimental.
|
||||
@ -246,48 +207,10 @@ csdr_through = False # Setting this True will print out how much data is going
|
||||
|
||||
nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
|
||||
|
||||
google_maps_api_key = ""
|
||||
|
||||
# how long should positions be visible on the map?
|
||||
# they will start fading out after half of that
|
||||
# in seconds; default: 2 hours
|
||||
map_position_retention_time = 2 * 60 * 60
|
||||
|
||||
# wsjt decoder queue configuration
|
||||
# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount
|
||||
# of time (6.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
|
||||
# to mitigate this, the recordings will be queued and processed in sequence.
|
||||
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
|
||||
wsjt_queue_workers = 2
|
||||
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up
|
||||
# if you are running background services, make sure this number is high enough to accept the task influx during peaks
|
||||
# i.e. this should be higher than the number of wsjt services running at the same time
|
||||
wsjt_queue_length = 10
|
||||
# wsjt decoding depth will allow more results, but will also consume more cpu
|
||||
wsjt_decoding_depth = 3
|
||||
# can also be set for each mode separately
|
||||
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
|
||||
wsjt_decoding_depths = {"jt65": 1}
|
||||
|
||||
temporary_directory = "/tmp"
|
||||
|
||||
services_enabled = False
|
||||
services_decoders = ["ft8", "ft4", "wspr", "packet"]
|
||||
|
||||
# === aprs igate settings ===
|
||||
# if you want to share your APRS decodes with the aprs network, configure these settings accordingly
|
||||
aprs_callsign = "N0CALL"
|
||||
aprs_igate_enabled = False
|
||||
aprs_igate_server = "euro.aprs2.net"
|
||||
aprs_igate_password = ""
|
||||
# beacon uses the receiver_gps setting, so if you enable this, make sure the location is correct there
|
||||
aprs_igate_beacon = False
|
||||
|
||||
# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols)
|
||||
aprs_symbols_path = "/opt/aprs-symbols/png"
|
||||
|
||||
# === PSK Reporter setting ===
|
||||
# enable this if you want to upload all ft8, ft4 etc spots to pskreporter.info
|
||||
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator
|
||||
pskreporter_enabled = False
|
||||
pskreporter_callsign = "N0CALL"
|
||||
#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname]
|
||||
"""
|
||||
print "[openwebrx-config] Detecting external IP address..."
|
||||
import urllib2
|
||||
server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1]
|
||||
print "[openwebrx-config] External IP address detected:", server_hostname
|
||||
"""
|
||||
|
694
csdr.py
Normal file → Executable file
@ -4,7 +4,6 @@ OpenWebRX csdr plugin: do the signal processing with csdr
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -22,52 +21,17 @@ OpenWebRX csdr plugin: do the signal processing with csdr
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
import os
|
||||
import code
|
||||
import signal
|
||||
import threading
|
||||
import math
|
||||
from functools import partial
|
||||
import fcntl
|
||||
|
||||
from owrx.kiss import KissClient, DirewolfConfig
|
||||
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper
|
||||
class dsp:
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class output(object):
|
||||
def send_output(self, t, read_fn):
|
||||
if not self.supports_type(t):
|
||||
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data
|
||||
logger.warning("dumping output of type %s since it is not supported.", t)
|
||||
threading.Thread(target=self.pump(read_fn, lambda x: None)).start()
|
||||
return
|
||||
self.receive_output(t, read_fn)
|
||||
|
||||
def receive_output(self, t, read_fn):
|
||||
pass
|
||||
|
||||
def pump(self, read, write):
|
||||
def copy():
|
||||
run = True
|
||||
while run:
|
||||
data = read()
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
run = False
|
||||
else:
|
||||
write(data)
|
||||
|
||||
return copy
|
||||
|
||||
def supports_type(self, t):
|
||||
return True
|
||||
|
||||
|
||||
class dsp(object):
|
||||
def __init__(self, output):
|
||||
def __init__(self):
|
||||
self.samp_rate = 250000
|
||||
self.output_rate = 11025
|
||||
self.output_rate = 11025 #this is default, and cannot be set at the moment
|
||||
self.fft_size = 1024
|
||||
self.fft_fps = 5
|
||||
self.offset_freq = 0
|
||||
@ -81,12 +45,13 @@ class dsp(object):
|
||||
self.fft_compression = "none"
|
||||
self.demodulator = "nfm"
|
||||
self.name = "csdr"
|
||||
self.format_conversion = "csdr convert_u8_f"
|
||||
self.base_bufsize = 512
|
||||
self.nc_port = None
|
||||
self.nc_port = 4951
|
||||
self.csdr_dynamic_bufsize = False
|
||||
self.csdr_print_bufsizes = False
|
||||
self.csdr_through = False
|
||||
self.squelch_level = -150
|
||||
self.squelch_level = 0
|
||||
self.fft_averages = 50
|
||||
self.iqtee = False
|
||||
self.iqtee2 = False
|
||||
@ -94,180 +59,63 @@ 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.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "iqtee_pipe", "iqtee2_pipe"]
|
||||
self.secondary_pipe_names=["secondary_shift_pipe"]
|
||||
self.secondary_offset_freq = 1000
|
||||
self.unvoiced_quality = 1
|
||||
self.modification_lock = threading.Lock()
|
||||
self.output = output
|
||||
self.temporary_directory = "/tmp"
|
||||
self.is_service = False
|
||||
self.direwolf_config = None
|
||||
self.direwolf_port = None
|
||||
|
||||
def set_service(self, flag=True):
|
||||
self.is_service = flag
|
||||
|
||||
def set_temporary_directory(self, what):
|
||||
self.temporary_directory = what
|
||||
|
||||
def chain(self,which):
|
||||
chain = ["nc -v 127.0.0.1 {nc_port}"]
|
||||
if self.csdr_dynamic_bufsize:
|
||||
chain += ["csdr setbuf {start_bufsize}"]
|
||||
if self.csdr_through:
|
||||
chain += ["csdr through"]
|
||||
any_chain_base="nc -v 127.0.0.1 {nc_port} | "
|
||||
if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | "
|
||||
if self.csdr_through: any_chain_base+="csdr through | "
|
||||
any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | "
|
||||
if which == "fft":
|
||||
chain += [
|
||||
"csdr fft_cc {fft_size} {fft_block_size}",
|
||||
"csdr logpower_cf -70"
|
||||
if self.fft_averages == 0
|
||||
else "csdr logaveragepower_cf -70 {fft_size} {fft_averages}",
|
||||
"csdr fft_exchange_sides_ff {fft_size}",
|
||||
]
|
||||
fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \
|
||||
("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \
|
||||
"csdr fft_exchange_sides_ff {fft_size}"
|
||||
if self.fft_compression=="adpcm":
|
||||
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
|
||||
return chain
|
||||
chain += ["csdr shift_addition_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"]
|
||||
if self.output.supports_type("smeter"):
|
||||
chain += [
|
||||
"csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}"
|
||||
]
|
||||
return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}"
|
||||
else:
|
||||
return fft_chain_base
|
||||
chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | "
|
||||
if self.secondary_demodulator:
|
||||
if self.output.supports_type("secondary_fft"):
|
||||
chain += ["csdr tee {iqtee_pipe}"]
|
||||
chain += ["csdr tee {iqtee2_pipe}"]
|
||||
# early exit if we don't want audio
|
||||
if not self.output.supports_type("audio"):
|
||||
return chain
|
||||
# safe some cpu cycles... no need to decimate if decimation factor is 1
|
||||
last_decimation_block = (
|
||||
["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else []
|
||||
)
|
||||
if which == "nfm":
|
||||
chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"]
|
||||
chain += last_decimation_block
|
||||
chain += ["csdr deemphasis_nfm_ff {audio_rate}"]
|
||||
if self.get_audio_rate() != self.get_output_rate():
|
||||
chain += [
|
||||
"sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
|
||||
]
|
||||
else:
|
||||
chain += ["csdr convert_f_s16"]
|
||||
elif self.isDigitalVoice(which):
|
||||
chain += ["csdr fmdemod_quadri_cf", "dc_block "]
|
||||
chain += last_decimation_block
|
||||
# dsd modes
|
||||
if which in ["dstar", "nxdn"]:
|
||||
chain += ["csdr limit_ff", "csdr convert_f_s16"]
|
||||
if which == "dstar":
|
||||
chain += ["dsd -fd -i - -o - -u {unvoiced_quality} -g -1 "]
|
||||
elif which == "nxdn":
|
||||
chain += ["dsd -fi -i - -o - -u {unvoiced_quality} -g -1 "]
|
||||
chain += ["CSDR_FIXED_BUFSIZE=32 csdr convert_s16_f"]
|
||||
max_gain = 5
|
||||
# digiham modes
|
||||
else:
|
||||
chain += ["rrc_filter", "gfsk_demodulator"]
|
||||
if which == "dmr":
|
||||
chain += [
|
||||
"dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}",
|
||||
"mbe_synthesizer -f -u {unvoiced_quality}",
|
||||
]
|
||||
elif which == "ysf":
|
||||
chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y -f -u {unvoiced_quality}"]
|
||||
max_gain = 0.0005
|
||||
chain += [
|
||||
"digitalvoice_filter -f",
|
||||
"CSDR_FIXED_BUFSIZE=32 csdr agc_ff 160000 0.8 1 0.0000001 {max_gain}".format(max_gain=max_gain),
|
||||
"sox -t raw -r 8000 -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
|
||||
]
|
||||
elif which == "am":
|
||||
chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"]
|
||||
chain += last_decimation_block
|
||||
chain += ["csdr agc_ff", "csdr limit_ff", "csdr convert_f_s16"]
|
||||
elif which == "ssb":
|
||||
chain += ["csdr realpart_cf"]
|
||||
chain += last_decimation_block
|
||||
chain += ["csdr agc_ff", "csdr limit_ff"]
|
||||
# fixed sample rate necessary for the wsjt-x tools. fix with sox...
|
||||
if self.get_audio_rate() != self.get_output_rate():
|
||||
chain += [
|
||||
"sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
|
||||
]
|
||||
else:
|
||||
chain += ["csdr convert_f_s16"]
|
||||
|
||||
chain_begin+="csdr tee {iqtee_pipe} | "
|
||||
chain_begin+="csdr tee {iqtee2_pipe} | "
|
||||
chain_end = ""
|
||||
if self.audio_compression=="adpcm":
|
||||
chain += ["csdr encode_ima_adpcm_i16_u8"]
|
||||
return chain
|
||||
chain_end = " | csdr encode_ima_adpcm_i16_u8"
|
||||
if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_s16"+chain_end
|
||||
elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
|
||||
elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end
|
||||
|
||||
def secondary_chain(self, which):
|
||||
chain = ["cat {input_pipe}"]
|
||||
secondary_chain_base="cat {input_pipe} | "
|
||||
if which == "fft":
|
||||
chain += [
|
||||
"csdr realpart_cf",
|
||||
"csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size}",
|
||||
"csdr logpower_cf -70",
|
||||
]
|
||||
if self.fft_compression == "adpcm":
|
||||
chain += ["csdr compress_fft_adpcm_f_u8 {secondary_fft_size}"]
|
||||
return chain
|
||||
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
|
||||
elif which == "bpsk31":
|
||||
return chain + [
|
||||
"csdr shift_addition_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):
|
||||
chain += ["csdr realpart_cf"]
|
||||
if self.last_decimation != 1.0:
|
||||
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
||||
return chain + ["csdr limit_ff", "csdr convert_f_s16"]
|
||||
elif which == "packet":
|
||||
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 secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \
|
||||
"csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \
|
||||
"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"
|
||||
|
||||
def set_secondary_demodulator(self, what):
|
||||
if self.get_secondary_demodulator() == what:
|
||||
return
|
||||
self.secondary_demodulator = what
|
||||
self.calculate_decimation()
|
||||
self.restart()
|
||||
|
||||
def secondary_fft_block_size(self):
|
||||
return (self.samp_rate / self.decimation) / (
|
||||
self.fft_fps * 2
|
||||
) # *2 is there because we do FFT on real signal here
|
||||
return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here
|
||||
|
||||
def secondary_decimation(self):
|
||||
return 1 #currently unused
|
||||
|
||||
def secondary_bpf_cutoff(self):
|
||||
if self.secondary_demodulator == "bpsk31":
|
||||
return 31.25 / self.if_samp_rate()
|
||||
return (31.25/2) / self.if_samp_rate()
|
||||
return 0
|
||||
|
||||
def secondary_bpf_transition_bw(self):
|
||||
if self.secondary_demodulator == "bpsk31":
|
||||
return 31.25 / self.if_samp_rate()
|
||||
return (31.25/2) / self.if_samp_rate()
|
||||
return 0
|
||||
|
||||
def secondary_samples_per_bits(self):
|
||||
@ -280,109 +128,71 @@ class dsp(object):
|
||||
return 31.25
|
||||
|
||||
def start_secondary_demodulator(self):
|
||||
if not self.secondary_demodulator:
|
||||
return
|
||||
logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate())
|
||||
secondary_command_demod = " | ".join(self.secondary_chain(self.secondary_demodulator))
|
||||
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod)
|
||||
self.try_create_configs(secondary_command_demod)
|
||||
if(not self.secondary_demodulator): return
|
||||
print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate()
|
||||
secondary_command_fft=self.secondary_chain("fft")
|
||||
secondary_command_demod=self.secondary_chain(self.secondary_demodulator)
|
||||
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft)
|
||||
|
||||
secondary_command_demod = secondary_command_demod.format(
|
||||
input_pipe=self.iqtee2_pipe,
|
||||
secondary_shift_pipe=self.secondary_shift_pipe,
|
||||
secondary_decimation=self.secondary_decimation(),
|
||||
secondary_samples_per_bits=self.secondary_samples_per_bits(),
|
||||
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
|
||||
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
|
||||
if_samp_rate=self.if_samp_rate(),
|
||||
last_decimation=self.last_decimation,
|
||||
audio_rate=self.get_audio_rate(),
|
||||
direwolf_config=self.direwolf_config,
|
||||
secondary_command_fft=secondary_command_fft.format( \
|
||||
input_pipe=self.iqtee_pipe, \
|
||||
secondary_fft_input_size=self.secondary_fft_size, \
|
||||
secondary_fft_size=self.secondary_fft_size, \
|
||||
secondary_fft_block_size=self.secondary_fft_block_size(), \
|
||||
)
|
||||
secondary_command_demod=secondary_command_demod.format( \
|
||||
input_pipe=self.iqtee2_pipe, \
|
||||
secondary_shift_pipe=self.secondary_shift_pipe, \
|
||||
secondary_decimation=self.secondary_decimation(), \
|
||||
secondary_samples_per_bits=self.secondary_samples_per_bits(), \
|
||||
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \
|
||||
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \
|
||||
if_samp_rate=self.if_samp_rate()
|
||||
)
|
||||
|
||||
logger.debug("secondary command (demod) = %s", secondary_command_demod)
|
||||
print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft
|
||||
print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod
|
||||
#code.interact(local=locals())
|
||||
my_env=os.environ.copy()
|
||||
#if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
||||
if self.csdr_print_bufsizes:
|
||||
my_env["CSDR_PRINT_BUFSIZES"] = "1"
|
||||
if self.output.supports_type("secondary_fft"):
|
||||
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
|
||||
secondary_command_fft = secondary_command_fft.format(
|
||||
input_pipe=self.iqtee_pipe,
|
||||
secondary_fft_input_size=self.secondary_fft_size,
|
||||
secondary_fft_size=self.secondary_fft_size,
|
||||
secondary_fft_block_size=self.secondary_fft_block_size(),
|
||||
)
|
||||
logger.debug("secondary command (fft) = %s", secondary_command_fft)
|
||||
|
||||
self.secondary_process_fft = subprocess.Popen(
|
||||
secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env
|
||||
)
|
||||
self.output.send_output(
|
||||
"secondary_fft",
|
||||
partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())),
|
||||
)
|
||||
|
||||
# direwolf does not provide any meaningful data on stdout
|
||||
# more specifically, it doesn't provide any data. if however, for any strange reason, it would start to do so,
|
||||
# it would block if not read. by piping it to devnull, we avoid a potential pitfall here.
|
||||
secondary_output = subprocess.DEVNULL if self.isPacket() else subprocess.PIPE
|
||||
self.secondary_process_demod = subprocess.Popen(
|
||||
secondary_command_demod, stdout=secondary_output, shell=True, preexec_fn=os.setpgrp, env=my_env
|
||||
)
|
||||
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
|
||||
self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
|
||||
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)"
|
||||
self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes
|
||||
print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes
|
||||
self.secondary_processes_running = True
|
||||
|
||||
if self.isWsjtMode():
|
||||
smd = self.get_secondary_demodulator()
|
||||
if smd == "ft8":
|
||||
chopper = Ft8Chopper(self.secondary_process_demod.stdout)
|
||||
elif smd == "wspr":
|
||||
chopper = WsprChopper(self.secondary_process_demod.stdout)
|
||||
elif smd == "jt65":
|
||||
chopper = Jt65Chopper(self.secondary_process_demod.stdout)
|
||||
elif smd == "jt9":
|
||||
chopper = Jt9Chopper(self.secondary_process_demod.stdout)
|
||||
elif smd == "ft4":
|
||||
chopper = Ft4Chopper(self.secondary_process_demod.stdout)
|
||||
chopper.start()
|
||||
self.output.send_output("wsjt_demod", chopper.read)
|
||||
elif self.isPacket():
|
||||
# we best get the ax25 packets from the kiss socket
|
||||
kiss = KissClient(self.direwolf_port)
|
||||
self.output.send_output("packet_demod", kiss.read)
|
||||
else:
|
||||
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
|
||||
|
||||
#open control pipes for csdr and send initialization data
|
||||
# print "==========> 1"
|
||||
if self.secondary_shift_pipe != None: #TODO digimodes
|
||||
# print "==========> 2", self.secondary_shift_pipe
|
||||
self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes
|
||||
# print "==========> 3"
|
||||
self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes
|
||||
# print "==========> 4"
|
||||
|
||||
self.set_pipe_nonblocking(self.secondary_process_demod.stdout)
|
||||
self.set_pipe_nonblocking(self.secondary_process_fft.stdout)
|
||||
|
||||
def set_secondary_offset_freq(self, value):
|
||||
self.secondary_offset_freq=value
|
||||
if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
|
||||
if self.secondary_processes_running:
|
||||
self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate()))
|
||||
self.secondary_shift_pipe_file.flush()
|
||||
|
||||
def stop_secondary_demodulator(self):
|
||||
if self.secondary_processes_running == False:
|
||||
return
|
||||
if self.secondary_processes_running == False: return
|
||||
self.try_delete_pipes(self.secondary_pipe_names)
|
||||
self.try_delete_configs()
|
||||
if self.secondary_process_fft:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
if self.secondary_process_demod:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
|
||||
if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
|
||||
self.secondary_processes_running = False
|
||||
|
||||
def read_secondary_demod(self, size):
|
||||
return self.secondary_process_demod.stdout.read(size)
|
||||
|
||||
def read_secondary_fft(self, size):
|
||||
return self.secondary_process_fft.stdout.read(size)
|
||||
|
||||
def get_secondary_demodulator(self):
|
||||
return self.secondary_demodulator
|
||||
|
||||
@ -393,46 +203,24 @@ class dsp(object):
|
||||
def set_audio_compression(self,what):
|
||||
self.audio_compression = what
|
||||
|
||||
def get_audio_bytes_to_read(self):
|
||||
# desired latency: 5ms
|
||||
# uncompressed audio has 16 bits = 2 bytes per sample
|
||||
base = self.output_rate * 0.005 * 2
|
||||
# adpcm compresses the bitstream by 4
|
||||
if self.audio_compression == "adpcm":
|
||||
base = base / 4
|
||||
return int(base)
|
||||
|
||||
def set_fft_compression(self,what):
|
||||
self.fft_compression = what
|
||||
|
||||
def get_fft_bytes_to_read(self):
|
||||
if self.fft_compression == "none":
|
||||
return self.fft_size * 4
|
||||
if self.fft_compression == "adpcm":
|
||||
return int((self.fft_size / 2) + (10 / 2))
|
||||
if self.fft_compression=="none": return self.fft_size*4
|
||||
if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2)
|
||||
|
||||
def get_secondary_fft_bytes_to_read(self):
|
||||
if self.fft_compression == "none":
|
||||
return self.secondary_fft_size * 4
|
||||
if self.fft_compression == "adpcm":
|
||||
return (self.secondary_fft_size / 2) + (10 / 2)
|
||||
if self.fft_compression=="none": return self.secondary_fft_size*4
|
||||
if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2)
|
||||
|
||||
def set_samp_rate(self,samp_rate):
|
||||
#to change this, restart is required
|
||||
self.samp_rate=samp_rate
|
||||
self.calculate_decimation()
|
||||
if self.running:
|
||||
self.restart()
|
||||
|
||||
def calculate_decimation(self):
|
||||
(self.decimation, self.last_decimation, _) = self.get_decimation(self.samp_rate, self.get_audio_rate())
|
||||
|
||||
def get_decimation(self, input_rate, output_rate):
|
||||
decimation = 1
|
||||
while input_rate / (decimation + 1) >= output_rate:
|
||||
decimation += 1
|
||||
fraction = float(input_rate / decimation) / output_rate
|
||||
intermediate_rate = input_rate / decimation
|
||||
return (decimation, fraction, intermediate_rate)
|
||||
self.decimation=1
|
||||
while self.samp_rate/(self.decimation+1)>self.output_rate:
|
||||
self.decimation+=1
|
||||
self.last_decimation=float(self.if_samp_rate())/self.output_rate
|
||||
|
||||
def if_samp_rate(self):
|
||||
return self.samp_rate/self.decimation
|
||||
@ -443,109 +231,62 @@ class dsp(object):
|
||||
def get_output_rate(self):
|
||||
return self.output_rate
|
||||
|
||||
def get_audio_rate(self):
|
||||
if self.isDigitalVoice() or self.isPacket():
|
||||
return 48000
|
||||
elif self.isWsjtMode():
|
||||
return 12000
|
||||
return self.get_output_rate()
|
||||
|
||||
def isDigitalVoice(self, demodulator=None):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_demodulator()
|
||||
return demodulator in ["dmr", "dstar", "nxdn", "ysf"]
|
||||
|
||||
def isWsjtMode(self, demodulator=None):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_secondary_demodulator()
|
||||
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
|
||||
|
||||
def isPacket(self, demodulator=None):
|
||||
if demodulator is None:
|
||||
demodulator = self.get_secondary_demodulator()
|
||||
return demodulator == "packet"
|
||||
|
||||
def set_output_rate(self,output_rate):
|
||||
if self.output_rate == output_rate:
|
||||
return
|
||||
self.output_rate=output_rate
|
||||
self.calculate_decimation()
|
||||
self.restart()
|
||||
self.set_samp_rate(self.samp_rate) #as it depends on output_rate
|
||||
|
||||
def set_demodulator(self,demodulator):
|
||||
if self.demodulator == demodulator:
|
||||
return
|
||||
#to change this, restart is required
|
||||
self.demodulator=demodulator
|
||||
self.calculate_decimation()
|
||||
self.restart()
|
||||
|
||||
def get_demodulator(self):
|
||||
return self.demodulator
|
||||
|
||||
def set_fft_size(self,fft_size):
|
||||
#to change this, restart is required
|
||||
self.fft_size=fft_size
|
||||
self.restart()
|
||||
|
||||
def set_fft_fps(self,fft_fps):
|
||||
#to change this, restart is required
|
||||
self.fft_fps=fft_fps
|
||||
self.restart()
|
||||
|
||||
def set_fft_averages(self,fft_averages):
|
||||
#to change this, restart is required
|
||||
self.fft_averages=fft_averages
|
||||
self.restart()
|
||||
|
||||
def fft_block_size(self):
|
||||
if self.fft_averages == 0:
|
||||
return self.samp_rate / self.fft_fps
|
||||
else:
|
||||
return self.samp_rate / self.fft_fps / self.fft_averages
|
||||
if self.fft_averages == 0: return self.samp_rate/self.fft_fps
|
||||
else: return self.samp_rate/self.fft_fps/self.fft_averages
|
||||
|
||||
def set_format_conversion(self,format_conversion):
|
||||
self.format_conversion=format_conversion
|
||||
|
||||
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()
|
||||
|
||||
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(
|
||||
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
|
||||
)
|
||||
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
|
||||
self.bpf_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
|
||||
def get_bpf(self):
|
||||
return [self.low_cut, self.high_cut]
|
||||
|
||||
def convertToLinear(self, db):
|
||||
return float(math.pow(10, db / 10))
|
||||
|
||||
def set_squelch_level(self, squelch_level):
|
||||
self.squelch_level=squelch_level
|
||||
# no squelch required on digital voice modes
|
||||
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() 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.write( "%g\n"%(float(self.squelch_level)) )
|
||||
self.squelch_pipe_file.flush()
|
||||
self.modification_lock.release()
|
||||
|
||||
def set_unvoiced_quality(self, q):
|
||||
self.unvoiced_quality = q
|
||||
self.restart()
|
||||
|
||||
def get_unvoiced_quality(self):
|
||||
return self.unvoiced_quality
|
||||
|
||||
def set_dmr_filter(self, filter):
|
||||
if self.dmr_control_pipe_file:
|
||||
self.dmr_control_pipe_file.write("{0}\n".format(filter))
|
||||
self.dmr_control_pipe_file.flush()
|
||||
def get_smeter_level(self):
|
||||
if self.running:
|
||||
line=self.smeter_pipe_file.readline()
|
||||
return float(line[:-1])
|
||||
|
||||
def mkfifo(self,path):
|
||||
try:
|
||||
@ -558,7 +299,9 @@ class dsp(object):
|
||||
return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
|
||||
|
||||
def try_create_pipes(self, pipe_names, command_base):
|
||||
# print "try_create_pipes"
|
||||
for pipe_name in pipe_names:
|
||||
# print "\t"+pipe_name
|
||||
if "{"+pipe_name+"}" in command_base:
|
||||
setattr(self, pipe_name, self.pipe_base_path+pipe_name)
|
||||
self.mkfifo(getattr(self, pipe_name))
|
||||
@ -569,172 +312,113 @@ class dsp(object):
|
||||
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()")
|
||||
try: os.unlink(pipe_path)
|
||||
except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e
|
||||
|
||||
def try_create_configs(self, command):
|
||||
if "{direwolf_config}" in command:
|
||||
self.direwolf_config = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
|
||||
tmp_dir=self.temporary_directory, myid=id(self)
|
||||
)
|
||||
self.direwolf_port = KissClient.getFreePort()
|
||||
file = open(self.direwolf_config, "w")
|
||||
file.write(DirewolfConfig().getConfig(self.direwolf_port, self.is_service))
|
||||
file.close()
|
||||
else:
|
||||
self.direwolf_config = None
|
||||
self.direwolf_port = None
|
||||
|
||||
def try_delete_configs(self):
|
||||
if self.direwolf_config:
|
||||
try:
|
||||
os.unlink(self.direwolf_config)
|
||||
except FileNotFoundError:
|
||||
# result suits our expectations. fine :)
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("try_delete_configs()")
|
||||
self.direwolf_config = None
|
||||
def set_pipe_nonblocking(self, pipe):
|
||||
flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
|
||||
fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||
|
||||
def start(self):
|
||||
self.modification_lock.acquire()
|
||||
if self.running:
|
||||
self.modification_lock.release()
|
||||
return
|
||||
self.running = True
|
||||
|
||||
command_base = " | ".join(self.chain(self.demodulator))
|
||||
command_base=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))
|
||||
self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self))
|
||||
# self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None
|
||||
|
||||
self.try_create_pipes(self.pipe_names, command_base)
|
||||
|
||||
# if "{bpf_pipe}" in command_base:
|
||||
# self.bpf_pipe=pipe_base_path+"bpf"
|
||||
# self.mkfifo(self.bpf_pipe)
|
||||
# if "{shift_pipe}" in command_base:
|
||||
# self.shift_pipe=pipe_base_path+"shift"
|
||||
# self.mkfifo(self.shift_pipe)
|
||||
# if "{squelch_pipe}" in command_base:
|
||||
# self.squelch_pipe=pipe_base_path+"squelch"
|
||||
# self.mkfifo(self.squelch_pipe)
|
||||
# if "{smeter_pipe}" in command_base:
|
||||
# self.smeter_pipe=pipe_base_path+"smeter"
|
||||
# self.mkfifo(self.smeter_pipe)
|
||||
# if "{iqtee_pipe}" in command_base:
|
||||
# self.iqtee_pipe=pipe_base_path+"iqtee"
|
||||
# self.mkfifo(self.iqtee_pipe)
|
||||
# if "{iqtee2_pipe}" in command_base:
|
||||
# self.iqtee2_pipe=pipe_base_path+"iqtee2"
|
||||
# self.mkfifo(self.iqtee2_pipe)
|
||||
|
||||
#run the command
|
||||
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(),
|
||||
)
|
||||
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, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe )
|
||||
|
||||
logger.debug("Command = %s", command)
|
||||
print "[openwebrx-dsp-plugin:csdr] Command =",command
|
||||
#code.interact(local=locals())
|
||||
my_env=os.environ.copy()
|
||||
if self.csdr_dynamic_bufsize:
|
||||
my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
|
||||
if self.csdr_print_bufsizes:
|
||||
my_env["CSDR_PRINT_BUFSIZES"] = "1"
|
||||
if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
|
||||
if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1";
|
||||
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
|
||||
self.running = True
|
||||
|
||||
out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
|
||||
self.process = subprocess.Popen(command, stdout=out, shell=True, preexec_fn=os.setpgrp, env=my_env)
|
||||
|
||||
def watch_thread():
|
||||
rc = self.process.wait()
|
||||
logger.debug("dsp thread ended with rc=%d", rc)
|
||||
if rc == 0 and self.running and not self.modification_lock.locked():
|
||||
logger.debug("restarting since rc = 0, self.running = true, and no modification")
|
||||
self.restart()
|
||||
|
||||
threading.Thread(target=watch_thread).start()
|
||||
|
||||
if self.output.supports_type("audio"):
|
||||
self.output.send_output(
|
||||
"audio",
|
||||
partial(
|
||||
self.process.stdout.read,
|
||||
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
|
||||
),
|
||||
)
|
||||
|
||||
# open control pipes for csdr
|
||||
if self.bpf_pipe:
|
||||
#open control pipes for csdr and send initialization data
|
||||
if self.bpf_pipe != None:
|
||||
self.bpf_pipe_file=open(self.bpf_pipe,"w")
|
||||
if self.shift_pipe:
|
||||
self.set_bpf(self.low_cut,self.high_cut)
|
||||
if self.shift_pipe != None:
|
||||
self.shift_pipe_file=open(self.shift_pipe,"w")
|
||||
if self.squelch_pipe:
|
||||
self.set_offset_freq(self.offset_freq)
|
||||
if self.squelch_pipe != None:
|
||||
self.squelch_pipe_file=open(self.squelch_pipe,"w")
|
||||
self.set_squelch_level(self.squelch_level)
|
||||
if self.smeter_pipe != None:
|
||||
self.smeter_pipe_file=open(self.smeter_pipe,"r")
|
||||
self.set_pipe_nonblocking(self.smeter_pipe_file)
|
||||
|
||||
self.start_secondary_demodulator()
|
||||
|
||||
self.modification_lock.release()
|
||||
|
||||
# send initial config through the pipes
|
||||
if self.squelch_pipe:
|
||||
self.set_squelch_level(self.squelch_level)
|
||||
if self.shift_pipe:
|
||||
self.set_offset_freq(self.offset_freq)
|
||||
if self.bpf_pipe:
|
||||
self.set_bpf(self.low_cut, self.high_cut)
|
||||
if self.smeter_pipe:
|
||||
self.smeter_pipe_file = open(self.smeter_pipe, "r")
|
||||
|
||||
def read_smeter():
|
||||
raw = self.smeter_pipe_file.readline()
|
||||
if len(raw) == 0:
|
||||
return None
|
||||
else:
|
||||
return float(raw.rstrip("\n"))
|
||||
|
||||
self.output.send_output("smeter", read_smeter)
|
||||
if self.meta_pipe != None:
|
||||
# TODO make digiham output unicode and then change this here
|
||||
self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437")
|
||||
|
||||
def read_meta():
|
||||
raw = self.meta_pipe_file.readline()
|
||||
if len(raw) == 0:
|
||||
return None
|
||||
else:
|
||||
return raw.rstrip("\n")
|
||||
|
||||
self.output.send_output("meta", read_meta)
|
||||
|
||||
if self.dmr_control_pipe:
|
||||
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w")
|
||||
def read(self,size):
|
||||
return self.process.stdout.read(size)
|
||||
|
||||
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()
|
||||
#if(self.process.poll()!=None):return # returns None while subprocess is running
|
||||
#while(self.process.poll()==None):
|
||||
# #self.process.kill()
|
||||
# print "killproc",os.getpgid(self.process.pid),self.process.pid
|
||||
# os.killpg(self.process.pid, signal.SIGTERM)
|
||||
#
|
||||
# time.sleep(0.1)
|
||||
|
||||
self.try_delete_pipes(self.pipe_names)
|
||||
|
||||
self.modification_lock.release()
|
||||
# if self.bpf_pipe:
|
||||
# try: os.unlink(self.bpf_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe
|
||||
# if self.shift_pipe:
|
||||
# try: os.unlink(self.shift_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe
|
||||
# if self.squelch_pipe:
|
||||
# try: os.unlink(self.squelch_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe
|
||||
# if self.smeter_pipe:
|
||||
# try: os.unlink(self.smeter_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe
|
||||
# if self.iqtee_pipe:
|
||||
# try: os.unlink(self.iqtee_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe
|
||||
# if self.iqtee2_pipe:
|
||||
# try: os.unlink(self.iqtee2_pipe)
|
||||
# except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe
|
||||
|
||||
self.running = False
|
||||
|
||||
def restart(self):
|
||||
if not self.running:
|
||||
return
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
del self.process
|
||||
del(self.process)
|
||||
|
@ -1,6 +0,0 @@
|
||||
ARG ARCH
|
||||
FROM openwebrx-base:$ARCH
|
||||
|
||||
ADD docker/scripts/install-dependencies-airspy.sh /
|
||||
RUN /install-dependencies-airspy.sh
|
||||
|
@ -1,24 +0,0 @@
|
||||
ARG BASE_IMAGE
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
RUN ln -s /usr/local/lib /usr/local/lib64
|
||||
|
||||
ADD docker/scripts/direwolf-1.5.patch /
|
||||
ADD docker/scripts/install-dependencies.sh /
|
||||
RUN /install-dependencies.sh
|
||||
|
||||
ADD . /opt/openwebrx
|
||||
|
||||
WORKDIR /opt/openwebrx
|
||||
|
||||
RUN mkdir -p /tmp/openwebrx && \
|
||||
mv "/opt/openwebrx/config_webrx.py" "/opt/openwebrx/config_webrx.py.orig" && \
|
||||
sed 's/temporary_directory = "\/tmp"/temporary_directory = "\/tmp\/openwebrx"/' < "/opt/openwebrx/config_webrx.py.orig" > "/opt/openwebrx/config_webrx.py" && \
|
||||
rm "/opt/openwebrx/config_webrx.py.orig"
|
||||
|
||||
VOLUME /config
|
||||
|
||||
ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ]
|
||||
EXPOSE 8073
|
@ -1,14 +0,0 @@
|
||||
ARG ARCH
|
||||
FROM openwebrx-base:$ARCH
|
||||
|
||||
ADD docker/scripts/install-dependencies-*.sh /
|
||||
ADD docker/scripts/install-lib.*.patch /
|
||||
|
||||
RUN /install-dependencies-rtlsdr.sh
|
||||
RUN /install-dependencies-hackrf.sh
|
||||
RUN /install-dependencies-soapysdr.sh
|
||||
RUN /install-dependencies-sdrplay.sh
|
||||
RUN /install-dependencies-airspy.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
@ -1,6 +0,0 @@
|
||||
ARG ARCH
|
||||
FROM openwebrx-base:$ARCH
|
||||
|
||||
ADD docker/scripts/install-dependencies-hackrf.sh /
|
||||
RUN /install-dependencies-hackrf.sh
|
||||
|
@ -1,8 +0,0 @@
|
||||
ARG ARCH
|
||||
FROM openwebrx-base:$ARCH
|
||||
|
||||
ADD docker/scripts/install-dependencies-rtlsdr.sh /
|
||||
RUN /install-dependencies-rtlsdr.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
@ -1,9 +0,0 @@
|
||||
ARG ARCH
|
||||
FROM openwebrx-soapysdr-base:$ARCH
|
||||
|
||||
ADD docker/scripts/install-dependencies-sdrplay.sh /
|
||||
ADD docker/scripts/install-lib.*.patch /
|
||||
RUN /install-dependencies-sdrplay.sh
|
||||
|
||||
ADD docker/scripts/install-connectors.sh /
|
||||
RUN /install-connectors.sh
|
@ -1,6 +0,0 @@
|
||||
ARG ARCH
|
||||
FROM openwebrx-base:$ARCH
|
||||
|
||||
ADD docker/scripts/install-dependencies-soapysdr.sh /
|
||||
RUN /install-dependencies-soapysdr.sh
|
||||
|
@ -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,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
BUILD_PACKAGES="git cmake make gcc g++ musl-dev"
|
||||
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
|
||||
|
||||
git clone https://github.com/jketterl/owrx_connector.git
|
||||
cmakebuild owrx_connector
|
||||
|
||||
apk del .build-deps
|
@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb"
|
||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/airspy/airspyone_host.git
|
||||
cmakebuild airspyone_host
|
||||
|
||||
apk del .build-deps
|
@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb fftw udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/mossmann/hackrf.git
|
||||
cd hackrf
|
||||
cmakebuild host
|
||||
cd ..
|
||||
rm -rf hackrf
|
||||
|
||||
apk del .build-deps
|
@ -1,26 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb"
|
||||
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/osmocom/rtl-sdr.git
|
||||
cmakebuild rtl-sdr
|
||||
|
||||
apk del .build-deps
|
@ -1,47 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="libusb udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
case $ARCH in
|
||||
x86_64)
|
||||
BINARY=SDRplay_RSP_API-Linux-2.13.1.run
|
||||
;;
|
||||
armv*)
|
||||
BINARY=SDRplay_RSP_API-RPi-2.13.1.run
|
||||
;;
|
||||
esac
|
||||
|
||||
wget http://www.sdrplay.com/software/$BINARY
|
||||
sh $BINARY --noexec --target sdrplay
|
||||
patch --verbose -Np0 < /install-lib.$ARCH.patch
|
||||
|
||||
cd sdrplay
|
||||
./install_lib.sh
|
||||
cd ..
|
||||
rm -rf sdrplay
|
||||
rm $BINARY
|
||||
|
||||
git clone https://github.com/pothosware/SoapySDRPlay.git
|
||||
cmakebuild SoapySDRPlay
|
||||
|
||||
apk del .build-deps
|
@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="udev"
|
||||
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
|
||||
git clone https://github.com/pothosware/SoapySDR
|
||||
cmakebuild SoapySDR
|
||||
|
||||
git clone https://github.com/rxseger/rx_tools
|
||||
cmakebuild rx_tools
|
||||
|
||||
apk del .build-deps
|
@ -1,59 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
function cmakebuild() {
|
||||
cd $1
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install
|
||||
cd ../..
|
||||
rm -rf $1
|
||||
}
|
||||
|
||||
cd /tmp
|
||||
|
||||
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib"
|
||||
BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers"
|
||||
|
||||
apk add --no-cache $STATIC_PACKAGES
|
||||
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
|
||||
|
||||
git clone https://git.code.sf.net/p/itpp/git itpp
|
||||
cmakebuild itpp
|
||||
|
||||
git clone https://github.com/jketterl/csdr.git -b docker_fixes
|
||||
cd csdr
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
rm -rf csdr
|
||||
|
||||
git clone https://github.com/szechyjs/mbelib.git
|
||||
cmakebuild mbelib
|
||||
|
||||
git clone https://github.com/jketterl/digiham.git
|
||||
cmakebuild digiham
|
||||
|
||||
git clone https://github.com/f4exb/dsd.git
|
||||
cmakebuild dsd
|
||||
|
||||
WSJT_DIR=wsjtx-2.1.0
|
||||
WSJT_TGZ=${WSJT_DIR}.tgz
|
||||
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ
|
||||
tar xvfz $WSJT_TGZ
|
||||
cmakebuild $WSJT_DIR
|
||||
|
||||
git clone https://github.com/wb2osz/direwolf.git
|
||||
cd direwolf
|
||||
git checkout 1.5
|
||||
patch -Np1 < /direwolf-1.5.patch
|
||||
make
|
||||
make install
|
||||
cd ..
|
||||
rm -rf direwolf
|
||||
|
||||
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
|
||||
|
||||
apk del .build-deps
|
@ -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..."
|
||||
|
@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ ! -f /config/config_webrx.py ]] ; then
|
||||
cp config_webrx.py /config
|
||||
fi
|
||||
|
||||
rm config_webrx.py
|
||||
ln -s /config/config_webrx.py .
|
||||
|
||||
|
||||
_term() {
|
||||
echo "Caught signal!"
|
||||
kill -TERM "$child" 2>/dev/null
|
||||
}
|
||||
|
||||
trap _term SIGTERM SIGINT
|
||||
|
||||
python3 openwebrx.py $@ &
|
||||
|
||||
child=$!
|
||||
wait "$child"
|
||||
|
@ -1,12 +0,0 @@
|
||||
@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;
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
@import url("openwebrx-header.css");
|
||||
@import url("openwebrx-globals.css");
|
||||
|
||||
/* expandable photo not implemented on map page */
|
||||
#webrx-top-photo-clip {
|
||||
max-height: 67px;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#webrx-top-container {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.openwebrx-map {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 5px;
|
||||
padding-inline-start: 25px;
|
||||
}
|
||||
|
||||
.openwebrx-map-legend {
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.openwebrx-map-legend ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.openwebrx-map-legend li.square .illustration {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.openwebrx-map-legend select {
|
||||
background-color: #FFF;
|
||||
border-color: #DDD;
|
||||
padding: 5px;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
html, body
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
}
|
||||
|
@ -1,195 +0,0 @@
|
||||
#webrx-top-container
|
||||
{
|
||||
position: relative;
|
||||
z-index:1000;
|
||||
}
|
||||
|
||||
#webrx-top-photo
|
||||
{
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#webrx-top-photo-clip
|
||||
{
|
||||
min-height: 67px;
|
||||
max-height: 350px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.webrx-top-bar-parts
|
||||
{
|
||||
height:67px;
|
||||
}
|
||||
|
||||
#webrx-top-bar
|
||||
{
|
||||
background: rgba(128, 128, 128, 0.15);
|
||||
margin:0;
|
||||
padding:0;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#webrx-top-logo
|
||||
{
|
||||
padding: 12px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#webrx-ha5kfu-top-logo
|
||||
{
|
||||
float: right;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
#webrx-rx-avatar
|
||||
{
|
||||
background-color: rgba(154, 154, 154, .5);
|
||||
border-radius: 7px;
|
||||
float: left;
|
||||
margin: 7px;
|
||||
|
||||
cursor:pointer;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
#webrx-rx-texts {
|
||||
float: left;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#webrx-rx-texts div {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#webrx-rx-title
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
color: #909090;
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#webrx-rx-desc
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
font-size: 10pt;
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
#webrx-rx-desc a
|
||||
{
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow
|
||||
{
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 470px;
|
||||
top: 51px;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow a
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow-down
|
||||
{
|
||||
display:none;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons ul
|
||||
{
|
||||
display: table;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-main-buttons ul li
|
||||
{
|
||||
display: table-cell;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons li:hover
|
||||
{
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons li:active
|
||||
{
|
||||
background-color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-main-buttons
|
||||
{
|
||||
float: right;
|
||||
margin:0;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 4px #000000;
|
||||
text-align: center;
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-title
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 78px;
|
||||
color: White;
|
||||
font-size: 16pt;
|
||||
text-shadow: 1px 1px 4px #444;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 109px;
|
||||
color: White;
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
text-shadow: 0px 0px 6px #444;
|
||||
opacity: 1;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc a
|
||||
{
|
||||
color: #5ca8ff;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
<HTML><HEAD>
|
||||
<TITLE>OpenWebRX Feature report</TITLE>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="static/css/features.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
|
||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/features.js"></script>
|
||||
</HEAD><BODY>
|
||||
${header}
|
||||
<div class="container">
|
||||
<h1>OpenWebRX Feature Report</h1>
|
||||
<table class="features table">
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Requirement</th>
|
||||
<th>Description</th>
|
||||
<th>Available</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</BODY></HTML>
|
@ -1,24 +0,0 @@
|
||||
$(function(){
|
||||
var converter = new showdown.Converter();
|
||||
$.ajax('/api/features').done(function(data){
|
||||
var $table = $('table.features');
|
||||
$.each(data, function(name, details) {
|
||||
var requirements = $.map(details.requirements, function(r, name){
|
||||
return '<tr>' +
|
||||
'<td></td>' +
|
||||
'<td>' + name + '</td>' +
|
||||
'<td>' + converter.makeHtml(r.description) + '</td>' +
|
||||
'<td>' + (r.available ? 'YES' : 'NO') + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
$table.append(
|
||||
'<tr>' +
|
||||
'<td colspan=2>' + name + '</td>' +
|
||||
'<td>' + converter.makeHtml(details.description) + '</td>' +
|
||||
'<td>' + (details.available ? 'YES' : 'NO') + '</td>' +
|
||||
'</tr>' +
|
||||
requirements.join("")
|
||||
);
|
||||
})
|
||||
});
|
||||
});
|
@ -1,77 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="5.6444445mm"
|
||||
height="9.847393mm"
|
||||
viewBox="0 0 20 34.892337"
|
||||
id="svg3455"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="Map Pin.svg">
|
||||
<defs
|
||||
id="defs3457" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="12.181359"
|
||||
inkscape:cx="8.4346812"
|
||||
inkscape:cy="14.715224"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1024"
|
||||
inkscape:window-height="705"
|
||||
inkscape:window-x="-4"
|
||||
inkscape:window-y="-4"
|
||||
inkscape:window-maximized="1"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata3460">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-814.59595,-274.38623)">
|
||||
<g
|
||||
id="g3477"
|
||||
transform="matrix(1.1855854,0,0,1.1855854,-151.17715,-57.3976)">
|
||||
<path
|
||||
sodipodi:nodetypes="sscccccsscs"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4337-3"
|
||||
d="m 817.11249,282.97118 c -1.25816,1.34277 -2.04623,3.29881 -2.01563,5.13867 0.0639,3.84476 1.79693,5.3002 4.56836,10.59179 0.99832,2.32851 2.04027,4.79237 3.03125,8.87305 0.13772,0.60193 0.27203,1.16104 0.33416,1.20948 0.0621,0.0485 0.19644,-0.51262 0.33416,-1.11455 0.99098,-4.08068 2.03293,-6.54258 3.03125,-8.87109 2.77143,-5.29159 4.50444,-6.74704 4.56836,-10.5918 0.0306,-1.83986 -0.75942,-3.79785 -2.01758,-5.14062 -1.43724,-1.53389 -3.60504,-2.66908 -5.91619,-2.71655 -2.31115,-0.0475 -4.4809,1.08773 -5.91814,2.62162 z"
|
||||
style="display:inline;opacity:1;fill:#ff4646;fill-opacity:1;stroke:#d73534;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<circle
|
||||
r="3.0355"
|
||||
cy="288.25278"
|
||||
cx="823.03064"
|
||||
id="path3049"
|
||||
style="display:inline;opacity:1;fill:#590000;fill-opacity:1;stroke-width:0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.8 KiB |
BIN
htdocs/gfx/openwebrx-avatar-background.png
Normal file
After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 970 B |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 797 B |
85
htdocs/inactive.html
Normal file
@ -0,0 +1,85 @@
|
||||
<html>
|
||||
<!--
|
||||
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-->
|
||||
<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<style>
|
||||
html, body
|
||||
{
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
img.logo
|
||||
{
|
||||
margin-top: 120px;
|
||||
}
|
||||
div.frame
|
||||
{
|
||||
text-align: left;
|
||||
margin:0px auto;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
div.panel
|
||||
{
|
||||
text-align: center;
|
||||
background-color:#777777;
|
||||
border-radius: 15px;
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
color: White;
|
||||
font-size: 13pt;
|
||||
/*text-shadow: 1px 1px 4px #444;*/
|
||||
font-family: sans;
|
||||
}
|
||||
|
||||
div.alt
|
||||
{
|
||||
font-size: 10pt;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
body div a
|
||||
{
|
||||
color: #5ca8ff;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
span.browser
|
||||
{
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="frame">
|
||||
<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
|
||||
<div class="panel">
|
||||
Sorry, the receiver is inactive due to internal error.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,28 +0,0 @@
|
||||
<div id="webrx-top-container">
|
||||
<div id="webrx-top-photo-clip">
|
||||
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
|
||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
||||
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
|
||||
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
|
||||
<div id="webrx-rx-texts">
|
||||
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
|
||||
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
|
||||
</div>
|
||||
<div id="openwebrx-rx-details-arrow">
|
||||
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a>
|
||||
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
|
||||
</div>
|
||||
<section id="openwebrx-main-buttons">
|
||||
<ul>
|
||||
<li data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li>
|
||||
<li data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li>
|
||||
<li data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
|
||||
<li><a href="/map" target="_blank"><img src="static/gfx/openwebrx-panel-map.png" /><br/>Map</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<div id="webrx-rx-photo-title"></div>
|
||||
<div id="webrx-rx-photo-desc"></div>
|
||||
</div>
|
||||
</div>
|
@ -1,259 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<!--
|
||||
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
||||
<script src="static/sdr.js"></script>
|
||||
<script src="static/mathbox-bundle.min.js"></script>
|
||||
<script src="static/openwebrx.js"></script>
|
||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/lib/jquery.nanoscroller.js"></script>
|
||||
<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>
|
||||
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
|
||||
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body onload="openwebrx_init();">
|
||||
<div id="webrx-page-container">
|
||||
${header}
|
||||
<div id="openwebrx-frequency-container">
|
||||
<div id="openwebrx-bookmarks-container"></div>
|
||||
<div id="openwebrx-scale-container">
|
||||
<canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-mathbox-container"> </div>
|
||||
<div id="webrx-canvas-background">
|
||||
<div id="webrx-canvas-container">
|
||||
<!-- add canvas here by javascript -->
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
<div id="openwebrx-digimode-content-container">
|
||||
<div class="gradient"></div>
|
||||
<div id="openwebrx-digimode-content">
|
||||
<span id="openwebrx-cursor-blink"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
|
||||
<thead><tr>
|
||||
<th>UTC</th>
|
||||
<th class="decimal">dB</th>
|
||||
<th class="decimal">DT</th>
|
||||
<th class="decimal freq">Freq</th>
|
||||
<th class="message">Message</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<table class="openwebrx-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
|
||||
<thead><tr>
|
||||
<th>UTC</th>
|
||||
<th class="callsign">Callsign</th>
|
||||
<th class="coord">Coord</th>
|
||||
<th class="message">Comment</th>
|
||||
</tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-ysf" style="display: none;" data-panel-name="metadata-ysf">
|
||||
<div class="openwebrx-meta-frame">
|
||||
<div class="openwebrx-meta-slot">
|
||||
<div class="openwebrx-ysf-mode openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-ysf-source openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-ysf-up openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-ysf-down openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dmr" style="display: none;" data-panel-name="metadata-dmr">
|
||||
<div class="openwebrx-meta-frame">
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 1</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
<div class="openwebrx-meta-slot openwebrx-dmr-timeslot-panel">
|
||||
<div class="openwebrx-dmr-slot">Timeslot 2</div>
|
||||
<div class="openwebrx-meta-user-image"></div>
|
||||
<div class="openwebrx-dmr-id openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-name openwebrx-meta-autoclear"></div>
|
||||
<div class="openwebrx-dmr-target openwebrx-meta-autoclear"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
|
||||
<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://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a></div>
|
||||
<div>Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</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>
|
||||
</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">---.--- MHz</div>
|
||||
<div id="webrx-mouse-freq">---.--- MHz</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">
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<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="ft8" data-feature="wsjt-x">FT8</option>
|
||||
<option value="wspr" data-feature="wsjt-x">WSPR</option>
|
||||
<option value="jt65" data-feature="wsjt-x">JT65</option>
|
||||
<option value="jt9" data-feature="wsjt-x">JT9</option>
|
||||
<option value="ft4" data-feature="wsjt-x">FT4</option>
|
||||
<option value="packet" data-feature="packet">Packet</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
||||
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
||||
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="static/gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
|
||||
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="static/gfx/openwebrx-zoom-in.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="static/gfx/openwebrx-zoom-out.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="static/gfx/openwebrx-zoom-in-total.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="static/gfx/openwebrx-zoom-out-total.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="static/gfx/openwebrx-3d-spectrum.png" /></div>
|
||||
<div id="openwebrx-smeter-db">0 dB</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div id="openwebrx-smeter-outer">
|
||||
<div id="openwebrx-smeter-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-autoplay-overlay" style="display:none;">
|
||||
<div class="overlay-content">
|
||||
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
|
||||
<div>Start OpenWebRX</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-dialog-bookmark" class="openwebrx-dialog" style="display:none;">
|
||||
<form>
|
||||
<div class="form-field">
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name" required="required">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="frequency">Frequency:</label>
|
||||
<input type="number" id="frequency" name="frequency">
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label for="modulation">Modulation:</label>
|
||||
<select name="modulation" id="modulation">
|
||||
<option value="nfm">FM</option>
|
||||
<option value="am">AM</option>
|
||||
<option value="usb">USB</option>
|
||||
<option value="lsb">LSB</option>
|
||||
<option value="cw">CW</option>
|
||||
<option value="dmr">DMR</option>
|
||||
<option value="dstar">D-Star</option>
|
||||
<option value="nxdn">NXDN</option>
|
||||
<option value="ysf">YSF</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="openwebrx-button" data-action="cancel">Cancel</div>
|
||||
<div class="openwebrx-button" data-action="submit">Ok</div>
|
||||
</div>
|
||||
<input type="submit" style="display:none;">
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
180
htdocs/index.wrx
Normal file
@ -0,0 +1,180 @@
|
||||
<!DOCTYPE HTML>
|
||||
<!--
|
||||
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
|
||||
<script type="text/javascript">
|
||||
//Global variables
|
||||
var client_id="%[CLIENT_ID]";
|
||||
var ws_url="%[WS_URL]";
|
||||
var rx_photo_height=%[RX_PHOTO_HEIGHT];
|
||||
var audio_buffering_fill_to=%[AUDIO_BUFSIZE];
|
||||
var starting_mod="%[START_MOD]";
|
||||
var starting_offset_frequency = %[START_OFFSET_FREQ];
|
||||
var waterfall_colors=%[WATERFALL_COLORS];
|
||||
var waterfall_min_level_default=%[WATERFALL_MIN_LEVEL];
|
||||
var waterfall_max_level_default=%[WATERFALL_MAX_LEVEL];
|
||||
var waterfall_auto_level_margin=%[WATERFALL_AUTO_LEVEL_MARGIN];
|
||||
var server_enable_digimodes=%[DIGIMODES_ENABLE];
|
||||
var mathbox_waterfall_frequency_resolution=%[MATHBOX_WATERFALL_FRES];
|
||||
var mathbox_waterfall_history_length=%[MATHBOX_WATERFALL_THIST];
|
||||
var mathbox_waterfall_colors=%[MATHBOX_WATERFALL_COLORS];
|
||||
</script>
|
||||
<script src="sdr.js"></script>
|
||||
<script src="mathbox-bundle.min.js"></script>
|
||||
<script src="openwebrx.js"></script>
|
||||
<script src="jquery-3.2.1.min.js"></script>
|
||||
<script src="jquery.nanoscroller.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="nanoscroller.css" />
|
||||
<link rel="stylesheet" type="text/css" href="openwebrx.css" />
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body onload="openwebrx_init();">
|
||||
<div id="webrx-page-container">
|
||||
<div id="webrx-top-container">
|
||||
<div id="webrx-top-photo-clip">
|
||||
<img src="gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/>
|
||||
<div id="webrx-rx-photo-title">%[RX_PHOTO_TITLE]</div>
|
||||
<div id="webrx-rx-photo-desc">%[RX_PHOTO_DESC]</div>
|
||||
</div>
|
||||
<div id="webrx-top-bar-background" class="webrx-top-bar-parts"></div>
|
||||
<div id="webrx-top-bar" class="webrx-top-bar-parts">
|
||||
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a>
|
||||
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a>
|
||||
<img id="webrx-rx-avatar-background" src="gfx/openwebrx-avatar-background.png" onclick="toggle_rx_photo();"/>
|
||||
<img id="webrx-rx-avatar" src="gfx/openwebrx-avatar.png" onclick="toggle_rx_photo();"/>
|
||||
<div id="webrx-rx-title" onclick="toggle_rx_photo();">%[RX_TITLE]</div>
|
||||
<div id="webrx-rx-desc" onclick="toggle_rx_photo();">%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, <a href="https://www.google.hu/maps/place/%[RX_GPS]" target="_blank" onclick="dont_toggle_rx_photo();">[maps]</a></div>
|
||||
<div id="openwebrx-rx-details-arrow">
|
||||
<a id="openwebrx-rx-details-arrow-up" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow-up.png" /></a>
|
||||
<a id="openwebrx-rx-details-arrow-down" onclick="toggle_rx_photo();"><img src="gfx/openwebrx-rx-details-arrow.png" /></a>
|
||||
</div>
|
||||
<section id="openwebrx-main-buttons">
|
||||
<ul>
|
||||
<li onmouseup="toggle_panel('openwebrx-panel-status');"><img src="gfx/openwebrx-panel-status.png" /><br/>Status</li>
|
||||
<li onmouseup="toggle_panel('openwebrx-panel-log');"><img src="gfx/openwebrx-panel-log.png" /><br/>Log</li>
|
||||
<li onmouseup="toggle_panel('openwebrx-panel-receiver');"><img src="gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div id="webrx-main-container">
|
||||
<div id="openwebrx-scale-container">
|
||||
<canvas id="openwebrx-scale-canvas" width="0" height="0"></canvas>
|
||||
</div>
|
||||
<div id="openwebrx-mathbox-container"> </div>
|
||||
<div id="webrx-canvas-container">
|
||||
<div id="openwebrx-phantom-canvas"></div>
|
||||
<!-- add canvas here by javascript -->
|
||||
</div>
|
||||
<div id="openwebrx-panels-container">
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" data-panel-pos="right" data-panel-order="0" data-panel-size="259,115">
|
||||
<div id="webrx-actual-freq">---.--- MHz</div>
|
||||
<div id="webrx-mouse-freq">---.--- MHz</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
|
||||
onclick="demodulator_analog_replace('nfm');">FM</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
|
||||
onclick="demodulator_analog_replace('am');">AM</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
|
||||
onclick="demodulator_analog_replace('lsb');">LSB</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
|
||||
onclick="demodulator_analog_replace('usb');">USB</div>
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
|
||||
onclick="demodulator_analog_replace('cw');">CW</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
|
||||
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
|
||||
<option value="none"></option>
|
||||
<option value="bpsk31">BPSK31</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
|
||||
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
|
||||
<div title="Auto-adjust waterfall colors" id="openwebrx-waterfall-colors-auto" class="openwebrx-button" onclick="waterfall_measure_minmax_now=true;"><img src="gfx/openwebrx-waterfall-auto.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()">
|
||||
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
|
||||
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInOneStep();" title="Zoom in one step"> <img src="gfx/openwebrx-zoom-in.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutOneStep();" title="Zoom out one step"> <img src="gfx/openwebrx-zoom-out.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomInTotal();" title="Zoom in totally"><img src="gfx/openwebrx-zoom-in-total.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="zoomOutTotal();" title="Zoom out totally"><img src="gfx/openwebrx-zoom-out-total.png" /></div>
|
||||
<div class="openwebrx-button openwebrx-square-button" onclick="mathbox_toggle();" title="Toggle 3D view"><img src="gfx/openwebrx-3d-spectrum.png" /></div>
|
||||
<div id="openwebrx-smeter-db">0 dB</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel-line">
|
||||
<div id="openwebrx-smeter-outer">
|
||||
<div id="openwebrx-smeter-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" data-panel-pos="left" data-panel-order="1" data-panel-size="619,137">
|
||||
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
|
||||
<div class="nano-content">
|
||||
<div id="openwebrx-client-log-title">OpenWebRX client log</strong><span id="openwebrx-problems"></span></div>
|
||||
<span id="openwebrx-client-1">Author: </span><a href="http://blog.sdr.hu/about" target="_blank">András Retzler, HA7ILM</a><br />You can <a href="http://blog.sdr.hu/support" target="_blank">donate</a> to say thanks for former development (this is the final version).<br/>
|
||||
<div id="openwebrx-debugdiv"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" data-panel-pos="left" data-panel-order="0" data-panel-size="615,50" data-panel-transparent="true">
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div>
|
||||
</div>
|
||||
<div class="openwebrx-panel" data-panel-name="client-under-devel" data-panel-pos="none" data-panel-order="0" data-panel-size="245,55" style="background-color: Red;">
|
||||
<span style="font-size: 15pt; font-weight: bold;">Under construction</span>
|
||||
<br />We're working on the code right now, so the application might fail.
|
||||
</div>
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-digimodes" data-panel-name="digimodes" data-panel-pos="left" data-panel-order="2" data-panel-size="619,210">
|
||||
<div id="openwebrx-digimode-canvas-container">
|
||||
<div id="openwebrx-digimode-select-channel"></div>
|
||||
</div>
|
||||
<div id="openwebrx-digimode-content-container">
|
||||
<div class="gradient"></div>
|
||||
<div id="openwebrx-digimode-content">
|
||||
<span id="openwebrx-cursor-blink"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="openwebrx-big-grey" onclick="iosPlayButtonClick();">
|
||||
<div id="openwebrx-play-button-text">
|
||||
<img id="openwebrx-play-button" src="gfx/openwebrx-play-button.png" />
|
||||
<br /><br />Start OpenWebRX
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,91 +0,0 @@
|
||||
function AprsMarker() {}
|
||||
|
||||
AprsMarker.prototype = new google.maps.OverlayView();
|
||||
|
||||
AprsMarker.prototype.draw = function() {
|
||||
var div = this.div;
|
||||
var overlay = this.overlay;
|
||||
if (!div || !overlay) return;
|
||||
|
||||
if (this.symbol) {
|
||||
var tableId = this.symbol.table === '/' ? 0 : 1;
|
||||
div.style.background = 'url(/aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)';
|
||||
div.style['background-size'] = '384px 144px';
|
||||
div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px';
|
||||
div.style['background-position-y'] = -Math.floor(this.symbol.index / 16) * 24 + 'px';
|
||||
}
|
||||
|
||||
if (this.course) {
|
||||
if (this.course > 180) {
|
||||
div.style.transform = 'scalex(-1) rotate(' + (270 - this.course) + 'deg)'
|
||||
} else {
|
||||
div.style.transform = 'rotate(' + (this.course - 90) + 'deg)';
|
||||
}
|
||||
} else {
|
||||
div.style.transform = null;
|
||||
}
|
||||
|
||||
if (this.symbol.table !== '/' && this.symbol.table !== '\\') {
|
||||
overlay.style.display = 'block';
|
||||
overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px';
|
||||
overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px';
|
||||
} else {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
|
||||
if (this.opacity) {
|
||||
div.style.opacity = this.opacity;
|
||||
} else {
|
||||
div.style.opacity = null;
|
||||
}
|
||||
|
||||
var point = this.getProjection().fromLatLngToDivPixel(this.position);
|
||||
|
||||
if (point) {
|
||||
div.style.left = point.x - 12 + 'px';
|
||||
div.style.top = point.y - 12 + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
AprsMarker.prototype.setOptions = function(options) {
|
||||
google.maps.OverlayView.prototype.setOptions.apply(this, arguments);
|
||||
this.draw();
|
||||
};
|
||||
|
||||
AprsMarker.prototype.onAdd = function() {
|
||||
var div = this.div = document.createElement('div');
|
||||
|
||||
div.style.position = 'absolute';
|
||||
div.style.cursor = 'pointer';
|
||||
div.style.width = '24px';
|
||||
div.style.height = '24px';
|
||||
|
||||
var overlay = this.overlay = document.createElement('div');
|
||||
overlay.style.width = '24px';
|
||||
overlay.style.height = '24px';
|
||||
overlay.style.background = 'url(/aprs-symbols/aprs-symbols-24-2@2x.png)';
|
||||
overlay.style['background-size'] = '384px 144px';
|
||||
overlay.style.display = 'none';
|
||||
|
||||
div.appendChild(overlay);
|
||||
|
||||
var self = this;
|
||||
google.maps.event.addDomListener(div, "click", function(event) {
|
||||
event.stopPropagation();
|
||||
google.maps.event.trigger(self, "click", event);
|
||||
});
|
||||
|
||||
var panes = this.getPanes();
|
||||
panes.overlayImage.appendChild(div);
|
||||
};
|
||||
|
||||
AprsMarker.prototype.remove = function() {
|
||||
if (this.div) {
|
||||
this.div.parentNode.removeChild(this.div);
|
||||
this.div = null;
|
||||
}
|
||||
};
|
||||
|
||||
AprsMarker.prototype.getAnchorPoint = function() {
|
||||
return new google.maps.Point(0, -12);
|
||||
};
|
@ -1,230 +0,0 @@
|
||||
// this controls if the new AudioWorklet API should be used if available.
|
||||
// the engine will still fall back to the ScriptProcessorNode if this is set to true but not available in the browser.
|
||||
var useAudioWorklets = true;
|
||||
|
||||
function AudioEngine(maxBufferLength, audioReporter) {
|
||||
this.audioReporter = audioReporter;
|
||||
this.initStats();
|
||||
this.resetStats();
|
||||
var ctx = window.AudioContext || window.webkitAudioContext;
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
this.audioContext = new ctx();
|
||||
this.allowed = this.audioContext.state === 'running';
|
||||
this.started = false;
|
||||
|
||||
this.audioCodec = new sdrjs.ImaAdpcm();
|
||||
this.compression = 'none';
|
||||
|
||||
this.setupResampling();
|
||||
this.resampler = new sdrjs.RationalResamplerFF(this.resamplingFactor, 1);
|
||||
|
||||
this.maxBufferSize = maxBufferLength * this.getSampleRate();
|
||||
}
|
||||
|
||||
AudioEngine.prototype.start = function(callback) {
|
||||
var me = this;
|
||||
if (me.resamplingFactor === 0) return; //if failed to find a valid resampling factor...
|
||||
if (me.started) {
|
||||
if (callback) callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
me.audioContext.resume().then(function(){
|
||||
me.allowed = me.audioContext.state === 'running';
|
||||
if (!me.allowed) {
|
||||
if (callback) callback(false);
|
||||
return;
|
||||
}
|
||||
me.started = true;
|
||||
|
||||
me.gainNode = me.audioContext.createGain();
|
||||
me.gainNode.connect(me.audioContext.destination);
|
||||
|
||||
if (useAudioWorklets && me.audioContext.audioWorklet) {
|
||||
me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){
|
||||
me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', {
|
||||
numberOfInputs: 0,
|
||||
numberOfOutputs: 1,
|
||||
outputChannelCount: [1],
|
||||
processorOptions: {
|
||||
maxBufferSize: me.maxBufferSize
|
||||
}
|
||||
});
|
||||
me.audioNode.connect(me.gainNode);
|
||||
me.audioNode.port.addEventListener('message', function(m){
|
||||
var json = JSON.parse(m.data);
|
||||
if (typeof(json.buffersize) !== 'undefined') {
|
||||
me.audioReporter({
|
||||
buffersize: json.buffersize
|
||||
});
|
||||
}
|
||||
if (typeof(json.samplesProcessed) !== 'undefined') {
|
||||
me.audioSamples.add(json.samplesProcessed);
|
||||
}
|
||||
});
|
||||
me.audioNode.port.start();
|
||||
if (callback) callback(true, 'AudioWorklet');
|
||||
});
|
||||
} else {
|
||||
me.audioBuffers = [];
|
||||
|
||||
if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does
|
||||
AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array
|
||||
{
|
||||
var cd = this.getChannelData(channel);
|
||||
for (var i = 0; i < input.length; i++) cd[i] = input[i];
|
||||
}
|
||||
}
|
||||
|
||||
var bufferSize;
|
||||
if (me.audioContext.sampleRate < 44100 * 2)
|
||||
bufferSize = 4096;
|
||||
else if (me.audioContext.sampleRate >= 44100 * 2 && me.audioContext.sampleRate < 44100 * 4)
|
||||
bufferSize = 4096 * 2;
|
||||
else if (me.audioContext.sampleRate > 44100 * 4)
|
||||
bufferSize = 4096 * 4;
|
||||
|
||||
|
||||
function audio_onprocess(e) {
|
||||
var total = 0;
|
||||
var out = new Float32Array(bufferSize);
|
||||
while (me.audioBuffers.length) {
|
||||
var b = me.audioBuffers.shift();
|
||||
// not enough space to fit all data, so splice and put back in the queue
|
||||
if (total + b.length > bufferSize) {
|
||||
var spaceLeft = bufferSize - total;
|
||||
var tokeep = b.subarray(0, spaceLeft);
|
||||
out.set(tokeep, total);
|
||||
var tobuffer = b.subarray(spaceLeft, b.length);
|
||||
me.audioBuffers.unshift(tobuffer);
|
||||
total += spaceLeft;
|
||||
break;
|
||||
} else {
|
||||
out.set(b, total);
|
||||
total += b.length;
|
||||
}
|
||||
}
|
||||
|
||||
e.outputBuffer.copyToChannel(out, 0);
|
||||
me.audioSamples.add(total);
|
||||
|
||||
}
|
||||
|
||||
//on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor
|
||||
var method = 'createScriptProcessor';
|
||||
if (me.audioContext.createJavaScriptNode) {
|
||||
method = 'createJavaScriptNode';
|
||||
}
|
||||
me.audioNode = me.audioContext[method](bufferSize, 0, 1);
|
||||
me.audioNode.onaudioprocess = audio_onprocess;
|
||||
me.audioNode.connect(me.gainNode);
|
||||
if (callback) callback(true, 'ScriptProcessorNode');
|
||||
}
|
||||
|
||||
setInterval(me.reportStats.bind(me), 1000);
|
||||
});
|
||||
};
|
||||
|
||||
AudioEngine.prototype.isAllowed = function() {
|
||||
return this.allowed;
|
||||
};
|
||||
|
||||
AudioEngine.prototype.reportStats = function() {
|
||||
if (this.audioNode.port) {
|
||||
this.audioNode.port.postMessage(JSON.stringify({cmd:'getStats'}));
|
||||
} else {
|
||||
this.audioReporter({
|
||||
buffersize: this.getBuffersize()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
AudioEngine.prototype.initStats = function() {
|
||||
var me = this;
|
||||
var buildReporter = function(key) {
|
||||
return function(v){
|
||||
var report = {};
|
||||
report[key] = v;
|
||||
me.audioReporter(report);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.audioBytes = new Measurement();
|
||||
this.audioBytes.report(10000, 1000, buildReporter('audioByteRate'));
|
||||
|
||||
this.audioSamples = new Measurement();
|
||||
this.audioSamples.report(10000, 1000, buildReporter('audioRate'));
|
||||
};
|
||||
|
||||
AudioEngine.prototype.resetStats = function() {
|
||||
this.audioBytes.reset();
|
||||
this.audioSamples.reset();
|
||||
};
|
||||
|
||||
AudioEngine.prototype.setupResampling = function() { //both at the server and the client
|
||||
var output_range_max = 12000;
|
||||
var output_range_min = 8000;
|
||||
var targetRate = this.audioContext.sampleRate;
|
||||
var i = 1;
|
||||
while (true) {
|
||||
var audio_server_output_rate = Math.floor(targetRate / i);
|
||||
if (audio_server_output_rate < output_range_min) {
|
||||
this.resamplingFactor = 0;
|
||||
this.outputRate = 0;
|
||||
divlog('Your audio card sampling rate (' + targetRate + ') is not supported.<br />Please change your operating system default settings in order to fix this.', 1);
|
||||
break;
|
||||
} else if (audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) {
|
||||
this.resamplingFactor = i;
|
||||
this.outputRate = audio_server_output_rate;
|
||||
break; //okay, we're done
|
||||
}
|
||||
i++;
|
||||
}
|
||||
};
|
||||
|
||||
AudioEngine.prototype.getOutputRate = function() {
|
||||
return this.outputRate;
|
||||
};
|
||||
|
||||
AudioEngine.prototype.getSampleRate = function() {
|
||||
return this.audioContext.sampleRate;
|
||||
};
|
||||
|
||||
AudioEngine.prototype.pushAudio = function(data) {
|
||||
if (!this.audioNode) return;
|
||||
this.audioBytes.add(data.byteLength);
|
||||
var buffer;
|
||||
if (this.compression === "adpcm") {
|
||||
//resampling & ADPCM
|
||||
buffer = this.audioCodec.decode(new Uint8Array(data));
|
||||
} else {
|
||||
buffer = new Int16Array(data);
|
||||
}
|
||||
buffer = this.resampler.process(sdrjs.ConvertI16_F(buffer));
|
||||
if (this.audioNode.port) {
|
||||
// AudioWorklets supported
|
||||
this.audioNode.port.postMessage(buffer);
|
||||
} else {
|
||||
// silently drop excess samples
|
||||
if (this.getBuffersize() + buffer.length <= this.maxBufferSize) {
|
||||
this.audioBuffers.push(buffer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AudioEngine.prototype.setCompression = function(compression) {
|
||||
this.compression = compression;
|
||||
};
|
||||
|
||||
AudioEngine.prototype.setVolume = function(volume) {
|
||||
this.gainNode.gain.value = volume;
|
||||
};
|
||||
|
||||
AudioEngine.prototype.getBuffersize = function() {
|
||||
// only available when using ScriptProcessorNode
|
||||
if (!this.audioBuffers) return 0;
|
||||
return this.audioBuffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0);
|
||||
};
|
@ -1,58 +0,0 @@
|
||||
class OwrxAudioProcessor extends AudioWorkletProcessor {
|
||||
constructor(options){
|
||||
super(options);
|
||||
// initialize ringbuffer, make sure it aligns with the expected buffer size of 128
|
||||
this.bufferSize = Math.round(options.processorOptions.maxBufferSize / 128) * 128;
|
||||
this.audioBuffer = new Float32Array(this.bufferSize);
|
||||
this.inPos = 0;
|
||||
this.outPos = 0;
|
||||
this.samplesProcessed = 0;
|
||||
this.port.addEventListener('message', (m) => {
|
||||
if (typeof(m.data) === 'string') {
|
||||
const json = JSON.parse(m.data);
|
||||
if (json.cmd && json.cmd === 'getStats') {
|
||||
this.reportStats();
|
||||
}
|
||||
} else {
|
||||
// the ringbuffer size is aligned to the output buffer size, which means that the input buffers might
|
||||
// need to wrap around the end of the ringbuffer, back to the start.
|
||||
// it is better to have this processing here instead of in the time-critical process function.
|
||||
if (this.inPos + m.data.length <= this.bufferSize) {
|
||||
// we have enough space, so just copy data over.
|
||||
this.audioBuffer.set(m.data, this.inPos);
|
||||
} else {
|
||||
// we don't have enough space, so we need to split the data.
|
||||
const remaining = this.bufferSize - this.inPos;
|
||||
this.audioBuffer.set(m.data.subarray(0, remaining), this.inPos);
|
||||
this.audioBuffer.set(m.data.subarray(remaining));
|
||||
}
|
||||
this.inPos = (this.inPos + m.data.length) % this.bufferSize;
|
||||
}
|
||||
});
|
||||
this.port.addEventListener('messageerror', console.error);
|
||||
this.port.start();
|
||||
}
|
||||
process(inputs, outputs) {
|
||||
if (this.remaining() < 128) return true;
|
||||
outputs[0].forEach((output) => {
|
||||
output.set(this.audioBuffer.subarray(this.outPos, this.outPos + 128));
|
||||
});
|
||||
this.outPos = (this.outPos + 128) % this.bufferSize;
|
||||
this.samplesProcessed += 128;
|
||||
return true;
|
||||
}
|
||||
remaining() {
|
||||
const mod = (this.inPos - this.outPos) % this.bufferSize;
|
||||
if (mod >= 0) return mod;
|
||||
return mod + this.bufferSize;
|
||||
}
|
||||
reportStats() {
|
||||
this.port.postMessage(JSON.stringify({
|
||||
buffersize: this.remaining(),
|
||||
samplesProcessed: this.samplesProcessed
|
||||
}));
|
||||
this.samplesProcessed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('openwebrx-audio-processor', OwrxAudioProcessor);
|
@ -1,177 +0,0 @@
|
||||
function BookmarkBar() {
|
||||
var me = this;
|
||||
me.localBookmarks = new BookmarkLocalStorage();
|
||||
me.$container = $("#openwebrx-bookmarks-container");
|
||||
me.bookmarks = {};
|
||||
|
||||
me.$container.on('click', '.bookmark', function(e){
|
||||
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;
|
||||
demodulator_set_offset_frequency(0, b.frequency - center_freq);
|
||||
if (b.modulation) {
|
||||
demodulator_analog_replace(b.modulation);
|
||||
} else if (b.digital_modulation) {
|
||||
demodulator_digital_replace(b.digital_modulation);
|
||||
}
|
||||
$bookmark.addClass('selected');
|
||||
});
|
||||
|
||||
me.$container.on('click', '.action[data-action=edit]', function(e){
|
||||
e.stopPropagation();
|
||||
var $bookmark = $(e.target).closest('.bookmark');
|
||||
me.showEditDialog($bookmark.data());
|
||||
});
|
||||
|
||||
me.$container.on('click', '.action[data-action=delete]', function(e){
|
||||
e.stopPropagation();
|
||||
var $bookmark = $(e.target).closest('.bookmark');
|
||||
me.localBookmarks.deleteBookmark($bookmark.data());
|
||||
me.loadLocalBookmarks();
|
||||
});
|
||||
|
||||
var $bookmarkButton = $('#openwebrx-panel-receiver').find('.openwebrx-bookmark-button');
|
||||
if (typeof(Storage) !== 'undefined') {
|
||||
$bookmarkButton.show();
|
||||
} else {
|
||||
$bookmarkButton.hide();
|
||||
}
|
||||
$bookmarkButton.click(function(){
|
||||
me.showEditDialog();
|
||||
});
|
||||
|
||||
me.$dialog = $("#openwebrx-dialog-bookmark");
|
||||
me.$dialog.find('.openwebrx-button[data-action=cancel]').click(function(){
|
||||
me.$dialog.hide();
|
||||
});
|
||||
me.$dialog.find('.openwebrx-button[data-action=submit]').click(function(){
|
||||
me.storeBookmark();
|
||||
});
|
||||
me.$dialog.find('form').on('submit', function(e){
|
||||
e.preventDefault();
|
||||
me.storeBookmark();
|
||||
});
|
||||
}
|
||||
|
||||
BookmarkBar.prototype.position = function(){
|
||||
var range = get_visible_freq_range();
|
||||
$('#openwebrx-bookmarks-container').find('.bookmark').each(function(){
|
||||
$(this).css('left', scale_px_from_freq($(this).data('frequency'), range));
|
||||
});
|
||||
};
|
||||
|
||||
BookmarkBar.prototype.loadLocalBookmarks = function(){
|
||||
var bwh = bandwidth / 2;
|
||||
var start = center_freq - bwh;
|
||||
var end = center_freq + bwh;
|
||||
var bookmarks = this.localBookmarks.getBookmarks().filter(function(b){
|
||||
return b.frequency >= start && b.frequency <= end;
|
||||
});
|
||||
this.replace_bookmarks(bookmarks, 'local', true);
|
||||
};
|
||||
|
||||
BookmarkBar.prototype.replace_bookmarks = function(bookmarks, source, editable) {
|
||||
editable = !!editable;
|
||||
bookmarks = bookmarks.map(function(b){
|
||||
b.source = source;
|
||||
b.editable = editable;
|
||||
return b;
|
||||
});
|
||||
this.bookmarks[source] = bookmarks;
|
||||
this.render();
|
||||
};
|
||||
|
||||
BookmarkBar.prototype.render = function(){
|
||||
var bookmarks = Object.values(this.bookmarks).reduce(function(l, v){ return l.concat(v); });
|
||||
bookmarks = bookmarks.sort(function(a, b){ return a.frequency - b.frequency; });
|
||||
var elements = bookmarks.map(function(b){
|
||||
var $bookmark = $(
|
||||
'<div class="bookmark" data-source="' + b.source + '"' + (b.editable?' editable="editable"':'') + '>' +
|
||||
'<div class="bookmark-actions">' +
|
||||
'<div class="openwebrx-button action" data-action="edit"><img src="static/gfx/openwebrx-edit.png"></div>' +
|
||||
'<div class="openwebrx-button action" data-action="delete"><img src="static/gfx/openwebrx-trashcan.png"></div>' +
|
||||
'</div>' +
|
||||
'<div class="bookmark-content">' + b.name + '</div>' +
|
||||
'</div>'
|
||||
);
|
||||
$bookmark.data(b);
|
||||
return $bookmark;
|
||||
});
|
||||
this.$container.find('.bookmark').remove();
|
||||
this.$container.append(elements);
|
||||
this.position();
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
['name', 'frequency', 'modulation'].forEach(function(key){
|
||||
$form.find('#' + key).val(bookmark[key]);
|
||||
});
|
||||
this.$dialog.data('id', bookmark.id);
|
||||
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;
|
||||
}
|
||||
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; }));
|
||||
} else {
|
||||
bookmark.id = 1;
|
||||
}
|
||||
}
|
||||
|
||||
bookmarks = bookmarks.filter(function(b) { return b.id !== bookmark.id; });
|
||||
bookmarks.push(bookmark);
|
||||
|
||||
me.localBookmarks.setBookmarks(bookmarks);
|
||||
me.loadLocalBookmarks();
|
||||
me.$dialog.hide();
|
||||
};
|
||||
|
||||
BookmarkLocalStorage = function(){
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.getBookmarks = function(){
|
||||
return JSON.parse(window.localStorage.getItem("bookmarks")) || [];
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){
|
||||
window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks));
|
||||
};
|
||||
|
||||
BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
|
||||
if (data.id) data = data.id;
|
||||
var bookmarks = this.getBookmarks();
|
||||
bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
|
||||
this.setBookmarks(bookmarks);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
@ -1,62 +0,0 @@
|
||||
function Measurement() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
Measurement.prototype.add = function(v) {
|
||||
this.value += v;
|
||||
};
|
||||
|
||||
Measurement.prototype.getValue = function() {
|
||||
return this.value;
|
||||
};
|
||||
|
||||
Measurement.prototype.getElapsed = function() {
|
||||
return new Date() - this.start;
|
||||
};
|
||||
|
||||
Measurement.prototype.getRate = function() {
|
||||
return this.getValue() / this.getElapsed();
|
||||
};
|
||||
|
||||
Measurement.prototype.reset = function() {
|
||||
this.value = 0;
|
||||
this.start = new Date();
|
||||
};
|
||||
|
||||
Measurement.prototype.report = function(range, interval, callback) {
|
||||
return new Reporter(this, range, interval, callback);
|
||||
};
|
||||
|
||||
function Reporter(measurement, range, interval, callback) {
|
||||
this.measurement = measurement;
|
||||
this.range = range;
|
||||
this.samples = [];
|
||||
this.callback = callback;
|
||||
this.interval = setInterval(this.report.bind(this), interval);
|
||||
}
|
||||
|
||||
Reporter.prototype.sample = function(){
|
||||
this.samples.push({
|
||||
timestamp: new Date(),
|
||||
value: this.measurement.getValue()
|
||||
});
|
||||
};
|
||||
|
||||
Reporter.prototype.report = function(){
|
||||
this.sample();
|
||||
var now = new Date();
|
||||
var minDate = now.getTime() - this.range;
|
||||
this.samples = this.samples.filter(function(s) {
|
||||
return s.timestamp.getTime() > minDate;
|
||||
});
|
||||
this.samples.sort(function(a, b) {
|
||||
return a.timestamp - b.timestamp;
|
||||
});
|
||||
var oldest = this.samples[0];
|
||||
var newest = this.samples[this.samples.length -1];
|
||||
var elapsed = newest.timestamp - oldest.timestamp;
|
||||
if (elapsed <= 0) return;
|
||||
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);
|
||||
};
|
@ -1,113 +0,0 @@
|
||||
ProgressBar = function(el) {
|
||||
this.$el = $(el);
|
||||
this.$innerText = this.$el.find('.openwebrx-progressbar-text');
|
||||
this.$innerBar = this.$el.find('.openwebrx-progressbar-bar');
|
||||
this.$innerBar.css('width', '0%');
|
||||
};
|
||||
|
||||
ProgressBar.prototype.set = function(val, text, over) {
|
||||
this.setValue(val);
|
||||
this.setText(text);
|
||||
this.setOver(over);
|
||||
};
|
||||
|
||||
ProgressBar.prototype.setValue = function(val) {
|
||||
if (val < 0) val = 0;
|
||||
if (val > 1) val = 1;
|
||||
this.$innerBar.stop().animate({width: val * 100 + '%'}, 700);
|
||||
};
|
||||
|
||||
ProgressBar.prototype.setText = function(text) {
|
||||
this.$innerText.html(text);
|
||||
};
|
||||
|
||||
ProgressBar.prototype.setOver = function(over) {
|
||||
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
|
||||
};
|
||||
|
||||
AudioBufferProgressBar = function(el, sampleRate) {
|
||||
ProgressBar.call(this, el);
|
||||
this.sampleRate = sampleRate;
|
||||
};
|
||||
|
||||
AudioBufferProgressBar.prototype = new ProgressBar();
|
||||
|
||||
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
|
||||
var audio_buffer_value = buffersize / this.sampleRate;
|
||||
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
|
||||
var underrun = audio_buffer_value === 0;
|
||||
var text = "buffer";
|
||||
if (overrun) {
|
||||
text = "overrun";
|
||||
}
|
||||
if (underrun) {
|
||||
text = "underrun";
|
||||
}
|
||||
this.set(audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun);
|
||||
};
|
||||
|
||||
|
||||
NetworkSpeedProgressBar = function(el) {
|
||||
ProgressBar.call(this, el);
|
||||
};
|
||||
|
||||
NetworkSpeedProgressBar.prototype = new ProgressBar();
|
||||
|
||||
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
|
||||
var speedInKilobits = speed * 8 / 1000;
|
||||
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
|
||||
};
|
||||
|
||||
AudioSpeedProgressBar = function(el) {
|
||||
ProgressBar.call(this, el);
|
||||
};
|
||||
|
||||
AudioSpeedProgressBar.prototype = new ProgressBar();
|
||||
|
||||
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.setAudioRate = function(audioRate) {
|
||||
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
|
||||
};
|
||||
|
||||
ClientsProgressBar = function(el) {
|
||||
ProgressBar.call(this, el);
|
||||
this.clients = 0;
|
||||
this.maxClients = 0;
|
||||
};
|
||||
|
||||
ClientsProgressBar.prototype = new ProgressBar();
|
||||
|
||||
ClientsProgressBar.prototype.setClients = function(clients) {
|
||||
this.clients = clients;
|
||||
this.render();
|
||||
};
|
||||
|
||||
ClientsProgressBar.prototype.setMaxClients = function(maxClients) {
|
||||
this.maxClients = maxClients;
|
||||
this.render();
|
||||
};
|
||||
|
||||
ClientsProgressBar.prototype.render = function() {
|
||||
this.set(this.clients / this.maxClients, "Clients [" + this.clients + "]", this.clients > this.maxClients * 0.85);
|
||||
};
|
||||
|
||||
CpuProgressBar = function(el) {
|
||||
ProgressBar.call(this, el);
|
||||
};
|
||||
|
||||
CpuProgressBar.prototype = new ProgressBar();
|
||||
|
||||
CpuProgressBar.prototype.setUsage = function(usage) {
|
||||
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
|
||||
};
|
58
htdocs/lib/chroma.min.js
vendored
@ -1,143 +0,0 @@
|
||||
/* Nite v1.7
|
||||
* A tiny library to create a night overlay over the map
|
||||
* Author: Rossen Georgiev @ https://github.com/rossengeorgiev
|
||||
* Requires: GMaps API 3
|
||||
*/
|
||||
|
||||
|
||||
var nite = {
|
||||
map: null,
|
||||
date: null,
|
||||
sun_position: null,
|
||||
earth_radius_meters: 6371008,
|
||||
marker_twilight_civil: null,
|
||||
marker_twilight_nautical: null,
|
||||
marker_twilight_astronomical: null,
|
||||
marker_night: null,
|
||||
|
||||
init: function(map) {
|
||||
if(typeof google === 'undefined'
|
||||
|| typeof google.maps === 'undefined') throw "Nite Overlay: no google.maps detected";
|
||||
|
||||
this.map = map;
|
||||
this.sun_position = this.calculatePositionOfSun();
|
||||
|
||||
this.marker_twilight_civil = new google.maps.Circle({
|
||||
map: this.map,
|
||||
center: this.getShadowPosition(),
|
||||
radius: this.getShadowRadiusFromAngle(0.566666),
|
||||
fillColor: "#000",
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
});
|
||||
this.marker_twilight_nautical = new google.maps.Circle({
|
||||
map: this.map,
|
||||
center: this.getShadowPosition(),
|
||||
radius: this.getShadowRadiusFromAngle(6),
|
||||
fillColor: "#000",
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
});
|
||||
this.marker_twilight_astronomical = new google.maps.Circle({
|
||||
map: this.map,
|
||||
center: this.getShadowPosition(),
|
||||
radius: this.getShadowRadiusFromAngle(12),
|
||||
fillColor: "#000",
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
});
|
||||
this.marker_night = new google.maps.Circle({
|
||||
map: this.map,
|
||||
center: this.getShadowPosition(),
|
||||
radius: this.getShadowRadiusFromAngle(18),
|
||||
fillColor: "#000",
|
||||
fillOpacity: 0.1,
|
||||
strokeOpacity: 0,
|
||||
clickable: false,
|
||||
editable: false
|
||||
});
|
||||
},
|
||||
getShadowRadiusFromAngle: function(angle) {
|
||||
var shadow_radius = this.earth_radius_meters * Math.PI * 0.5;
|
||||
var twilight_dist = ((this.earth_radius_meters * 2 * Math.PI) / 360) * angle;
|
||||
return shadow_radius - twilight_dist;
|
||||
},
|
||||
getSunPosition: function() {
|
||||
return this.sun_position;
|
||||
},
|
||||
getShadowPosition: function() {
|
||||
return (this.sun_position) ? new google.maps.LatLng(-this.sun_position.lat(), this.sun_position.lng() + 180) : null;
|
||||
},
|
||||
refresh: function() {
|
||||
if(!this.isVisible()) return;
|
||||
this.sun_position = this.calculatePositionOfSun(this.date);
|
||||
var shadow_position = this.getShadowPosition();
|
||||
this.marker_twilight_civil.setCenter(shadow_position);
|
||||
this.marker_twilight_nautical.setCenter(shadow_position);
|
||||
this.marker_twilight_astronomical.setCenter(shadow_position);
|
||||
this.marker_night.setCenter(shadow_position);
|
||||
},
|
||||
jday: function(date) {
|
||||
return (date.getTime() / 86400000.0) + 2440587.5;
|
||||
},
|
||||
calculatePositionOfSun: function(date) {
|
||||
date = (date instanceof Date) ? date : new Date();
|
||||
|
||||
var rad = 0.017453292519943295;
|
||||
|
||||
// based on NOAA solar calculations
|
||||
var ms_past_midnight = ((date.getUTCHours() * 60 + date.getUTCMinutes()) * 60 + date.getUTCSeconds()) * 1000 + date.getUTCMilliseconds();
|
||||
var jc = (this.jday(date) - 2451545)/36525;
|
||||
var mean_long_sun = (280.46646+jc*(36000.76983+jc*0.0003032)) % 360;
|
||||
var mean_anom_sun = 357.52911+jc*(35999.05029-0.0001537*jc);
|
||||
var sun_eq = Math.sin(rad*mean_anom_sun)*(1.914602-jc*(0.004817+0.000014*jc))+Math.sin(rad*2*mean_anom_sun)*(0.019993-0.000101*jc)+Math.sin(rad*3*mean_anom_sun)*0.000289;
|
||||
var sun_true_long = mean_long_sun + sun_eq;
|
||||
var sun_app_long = sun_true_long - 0.00569 - 0.00478*Math.sin(rad*125.04-1934.136*jc);
|
||||
var mean_obliq_ecliptic = 23+(26+((21.448-jc*(46.815+jc*(0.00059-jc*0.001813))))/60)/60;
|
||||
var obliq_corr = mean_obliq_ecliptic + 0.00256*Math.cos(rad*125.04-1934.136*jc);
|
||||
|
||||
var lat = Math.asin(Math.sin(rad*obliq_corr)*Math.sin(rad*sun_app_long)) / rad;
|
||||
|
||||
var eccent = 0.016708634-jc*(0.000042037+0.0000001267*jc);
|
||||
var y = Math.tan(rad*(obliq_corr/2))*Math.tan(rad*(obliq_corr/2));
|
||||
var rq_of_time = 4*((y*Math.sin(2*rad*mean_long_sun)-2*eccent*Math.sin(rad*mean_anom_sun)+4*eccent*y*Math.sin(rad*mean_anom_sun)*Math.cos(2*rad*mean_long_sun)-0.5*y*y*Math.sin(4*rad*mean_long_sun)-1.25*eccent*eccent*Math.sin(2*rad*mean_anom_sun))/rad);
|
||||
var true_solar_time_in_deg = ((ms_past_midnight+rq_of_time*60000) % 86400000) / 240000;
|
||||
|
||||
var lng = -((true_solar_time_in_deg < 0) ? true_solar_time_in_deg + 180 : true_solar_time_in_deg - 180);
|
||||
|
||||
return new google.maps.LatLng(lat, lng);
|
||||
},
|
||||
setDate: function(date) {
|
||||
this.date = date;
|
||||
this.refresh();
|
||||
},
|
||||
setMap: function(map) {
|
||||
this.map = map;
|
||||
this.marker_twilight_civil.setMap(this.map);
|
||||
this.marker_twilight_nautical.setMap(this.map);
|
||||
this.marker_twilight_astronomical.setMap(this.map);
|
||||
this.marker_night.setMap(this.map);
|
||||
},
|
||||
show: function() {
|
||||
this.marker_twilight_civil.setVisible(true);
|
||||
this.marker_twilight_nautical.setVisible(true);
|
||||
this.marker_twilight_astronomical.setVisible(true);
|
||||
this.marker_night.setVisible(true);
|
||||
this.refresh();
|
||||
},
|
||||
hide: function() {
|
||||
this.marker_twilight_civil.setVisible(false);
|
||||
this.marker_twilight_nautical.setVisible(false);
|
||||
this.marker_twilight_astronomical.setVisible(false);
|
||||
this.marker_night.setVisible(false);
|
||||
},
|
||||
isVisible: function() {
|
||||
return this.marker_night.getVisible();
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>OpenWebRX Map</title>
|
||||
<script src="static/lib/jquery-3.2.1.min.js"></script>
|
||||
<script src="static/lib/chroma.min.js"></script>
|
||||
<script src="static/map.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="static/css/map.css" />
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
${header}
|
||||
<div class="openwebrx-map"></div>
|
||||
<div class="openwebrx-map-legend">
|
||||
<h3>Colors</h3>
|
||||
<select id="openwebrx-map-colormode">
|
||||
<option value="byband" selected="selected">By Band</option>
|
||||
<option value="bymode">By Mode</option>
|
||||
</select>
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
356
htdocs/map.js
@ -1,356 +0,0 @@
|
||||
(function(){
|
||||
var protocol = 'ws';
|
||||
if (window.location.toString().startsWith('https://')) {
|
||||
protocol = 'wss';
|
||||
}
|
||||
|
||||
var query = window.location.search.replace(/^\?/, '').split('&').map(function(v){
|
||||
var s = v.split('=');
|
||||
var r = {};
|
||||
r[s[0]] = s.slice(1).join('=');
|
||||
return r;
|
||||
}).reduce(function(a, b){
|
||||
return a.assign(b);
|
||||
});
|
||||
|
||||
var expectedCallsign;
|
||||
if (query.callsign) expectedCallsign = query.callsign;
|
||||
var expectedLocator;
|
||||
if (query.locator) expectedLocator = query.locator;
|
||||
|
||||
var ws_url = protocol + "://" + (window.location.origin.split("://")[1]) + "/ws/";
|
||||
if (!("WebSocket" in window)) return;
|
||||
|
||||
var map;
|
||||
var markers = {};
|
||||
var rectangles = {};
|
||||
var updateQueue = [];
|
||||
|
||||
// reasonable default; will be overriden by server
|
||||
var retention_time = 2 * 60 * 60 * 1000;
|
||||
var strokeOpacity = 0.8;
|
||||
var fillOpacity = 0.35;
|
||||
|
||||
var colorKeys = {};
|
||||
var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl');
|
||||
var getColor = function(id){
|
||||
if (!id) return "#000000";
|
||||
if (!colorKeys[id]) {
|
||||
var keys = Object.keys(colorKeys);
|
||||
keys.push(id);
|
||||
keys.sort();
|
||||
var colors = colorScale.colors(keys.length);
|
||||
colorKeys = {};
|
||||
keys.forEach(function(key, index) {
|
||||
colorKeys[key] = colors[index];
|
||||
});
|
||||
reColor();
|
||||
updateLegend();
|
||||
}
|
||||
return colorKeys[id];
|
||||
}
|
||||
|
||||
// when the color palette changes, update all grid squares with new color
|
||||
var reColor = function() {
|
||||
$.each(rectangles, function(_, r) {
|
||||
var color = getColor(colorAccessor(r));
|
||||
r.setOptions({
|
||||
strokeColor: color,
|
||||
fillColor: color
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var colorMode = 'byband';
|
||||
var colorAccessor = function(r) {
|
||||
switch (colorMode) {
|
||||
case 'byband':
|
||||
return r.band;
|
||||
case 'bymode':
|
||||
return r.mode;
|
||||
}
|
||||
};
|
||||
|
||||
$(function(){
|
||||
$('#openwebrx-map-colormode').on('change', function(){
|
||||
colorMode = $(this).val();
|
||||
colorKeys = {};
|
||||
reColor();
|
||||
updateLegend();
|
||||
});
|
||||
});
|
||||
|
||||
var updateLegend = function() {
|
||||
var lis = $.map(colorKeys, function(value, key) {
|
||||
return '<li class="square"><span class="illustration" style="background-color:' + chroma(value).alpha(fillOpacity) + ';border-color:' + chroma(value).alpha(strokeOpacity) + ';"></span>' + key + '</li>';
|
||||
});
|
||||
$(".openwebrx-map-legend .content").html('<ul>' + lis.join('') + '</ul>');
|
||||
}
|
||||
|
||||
var processUpdates = function(updates) {
|
||||
if (typeof(AprsMarker) == 'undefined') {
|
||||
updateQueue = updateQueue.concat(updates);
|
||||
return;
|
||||
}
|
||||
updates.forEach(function(update){
|
||||
|
||||
switch (update.location.type) {
|
||||
case 'latlon':
|
||||
var pos = new google.maps.LatLng(update.location.lat, update.location.lon);
|
||||
var marker;
|
||||
var markerClass = google.maps.Marker;
|
||||
var aprsOptions = {}
|
||||
if (update.location.symbol) {
|
||||
markerClass = AprsMarker;
|
||||
aprsOptions.symbol = update.location.symbol;
|
||||
aprsOptions.course = update.location.course;
|
||||
aprsOptions.speed = update.location.speed;
|
||||
}
|
||||
if (markers[update.callsign]) {
|
||||
marker = markers[update.callsign];
|
||||
} else {
|
||||
marker = new markerClass();
|
||||
marker.addListener('click', function(){
|
||||
showMarkerInfoWindow(update.callsign, pos);
|
||||
});
|
||||
markers[update.callsign] = marker;
|
||||
}
|
||||
marker.setOptions($.extend({
|
||||
position: pos,
|
||||
map: map,
|
||||
title: update.callsign
|
||||
}, aprsOptions, getMarkerOpacityOptions(update.lastseen) ));
|
||||
marker.lastseen = update.lastseen;
|
||||
marker.mode = update.mode;
|
||||
marker.band = update.band;
|
||||
marker.comment = update.location.comment;
|
||||
|
||||
// TODO the trim should happen on the server side
|
||||
if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
|
||||
map.panTo(pos);
|
||||
showMarkerInfoWindow(update.callsign, pos);
|
||||
delete(expectedCallsign);
|
||||
}
|
||||
break;
|
||||
case 'locator':
|
||||
var loc = update.location.locator;
|
||||
var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]);
|
||||
var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2;
|
||||
var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1});
|
||||
var rectangle;
|
||||
// the accessor is designed to work on the rectangle... but it should work on the update object, too
|
||||
var color = getColor(colorAccessor(update));
|
||||
if (rectangles[update.callsign]) {
|
||||
rectangle = rectangles[update.callsign];
|
||||
} else {
|
||||
rectangle = new google.maps.Rectangle();
|
||||
rectangle.addListener('click', function(){
|
||||
showLocatorInfoWindow(this.locator, this.center);
|
||||
});
|
||||
rectangles[update.callsign] = rectangle;
|
||||
}
|
||||
rectangle.setOptions($.extend({
|
||||
strokeColor: color,
|
||||
strokeWeight: 2,
|
||||
fillColor: color,
|
||||
map: map,
|
||||
bounds:{
|
||||
north: lat,
|
||||
south: lat + 1,
|
||||
west: lon,
|
||||
east: lon + 2
|
||||
}
|
||||
}, getRectangleOpacityOptions(update.lastseen) ));
|
||||
rectangle.lastseen = update.lastseen;
|
||||
rectangle.locator = update.location.locator;
|
||||
rectangle.mode = update.mode;
|
||||
rectangle.band = update.band;
|
||||
rectangle.center = center;
|
||||
|
||||
if (expectedLocator && expectedLocator == update.location.locator) {
|
||||
map.panTo(center);
|
||||
showLocatorInfoWindow(expectedLocator, center);
|
||||
delete(expectedLocator);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var clearMap = function(){
|
||||
var reset = function(callsign, item) { item.setMap(); };
|
||||
$.each(markers, reset);
|
||||
$.each(rectangles, reset);
|
||||
markers = {};
|
||||
rectangles = {};
|
||||
};
|
||||
|
||||
var reconnect_timeout = false;
|
||||
|
||||
var connect = function(){
|
||||
var ws = new WebSocket(ws_url);
|
||||
ws.onopen = function(){
|
||||
ws.send("SERVER DE CLIENT client=map.js type=map");
|
||||
reconnect_timeout = false
|
||||
};
|
||||
|
||||
ws.onmessage = function(e){
|
||||
if (typeof e.data != 'string') {
|
||||
console.error("unsupported binary data on websocket; ignoring");
|
||||
return
|
||||
}
|
||||
if (e.data.substr(0, 16) == "CLIENT DE SERVER") {
|
||||
console.log("Server acknowledged WebSocket connection.");
|
||||
return
|
||||
}
|
||||
try {
|
||||
var json = JSON.parse(e.data);
|
||||
switch (json.type) {
|
||||
case "config":
|
||||
var config = json.value;
|
||||
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
|
||||
map = new google.maps.Map($('.openwebrx-map')[0], {
|
||||
center: {
|
||||
lat: config.receiver_gps[0],
|
||||
lng: config.receiver_gps[1]
|
||||
},
|
||||
zoom: 5
|
||||
});
|
||||
$.getScript("/static/lib/nite-overlay.js").done(function(){
|
||||
nite.init(map);
|
||||
setInterval(function() { nite.refresh() }, 10000); // every 10s
|
||||
});
|
||||
$.getScript('/static/lib/AprsMarker.js').done(function(){
|
||||
processUpdates(updateQueue);
|
||||
updateQueue = [];
|
||||
});
|
||||
map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($(".openwebrx-map-legend")[0]);
|
||||
});
|
||||
retention_time = config.map_position_retention_time * 1000;
|
||||
break;
|
||||
case "update":
|
||||
processUpdates(json.value);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// don't lose exception
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
ws.onclose = function(){
|
||||
clearMap();
|
||||
if (reconnect_timeout) {
|
||||
// max value: roundabout 8 and a half minutes
|
||||
reconnect_timeout = Math.min(reconnect_timeout * 2, 512000);
|
||||
} else {
|
||||
// initial value: 1s
|
||||
reconnect_timeout = 1000;
|
||||
}
|
||||
setTimeout(connect, reconnect_timeout);
|
||||
};
|
||||
|
||||
window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript
|
||||
ws.onclose = function () {};
|
||||
ws.close();
|
||||
};
|
||||
|
||||
/*
|
||||
ws.onerror = function(){
|
||||
console.info("websocket error");
|
||||
};
|
||||
*/
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
var infowindow;
|
||||
var showLocatorInfoWindow = function(locator, pos) {
|
||||
if (!infowindow) infowindow = new google.maps.InfoWindow();
|
||||
var inLocator = $.map(rectangles, function(r, callsign) {
|
||||
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
|
||||
}).filter(function(d) {
|
||||
return d.locator == locator;
|
||||
}).sort(function(a, b){
|
||||
return b.lastseen - a.lastseen;
|
||||
});
|
||||
infowindow.setContent(
|
||||
'<h3>Locator: ' + locator + '</h3>' +
|
||||
'<div>Active Callsigns:</div>' +
|
||||
'<ul>' +
|
||||
inLocator.map(function(i){
|
||||
var timestring = moment(i.lastseen).fromNow();
|
||||
var message = i.callsign + ' (' + timestring + ' using ' + i.mode;
|
||||
if (i.band) message += ' on ' + i.band;
|
||||
message += ')';
|
||||
return '<li>' + message + '</li>'
|
||||
}).join("") +
|
||||
'</ul>'
|
||||
);
|
||||
infowindow.setPosition(pos);
|
||||
infowindow.open(map);
|
||||
};
|
||||
|
||||
var showMarkerInfoWindow = function(callsign, pos) {
|
||||
if (!infowindow) infowindow = new google.maps.InfoWindow();
|
||||
var marker = markers[callsign];
|
||||
var timestring = moment(marker.lastseen).fromNow();
|
||||
var commentString = "";
|
||||
if (marker.comment) {
|
||||
commentString = '<div>' + marker.comment + '</div>';
|
||||
}
|
||||
infowindow.setContent(
|
||||
'<h3>' + callsign + '</h3>' +
|
||||
'<div>' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '</div>' +
|
||||
commentString
|
||||
);
|
||||
infowindow.open(map, marker);
|
||||
}
|
||||
|
||||
var getScale = function(lastseen) {
|
||||
var age = new Date().getTime() - lastseen;
|
||||
var scale = 1;
|
||||
if (age >= retention_time / 2) {
|
||||
scale = (retention_time - age) / (retention_time / 2);
|
||||
}
|
||||
return Math.max(0, Math.min(1, scale));
|
||||
};
|
||||
|
||||
var getRectangleOpacityOptions = function(lastseen) {
|
||||
var scale = getScale(lastseen);
|
||||
return {
|
||||
strokeOpacity: strokeOpacity * scale,
|
||||
fillOpacity: fillOpacity * scale
|
||||
};
|
||||
};
|
||||
|
||||
var getMarkerOpacityOptions = function(lastseen) {
|
||||
var scale = getScale(lastseen);
|
||||
return {
|
||||
opacity: scale
|
||||
};
|
||||
};
|
||||
|
||||
// fade out / remove positions after time
|
||||
setInterval(function(){
|
||||
var now = new Date().getTime();
|
||||
$.each(rectangles, function(callsign, m) {
|
||||
var age = now - m.lastseen;
|
||||
if (age > retention_time) {
|
||||
delete rectangles[callsign];
|
||||
m.setMap();
|
||||
return;
|
||||
}
|
||||
m.setOptions(getRectangleOpacityOptions(m.lastseen));
|
||||
});
|
||||
$.each(markers, function(callsign, m) {
|
||||
var age = now - m.lastseen;
|
||||
if (age > retention_time) {
|
||||
delete markers[callsign];
|
||||
m.setMap();
|
||||
return;
|
||||
}
|
||||
m.setOptions(getMarkerOpacityOptions(m.lastseen));
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
})();
|
@ -3,7 +3,6 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -19,10 +18,13 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
@import url("openwebrx-header.css");
|
||||
@import url("openwebrx-globals.css");
|
||||
|
||||
html, body {
|
||||
html, body
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -40,8 +42,6 @@ input[type=range]
|
||||
{
|
||||
-webkit-appearance: none;
|
||||
margin: 0 0;
|
||||
background: transparent;
|
||||
--track-background: #B6B6B6;
|
||||
}
|
||||
input[type=range]:focus
|
||||
{
|
||||
@ -57,7 +57,6 @@ input[type=range]::-webkit-slider-runnable-track
|
||||
background: #B6B6B6;
|
||||
/*border-radius: 11px;*/
|
||||
border: 1px solid #8A8A8A;
|
||||
background: var(--track-background);
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb
|
||||
@ -76,7 +75,6 @@ input[type=range]::-webkit-slider-thumb
|
||||
input[type=range]:focus::-webkit-slider-runnable-track
|
||||
{
|
||||
background: #B6B6B6;
|
||||
background: var(--track-background);
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track
|
||||
@ -86,7 +84,6 @@ input[type=range]::-moz-range-track
|
||||
animate: 0.2s;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
background: #B6B6B6;
|
||||
background: var(--track-background);
|
||||
border-radius: 11px;
|
||||
border: 1px solid #8A8A8A;
|
||||
}
|
||||
@ -150,130 +147,250 @@ input[type=range]:focus::-ms-fill-upper
|
||||
background: #B6B6B6;
|
||||
}
|
||||
|
||||
#webrx-top-container
|
||||
{
|
||||
position: relative;
|
||||
z-index:1000;
|
||||
}
|
||||
|
||||
.webrx-top-bar-parts
|
||||
{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width:100%;
|
||||
height:67px;
|
||||
}
|
||||
|
||||
#webrx-top-bar-background
|
||||
{
|
||||
background-color: #808080;
|
||||
opacity: 0.15;
|
||||
filter:alpha(opacity=15);
|
||||
}
|
||||
|
||||
#webrx-top-bar
|
||||
{
|
||||
margin:0;
|
||||
padding:0;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
#webrx-top-logo
|
||||
{
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
#webrx-ha5kfu-top-logo
|
||||
{
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
#webrx-top-photo
|
||||
{
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#webrx-rx-avatar-background
|
||||
{
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 285px;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
#webrx-rx-avatar
|
||||
{
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 289px;
|
||||
top: 10px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
#webrx-top-photo-clip
|
||||
{
|
||||
max-height: 350px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/*#webrx-bottom-bar
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 117px;
|
||||
background-image:url(gfx/webrx-bottom-bar.png);
|
||||
}*/
|
||||
|
||||
#webrx-page-container
|
||||
{
|
||||
height: 100%;
|
||||
min-height:100%;
|
||||
position:relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/*#webrx-photo-gradient-left
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
background-image:url(gfx/webrx-photo-gradient-corner.png);
|
||||
width: 59px;
|
||||
height: 92px;
|
||||
|
||||
}
|
||||
|
||||
#webrx-photo-gradient-middle
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 59px;
|
||||
right: 59px;
|
||||
height: 92px;
|
||||
background-image:url(gfx/webrx-photo-gradient-middle.png);
|
||||
}
|
||||
|
||||
#webrx-photo-gradient-right
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
background-image:url(gfx/webrx-photo-gradient-corner.png);
|
||||
width: 59px;
|
||||
height: 92px;
|
||||
-webkit-transform:scaleX(-1);
|
||||
-moz-transform:scaleX(-1);
|
||||
-ms-transform:scaleX(-1);
|
||||
-o-transform:scaleX(-1);
|
||||
transform:scaleX(-1);
|
||||
}*/
|
||||
|
||||
#webrx-rx-photo-title
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 78px;
|
||||
color: White;
|
||||
font-size: 16pt;
|
||||
text-shadow: 1px 1px 4px #444;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc
|
||||
{
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 109px;
|
||||
color: White;
|
||||
font-size: 10pt;
|
||||
font-weight: bold;
|
||||
text-shadow: 0px 0px 6px #444;
|
||||
opacity: 1;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
#webrx-rx-photo-desc a
|
||||
{
|
||||
/*color: #007df1;*/
|
||||
color: #5ca8ff;
|
||||
text-shadow: none;
|
||||
/*text-shadow: 0px 0px 7px #fff;*/
|
||||
}
|
||||
|
||||
#webrx-rx-title
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 350px;
|
||||
top: 13px;
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
color: #909090;
|
||||
font-size: 11pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#webrx-rx-desc
|
||||
{
|
||||
white-space:nowrap;
|
||||
overflow: hidden;
|
||||
cursor:pointer;
|
||||
font-size: 10pt;
|
||||
color: #909090;
|
||||
position: absolute;
|
||||
left: 350px;
|
||||
top: 34px;
|
||||
}
|
||||
|
||||
#webrx-rx-desc a
|
||||
{
|
||||
color: #909090;
|
||||
/*text-decoration: none;*/
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow
|
||||
{
|
||||
cursor:pointer;
|
||||
position: absolute;
|
||||
left: 470px;
|
||||
top: 51px;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow a
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#openwebrx-rx-details-arrow-down
|
||||
{
|
||||
display:none;
|
||||
}
|
||||
|
||||
/*canvas#waterfall-canvas
|
||||
{
|
||||
border-style: none;
|
||||
border-width: 1px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}*/
|
||||
|
||||
#openwebrx-scale-container
|
||||
{
|
||||
height: 47px;
|
||||
overflow: hidden;
|
||||
z-index:1000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#openwebrx-frequency-container {
|
||||
background-image: url("../gfx/openwebrx-scale-background.png");
|
||||
background-image: url("gfx/openwebrx-scale-background.png");
|
||||
background-repeat: repeat-x;
|
||||
background-size: cover;
|
||||
background-color: #444;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container
|
||||
{
|
||||
height: 25px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark {
|
||||
font-size: 12px;
|
||||
background-color: #FFFF00;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
padding: 2px 5px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
max-height: 14px;
|
||||
max-width: 50px;
|
||||
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark .bookmark-content {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark .bookmark-actions {
|
||||
display: none;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bookmark-actions .action {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.bookmark-actions .action img {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark.selected {
|
||||
z-index: 1010;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark:hover {
|
||||
z-index: 1011;
|
||||
max-height: none;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark[editable]:hover .bookmark-actions {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 5px solid transparent;
|
||||
border-top-color: #FFFF00;
|
||||
border-bottom: 0;
|
||||
margin-left: -5px;
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark[data-source=local] {
|
||||
background-color: #0FF;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark[data-source=local]:after {
|
||||
border-top-color: #0FF;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies] {
|
||||
background-color: #0F0;
|
||||
}
|
||||
|
||||
#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies]:after {
|
||||
border-top-color: #0F0;
|
||||
}
|
||||
|
||||
#webrx-canvas-background {
|
||||
flex-grow: 1;
|
||||
background-image: url('../gfx/openwebrx-background-cool-blue.png');
|
||||
background-repeat: no-repeat;
|
||||
background-color: #1e5f7f;
|
||||
background-size: cover;
|
||||
z-index:1000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#webrx-canvas-container
|
||||
{
|
||||
/*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 2000px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
/*background-color: #646464;*/
|
||||
/*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/
|
||||
background-image: url('gfx/openwebrx-background-cool-blue.png');
|
||||
background-repeat: no-repeat;
|
||||
background-color: #1e5f7f;
|
||||
cursor: crosshair;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#webrx-canvas-container canvas
|
||||
@ -282,17 +399,29 @@ input[type=range]:focus::-ms-fill-upper
|
||||
border-style: none;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
/*transition: left 200ms, width 200ms;*/
|
||||
}
|
||||
|
||||
#openwebrx-mathbox-container
|
||||
{
|
||||
flex-grow: 1;
|
||||
overflow: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#openwebrx-phantom-canvas
|
||||
{
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
/*#openwebrx-canvas-gradient-background
|
||||
{
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 396px;
|
||||
}*/
|
||||
|
||||
#openwebrx-log-scroll
|
||||
{
|
||||
/*overflow-y:auto;*/
|
||||
@ -303,12 +432,32 @@ input[type=range]:focus::-ms-fill-upper
|
||||
.nano .nano-pane { background: #444; }
|
||||
.nano .nano-slider { background: #eee !important; }
|
||||
|
||||
#webrx-main-container
|
||||
{
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.webrx-error
|
||||
{
|
||||
font-weight: bold;
|
||||
color: #ff6262;
|
||||
}
|
||||
|
||||
#openwebrx-problems span
|
||||
{
|
||||
background: #ff6262;
|
||||
padding: 3px;
|
||||
font-size: 8pt;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
-moz-border-radius: 4px;
|
||||
margin: 0px 2px 0px 2px;
|
||||
}
|
||||
|
||||
/*#webrx-freq-show
|
||||
{
|
||||
visibility: hidden;
|
||||
@ -328,15 +477,15 @@ input[type=range]:focus::-ms-fill-upper
|
||||
/* removed non-free fonts like that: */
|
||||
/*@font-face {
|
||||
font-family: 'unibody_8_pro_regregular';
|
||||
src: url('../gfx/unibody8pro-regular-webfont.eot');
|
||||
src: url('../gfx/unibody8pro-regular-webfont.ttf');
|
||||
src: url('gfx/unibody8pro-regular-webfont.eot');
|
||||
src: url('gfx/unibody8pro-regular-webfont.ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}*/
|
||||
|
||||
@font-face {
|
||||
font-family: 'expletus-sans-medium';
|
||||
src: url('../gfx/font-expletus-sans/ExpletusSans-Medium.ttf');
|
||||
src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
@ -363,37 +512,18 @@ input[type=range]:focus::-ms-fill-upper
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#openwebrx-panels-container-left,
|
||||
#openwebrx-panels-container-right {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#openwebrx-panels-container-left {
|
||||
left: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#openwebrx-panels-container-right {
|
||||
right: 0;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.openwebrx-panel
|
||||
{
|
||||
transform: perspective( 600px ) rotateX( 90deg );
|
||||
visibility: hidden;
|
||||
background-color: #575757;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
position: fixed;
|
||||
font-size: 10pt;
|
||||
border-radius: 15px;
|
||||
-moz-border-radius: 15px;
|
||||
margin: 5.9px;
|
||||
}
|
||||
|
||||
.openwebrx-panel a
|
||||
@ -444,18 +574,12 @@ input[type=range]:focus::-ms-fill-upper
|
||||
color: #FFFF50;
|
||||
}
|
||||
|
||||
.openwebrx-button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.openwebrx-demodulator-button
|
||||
{
|
||||
width: 38px;
|
||||
height: 19px;
|
||||
font-size: 12pt;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.openwebrx-square-button img
|
||||
@ -551,7 +675,7 @@ img.openwebrx-mirror-img
|
||||
|
||||
#openwebrx-panel-status
|
||||
{
|
||||
margin: 0 0 0 5.9px;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
background-color:rgba(0, 0, 0, 0);
|
||||
}
|
||||
@ -562,27 +686,54 @@ img.openwebrx-mirror-img
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons img
|
||||
{
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons ul
|
||||
{
|
||||
display: table;
|
||||
margin:0;
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-main-buttons ul li
|
||||
{
|
||||
display: table-cell;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons li:hover
|
||||
{
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
#openwebrx-main-buttons li:active
|
||||
{
|
||||
background-color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-main-buttons
|
||||
{
|
||||
position: absolute;
|
||||
right: 133px;
|
||||
top: 3px;
|
||||
margin:0;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 4px #000000;
|
||||
text-align: center;
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#openwebrx-panel-receiver
|
||||
{
|
||||
width:110px;
|
||||
}
|
||||
|
||||
|
||||
#openwebrx-panel-receiver .frequencies-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#openwebrx-panel-receiver .frequencies {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#openwebrx-panel-receiver .openwebrx-bookmark-button {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-mute-on
|
||||
{
|
||||
color: lime;
|
||||
@ -612,15 +763,6 @@ img.openwebrx-mirror-img
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.openwebrx-panel-flex-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.openwebrx-panel-line:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#openwebrx-smeter-outer
|
||||
{
|
||||
border-color: #888;
|
||||
@ -654,7 +796,7 @@ img.openwebrx-mirror-img
|
||||
font-family: 'expletus-sans-medium';
|
||||
}
|
||||
|
||||
#openwebrx-autoplay-overlay
|
||||
#openwebrx-big-grey
|
||||
{
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
@ -666,6 +808,9 @@ img.openwebrx-mirror-img
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1001;
|
||||
display: none;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 20pt;
|
||||
@ -673,19 +818,11 @@ img.openwebrx-mirror-img
|
||||
transition: opacity 0.3s linear;
|
||||
}
|
||||
|
||||
#openwebrx-autoplay-overlay img
|
||||
#openwebrx-big-grey img
|
||||
{
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#openwebrx-autoplay-overlay .overlay-content {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#openwebrx-digimode-canvas-container
|
||||
{
|
||||
/*margin: -10px -10px 10px -10px;*/
|
||||
@ -704,9 +841,10 @@ img.openwebrx-mirror-img
|
||||
transition: width 500ms, left 500ms;
|
||||
}
|
||||
|
||||
.openwebrx-panel select,
|
||||
.openwebrx-dialog select,
|
||||
.openwebrx-dialog input {
|
||||
#openwebrx-secondary-demod-listbox
|
||||
{
|
||||
width: 201px;
|
||||
height: 27px;
|
||||
border-radius: 5px;
|
||||
background-color: #373737;
|
||||
color: White;
|
||||
@ -718,29 +856,16 @@ img.openwebrx-mirror-img
|
||||
border-color: transparent;
|
||||
border-width: 0px;
|
||||
-moz-appearance: none;
|
||||
padding-left:3px;
|
||||
}
|
||||
|
||||
.openwebrx-panel select option,
|
||||
.openwebrx-dialog select option {
|
||||
#openwebrx-secondary-demod-listbox option
|
||||
{
|
||||
border-width: 0px;
|
||||
background-color: #373737;
|
||||
color: White;
|
||||
}
|
||||
|
||||
#openwebrx-secondary-demod-listbox
|
||||
{
|
||||
width: 173px;
|
||||
height: 27px;
|
||||
padding-left:3px;
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
#openwebrx-sdr-profiles-listbox {
|
||||
width: 100%;
|
||||
font-size: 10pt;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
#openwebrx-cursor-blink
|
||||
{
|
||||
animation: cursor-blink 1s infinite;
|
||||
@ -812,6 +937,11 @@ img.openwebrx-mirror-img
|
||||
transform-origin: 0% 50%;
|
||||
}
|
||||
|
||||
#openwebrx-digimode-content .part .subpart
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@keyframes new-digimode-data
|
||||
{
|
||||
0%{ opacity: 0; }
|
||||
@ -841,247 +971,3 @@ img.openwebrx-mirror-img
|
||||
border-color: Red;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot {
|
||||
width: 145px;
|
||||
height: 196px;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
|
||||
background-color: #676767;
|
||||
padding: 2px 0;
|
||||
color: #333;
|
||||
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot, .openwebrx-meta-slot.muted:before {
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.muted:before {
|
||||
display: block;
|
||||
content: "";
|
||||
background-image: url("../gfx/openwebrx-mute.png");
|
||||
width:100%;
|
||||
height:133px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active {
|
||||
background-color: #95bbdf;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before {
|
||||
content:"";
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: #ABFF00;
|
||||
border-radius: 50%;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot .openwebrx-meta-user-image {
|
||||
width:100%;
|
||||
height:133px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active .openwebrx-meta-user-image {
|
||||
background-image: url("../gfx/openwebrx-directcall.png");
|
||||
}
|
||||
|
||||
.openwebrx-meta-slot.active .openwebrx-meta-user-image.group {
|
||||
background-image: url("../gfx/openwebrx-groupcall.png");
|
||||
}
|
||||
|
||||
.openwebrx-dmr-timeslot-panel * {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.openwebrx-maps-pin {
|
||||
background-image: url("../gfx/google_maps_pin.svg");
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message,
|
||||
#openwebrx-panel-packet-message
|
||||
{
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message tbody,
|
||||
#openwebrx-panel-packet-message tbody
|
||||
{
|
||||
display: block;
|
||||
overflow: auto;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message thead tr,
|
||||
#openwebrx-panel-packet-message thead tr
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message th,
|
||||
#openwebrx-panel-wsjt-message td,
|
||||
#openwebrx-panel-packet-message th,
|
||||
#openwebrx-panel-packet-message td
|
||||
{
|
||||
width: 50px;
|
||||
text-align: left;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message .message {
|
||||
width: 380px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message .decimal {
|
||||
text-align: right;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-wsjt-message .decimal.freq {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-packet-message .message {
|
||||
width: 410px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-packet-message .callsign {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-packet-message .coord {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.aprs-symbol {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-size: 240px 90px;
|
||||
}
|
||||
|
||||
.aprs-symboltable-normal {
|
||||
background-image: url(../../aprs-symbols/aprs-symbols-24-0.png)
|
||||
}
|
||||
|
||||
.aprs-symboltable-alternate {
|
||||
background-image: url(../../aprs-symbols/aprs-symbols-24-1.png)
|
||||
}
|
||||
|
||||
.aprs-symboltable-overlay {
|
||||
background-image: url(../../aprs-symbols/aprs-symbols-24-2.png)
|
||||
}
|
||||
|
||||
.openwebrx-dialog {
|
||||
background-color: #575757;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
position: fixed;
|
||||
font-size: 10pt;
|
||||
border-radius: 15px;
|
||||
-moz-border-radius: 15px;
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.openwebrx-dialog .form-field {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.openwebrx-dialog .form-field:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.openwebrx-dialog label {
|
||||
display: inline-block;
|
||||
flex-grow: 0;
|
||||
width: 70px;
|
||||
padding-right: 20px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.openwebrx-dialog .form-field input,
|
||||
.openwebrx-dialog .form-field select {
|
||||
flex-grow: 1;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.openwebrx-dialog .form-field input {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.openwebrx-dialog .buttons {
|
||||
text-align: right;
|
||||
padding: 5px 5px 0;
|
||||
border-top: 1px solid #666;
|
||||
}
|
||||
|
||||
.openwebrx-dialog .buttons .openwebrx-button {
|
||||
font-size: 12pt;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel
|
||||
{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
|
||||
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container
|
||||
{
|
||||
height: 200px;
|
||||
margin: -10px;
|
||||
}
|
2763
htdocs/openwebrx.js
94
htdocs/retry.html
Normal file
@ -0,0 +1,94 @@
|
||||
<html>
|
||||
<!--
|
||||
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-->
|
||||
<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<style>
|
||||
html, body
|
||||
{
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
img.logo
|
||||
{
|
||||
margin-top: 120px;
|
||||
}
|
||||
div.frame
|
||||
{
|
||||
text-align: left;
|
||||
margin:0px auto;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
div.panel
|
||||
{
|
||||
text-align: center;
|
||||
background-color:#777777;
|
||||
border-radius: 15px;
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
color: White;
|
||||
font-size: 13pt;
|
||||
/*text-shadow: 1px 1px 4px #444;*/
|
||||
font-family: sans;
|
||||
}
|
||||
|
||||
div.alt
|
||||
{
|
||||
font-size: 10pt;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
body div a
|
||||
{
|
||||
color: #5ca8ff;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
span.browser
|
||||
{
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});}
|
||||
var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); }
|
||||
window.addEventListener("load",function(){rs=document.getElementById("reconnect-secs"); rt=document.getElementById("reconnect-text"); cnt=29;window.setInterval(function(){if(cnt<=-1) window.location.href=window.location.href.split("retry.")[0]; else if(cnt==0) {rt.innerHTML="Reconnecting..."; cnt--;} else rs.innerHTML=(cnt--).toString();},1000);},false);
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="frame">
|
||||
<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
|
||||
<div class="panel">
|
||||
There are no client slots left on this server.
|
||||
<div class="alt">
|
||||
Please wait until a client disconnects.<br /><span id="reconnect-text">We will try to reconnect in <span id="reconnect-secs">30</span> seconds...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
95
htdocs/upgrade.html
Normal file
@ -0,0 +1,95 @@
|
||||
<html>
|
||||
<!--
|
||||
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-->
|
||||
<head><title>OpenWebRX</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<style>
|
||||
html, body
|
||||
{
|
||||
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
img.logo
|
||||
{
|
||||
margin-top: 120px;
|
||||
}
|
||||
div.frame
|
||||
{
|
||||
text-align: left;
|
||||
margin:0px auto;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
div.panel
|
||||
{
|
||||
text-align: center;
|
||||
background-color:#777777;
|
||||
border-radius: 15px;
|
||||
padding: 12px;
|
||||
font-weight: bold;
|
||||
color: White;
|
||||
font-size: 13pt;
|
||||
/*text-shadow: 1px 1px 4px #444;*/
|
||||
font-family: sans;
|
||||
}
|
||||
|
||||
div.alt
|
||||
{
|
||||
font-size: 10pt;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
body div a
|
||||
{
|
||||
color: #5ca8ff;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
span.browser
|
||||
{
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});}
|
||||
var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); }
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="frame">
|
||||
<img class="logo" src="gfx/openwebrx-logo-big.png" style="height: 60px;"/>
|
||||
<div class="panel">
|
||||
Only the latest <span class="browser">Google Chrome</span> browser is supported at the moment.<br/>
|
||||
Please <a href="http://chrome.google.com/">download and install Google Chrome.</a><br />
|
||||
<div class="alt">
|
||||
Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected. <br />
|
||||
<a href="/?unsupported">Click here</a> if you still want to try OpenWebRX.</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
759
openwebrx.py
@ -1,66 +1,739 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/python2
|
||||
print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py
|
||||
"""
|
||||
|
||||
from http.server import HTTPServer
|
||||
from owrx.http import RequestHandler
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.feature import FeatureDetector
|
||||
from owrx.source import SdrService
|
||||
from socketserver import ThreadingMixIn
|
||||
from owrx.sdrhu import SdrHuUpdater
|
||||
from owrx.service import Services
|
||||
from owrx.websocket import WebSocketConnection
|
||||
from owrx.pskreporter import PskReporter
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
|
||||
import logging
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
|
||||
"""
|
||||
sw_version="v0.17"
|
||||
#0.15 (added nmux)
|
||||
|
||||
import os
|
||||
import code
|
||||
import importlib
|
||||
import csdr
|
||||
import thread
|
||||
import time
|
||||
import datetime
|
||||
import subprocess
|
||||
import os
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from SocketServer import ThreadingMixIn
|
||||
import fcntl
|
||||
import time
|
||||
import md5
|
||||
import random
|
||||
import threading
|
||||
import sys
|
||||
import traceback
|
||||
from collections import namedtuple
|
||||
import Queue
|
||||
import ctypes
|
||||
|
||||
#import rtl_mus
|
||||
import rxws
|
||||
import uuid
|
||||
import signal
|
||||
import socket
|
||||
|
||||
try: import sdrhu
|
||||
except: sdrhu=False
|
||||
avatar_ctime=""
|
||||
|
||||
#pypy compatibility
|
||||
try: import dl
|
||||
except: pass
|
||||
try: import __pypy__
|
||||
except: pass
|
||||
pypy="__pypy__" in globals()
|
||||
|
||||
"""
|
||||
def import_all_plugins(directory):
|
||||
for subdir in os.listdir(directory):
|
||||
if os.path.isdir(directory+subdir) and not subdir[0]=="_":
|
||||
exact_path=directory+subdir+"/plugin.py"
|
||||
if os.path.isfile(exact_path):
|
||||
importname=(directory+subdir+"/plugin").replace("/",".")
|
||||
print "[openwebrx-import] Found plugin:",importname
|
||||
importlib.import_module(importname)
|
||||
"""
|
||||
|
||||
class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
pass
|
||||
|
||||
def handle_signal(sig, frame):
|
||||
global spectrum_dsp
|
||||
if sig == signal.SIGUSR1:
|
||||
print "[openwebrx] Verbose status information on USR1 signal"
|
||||
print
|
||||
print "time.time() =", time.time()
|
||||
print "clients_mutex.locked() =", clients_mutex.locked()
|
||||
print "clients_mutex_locker =", clients_mutex_locker
|
||||
if server_fail: print "server_fail = ", server_fail
|
||||
print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick
|
||||
print
|
||||
print "clients:",len(clients)
|
||||
for client in clients:
|
||||
print
|
||||
for key in client._fields:
|
||||
print "\t%s = %s"%(key,str(getattr(client,key)))
|
||||
elif sig == signal.SIGUSR2:
|
||||
code.interact(local=globals())
|
||||
else:
|
||||
print "[openwebrx] Ctrl+C: aborting."
|
||||
cleanup_clients(True)
|
||||
spectrum_dsp.stop()
|
||||
os._exit(1) #not too graceful exit
|
||||
|
||||
def access_log(data):
|
||||
global logs
|
||||
logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n")
|
||||
logs.access_log.flush()
|
||||
|
||||
receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None
|
||||
|
||||
def main():
|
||||
print(
|
||||
"""
|
||||
global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs
|
||||
global serverfail, rtl_thread
|
||||
print
|
||||
print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package"
|
||||
print "_________________________________________________________________________________________________"
|
||||
print
|
||||
print "Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>"
|
||||
print
|
||||
|
||||
OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package
|
||||
_________________________________________________________________________________________________
|
||||
no_arguments=len(sys.argv)==1
|
||||
if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\""
|
||||
cfg=__import__("config_webrx" if no_arguments else sys.argv[1])
|
||||
for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"):
|
||||
if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters
|
||||
|
||||
Author contact info: Andras Retzler, HA7ILM <randras@sdr.hu>
|
||||
Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
|
||||
#Open log files
|
||||
logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})()
|
||||
|
||||
"""
|
||||
)
|
||||
#Set signal handler
|
||||
signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python
|
||||
signal.signal(signal.SIGUSR1, handle_signal)
|
||||
signal.signal(signal.SIGUSR2, handle_signal)
|
||||
|
||||
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
|
||||
#Pypy
|
||||
if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)"
|
||||
|
||||
featureDetector = FeatureDetector()
|
||||
if not featureDetector.is_available("core"):
|
||||
print(
|
||||
"you are missing required dependencies to run openwebrx. "
|
||||
"please check that the following core requirements are installed:"
|
||||
)
|
||||
print(", ".join(featureDetector.get_requirements("core")))
|
||||
#Change process name to "openwebrx" (to be seen in ps)
|
||||
try:
|
||||
for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
|
||||
if os.path.exists(libcpath):
|
||||
libc = dl.open(libcpath)
|
||||
libc.call("prctl", 15, "openwebrx", 0, 0, 0)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
#Start rtl thread
|
||||
if os.system("csdr 2> /dev/null") == 32512: #check for csdr
|
||||
print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n"
|
||||
return
|
||||
if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux
|
||||
print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n"
|
||||
return
|
||||
if cfg.start_rtl_thread:
|
||||
nmux_bufcnt = nmux_bufsize = 0
|
||||
while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096
|
||||
while nmux_bufsize * nmux_bufcnt < cfg.nmux_memory * 1e6: nmux_bufcnt += 1
|
||||
if nmux_bufcnt == 0 or nmux_bufsize == 0:
|
||||
print "[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
|
||||
return
|
||||
print "[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)
|
||||
cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, cfg.iq_server_port)
|
||||
rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=())
|
||||
rtl_thread.start()
|
||||
print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command
|
||||
print "[openwebrx-main] Waiting for I/Q server to start..."
|
||||
while True:
|
||||
testsock=socket.socket()
|
||||
try: testsock.connect(("127.0.0.1", cfg.iq_server_port))
|
||||
except:
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
testsock.close()
|
||||
break
|
||||
print "[openwebrx-main] I/Q server started."
|
||||
|
||||
#Initialize clients
|
||||
clients=[]
|
||||
clients_mutex=threading.Lock()
|
||||
lock_try_time=0
|
||||
|
||||
#Start watchdog thread
|
||||
print "[openwebrx-main] Starting watchdog threads."
|
||||
mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ())
|
||||
mutex_test_thread.start()
|
||||
mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ())
|
||||
mutex_watchdog_thread.start()
|
||||
|
||||
|
||||
#Start spectrum thread
|
||||
print "[openwebrx-main] Starting spectrum thread."
|
||||
spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ())
|
||||
spectrum_thread.start()
|
||||
#spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ())
|
||||
#spectrum_watchdog_thread.start()
|
||||
|
||||
get_cpu_usage()
|
||||
bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ())
|
||||
bcastmsg_thread.start()
|
||||
|
||||
#threading.Thread(target = measure_thread_function, args = ()).start()
|
||||
|
||||
#Start sdr.hu update thread
|
||||
if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing:
|
||||
print "[openwebrx-main] Starting sdr.hu update thread..."
|
||||
avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png"))
|
||||
sdrhu_thread=threading.Thread(target = sdrhu.run, args = ())
|
||||
sdrhu_thread.start()
|
||||
|
||||
#Start HTTP thread
|
||||
httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler)
|
||||
print('[openwebrx-main] Starting HTTP server.')
|
||||
access_log("Starting OpenWebRX...")
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
# This is a debug function below:
|
||||
measure_value=0
|
||||
def measure_thread_function():
|
||||
global measure_value
|
||||
while True:
|
||||
print "[openwebrx-measure] value is",measure_value
|
||||
measure_value=0
|
||||
time.sleep(1)
|
||||
|
||||
def bcastmsg_thread_function():
|
||||
global clients
|
||||
while True:
|
||||
time.sleep(3)
|
||||
try: cpu_usage=get_cpu_usage()
|
||||
except: cpu_usage=0
|
||||
cma("bcastmsg_thread")
|
||||
for i in range(0,len(clients)):
|
||||
clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients))
|
||||
cmr()
|
||||
|
||||
def mutex_test_thread_function():
|
||||
global clients_mutex, lock_try_time
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
lock_try_time=time.time()
|
||||
clients_mutex.acquire()
|
||||
clients_mutex.release()
|
||||
lock_try_time=0
|
||||
|
||||
def cma(what): #clients_mutex acquire
|
||||
global clients_mutex
|
||||
global clients_mutex_locker
|
||||
if not clients_mutex.locked(): clients_mutex_locker = what
|
||||
clients_mutex.acquire()
|
||||
|
||||
def cmr():
|
||||
global clients_mutex
|
||||
global clients_mutex_locker
|
||||
clients_mutex_locker = None
|
||||
clients_mutex.release()
|
||||
|
||||
def mutex_watchdog_thread_function():
|
||||
global lock_try_time
|
||||
global clients_mutex_locker
|
||||
global clients_mutex
|
||||
while True:
|
||||
if lock_try_time != 0 and time.time()-lock_try_time > 3.0:
|
||||
#if 3 seconds pass without unlock
|
||||
print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..."
|
||||
clients_mutex.release()
|
||||
time.sleep(0.5)
|
||||
|
||||
def spectrum_watchdog_thread_function():
|
||||
global spectrum_thread_watchdog_last_tick, receiver_failed
|
||||
while True:
|
||||
time.sleep(60)
|
||||
if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0:
|
||||
print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port."
|
||||
print "[openwebrx-spectrum-watchdog] Deactivating receiver."
|
||||
receiver_failed="spectrum"
|
||||
return
|
||||
|
||||
# Get error messages about unknown / unavailable features as soon as possible
|
||||
SdrService.loadProps()
|
||||
def check_server():
|
||||
global spectrum_dsp, server_fail, rtl_thread
|
||||
if server_fail: return server_fail
|
||||
#print spectrum_dsp.process.poll()
|
||||
if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed"
|
||||
#if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed"
|
||||
if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail
|
||||
return server_fail
|
||||
|
||||
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
|
||||
updater = SdrHuUpdater()
|
||||
updater.start()
|
||||
def apply_csdr_cfg_to_dsp(dsp):
|
||||
dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize
|
||||
dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes
|
||||
dsp.csdr_through = cfg.csdr_through
|
||||
|
||||
Services.start()
|
||||
def spectrum_thread_function():
|
||||
global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick
|
||||
spectrum_dsp=dsp=csdr.dsp()
|
||||
dsp.nc_port=cfg.iq_server_port
|
||||
dsp.set_demodulator("fft")
|
||||
dsp.set_samp_rate(cfg.samp_rate)
|
||||
dsp.set_fft_size(cfg.fft_size)
|
||||
dsp.set_fft_fps(cfg.fft_fps)
|
||||
dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0)
|
||||
dsp.set_fft_compression(cfg.fft_compression)
|
||||
dsp.set_format_conversion(cfg.format_conversion)
|
||||
apply_csdr_cfg_to_dsp(dsp)
|
||||
sleep_sec=0.87/cfg.fft_fps
|
||||
print "[openwebrx-spectrum] Spectrum thread initialized successfully."
|
||||
dsp.start()
|
||||
if cfg.csdr_dynamic_bufsize:
|
||||
dsp.read(8) #dummy read to skip bufsize & preamble
|
||||
print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1"
|
||||
print "[openwebrx-spectrum] Spectrum thread started."
|
||||
bytes_to_read=int(dsp.get_fft_bytes_to_read())
|
||||
spectrum_thread_counter=0
|
||||
while True:
|
||||
data=dsp.read(bytes_to_read)
|
||||
#print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()"
|
||||
if spectrum_thread_counter >= cfg.fft_fps:
|
||||
spectrum_thread_counter=0
|
||||
spectrum_thread_watchdog_last_tick = time.time() #once every second
|
||||
else: spectrum_thread_counter+=1
|
||||
cma("spectrum_thread")
|
||||
correction=0
|
||||
for i in range(0,len(clients)):
|
||||
i-=correction
|
||||
if (clients[i].ws_started):
|
||||
if clients[i].spectrum_queue.full():
|
||||
print "[openwebrx-spectrum] client spectrum queue full, closing it."
|
||||
close_client(i, False)
|
||||
correction+=1
|
||||
else:
|
||||
clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients
|
||||
cmr()
|
||||
|
||||
server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler)
|
||||
server.serve_forever()
|
||||
def get_client_by_id(client_id, use_mutex=True):
|
||||
global clients
|
||||
output=-1
|
||||
if use_mutex: cma("get_client_by_id")
|
||||
for i in range(0,len(clients)):
|
||||
if(clients[i].id==client_id):
|
||||
output=i
|
||||
break
|
||||
if use_mutex: cmr()
|
||||
if output==-1:
|
||||
raise ClientNotFoundException
|
||||
else:
|
||||
return output
|
||||
|
||||
def log_client(client, what):
|
||||
print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what)
|
||||
|
||||
def cleanup_clients(end_all=False):
|
||||
# - if a client doesn't open websocket for too long time, we drop it
|
||||
# - or if end_all is true, we drop all clients
|
||||
global clients
|
||||
cma("cleanup_clients")
|
||||
correction=0
|
||||
for i in range(0,len(clients)):
|
||||
i-=correction
|
||||
#print "cleanup_clients:: len(clients)=", len(clients), "i=", i
|
||||
if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45):
|
||||
if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket"
|
||||
close_client(i, False)
|
||||
correction+=1
|
||||
cmr()
|
||||
|
||||
def generate_client_id(ip):
|
||||
#add a client
|
||||
global clients
|
||||
new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat")
|
||||
new_client.id=md5.md5(str(random.random())).hexdigest()
|
||||
new_client.gen_time=time.time()
|
||||
new_client.ws_started=False # to check whether client has ever tried to open the websocket
|
||||
new_client.spectrum_queue=Queue.Queue(1000)
|
||||
new_client.ip=ip
|
||||
new_client.bcastmsg=""
|
||||
new_client.closed=[False] #byref, not exactly sure if required
|
||||
new_client.dsp=None
|
||||
cma("generate_client_id")
|
||||
clients.append(new_client)
|
||||
log_client(new_client,"client added. Clients now: {0}".format(len(clients)))
|
||||
cmr()
|
||||
cleanup_clients()
|
||||
return new_client.id
|
||||
|
||||
def close_client(i, use_mutex=True):
|
||||
global clients
|
||||
log_client(clients[i],"client being closed.")
|
||||
if use_mutex: cma("close_client")
|
||||
try:
|
||||
clients[i].dsp.stop()
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value
|
||||
traceback.print_tb(exc_traceback)
|
||||
clients[i].closed[0]=True
|
||||
access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")")
|
||||
del clients[i]
|
||||
if use_mutex: cmr()
|
||||
|
||||
# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python
|
||||
# some ideas are used from the artice above
|
||||
|
||||
class WebRXHandler(BaseHTTPRequestHandler):
|
||||
def proc_read_thread():
|
||||
pass
|
||||
|
||||
def send_302(self,what):
|
||||
self.send_response(302)
|
||||
self.send_header('Content-type','text/html')
|
||||
self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what))
|
||||
self.end_headers()
|
||||
self.wfile.write("<html><body><h1>Object moved</h1>Please <a href=\"/{0}\">click here</a> to continue.</body></html>".format(what))
|
||||
|
||||
|
||||
def do_GET(self):
|
||||
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version, receiver_failed
|
||||
rootdir = 'htdocs'
|
||||
self.path=self.path.replace("..","")
|
||||
path_temp_parts=self.path.split("?")
|
||||
self.path=path_temp_parts[0]
|
||||
request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else ""
|
||||
access_log("GET "+self.path+" from "+self.client_address[0])
|
||||
try:
|
||||
if self.path=="/":
|
||||
self.path="/index.wrx"
|
||||
# there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python
|
||||
#if self.path[:5]=="/lock": cma("do_GET /lock/") # to test mutex_watchdog_thread. Do not uncomment in production environment!
|
||||
if self.path[:4]=="/ws/":
|
||||
print "[openwebrx-ws] Client requested WebSocket connection"
|
||||
if receiver_failed: self.send_error(500,"Internal server error")
|
||||
try:
|
||||
# ========= WebSocket handshake =========
|
||||
ws_success=True
|
||||
try:
|
||||
rxws.handshake(self)
|
||||
cma("do_GET /ws/")
|
||||
client_i=get_client_by_id(self.path[4:], False)
|
||||
myclient=clients[client_i]
|
||||
except rxws.WebSocketException: ws_success=False
|
||||
except ClientNotFoundException: ws_success=False
|
||||
finally:
|
||||
if clients_mutex.locked(): cmr()
|
||||
if not ws_success:
|
||||
self.send_error(400, 'Bad request.')
|
||||
return
|
||||
|
||||
# ========= Client handshake =========
|
||||
if myclient.ws_started:
|
||||
print "[openwebrx-httpd] error: second WS connection with the same client id, throwing it."
|
||||
self.send_error(400, 'Bad request.') #client already started
|
||||
return
|
||||
rxws.send(self, "CLIENT DE SERVER openwebrx.py")
|
||||
client_ans=rxws.recv(self, True)
|
||||
if client_ans[:16]!="SERVER DE CLIENT":
|
||||
rxws.send("ERR Bad answer.")
|
||||
return
|
||||
myclient.ws_started=True
|
||||
#send default parameters
|
||||
rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} audio_compression={4} fft_compression={5} max_clients={6} setup".format(str(cfg.shown_center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps,cfg.audio_compression,cfg.fft_compression,cfg.max_clients))
|
||||
|
||||
# ========= Initialize DSP =========
|
||||
dsp=csdr.dsp()
|
||||
dsp_initialized=False
|
||||
dsp.set_audio_compression(cfg.audio_compression)
|
||||
dsp.set_fft_compression(cfg.fft_compression) #used by secondary chains
|
||||
dsp.set_format_conversion(cfg.format_conversion)
|
||||
dsp.set_offset_freq(0)
|
||||
dsp.set_bpf(-4000,4000)
|
||||
dsp.set_secondary_fft_size(cfg.digimodes_fft_size)
|
||||
dsp.nc_port=cfg.iq_server_port
|
||||
apply_csdr_cfg_to_dsp(dsp)
|
||||
myclient.dsp=dsp
|
||||
do_secondary_demod=False
|
||||
access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")")
|
||||
|
||||
while True:
|
||||
myclient.loopstat=0
|
||||
if myclient.closed[0]:
|
||||
print "[openwebrx-httpd:ws] client closed by other thread"
|
||||
break
|
||||
|
||||
# ========= send audio =========
|
||||
if dsp_initialized:
|
||||
myclient.loopstat=10
|
||||
temp_audio_data=dsp.read(256)
|
||||
myclient.loopstat=11
|
||||
rxws.send(self, temp_audio_data, "AUD ")
|
||||
|
||||
# ========= send spectrum =========
|
||||
while not myclient.spectrum_queue.empty():
|
||||
myclient.loopstat=20
|
||||
spectrum_data=myclient.spectrum_queue.get()
|
||||
#spectrum_data_mid=len(spectrum_data[0])/2
|
||||
#rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ")
|
||||
# (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it)
|
||||
myclient.loopstat=21
|
||||
rxws.send(self, spectrum_data[0],"FFT ")
|
||||
|
||||
# ========= send smeter_level =========
|
||||
smeter_level=None
|
||||
while True:
|
||||
try:
|
||||
myclient.loopstat=30
|
||||
smeter_level=dsp.get_smeter_level()
|
||||
if smeter_level == None: break
|
||||
except:
|
||||
break
|
||||
if smeter_level!=None:
|
||||
myclient.loopstat=31
|
||||
rxws.send(self, "MSG s={0}".format(smeter_level))
|
||||
|
||||
# ========= send bcastmsg =========
|
||||
if myclient.bcastmsg!="":
|
||||
myclient.loopstat=40
|
||||
rxws.send(self,myclient.bcastmsg)
|
||||
myclient.bcastmsg=""
|
||||
|
||||
# ========= send secondary =========
|
||||
if do_secondary_demod:
|
||||
myclient.loopstat=41
|
||||
while True:
|
||||
try:
|
||||
secondary_spectrum_data=dsp.read_secondary_fft(dsp.get_secondary_fft_bytes_to_read())
|
||||
if len(secondary_spectrum_data) == 0: break
|
||||
# print "len(secondary_spectrum_data)", len(secondary_spectrum_data) #TODO digimodes
|
||||
rxws.send(self, secondary_spectrum_data, "FFTS")
|
||||
except: break
|
||||
myclient.loopstat=42
|
||||
while True:
|
||||
try:
|
||||
myclient.loopstat=422
|
||||
secondary_demod_data=dsp.read_secondary_demod(1)
|
||||
myclient.loopstat=423
|
||||
if len(secondary_demod_data) == 0: break
|
||||
# print "len(secondary_demod_data)", len(secondary_demod_data), secondary_demod_data #TODO digimodes
|
||||
rxws.send(self, secondary_demod_data, "DAT ")
|
||||
except: break
|
||||
|
||||
# ========= process commands =========
|
||||
while True:
|
||||
myclient.loopstat=50
|
||||
rdata=rxws.recv(self, False)
|
||||
myclient.loopstat=51
|
||||
#try:
|
||||
if not rdata: break
|
||||
elif rdata[:3]=="SET":
|
||||
print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata)
|
||||
pairs=rdata[4:].split(" ")
|
||||
bpf_set=False
|
||||
new_bpf=dsp.get_bpf()
|
||||
filter_limit=dsp.get_output_rate()/2
|
||||
for pair in pairs:
|
||||
param_name, param_value = pair.split("=")
|
||||
if param_name == "low_cut" and -filter_limit <= int(param_value) <= filter_limit:
|
||||
bpf_set=True
|
||||
new_bpf[0]=int(param_value)
|
||||
elif param_name == "high_cut" and -filter_limit <= int(param_value) <= filter_limit:
|
||||
bpf_set=True
|
||||
new_bpf[1]=int(param_value)
|
||||
elif param_name == "offset_freq" and -cfg.samp_rate/2 <= int(param_value) <= cfg.samp_rate/2:
|
||||
myclient.loopstat=510
|
||||
dsp.set_offset_freq(int(param_value))
|
||||
elif param_name == "squelch_level" and float(param_value) >= 0:
|
||||
myclient.loopstat=520
|
||||
dsp.set_squelch_level(float(param_value))
|
||||
elif param_name=="mod":
|
||||
if (dsp.get_demodulator()!=param_value):
|
||||
myclient.loopstat=530
|
||||
if dsp_initialized: dsp.stop()
|
||||
dsp.set_demodulator(param_value)
|
||||
if dsp_initialized: dsp.start()
|
||||
elif param_name == "output_rate":
|
||||
if not dsp_initialized:
|
||||
myclient.loopstat=540
|
||||
dsp.set_output_rate(int(param_value))
|
||||
myclient.loopstat=541
|
||||
dsp.set_samp_rate(cfg.samp_rate)
|
||||
elif param_name=="action" and param_value=="start":
|
||||
if not dsp_initialized:
|
||||
myclient.loopstat=550
|
||||
dsp.start()
|
||||
dsp_initialized=True
|
||||
elif param_name=="secondary_mod" and cfg.digimodes_enable:
|
||||
if (dsp.get_secondary_demodulator() != param_value):
|
||||
if dsp_initialized: dsp.stop()
|
||||
if param_value == "off":
|
||||
dsp.set_secondary_demodulator(None)
|
||||
do_secondary_demod = False
|
||||
else:
|
||||
dsp.set_secondary_demodulator(param_value)
|
||||
do_secondary_demod = True
|
||||
rxws.send(self, "MSG secondary_fft_size={0} if_samp_rate={1} secondary_bw={2} secondary_setup".format(cfg.digimodes_fft_size, dsp.if_samp_rate(), dsp.secondary_bw()))
|
||||
if dsp_initialized: dsp.start()
|
||||
elif param_name=="secondary_offset_freq" and 0 <= int(param_value) <= dsp.if_samp_rate()/2 and cfg.digimodes_enable:
|
||||
dsp.set_secondary_offset_freq(int(param_value))
|
||||
else:
|
||||
print "[openwebrx-httpd:ws] invalid parameter"
|
||||
if bpf_set:
|
||||
myclient.loopstat=560
|
||||
dsp.set_bpf(*new_bpf)
|
||||
#code.interact(local=locals())
|
||||
except:
|
||||
myclient.loopstat=990
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
print "[openwebrx-httpd:ws] exception: ",exc_type,exc_value
|
||||
traceback.print_tb(exc_traceback) #TODO digimodes
|
||||
#if exc_value[0]==32: #"broken pipe", client disconnected
|
||||
# pass
|
||||
#elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected
|
||||
# pass
|
||||
#else:
|
||||
# print "[openwebrx-httpd] error in /ws/ handler: ",exc_type,exc_value
|
||||
# traceback.print_tb(exc_traceback)
|
||||
|
||||
#stop dsp for the disconnected client
|
||||
myclient.loopstat=991
|
||||
try:
|
||||
dsp.stop()
|
||||
del dsp
|
||||
except:
|
||||
print "[openwebrx-httpd] error in dsp.stop()"
|
||||
|
||||
#delete disconnected client
|
||||
myclient.loopstat=992
|
||||
try:
|
||||
cma("do_GET /ws/ delete disconnected")
|
||||
id_to_close=get_client_by_id(myclient.id,False)
|
||||
close_client(id_to_close,False)
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
print "[openwebrx-httpd] client cannot be closed: ",exc_type,exc_value
|
||||
traceback.print_tb(exc_traceback)
|
||||
finally:
|
||||
cmr()
|
||||
myclient.loopstat=1000
|
||||
return
|
||||
elif self.path in ("/status", "/status/"):
|
||||
#self.send_header('Content-type','text/plain')
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
getbands=lambda: str(int(cfg.shown_center_freq-cfg.samp_rate/2))+"-"+str(int(cfg.shown_center_freq+cfg.samp_rate/2))
|
||||
self.wfile.write("status="+("inactive" if receiver_failed else "active")+"\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\nusers_max="+str(cfg.max_clients)+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n")
|
||||
print "[openwebrx-httpd] GET /status/ from",self.client_address[0]
|
||||
else:
|
||||
f=open(rootdir+self.path)
|
||||
data=f.read()
|
||||
extension=self.path[(len(self.path)-4):len(self.path)]
|
||||
extension=extension[2:] if extension[1]=='.' else extension[1:]
|
||||
checkresult=check_server()
|
||||
if extension == "wrx" and (checkresult or receiver_failed):
|
||||
self.send_302("inactive.html")
|
||||
return
|
||||
anyStringsPresentInUserAgent=lambda a: reduce(lambda x,y:x or y, map(lambda b:self.headers['user-agent'].count(b), a), False)
|
||||
if extension == "wrx" and ( (not anyStringsPresentInUserAgent(("Chrome","Firefox","Googlebot","iPhone","iPad","iPod"))) if 'user-agent' in self.headers.keys() else True ) and (not request_param.count("unsupported")):
|
||||
self.send_302("upgrade.html")
|
||||
return
|
||||
if extension == "wrx":
|
||||
cleanup_clients(False)
|
||||
if cfg.max_clients<=len(clients):
|
||||
self.send_302("retry.html")
|
||||
return
|
||||
self.send_response(200)
|
||||
if(("wrx","html","htm").count(extension)):
|
||||
self.send_header('Content-type','text/html')
|
||||
elif(extension=="js"):
|
||||
self.send_header('Content-type','text/javascript')
|
||||
elif(extension=="css"):
|
||||
self.send_header('Content-type','text/css')
|
||||
self.end_headers()
|
||||
if extension == "wrx":
|
||||
replace_dictionary=(
|
||||
("%[RX_PHOTO_DESC]",cfg.photo_desc),
|
||||
("%[CLIENT_ID]", generate_client_id(self.client_address[0])) if "%[CLIENT_ID]" in data else "",
|
||||
("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"),
|
||||
("%[RX_TITLE]",cfg.receiver_name),
|
||||
("%[RX_LOC]",cfg.receiver_location),
|
||||
("%[RX_QRA]",cfg.receiver_qra),
|
||||
("%[RX_ASL]",str(cfg.receiver_asl)),
|
||||
("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])),
|
||||
("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title),
|
||||
("%[RX_ADMIN]",cfg.receiver_admin),
|
||||
("%[RX_ANT]",cfg.receiver_ant),
|
||||
("%[RX_DEVICE]",cfg.receiver_device),
|
||||
("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)),
|
||||
("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)),
|
||||
("%[START_MOD]",cfg.start_mod),
|
||||
("%[WATERFALL_COLORS]",cfg.waterfall_colors),
|
||||
("%[WATERFALL_MIN_LEVEL]",str(cfg.waterfall_min_level)),
|
||||
("%[WATERFALL_MAX_LEVEL]",str(cfg.waterfall_max_level)),
|
||||
("%[WATERFALL_AUTO_LEVEL_MARGIN]","[%d,%d]"%cfg.waterfall_auto_level_margin),
|
||||
("%[DIGIMODES_ENABLE]",("true" if cfg.digimodes_enable else "false")),
|
||||
("%[MATHBOX_WATERFALL_FRES]",str(cfg.mathbox_waterfall_frequency_resolution)),
|
||||
("%[MATHBOX_WATERFALL_THIST]",str(cfg.mathbox_waterfall_history_length)),
|
||||
("%[MATHBOX_WATERFALL_COLORS]",cfg.mathbox_waterfall_colors)
|
||||
)
|
||||
for rule in replace_dictionary:
|
||||
while data.find(rule[0])!=-1:
|
||||
data=data.replace(rule[0],rule[1])
|
||||
self.wfile.write(data)
|
||||
f.close()
|
||||
return
|
||||
except IOError:
|
||||
self.send_error(404, 'Invalid path.')
|
||||
except:
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
print "[openwebrx-httpd] error (@outside):", exc_type, exc_value
|
||||
traceback.print_tb(exc_traceback)
|
||||
|
||||
|
||||
class ClientNotFoundException(Exception):
|
||||
pass
|
||||
|
||||
last_worktime=0
|
||||
last_idletime=0
|
||||
|
||||
def get_cpu_usage():
|
||||
global last_worktime, last_idletime
|
||||
try:
|
||||
f=open("/proc/stat","r")
|
||||
except:
|
||||
return 0 #Workaround, possibly we're on a Mac
|
||||
line=""
|
||||
while not "cpu " in line: line=f.readline()
|
||||
f.close()
|
||||
spl=line.split(" ")
|
||||
worktime=int(spl[2])+int(spl[3])+int(spl[4])
|
||||
idletime=int(spl[5])
|
||||
dworktime=(worktime-last_worktime)
|
||||
didletime=(idletime-last_idletime)
|
||||
rate=float(dworktime)/(didletime+dworktime)
|
||||
last_worktime=worktime
|
||||
last_idletime=idletime
|
||||
if(last_worktime==0): return 0
|
||||
return rate
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
WebSocketConnection.closeAll()
|
||||
Services.stop()
|
||||
PskReporter.stop()
|
||||
|
577
owrx/aprs.py
@ -1,577 +0,0 @@
|
||||
from owrx.kiss import KissDeframer
|
||||
from owrx.map import Map, LatLngLocation
|
||||
from owrx.bands import Bandplan
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
from datetime import datetime, timezone
|
||||
import re
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# speed is in knots... convert to metric (km/h)
|
||||
knotsToKilometers = 1.852
|
||||
feetToMeters = 0.3048
|
||||
milesToKilometers = 1.609344
|
||||
inchesToMilimeters = 25.4
|
||||
|
||||
|
||||
def fahrenheitToCelsius(f):
|
||||
return (f - 32) * 5 / 9
|
||||
|
||||
|
||||
# not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it.
|
||||
encoding = "utf-8"
|
||||
|
||||
# regex for altitute in comment field
|
||||
altitudeRegex = re.compile("(^.*)\\/A=([0-9]{6})(.*$)")
|
||||
|
||||
# regex for parsing third-party headers
|
||||
thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z0-9-]+\\*?)):(.*)$")
|
||||
|
||||
# regex for getting the message id out of message
|
||||
messageIdRegex = re.compile("^(.*){([0-9]{1,5})$")
|
||||
|
||||
|
||||
def decodeBase91(input):
|
||||
base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0
|
||||
return base + (ord(input[-1]) - 33)
|
||||
|
||||
|
||||
def getSymbolData(symbol, table):
|
||||
return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33}
|
||||
|
||||
|
||||
class Ax25Parser(object):
|
||||
def parse(self, ax25frame):
|
||||
control_pid = ax25frame.find(bytes([0x03, 0xF0]))
|
||||
if control_pid % 7 > 0:
|
||||
logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data")
|
||||
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i : i + n]
|
||||
|
||||
return {
|
||||
"destination": self.extractCallsign(ax25frame[0:7]),
|
||||
"source": self.extractCallsign(ax25frame[7:14]),
|
||||
"path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)],
|
||||
"data": ax25frame[control_pid + 2 :],
|
||||
}
|
||||
|
||||
def extractCallsign(self, input):
|
||||
cs = bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip()
|
||||
ssid = (input[6] & 0b00011110) >> 1
|
||||
if ssid > 0:
|
||||
return "{callsign}-{ssid}".format(callsign=cs, ssid=ssid)
|
||||
else:
|
||||
return cs
|
||||
|
||||
|
||||
class WeatherMapping(object):
|
||||
def __init__(self, char, key, length, scale=None):
|
||||
self.char = char
|
||||
self.key = key
|
||||
self.length = length
|
||||
self.scale = scale
|
||||
|
||||
def matches(self, input):
|
||||
return self.char == input[0] and len(input) > self.length
|
||||
|
||||
def updateWeather(self, weather, input):
|
||||
def deepApply(obj, key, v):
|
||||
keys = key.split(".")
|
||||
if len(keys) > 1:
|
||||
if not keys[0] in obj:
|
||||
obj[keys[0]] = {}
|
||||
deepApply(obj[keys[0]], ".".join(keys[1:]), v)
|
||||
else:
|
||||
obj[key] = v
|
||||
|
||||
try:
|
||||
value = int(input[1 : 1 + self.length])
|
||||
if self.scale:
|
||||
value = self.scale(value)
|
||||
deepApply(weather, self.key, value)
|
||||
except ValueError:
|
||||
pass
|
||||
remain = input[1 + self.length :]
|
||||
return weather, remain
|
||||
|
||||
|
||||
class WeatherParser(object):
|
||||
mappings = [
|
||||
WeatherMapping("c", "wind.direction", 3),
|
||||
WeatherMapping("s", "wind.speed", 3, lambda x: x * milesToKilometers),
|
||||
WeatherMapping("g", "wind.gust", 3, lambda x: x * milesToKilometers),
|
||||
WeatherMapping("t", "temperature", 3, fahrenheitToCelsius),
|
||||
WeatherMapping("r", "rain.hour", 3, lambda x: x / 100 * inchesToMilimeters),
|
||||
WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * inchesToMilimeters),
|
||||
WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * inchesToMilimeters),
|
||||
WeatherMapping("h", "humidity", 2),
|
||||
WeatherMapping("b", "barometricpressure", 5, lambda x: x / 10),
|
||||
WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4),
|
||||
]
|
||||
|
||||
def __init__(self, data, weather={}):
|
||||
self.data = data
|
||||
self.weather = weather
|
||||
|
||||
def getWeather(self):
|
||||
doWork = True
|
||||
weather = self.weather
|
||||
while doWork:
|
||||
mapping = next((m for m in WeatherParser.mappings if m.matches(self.data)), None)
|
||||
if mapping:
|
||||
(weather, remain) = mapping.updateWeather(weather, self.data)
|
||||
self.data = remain
|
||||
doWork = len(self.data) > 0
|
||||
else:
|
||||
doWork = False
|
||||
return weather
|
||||
|
||||
def getRemainder(self):
|
||||
return self.data
|
||||
|
||||
|
||||
class AprsLocation(LatLngLocation):
|
||||
def __init__(self, data):
|
||||
super().__init__(data["lat"], data["lon"])
|
||||
self.data = data
|
||||
|
||||
def __dict__(self):
|
||||
res = super(AprsLocation, self).__dict__()
|
||||
for key in ["comment", "symbol", "course", "speed"]:
|
||||
if key in self.data:
|
||||
res[key] = self.data[key]
|
||||
return res
|
||||
|
||||
|
||||
class AprsParser(object):
|
||||
def __init__(self, handler):
|
||||
self.ax25parser = Ax25Parser()
|
||||
self.deframer = KissDeframer()
|
||||
self.dial_freq = None
|
||||
self.band = None
|
||||
self.handler = handler
|
||||
self.metric = self.getMetric()
|
||||
|
||||
def setDialFrequency(self, freq):
|
||||
self.dial_freq = freq
|
||||
self.band = Bandplan.getSharedInstance().findBand(freq)
|
||||
self.metric = self.getMetric()
|
||||
|
||||
def getMetric(self):
|
||||
band = "unknown"
|
||||
if self.band is not None:
|
||||
band = self.band.getName()
|
||||
name = "aprs.decodes.{band}.aprs".format(band=band)
|
||||
metrics = Metrics.getSharedInstance()
|
||||
metric = metrics.getMetric(name)
|
||||
if metric is None:
|
||||
metric = CounterMetric()
|
||||
metrics.addMetric(name, metric)
|
||||
return metric
|
||||
|
||||
def parse(self, raw):
|
||||
for frame in self.deframer.parse(raw):
|
||||
try:
|
||||
data = self.ax25parser.parse(frame)
|
||||
|
||||
# TODO how can we tell if this is an APRS frame at all?
|
||||
aprsData = self.parseAprsData(data)
|
||||
|
||||
logger.debug("decoded APRS data: %s", aprsData)
|
||||
self.updateMap(aprsData)
|
||||
self.metric.inc()
|
||||
self.handler.write_aprs_data(aprsData)
|
||||
except Exception:
|
||||
logger.exception("exception while parsing aprs data")
|
||||
|
||||
def updateMap(self, mapData):
|
||||
if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData:
|
||||
mapData = mapData["data"]
|
||||
if "lat" in mapData and "lon" in mapData:
|
||||
loc = AprsLocation(mapData)
|
||||
source = mapData["source"]
|
||||
if "type" in mapData:
|
||||
if mapData["type"] == "item":
|
||||
source = mapData["item"]
|
||||
elif mapData["type"] == "object":
|
||||
source = mapData["object"]
|
||||
Map.getSharedInstance().updateLocation(source, loc, "APRS", self.band)
|
||||
|
||||
def hasCompressedCoordinates(self, raw):
|
||||
return raw[0] == "/" or raw[0] == "\\"
|
||||
|
||||
def parseUncompressedCoordinates(self, raw):
|
||||
lat = int(raw[0:2]) + float(raw[2:7]) / 60
|
||||
if raw[7] == "S":
|
||||
lat *= -1
|
||||
lon = int(raw[9:12]) + float(raw[12:17]) / 60
|
||||
if raw[17] == "W":
|
||||
lon *= -1
|
||||
return {"lat": lat, "lon": lon, "symbol": getSymbolData(raw[18], raw[8])}
|
||||
|
||||
def parseCompressedCoordinates(self, raw):
|
||||
return {
|
||||
"lat": 90 - decodeBase91(raw[1:5]) / 380926,
|
||||
"lon": -180 + decodeBase91(raw[5:9]) / 190463,
|
||||
"symbol": getSymbolData(raw[9], raw[0]),
|
||||
}
|
||||
|
||||
def parseTimestamp(self, raw):
|
||||
now = datetime.now()
|
||||
if raw[6] == "h":
|
||||
ts = datetime.strptime(raw[0:6], "%H%M%S")
|
||||
ts = ts.replace(year=now.year, month=now.month, day=now.month, tzinfo=timezone.utc)
|
||||
else:
|
||||
ts = datetime.strptime(raw[0:6], "%d%H%M")
|
||||
ts = ts.replace(year=now.year, month=now.month)
|
||||
if raw[6] == "z":
|
||||
ts = ts.replace(tzinfo=timezone.utc)
|
||||
elif raw[6] == "/":
|
||||
ts = ts.replace(tzinfo=now.tzinfo)
|
||||
else:
|
||||
logger.warning("invalid timezone info byte: %s", raw[6])
|
||||
return int(ts.timestamp() * 1000)
|
||||
|
||||
def parseStatusUpate(self, raw):
|
||||
res = {"type": "status"}
|
||||
if raw[6] == "z":
|
||||
res["timestamp"] = self.parseTimestamp(raw[0:7])
|
||||
res["comment"] = raw[7:]
|
||||
else:
|
||||
res["comment"] = raw
|
||||
return res
|
||||
|
||||
def parseAprsData(self, data):
|
||||
information = data["data"]
|
||||
|
||||
# forward some of the ax25 data
|
||||
aprsData = {"source": data["source"], "destination": data["destination"], "path": data["path"]}
|
||||
|
||||
if information[0] == 0x1C or information[0] == ord("`") or information[0] == ord("'"):
|
||||
aprsData.update(MicEParser().parse(data))
|
||||
return aprsData
|
||||
|
||||
information = information.decode(encoding, "replace")
|
||||
|
||||
# APRS data type identifier
|
||||
dti = information[0]
|
||||
|
||||
if dti == "!" or dti == "=":
|
||||
# position without timestamp
|
||||
aprsData.update(self.parseRegularAprsData(information[1:]))
|
||||
elif dti == "/" or dti == "@":
|
||||
# position with timestamp
|
||||
aprsData["timestamp"] = self.parseTimestamp(information[1:8])
|
||||
aprsData.update(self.parseRegularAprsData(information[8:]))
|
||||
elif dti == ">":
|
||||
# status update
|
||||
aprsData.update(self.parseStatusUpate(information[1:]))
|
||||
elif dti == "}":
|
||||
# third party
|
||||
aprsData.update(self.parseThirdpartyAprsData(information[1:]))
|
||||
elif dti == ":":
|
||||
# message
|
||||
aprsData.update(self.parseMessage(information[1:]))
|
||||
elif dti == ";":
|
||||
# object
|
||||
aprsData.update(self.parseObject(information[1:]))
|
||||
elif dti == ")":
|
||||
# item
|
||||
aprsData.update(self.parseItem(information[1:]))
|
||||
|
||||
return aprsData
|
||||
|
||||
def parseObject(self, information):
|
||||
result = {"type": "object"}
|
||||
if len(information) > 16:
|
||||
result["object"] = information[0:9].strip()
|
||||
result["live"] = information[9] == "*"
|
||||
result["timestamp"] = self.parseTimestamp(information[10:17])
|
||||
result.update(self.parseRegularAprsData(information[17:]))
|
||||
# override type, losing information about compression
|
||||
result["type"] = "object"
|
||||
return result
|
||||
|
||||
def parseItem(self, information):
|
||||
result = {"type": "item"}
|
||||
if len(information) > 3:
|
||||
indexes = [information[0:10].find(p) for p in ["!", "_"]]
|
||||
filtered = [i for i in indexes if i >= 3]
|
||||
filtered.sort()
|
||||
if len(filtered):
|
||||
index = filtered[0]
|
||||
result["item"] = information[0:index]
|
||||
result["live"] = information[index] == "!"
|
||||
result.update(self.parseRegularAprsData(information[index + 1 :]))
|
||||
# override type, losing information about compression
|
||||
result["type"] = "item"
|
||||
return result
|
||||
|
||||
def parseMessage(self, information):
|
||||
result = {"type": "message"}
|
||||
if len(information) > 9 and information[9] == ":":
|
||||
result["adressee"] = information[0:9]
|
||||
message = information[10:]
|
||||
if len(message) > 3 and message[0:3] == "ack":
|
||||
result["type"] = "messageacknowledgement"
|
||||
result["messageid"] = int(message[3:8])
|
||||
elif len(message) > 3 and message[0:3] == "rej":
|
||||
result["type"] = "messagerejection"
|
||||
result["messageid"] = int(message[3:8])
|
||||
else:
|
||||
matches = messageIdRegex.match(message)
|
||||
if matches:
|
||||
result["messageid"] = int(matches.group(2))
|
||||
message = matches.group(1)
|
||||
result["message"] = message
|
||||
return result
|
||||
|
||||
def parseThirdpartyAprsData(self, information):
|
||||
matches = thirdpartyeRegex.match(information)
|
||||
if matches:
|
||||
path = matches.group(2).split(",")
|
||||
destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None)
|
||||
data = self.parseAprsData(
|
||||
{
|
||||
"source": matches.group(1).upper(),
|
||||
"destination": destination,
|
||||
"path": path,
|
||||
"data": matches.group(6).encode(encoding),
|
||||
}
|
||||
)
|
||||
return {"type": "thirdparty", "data": data}
|
||||
|
||||
return {"type": "thirdparty"}
|
||||
|
||||
def parseRegularAprsData(self, information):
|
||||
if self.hasCompressedCoordinates(information):
|
||||
aprsData = self.parseCompressedCoordinates(information[0:10])
|
||||
aprsData["type"] = "compressed"
|
||||
if information[10] != " ":
|
||||
if information[10] == "{":
|
||||
# pre-calculated radio range
|
||||
aprsData["range"] = 2 * 1.08 ** (ord(information[11]) - 33) * milesToKilometers
|
||||
else:
|
||||
aprsData["course"] = (ord(information[10]) - 33) * 4
|
||||
# speed is in knots... convert to metric (km/h)
|
||||
aprsData["speed"] = (1.08 ** (ord(information[11]) - 33) - 1) * knotsToKilometers
|
||||
# compression type
|
||||
t = ord(information[12])
|
||||
aprsData["fix"] = (t & 0b00100000) > 0
|
||||
sources = ["other", "GLL", "GGA", "RMC"]
|
||||
aprsData["nmeasource"] = sources[(t & 0b00011000) >> 3]
|
||||
origins = [
|
||||
"Compressed",
|
||||
"TNC BText",
|
||||
"Software",
|
||||
"[tbd]",
|
||||
"KPC3",
|
||||
"Pico",
|
||||
"Other tracker",
|
||||
"Digipeater conversion",
|
||||
]
|
||||
aprsData["compressionorigin"] = origins[t & 0b00000111]
|
||||
comment = information[13:]
|
||||
else:
|
||||
aprsData = self.parseUncompressedCoordinates(information[0:19])
|
||||
aprsData["type"] = "regular"
|
||||
comment = information[19:]
|
||||
|
||||
def decodeHeightGainDirectivity(comment):
|
||||
res = {"height": 2 ** int(comment[4]) * 10 * feetToMeters, "gain": int(comment[5])}
|
||||
directivity = int(comment[6])
|
||||
if directivity == 0:
|
||||
res["directivity"] = "omni"
|
||||
elif 0 < directivity < 9:
|
||||
res["directivity"] = directivity * 45
|
||||
return res
|
||||
|
||||
# aprs data extensions
|
||||
# yes, weather stations are officially identified by their symbols. go figure...
|
||||
if "symbol" in aprsData and aprsData["symbol"]["index"] == 62:
|
||||
# weather report
|
||||
weather = {}
|
||||
if len(comment) > 6 and comment[3] == "/":
|
||||
try:
|
||||
weather["wind"] = {"direction": int(comment[0:3]), "speed": int(comment[4:7]) * milesToKilometers}
|
||||
except ValueError:
|
||||
pass
|
||||
comment = comment[7:]
|
||||
|
||||
parser = WeatherParser(comment, weather)
|
||||
aprsData["weather"] = parser.getWeather()
|
||||
comment = parser.getRemainder()
|
||||
elif len(comment) > 6:
|
||||
if comment[3] == "/":
|
||||
# course and speed
|
||||
# for a weather report, this would be wind direction and speed
|
||||
try:
|
||||
aprsData["course"] = int(comment[0:3])
|
||||
aprsData["speed"] = int(comment[4:7]) * knotsToKilometers
|
||||
except ValueError:
|
||||
pass
|
||||
comment = comment[7:]
|
||||
elif comment[0:3] == "PHG":
|
||||
# station power and effective antenna height/gain/directivity
|
||||
try:
|
||||
powerCodes = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
|
||||
aprsData["power"] = powerCodes[int(comment[3])]
|
||||
aprsData.update(decodeHeightGainDirectivity(comment))
|
||||
except ValueError:
|
||||
pass
|
||||
comment = comment[7:]
|
||||
elif comment[0:3] == "RNG":
|
||||
# pre-calculated radio range
|
||||
try:
|
||||
aprsData["range"] = int(comment[3:7]) * milesToKilometers
|
||||
except ValueError:
|
||||
pass
|
||||
comment = comment[7:]
|
||||
elif comment[0:3] == "DFS":
|
||||
# direction finding signal strength and antenna height/gain
|
||||
try:
|
||||
aprsData["strength"] = int(comment[3])
|
||||
aprsData.update(decodeHeightGainDirectivity(comment))
|
||||
except ValueError:
|
||||
pass
|
||||
comment = comment[7:]
|
||||
|
||||
matches = altitudeRegex.match(comment)
|
||||
if matches:
|
||||
aprsData["altitude"] = int(matches.group(2)) * feetToMeters
|
||||
comment = matches.group(1) + matches.group(3)
|
||||
|
||||
aprsData["comment"] = comment
|
||||
|
||||
return aprsData
|
||||
|
||||
|
||||
class MicEParser(object):
|
||||
def extractNumber(self, input):
|
||||
n = ord(input)
|
||||
if n >= ord("P"):
|
||||
return n - ord("P")
|
||||
if n >= ord("A"):
|
||||
return n - ord("A")
|
||||
return n - ord("0")
|
||||
|
||||
def listToNumber(self, input):
|
||||
base = self.listToNumber(input[:-1]) * 10 if len(input) > 1 else 0
|
||||
return base + input[-1]
|
||||
|
||||
def extractAltitude(self, comment):
|
||||
if len(comment) < 4 or comment[3] != "}":
|
||||
return (comment, None)
|
||||
return comment[4:], decodeBase91(comment[:3]) - 10000
|
||||
|
||||
def extractDevice(self, comment):
|
||||
if len(comment) > 0:
|
||||
if comment[0] == ">":
|
||||
if len(comment) > 1:
|
||||
if comment[-1] == "=":
|
||||
return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D72"}
|
||||
if comment[-1] == "^":
|
||||
return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D74"}
|
||||
return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D7A"}
|
||||
if comment[0] == "]":
|
||||
if len(comment) > 1 and comment[-1] == "=":
|
||||
return comment[1:-1], {"manufacturer": "Kenwood", "device": "TM-D710"}
|
||||
return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D700"}
|
||||
if len(comment) > 2 and (comment[0] == "`" or comment[0] == "'"):
|
||||
if comment[-2] == "_":
|
||||
devices = {
|
||||
"b": "VX-8",
|
||||
'"': "FTM-350",
|
||||
"#": "VX-8G",
|
||||
"$": "FT1D",
|
||||
"%": "FTM-400DR",
|
||||
")": "FTM-100D",
|
||||
"(": "FT2D",
|
||||
"0": "FT3D",
|
||||
}
|
||||
return comment[1:-2], {"manufacturer": "Yaesu", "device": devices.get(comment[-1], "Unknown")}
|
||||
if comment[-2:] == " X":
|
||||
return comment[1:-2], {"manufacturer": "SainSonic", "device": "AP510"}
|
||||
if comment[-2] == "(":
|
||||
devices = {"5": "D578UV", "8": "D878UV"}
|
||||
return comment[1:-2], {"manufacturer": "Anytone", "device": devices.get(comment[-1], "Unknown")}
|
||||
if comment[-2] == "|":
|
||||
devices = {"3": "TinyTrack3", "4": "TinyTrack4"}
|
||||
return comment[1:-2], {"manufacturer": "Byonics", "device": devices.get(comment[-1], "Unknown")}
|
||||
if comment[-2:] == "^v":
|
||||
return comment[1:-2], {"manufacturer": "HinzTec", "device": "anyfrog"}
|
||||
if comment[-2] == ":":
|
||||
devices = {"4": "P4dragon DR-7400 modem", "8": "P4dragon DR-7800 modem"}
|
||||
return (
|
||||
comment[1:-2],
|
||||
{"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")},
|
||||
)
|
||||
if comment[-2:] == "~v":
|
||||
return comment[1:-2], {"manufacturer": "Other", "device": "Other"}
|
||||
return comment[1:-2], None
|
||||
return comment, None
|
||||
|
||||
def parse(self, data):
|
||||
information = data["data"]
|
||||
destination = data["destination"]
|
||||
|
||||
rawLatitude = [self.extractNumber(c) for c in destination[0:6]]
|
||||
lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000
|
||||
if ord(destination[3]) <= ord("9"):
|
||||
lat *= -1
|
||||
|
||||
lon = information[1] - 28
|
||||
if ord(destination[4]) >= ord("P"):
|
||||
lon += 100
|
||||
if 180 <= lon <= 189:
|
||||
lon -= 80
|
||||
if 190 <= lon <= 199:
|
||||
lon -= 190
|
||||
|
||||
minutes = information[2] - 28
|
||||
if minutes >= 60:
|
||||
minutes -= 60
|
||||
|
||||
lon += minutes / 60 + (information[3] - 28) / 6000
|
||||
|
||||
if ord(destination[5]) >= ord("P"):
|
||||
lon *= -1
|
||||
|
||||
speed = (information[4] - 28) * 10
|
||||
dc28 = information[5] - 28
|
||||
speed += int(dc28 / 10)
|
||||
course = (dc28 % 10) * 100
|
||||
course += information[6] - 28
|
||||
if speed >= 800:
|
||||
speed -= 800
|
||||
if course >= 400:
|
||||
course -= 400
|
||||
# speed is in knots... convert to metric (km/h)
|
||||
speed *= knotsToKilometers
|
||||
|
||||
comment = information[9:].decode(encoding, "replace").strip()
|
||||
(comment, altitude) = self.extractAltitude(comment)
|
||||
|
||||
(comment, device) = self.extractDevice(comment)
|
||||
|
||||
# altitude might be inside the device string, so repeat and choose one
|
||||
(comment, insideAltitude) = self.extractAltitude(comment)
|
||||
altitude = next((a for a in [altitude, insideAltitude] if a is not None), None)
|
||||
|
||||
return {
|
||||
"fix": information[0] == ord("`") or information[0] == 0x1C,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"comment": comment,
|
||||
"altitude": altitude,
|
||||
"speed": speed,
|
||||
"course": course,
|
||||
"device": device,
|
||||
"type": "Mic-E",
|
||||
"symbol": getSymbolData(chr(information[7]), chr(information[8])),
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import json
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Band(object):
|
||||
def __init__(self, dict):
|
||||
self.name = dict["name"]
|
||||
self.lower_bound = dict["lower_bound"]
|
||||
self.upper_bound = dict["upper_bound"]
|
||||
self.frequencies = []
|
||||
if "frequencies" in dict:
|
||||
for (mode, freqs) in dict["frequencies"].items():
|
||||
if not isinstance(freqs, list):
|
||||
freqs = [freqs]
|
||||
for f in freqs:
|
||||
if not self.inBand(f):
|
||||
logger.warning(
|
||||
"Frequency for {mode} on {band} is not within band limits: {frequency}".format(
|
||||
mode=mode, frequency=f, band=self.name
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.frequencies.append({"mode": mode, "frequency": f})
|
||||
|
||||
def inBand(self, freq):
|
||||
return self.lower_bound <= freq <= self.upper_bound
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def getDialFrequencies(self, range):
|
||||
(low, hi) = range
|
||||
return [e for e in self.frequencies if low <= e["frequency"] <= hi]
|
||||
|
||||
|
||||
class Bandplan(object):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if Bandplan.sharedInstance is None:
|
||||
Bandplan.sharedInstance = Bandplan()
|
||||
return Bandplan.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
f = open("bands.json", "r")
|
||||
bands_json = json.load(f)
|
||||
f.close()
|
||||
self.bands = [Band(d) for d in bands_json]
|
||||
|
||||
def findBands(self, freq):
|
||||
return [band for band in self.bands if band.inBand(freq)]
|
||||
|
||||
def findBand(self, freq):
|
||||
bands = self.findBands(freq)
|
||||
if bands:
|
||||
return bands[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def collectDialFrequencies(self, range):
|
||||
return [e for b in self.bands for e in b.getDialFrequencies(range)]
|
@ -1,44 +0,0 @@
|
||||
import json
|
||||
|
||||
|
||||
class Bookmark(object):
|
||||
def __init__(self, j):
|
||||
self.name = j["name"]
|
||||
self.frequency = j["frequency"]
|
||||
self.modulation = j["modulation"]
|
||||
|
||||
def getName(self):
|
||||
return self.name
|
||||
|
||||
def getFrequency(self):
|
||||
return self.frequency
|
||||
|
||||
def getModulation(self):
|
||||
return self.modulation
|
||||
|
||||
def __dict__(self):
|
||||
return {
|
||||
"name": self.getName(),
|
||||
"frequency": self.getFrequency(),
|
||||
"modulation": self.getModulation(),
|
||||
}
|
||||
|
||||
|
||||
class Bookmarks(object):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if Bookmarks.sharedInstance is None:
|
||||
Bookmarks.sharedInstance = Bookmarks()
|
||||
return Bookmarks.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
f = open("bookmarks.json", "r")
|
||||
bookmarks_json = json.load(f)
|
||||
f.close()
|
||||
self.bookmarks = [Bookmark(d) for d in bookmarks_json]
|
||||
|
||||
def getBookmarks(self, range):
|
||||
(lo, hi) = range
|
||||
return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi]
|
137
owrx/config.py
@ -1,137 +0,0 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Subscription(object):
|
||||
def __init__(self, subscriptee, subscriber):
|
||||
self.subscriptee = subscriptee
|
||||
self.subscriber = subscriber
|
||||
|
||||
def call(self, *args, **kwargs):
|
||||
self.subscriber(*args, **kwargs)
|
||||
|
||||
def cancel(self):
|
||||
self.subscriptee.unwire(self)
|
||||
|
||||
|
||||
class Property(object):
|
||||
def __init__(self, value=None):
|
||||
self.value = value
|
||||
self.subscribers = []
|
||||
|
||||
def getValue(self):
|
||||
return self.value
|
||||
|
||||
def setValue(self, value):
|
||||
if self.value == value:
|
||||
return self
|
||||
self.value = value
|
||||
for c in self.subscribers:
|
||||
try:
|
||||
c.call(self.value)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return self
|
||||
|
||||
def wire(self, callback):
|
||||
sub = Subscription(self, callback)
|
||||
self.subscribers.append(sub)
|
||||
if not self.value is None:
|
||||
sub.call(self.value)
|
||||
return sub
|
||||
|
||||
def unwire(self, sub):
|
||||
try:
|
||||
self.subscribers.remove(sub)
|
||||
except ValueError:
|
||||
# happens when already removed before
|
||||
pass
|
||||
return self
|
||||
|
||||
|
||||
class PropertyManager(object):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if PropertyManager.sharedInstance is None:
|
||||
PropertyManager.sharedInstance = PropertyManager()
|
||||
return PropertyManager.sharedInstance
|
||||
|
||||
def collect(self, *props):
|
||||
return PropertyManager(
|
||||
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props}
|
||||
)
|
||||
|
||||
def __init__(self, properties=None):
|
||||
self.properties = {}
|
||||
self.subscribers = []
|
||||
if properties is not None:
|
||||
for (name, prop) in properties.items():
|
||||
self.add(name, prop)
|
||||
|
||||
def add(self, name, prop):
|
||||
self.properties[name] = prop
|
||||
|
||||
def fireCallbacks(value):
|
||||
for c in self.subscribers:
|
||||
try:
|
||||
c.call(name, value)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
prop.wire(fireCallbacks)
|
||||
return self
|
||||
|
||||
def __contains__(self, name):
|
||||
return self.hasProperty(name)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.getPropertyValue(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
if not self.hasProperty(name):
|
||||
self.add(name, Property())
|
||||
self.getProperty(name).setValue(value)
|
||||
|
||||
def __dict__(self):
|
||||
return {k: v.getValue() for k, v in self.properties.items()}
|
||||
|
||||
def hasProperty(self, name):
|
||||
return name in self.properties
|
||||
|
||||
def getProperty(self, name):
|
||||
if not self.hasProperty(name):
|
||||
self.add(name, Property())
|
||||
return self.properties[name]
|
||||
|
||||
def getPropertyValue(self, name):
|
||||
return self.getProperty(name).getValue()
|
||||
|
||||
def wire(self, callback):
|
||||
sub = Subscription(self, callback)
|
||||
self.subscribers.append(sub)
|
||||
return sub
|
||||
|
||||
def unwire(self, sub):
|
||||
try:
|
||||
self.subscribers.remove(sub)
|
||||
except ValueError:
|
||||
# happens when already removed before
|
||||
pass
|
||||
return self
|
||||
|
||||
def defaults(self, other_pm):
|
||||
for (key, p) in self.properties.items():
|
||||
if p.getValue() is None:
|
||||
p.setValue(other_pm[key])
|
||||
return self
|
||||
|
||||
def loadConfig(self, filename):
|
||||
cfg = __import__(filename)
|
||||
for name, value in cfg.__dict__.items():
|
||||
if name.startswith("__"):
|
||||
continue
|
||||
self[name] = value
|
||||
return self
|
@ -1,341 +0,0 @@
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.source import DspManager, CpuUsageThread, SdrService, ClientRegistry
|
||||
from owrx.feature import FeatureDetector
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.bands import Bandplan
|
||||
from owrx.bookmarks import Bookmarks
|
||||
from owrx.map import Map
|
||||
from owrx.locator import Locator
|
||||
from multiprocessing import Queue
|
||||
import json
|
||||
import threading
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, conn):
|
||||
self.conn = conn
|
||||
self.multiprocessingPipe = Queue()
|
||||
|
||||
def mp_passthru():
|
||||
run = True
|
||||
while run:
|
||||
try:
|
||||
data = self.multiprocessingPipe.get()
|
||||
self.send(data)
|
||||
except (EOFError, OSError):
|
||||
run = False
|
||||
|
||||
threading.Thread(target=mp_passthru).start()
|
||||
|
||||
def send(self, data):
|
||||
self.conn.send(data)
|
||||
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
self.multiprocessingPipe.close()
|
||||
|
||||
def mp_send(self, data):
|
||||
self.multiprocessingPipe.put(data, block=False)
|
||||
|
||||
def handleTextMessage(self, conn, message):
|
||||
pass
|
||||
|
||||
def handleBinaryMessage(self, conn, data):
|
||||
logger.error("unsupported binary message, discarding")
|
||||
|
||||
def handleClose(self):
|
||||
self.close()
|
||||
|
||||
|
||||
class OpenWebRxReceiverClient(Client):
|
||||
config_keys = [
|
||||
"waterfall_colors",
|
||||
"waterfall_min_level",
|
||||
"waterfall_max_level",
|
||||
"waterfall_auto_level_margin",
|
||||
"samp_rate",
|
||||
"fft_size",
|
||||
"fft_fps",
|
||||
"audio_compression",
|
||||
"fft_compression",
|
||||
"max_clients",
|
||||
"start_mod",
|
||||
"start_freq",
|
||||
"center_freq",
|
||||
"mathbox_waterfall_colors",
|
||||
"mathbox_waterfall_history_length",
|
||||
"mathbox_waterfall_frequency_resolution",
|
||||
"initial_squelch_level",
|
||||
"profile_id",
|
||||
]
|
||||
|
||||
def __init__(self, conn):
|
||||
super().__init__(conn)
|
||||
|
||||
self.dsp = None
|
||||
self.sdr = None
|
||||
self.configSub = None
|
||||
|
||||
ClientRegistry.getSharedInstance().addClient(self)
|
||||
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
|
||||
self.setSdr()
|
||||
|
||||
# send receiver info
|
||||
receiver_keys = [
|
||||
"receiver_name",
|
||||
"receiver_location",
|
||||
"receiver_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)
|
||||
|
||||
profiles = [
|
||||
{"name": s.getName() + " " + p["name"], "id": sid + "|" + pid}
|
||||
for (sid, s) in SdrService.getSources().items()
|
||||
for (pid, p) in s.getProfiles().items()
|
||||
]
|
||||
self.write_profiles(profiles)
|
||||
|
||||
features = FeatureDetector().feature_availability()
|
||||
self.write_features(features)
|
||||
|
||||
CpuUsageThread.getSharedInstance().add_client(self)
|
||||
|
||||
def handleTextMessage(self, conn, message):
|
||||
try:
|
||||
message = json.loads(message)
|
||||
if "type" in message:
|
||||
if message["type"] == "dspcontrol":
|
||||
if "action" in message and message["action"] == "start":
|
||||
self.startDsp()
|
||||
|
||||
if "params" in message:
|
||||
params = message["params"]
|
||||
self.setDspProperties(params)
|
||||
|
||||
if message["type"] == "config":
|
||||
if "params" in message:
|
||||
self.setParams(message["params"])
|
||||
if message["type"] == "setsdr":
|
||||
if "params" in message:
|
||||
self.setSdr(message["params"]["sdr"])
|
||||
if message["type"] == "selectprofile":
|
||||
if "params" in message and "profile" in message["params"]:
|
||||
profile = message["params"]["profile"].split("|")
|
||||
self.setSdr(profile[0])
|
||||
self.sdr.activateProfile(profile[1])
|
||||
else:
|
||||
logger.warning("received message without type: {0}".format(message))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("message is not json: {0}".format(message))
|
||||
|
||||
def setSdr(self, id=None):
|
||||
next = SdrService.getSource(id)
|
||||
|
||||
if next is None:
|
||||
self.handleSdrFailure("sdr device failed")
|
||||
return
|
||||
|
||||
if next == self.sdr:
|
||||
return
|
||||
|
||||
self.stopDsp()
|
||||
|
||||
if self.configSub is not None:
|
||||
self.configSub.cancel()
|
||||
self.configSub = None
|
||||
|
||||
self.sdr = next
|
||||
|
||||
self.startDsp()
|
||||
|
||||
# send initial config
|
||||
configProps = (
|
||||
self.sdr.getProps()
|
||||
.collect(*OpenWebRxReceiverClient.config_keys)
|
||||
.defaults(PropertyManager.getSharedInstance())
|
||||
)
|
||||
|
||||
def sendConfig(key, value):
|
||||
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys)
|
||||
# TODO mathematical properties? hmmmm
|
||||
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
|
||||
# TODO this is a hack to support multiple sdrs
|
||||
config["sdr_id"] = self.sdr.getId()
|
||||
self.write_config(config)
|
||||
|
||||
cf = configProps["center_freq"]
|
||||
srh = configProps["samp_rate"] / 2
|
||||
frequencyRange = (cf - srh, cf + srh)
|
||||
self.write_dial_frequendies(Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange))
|
||||
bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)]
|
||||
self.write_bookmarks(bookmarks)
|
||||
|
||||
self.configSub = configProps.wire(sendConfig)
|
||||
sendConfig(None, None)
|
||||
|
||||
self.sdr.addSpectrumClient(self)
|
||||
|
||||
def handleSdrFailure(self, message):
|
||||
self.write_sdr_error(message)
|
||||
|
||||
def startDsp(self):
|
||||
if self.dsp is None:
|
||||
self.dsp = DspManager(self, self.sdr)
|
||||
self.dsp.start()
|
||||
|
||||
def close(self):
|
||||
self.stopDsp()
|
||||
CpuUsageThread.getSharedInstance().remove_client(self)
|
||||
ClientRegistry.getSharedInstance().removeClient(self)
|
||||
if self.configSub is not None:
|
||||
self.configSub.cancel()
|
||||
self.configSub = None
|
||||
super().close()
|
||||
|
||||
def stopDsp(self):
|
||||
if self.dsp is not None:
|
||||
self.dsp.stop()
|
||||
self.dsp = None
|
||||
if self.sdr is not None:
|
||||
self.sdr.removeSpectrumClient(self)
|
||||
|
||||
def setParams(self, params):
|
||||
# only the keys in the protected property manager can be overridden from the web
|
||||
protected = (
|
||||
self.sdr.getProps()
|
||||
.collect("samp_rate", "center_freq", "rf_gain", "type", "if_gain")
|
||||
.defaults(PropertyManager.getSharedInstance())
|
||||
)
|
||||
for key, value in params.items():
|
||||
protected[key] = value
|
||||
|
||||
def setDspProperties(self, params):
|
||||
for key, value in params.items():
|
||||
self.dsp.setProperty(key, value)
|
||||
|
||||
def write_spectrum_data(self, data):
|
||||
self.mp_send(bytes([0x01]) + data)
|
||||
|
||||
def write_dsp_data(self, data):
|
||||
self.send(bytes([0x02]) + data)
|
||||
|
||||
def write_s_meter_level(self, level):
|
||||
self.send({"type": "smeter", "value": level})
|
||||
|
||||
def write_cpu_usage(self, usage):
|
||||
self.mp_send({"type": "cpuusage", "value": usage})
|
||||
|
||||
def write_clients(self, clients):
|
||||
self.mp_send({"type": "clients", "value": clients})
|
||||
|
||||
def write_secondary_fft(self, data):
|
||||
self.send(bytes([0x03]) + data)
|
||||
|
||||
def write_secondary_demod(self, data):
|
||||
message = data.decode("ascii")
|
||||
self.send({"type": "secondary_demod", "value": message})
|
||||
|
||||
def write_secondary_dsp_config(self, cfg):
|
||||
self.send({"type": "secondary_config", "value": cfg})
|
||||
|
||||
def write_config(self, cfg):
|
||||
self.send({"type": "config", "value": cfg})
|
||||
|
||||
def write_receiver_details(self, details):
|
||||
self.send({"type": "receiver_details", "value": details})
|
||||
|
||||
def write_profiles(self, profiles):
|
||||
self.send({"type": "profiles", "value": profiles})
|
||||
|
||||
def write_features(self, features):
|
||||
self.send({"type": "features", "value": features})
|
||||
|
||||
def write_metadata(self, metadata):
|
||||
self.send({"type": "metadata", "value": metadata})
|
||||
|
||||
def write_wsjt_message(self, message):
|
||||
self.send({"type": "wsjt_message", "value": message})
|
||||
|
||||
def write_dial_frequendies(self, frequencies):
|
||||
self.send({"type": "dial_frequencies", "value": frequencies})
|
||||
|
||||
def write_bookmarks(self, bookmarks):
|
||||
self.send({"type": "bookmarks", "value": bookmarks})
|
||||
|
||||
def write_aprs_data(self, data):
|
||||
self.send({"type": "aprs_data", "value": data})
|
||||
|
||||
def write_sdr_error(self, message):
|
||||
self.send({"type": "sdr_error", "value": message})
|
||||
|
||||
|
||||
class MapConnection(Client):
|
||||
def __init__(self, conn):
|
||||
super().__init__(conn)
|
||||
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
|
||||
|
||||
Map.getSharedInstance().addClient(self)
|
||||
|
||||
def handleTextMessage(self, conn, message):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
Map.getSharedInstance().removeClient(self)
|
||||
super().close()
|
||||
|
||||
def write_config(self, cfg):
|
||||
self.send({"type": "config", "value": cfg})
|
||||
|
||||
def write_update(self, update):
|
||||
self.mp_send({"type": "update", "value": update})
|
||||
|
||||
|
||||
class WebSocketMessageHandler(object):
|
||||
def __init__(self):
|
||||
self.handshake = None
|
||||
|
||||
def handleTextMessage(self, conn, message):
|
||||
if message[:16] == "SERVER DE CLIENT":
|
||||
meta = message[17:].split(" ")
|
||||
self.handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)}
|
||||
|
||||
conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version))
|
||||
logger.debug("client connection intitialized")
|
||||
|
||||
if "type" in self.handshake:
|
||||
if self.handshake["type"] == "receiver":
|
||||
client = OpenWebRxReceiverClient(conn)
|
||||
if self.handshake["type"] == "map":
|
||||
client = MapConnection(conn)
|
||||
# backwards compatibility
|
||||
else:
|
||||
client = OpenWebRxReceiverClient(conn)
|
||||
|
||||
# hand off all further communication to the correspondig connection
|
||||
conn.setMessageHandler(client)
|
||||
|
||||
return
|
||||
|
||||
if not self.handshake:
|
||||
logger.warning("not answering client request since handshake is not complete")
|
||||
return
|
||||
|
||||
def handleBinaryMessage(self, conn, data):
|
||||
pass
|
||||
|
||||
def handleClose(self):
|
||||
pass
|
@ -1,155 +0,0 @@
|
||||
import os
|
||||
import mimetypes
|
||||
import json
|
||||
from datetime import datetime
|
||||
from string import Template
|
||||
from owrx.websocket import WebSocketConnection
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.source import ClientRegistry
|
||||
from owrx.connection import WebSocketMessageHandler
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.feature import FeatureDetector
|
||||
from owrx.metrics import Metrics
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Controller(object):
|
||||
def __init__(self, handler, request):
|
||||
self.handler = handler
|
||||
self.request = request
|
||||
|
||||
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None):
|
||||
self.handler.send_response(code)
|
||||
if content_type is not None:
|
||||
self.handler.send_header("Content-Type", content_type)
|
||||
if last_modified is not None:
|
||||
self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"))
|
||||
if max_age is not None:
|
||||
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
|
||||
self.handler.end_headers()
|
||||
if type(content) == str:
|
||||
content = content.encode()
|
||||
self.handler.wfile.write(content)
|
||||
|
||||
|
||||
class StatusController(Controller):
|
||||
def handle_request(self):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
# TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
|
||||
vars = {
|
||||
"status": "active",
|
||||
"name": pm["receiver_name"],
|
||||
"op_email": pm["receiver_admin"],
|
||||
"users": ClientRegistry.getSharedInstance().clientCount(),
|
||||
"users_max": pm["max_clients"],
|
||||
"gps": pm["receiver_gps"],
|
||||
"asl": pm["receiver_asl"],
|
||||
"loc": pm["receiver_location"],
|
||||
"sw_version": openwebrx_version,
|
||||
"avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"),
|
||||
}
|
||||
self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))
|
||||
|
||||
|
||||
class AssetsController(Controller):
|
||||
def __init__(self, handler, request, path):
|
||||
if not path.endswith("/"):
|
||||
path += "/"
|
||||
self.path = path
|
||||
super().__init__(handler, request)
|
||||
|
||||
def serve_file(self, file, content_type=None):
|
||||
try:
|
||||
modified = datetime.fromtimestamp(os.path.getmtime(self.path + file))
|
||||
|
||||
if "If-Modified-Since" in self.handler.headers:
|
||||
client_modified = datetime.strptime(
|
||||
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
|
||||
)
|
||||
if modified <= client_modified:
|
||||
self.send_response("", code=304)
|
||||
return
|
||||
|
||||
f = open(self.path + file, "rb")
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
if content_type is None:
|
||||
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
|
||||
self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600)
|
||||
except FileNotFoundError:
|
||||
self.send_response("file not found", code=404)
|
||||
|
||||
def handle_request(self):
|
||||
filename = self.request.matches.group(1)
|
||||
self.serve_file(filename)
|
||||
|
||||
|
||||
class OwrxAssetsController(AssetsController):
|
||||
def __init__(self, handler, request):
|
||||
super().__init__(handler, request, "htdocs/")
|
||||
|
||||
|
||||
class AprsSymbolsController(AssetsController):
|
||||
def __init__(self, handler, request):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
super().__init__(handler, request, pm["aprs_symbols_path"])
|
||||
|
||||
|
||||
class TemplateController(Controller):
|
||||
def render_template(self, file, **vars):
|
||||
f = open("htdocs/" + file, "r", encoding="utf-8")
|
||||
template = Template(f.read())
|
||||
f.close()
|
||||
|
||||
return template.safe_substitute(**vars)
|
||||
|
||||
def serve_template(self, file, **vars):
|
||||
self.send_response(self.render_template(file, **vars), content_type="text/html")
|
||||
|
||||
def default_variables(self):
|
||||
return {}
|
||||
|
||||
|
||||
class WebpageController(TemplateController):
|
||||
def template_variables(self):
|
||||
header = self.render_template("include/header.include.html")
|
||||
return {"header": header}
|
||||
|
||||
|
||||
class IndexController(WebpageController):
|
||||
def handle_request(self):
|
||||
self.serve_template("index.html", **self.template_variables())
|
||||
|
||||
|
||||
class MapController(WebpageController):
|
||||
def handle_request(self):
|
||||
# TODO check if we have a google maps api key first?
|
||||
self.serve_template("map.html", **self.template_variables())
|
||||
|
||||
|
||||
class FeatureController(WebpageController):
|
||||
def handle_request(self):
|
||||
self.serve_template("features.html", **self.template_variables())
|
||||
|
||||
|
||||
class ApiController(Controller):
|
||||
def handle_request(self):
|
||||
data = json.dumps(FeatureDetector().feature_report())
|
||||
self.send_response(data, content_type="application/json")
|
||||
|
||||
|
||||
class MetricsController(Controller):
|
||||
def handle_request(self):
|
||||
data = json.dumps(Metrics.getSharedInstance().getMetrics())
|
||||
self.send_response(data, content_type="application/json")
|
||||
|
||||
|
||||
class WebSocketController(Controller):
|
||||
def handle_request(self):
|
||||
conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
|
||||
# enter read loop
|
||||
conn.handle()
|
263
owrx/feature.py
@ -1,263 +0,0 @@
|
||||
import subprocess
|
||||
from functools import reduce
|
||||
from operator import and_
|
||||
import re
|
||||
from distutils.version import LooseVersion
|
||||
import inspect
|
||||
from owrx.config import PropertyManager
|
||||
import shlex
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnknownFeatureException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FeatureDetector(object):
|
||||
features = {
|
||||
"core": ["csdr", "nmux", "nc"],
|
||||
"rtl_sdr": ["rtl_sdr"],
|
||||
"rtl_sdr_connector": ["rtl_connector"],
|
||||
"sdrplay": ["rx_tools"],
|
||||
"sdrplay_connector": ["soapy_connector"],
|
||||
"hackrf": ["hackrf_transfer"],
|
||||
"airspy": ["airspy_rx"],
|
||||
"airspy_connector": ["soapy_connector"],
|
||||
"digital_voice_digiham": ["digiham", "sox"],
|
||||
"digital_voice_dsd": ["dsd", "sox", "digiham"],
|
||||
"wsjt-x": ["wsjtx", "sox"],
|
||||
"packet": ["direwolf"],
|
||||
}
|
||||
|
||||
def feature_availability(self):
|
||||
return {name: self.is_available(name) for name in FeatureDetector.features}
|
||||
|
||||
def feature_report(self):
|
||||
def requirement_details(name):
|
||||
available = self.has_requirement(name)
|
||||
return {
|
||||
"available": available,
|
||||
# as of now, features are always enabled as soon as they are available. this may change in the future.
|
||||
"enabled": available,
|
||||
"description": self.get_requirement_description(name),
|
||||
}
|
||||
|
||||
def feature_details(name):
|
||||
return {
|
||||
"description": "",
|
||||
"available": self.is_available(name),
|
||||
"requirements": {name: requirement_details(name) for name in self.get_requirements(name)},
|
||||
}
|
||||
|
||||
return {name: feature_details(name) for name in FeatureDetector.features}
|
||||
|
||||
def is_available(self, feature):
|
||||
return self.has_requirements(self.get_requirements(feature))
|
||||
|
||||
def get_requirements(self, feature):
|
||||
try:
|
||||
return FeatureDetector.features[feature]
|
||||
except KeyError:
|
||||
raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature))
|
||||
|
||||
def has_requirements(self, requirements):
|
||||
passed = True
|
||||
for requirement in requirements:
|
||||
passed = passed and self.has_requirement(requirement)
|
||||
return passed
|
||||
|
||||
def _get_requirement_method(self, requirement):
|
||||
methodname = "has_" + requirement
|
||||
if hasattr(self, methodname) and callable(getattr(self, methodname)):
|
||||
return getattr(self, methodname)
|
||||
return None
|
||||
|
||||
def has_requirement(self, requirement):
|
||||
method = self._get_requirement_method(requirement)
|
||||
if method is not None:
|
||||
return method()
|
||||
else:
|
||||
logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement))
|
||||
return False
|
||||
|
||||
def get_requirement_description(self, requirement):
|
||||
return inspect.getdoc(self._get_requirement_method(requirement))
|
||||
|
||||
def command_is_runnable(self, command):
|
||||
tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"]
|
||||
cmd = shlex.split(command)
|
||||
try:
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir)
|
||||
return process.wait() != 32512
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def has_csdr(self):
|
||||
"""
|
||||
OpenWebRX uses the demodulator and pipeline tools provided by the csdr project. Please check out [the project
|
||||
page on github](https://github.com/simonyiszk/csdr) for further details and installation instructions.
|
||||
"""
|
||||
return self.command_is_runnable("csdr")
|
||||
|
||||
def has_nmux(self):
|
||||
"""
|
||||
Nmux is another tool provided by the csdr project. It is used for internal multiplexing of the IQ data streams.
|
||||
If you're missing nmux even though you have csdr installed, please update your csdr version.
|
||||
"""
|
||||
return self.command_is_runnable("nmux --help")
|
||||
|
||||
def has_nc(self):
|
||||
"""
|
||||
Nc is the client used to connect to the nmux multiplexer. It is provided by either the BSD netcat (recommended
|
||||
for better performance) or GNU netcat packages. Please check your distribution package manager for options.
|
||||
"""
|
||||
return self.command_is_runnable("nc --help")
|
||||
|
||||
def has_rtl_sdr(self):
|
||||
"""
|
||||
The rtl-sdr command is required to read I/Q data from an RTL SDR USB-Stick. It is available in most
|
||||
distribution package managers.
|
||||
"""
|
||||
return self.command_is_runnable("rtl_sdr --help")
|
||||
|
||||
def has_rx_tools(self):
|
||||
"""
|
||||
The rx_tools package can be used to interface with SDR devices compatible with SoapySDR. It is currently used
|
||||
to connect to SDRPlay devices. Please check the following pages for more details:
|
||||
|
||||
* [rx_tools GitHub page](https://github.com/rxseger/rx_tools)
|
||||
* [SoapySDR Project wiki](https://github.com/pothosware/SoapySDR/wiki)
|
||||
* [SDRPlay homepage](https://www.sdrplay.com/)
|
||||
"""
|
||||
return self.command_is_runnable("rx_sdr --help")
|
||||
|
||||
def has_hackrf_transfer(self):
|
||||
"""
|
||||
To use a HackRF, compile the HackRF host tools from its "stdout" branch:
|
||||
```
|
||||
git clone https://github.com/mossmann/hackrf/
|
||||
cd hackrf
|
||||
git fetch
|
||||
git checkout origin/stdout
|
||||
cd host
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DINSTALL_UDEV_RULES=ON
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
"""
|
||||
# TODO i don't have a hackrf, so somebody doublecheck this.
|
||||
# TODO also check if it has the stdout feature
|
||||
return self.command_is_runnable("hackrf_transfer --help")
|
||||
|
||||
def has_digiham(self):
|
||||
"""
|
||||
To use digital voice modes, the digiham package is required. You can find the package and installation
|
||||
instructions [here](https://github.com/jketterl/digiham).
|
||||
|
||||
Please note: there is close interaction between digiham and openwebrx, so older versions will probably not work.
|
||||
If you have an older verison of digiham installed, please update it along with openwebrx.
|
||||
As of now, we require version 0.2 of digiham.
|
||||
"""
|
||||
required_version = LooseVersion("0.2")
|
||||
|
||||
digiham_version_regex = re.compile("^digiham version (.*)$")
|
||||
|
||||
def check_digiham_version(command):
|
||||
try:
|
||||
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
|
||||
matches = digiham_version_regex.match(process.stdout.readline().decode())
|
||||
if matches is None:
|
||||
return False
|
||||
version = LooseVersion(matches.group(1))
|
||||
process.wait(1)
|
||||
return version >= required_version
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
return reduce(
|
||||
and_,
|
||||
map(
|
||||
check_digiham_version,
|
||||
[
|
||||
"rrc_filter",
|
||||
"ysf_decoder",
|
||||
"dmr_decoder",
|
||||
"mbe_synthesizer",
|
||||
"gfsk_demodulator",
|
||||
"digitalvoice_filter",
|
||||
],
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
def _check_connector(self, command):
|
||||
required_version = LooseVersion("0.1")
|
||||
|
||||
owrx_connector_version_regex = re.compile("^owrx-connector version (.*)$")
|
||||
|
||||
try:
|
||||
process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE)
|
||||
matches = owrx_connector_version_regex.match(process.stdout.readline().decode())
|
||||
if matches is None:
|
||||
return False
|
||||
version = LooseVersion(matches.group(1))
|
||||
process.wait(1)
|
||||
return version >= required_version
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def has_rtl_connector(self):
|
||||
"""
|
||||
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
|
||||
frequency switching, uses less CPU and can even provide more stability in some cases.
|
||||
|
||||
You can get it here: https://github.com/jketterl/owrx_connector
|
||||
"""
|
||||
return self._check_connector("rtl_connector")
|
||||
|
||||
def has_soapy_connector(self):
|
||||
"""
|
||||
The owrx_connector package offers direct interfacing between your hardware and openwebrx. It allows quicker
|
||||
frequency switching, uses less CPU and can even provide more stability in some cases.
|
||||
|
||||
You can get it here: https://github.com/jketterl/owrx_connector
|
||||
"""
|
||||
return self._check_connector("soapy_connector")
|
||||
|
||||
def has_dsd(self):
|
||||
"""
|
||||
The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version
|
||||
modified by F4EXB that provides stdin/stdout support. You can find it [here](https://github.com/f4exb/dsd).
|
||||
"""
|
||||
return self.command_is_runnable("dsd")
|
||||
|
||||
def has_sox(self):
|
||||
"""
|
||||
The sox audio library is used to convert between the typical 8 kHz audio sampling rate used by digital modes and
|
||||
the audio sampling rate requested by the client.
|
||||
|
||||
It is available for most distributions through the respective package manager.
|
||||
"""
|
||||
return self.command_is_runnable("sox")
|
||||
|
||||
def has_direwolf(self):
|
||||
return self.command_is_runnable("direwolf --help")
|
||||
|
||||
def has_airspy_rx(self):
|
||||
"""
|
||||
In order to use an Airspy Receiver, you need to install the airspy_rx receiver software.
|
||||
"""
|
||||
return self.command_is_runnable("airspy_rx --help")
|
||||
|
||||
def has_wsjtx(self):
|
||||
"""
|
||||
To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the
|
||||
[WSJT-X homepage](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) for ready-made packages or instructions
|
||||
on how to build from source.
|
||||
"""
|
||||
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)
|
76
owrx/http.py
@ -1,76 +0,0 @@
|
||||
from owrx.controllers import (
|
||||
StatusController,
|
||||
IndexController,
|
||||
OwrxAssetsController,
|
||||
WebSocketController,
|
||||
MapController,
|
||||
FeatureController,
|
||||
ApiController,
|
||||
MetricsController,
|
||||
AprsSymbolsController,
|
||||
)
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import re
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class RequestHandler(BaseHTTPRequestHandler):
|
||||
def __init__(self, request, client_address, server):
|
||||
self.router = Router()
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
logger.debug("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args)
|
||||
|
||||
def do_GET(self):
|
||||
self.router.route(self)
|
||||
|
||||
|
||||
class Request(object):
|
||||
def __init__(self, query=None, matches=None):
|
||||
self.query = query
|
||||
self.matches = matches
|
||||
|
||||
|
||||
class Router(object):
|
||||
mappings = [
|
||||
{"route": "/", "controller": IndexController},
|
||||
{"route": "/status", "controller": StatusController},
|
||||
{"regex": "/static/(.+)", "controller": OwrxAssetsController},
|
||||
{"regex": "/aprs-symbols/(.+)", "controller": AprsSymbolsController},
|
||||
{"route": "/ws/", "controller": WebSocketController},
|
||||
{"regex": "(/favicon.ico)", "controller": OwrxAssetsController},
|
||||
# backwards compatibility for the sdr.hu portal
|
||||
{"regex": "/(gfx/openwebrx-avatar.png)", "controller": OwrxAssetsController},
|
||||
{"route": "/map", "controller": MapController},
|
||||
{"route": "/features", "controller": FeatureController},
|
||||
{"route": "/api/features", "controller": ApiController},
|
||||
{"route": "/metrics", "controller": MetricsController},
|
||||
]
|
||||
|
||||
def find_controller(self, path):
|
||||
for m in Router.mappings:
|
||||
if "route" in m:
|
||||
if m["route"] == path:
|
||||
return (m["controller"], None)
|
||||
if "regex" in m:
|
||||
regex = re.compile(m["regex"])
|
||||
matches = regex.match(path)
|
||||
if matches:
|
||||
return (m["controller"], matches)
|
||||
|
||||
def route(self, handler):
|
||||
url = urlparse(handler.path)
|
||||
res = self.find_controller(url.path)
|
||||
if res is not None:
|
||||
(controller, matches) = res
|
||||
query = parse_qs(url.query)
|
||||
request = Request(query, matches)
|
||||
controller(handler, request).handle_request()
|
||||
else:
|
||||
handler.send_error(404, "Not Found", "The page you requested could not be found.")
|
104
owrx/kiss.py
@ -1,104 +0,0 @@
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
import random
|
||||
from owrx.config import PropertyManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FEND = 0xC0
|
||||
FESC = 0xDB
|
||||
TFEND = 0xDC
|
||||
TFESC = 0xDD
|
||||
|
||||
|
||||
class DirewolfConfig(object):
|
||||
def getConfig(self, port, is_service):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
|
||||
config = """
|
||||
ACHANNELS 1
|
||||
|
||||
CHANNEL 0
|
||||
MYCALL {callsign}
|
||||
MODEM 1200
|
||||
|
||||
KISSPORT {port}
|
||||
AGWPORT off
|
||||
""".format(
|
||||
port=port, callsign=pm["aprs_callsign"]
|
||||
)
|
||||
|
||||
if is_service and pm["aprs_igate_enabled"]:
|
||||
config += """
|
||||
IGSERVER {server}
|
||||
IGLOGIN {callsign} {password}
|
||||
""".format(
|
||||
server=pm["aprs_igate_server"], callsign=pm["aprs_callsign"], password=pm["aprs_igate_password"]
|
||||
)
|
||||
|
||||
if pm["aprs_igate_beacon"]:
|
||||
(lat, lon) = pm["receiver_gps"]
|
||||
lat = "{0}^{1:.2f}{2}".format(int(lat), (lat - int(lat)) * 60, "N" if lat > 0 else "S")
|
||||
lon = "{0}^{1:.2f}{2}".format(int(lon), (lon - int(lon)) * 60, "E" if lon > 0 else "W")
|
||||
|
||||
config += """
|
||||
PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=R lat={lat} long={lon} comment="OpenWebRX APRS gateway"
|
||||
""".format(
|
||||
lat=lat, lon=lon
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
class KissClient(object):
|
||||
@staticmethod
|
||||
def getFreePort():
|
||||
# direwolf has some strange hardcoded port ranges
|
||||
while True:
|
||||
try:
|
||||
port = random.randrange(1024, 49151)
|
||||
# test if port is available for use
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("localhost", port))
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.close()
|
||||
return port
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def __init__(self, port):
|
||||
time.sleep(1)
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.connect(("localhost", port))
|
||||
|
||||
def read(self):
|
||||
return self.socket.recv(1)
|
||||
|
||||
|
||||
class KissDeframer(object):
|
||||
def __init__(self):
|
||||
self.escaped = False
|
||||
self.buf = bytearray()
|
||||
|
||||
def parse(self, input):
|
||||
frames = []
|
||||
for b in input:
|
||||
if b == FESC:
|
||||
self.escaped = True
|
||||
elif self.escaped:
|
||||
if b == TFEND:
|
||||
self.buf.append(FEND)
|
||||
elif b == TFESC:
|
||||
self.buf.append(FESC)
|
||||
else:
|
||||
logger.warning("invalid escape char: %s", str(input[0]))
|
||||
self.escaped = False
|
||||
elif input[0] == FEND:
|
||||
# data frames start with 0x00
|
||||
if len(self.buf) > 1 and self.buf[0] == 0x00:
|
||||
frames += [self.buf[1:]]
|
||||
self.buf = bytearray()
|
||||
else:
|
||||
self.buf.append(b)
|
||||
return frames
|
@ -1,24 +0,0 @@
|
||||
class Locator(object):
|
||||
@staticmethod
|
||||
def fromCoordinates(coordinates, depth=3):
|
||||
|
||||
lat, lon = coordinates
|
||||
|
||||
lon = lon + 180
|
||||
lat = lat + 90
|
||||
|
||||
res = ""
|
||||
res += chr(65 + int(lon / 20))
|
||||
res += chr(65 + int(lat / 10))
|
||||
if depth >= 2:
|
||||
lon = lon % 20
|
||||
lat = lat % 10
|
||||
res += str(int(lon / 2))
|
||||
res += str(int(lat))
|
||||
if depth >= 3:
|
||||
lon = lon % 2
|
||||
lat = lat % 1
|
||||
res += chr(97 + int(lon * 12))
|
||||
res += chr(97 + int(lat * 24))
|
||||
|
||||
return res
|
134
owrx/map.py
@ -1,134 +0,0 @@
|
||||
from datetime import datetime, timedelta
|
||||
import threading, time
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.bands import Band
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Location(object):
|
||||
def __dict__(self):
|
||||
return {}
|
||||
|
||||
|
||||
class Map(object):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if Map.sharedInstance is None:
|
||||
Map.sharedInstance = Map()
|
||||
return Map.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
self.positions = {}
|
||||
self.positionsLock = threading.Lock()
|
||||
|
||||
def removeLoop():
|
||||
loops = 0
|
||||
while True:
|
||||
try:
|
||||
self.removeOldPositions()
|
||||
except Exception:
|
||||
logger.exception("error while removing old map positions")
|
||||
loops += 1
|
||||
# rebuild the positions dictionary every once in a while, it consumes lots of memory otherwise
|
||||
if loops == 60:
|
||||
try:
|
||||
self.rebuildPositions()
|
||||
except Exception:
|
||||
logger.exception("error while rebuilding positions")
|
||||
loops = 0
|
||||
time.sleep(60)
|
||||
|
||||
threading.Thread(target=removeLoop, daemon=True).start()
|
||||
super().__init__()
|
||||
|
||||
def broadcast(self, update):
|
||||
for c in self.clients:
|
||||
c.write_update(update)
|
||||
|
||||
def addClient(self, client):
|
||||
self.clients.append(client)
|
||||
client.write_update(
|
||||
[
|
||||
{
|
||||
"callsign": callsign,
|
||||
"location": record["location"].__dict__(),
|
||||
"lastseen": record["updated"].timestamp() * 1000,
|
||||
"mode": record["mode"],
|
||||
"band": record["band"].getName() if record["band"] is not None else None,
|
||||
}
|
||||
for (callsign, record) in self.positions.items()
|
||||
]
|
||||
)
|
||||
|
||||
def removeClient(self, client):
|
||||
try:
|
||||
self.clients.remove(client)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def updateLocation(self, callsign, loc: Location, mode: str, band: Band = None):
|
||||
ts = datetime.now()
|
||||
with self.positionsLock:
|
||||
self.positions[callsign] = {"location": loc, "updated": ts, "mode": mode, "band": band}
|
||||
self.broadcast(
|
||||
[
|
||||
{
|
||||
"callsign": callsign,
|
||||
"location": loc.__dict__(),
|
||||
"lastseen": ts.timestamp() * 1000,
|
||||
"mode": mode,
|
||||
"band": band.getName() if band is not None else None,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def touchLocation(self, callsign):
|
||||
# not implemented on the client side yet, so do not use!
|
||||
ts = datetime.now()
|
||||
with self.positionsLock:
|
||||
if callsign in self.positions:
|
||||
self.positions[callsign]["updated"] = ts
|
||||
self.broadcast([{"callsign": callsign, "lastseen": ts.timestamp() * 1000}])
|
||||
|
||||
def removeLocation(self, callsign):
|
||||
with self.positionsLock:
|
||||
del self.positions[callsign]
|
||||
# TODO broadcast removal to clients
|
||||
|
||||
def removeOldPositions(self):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
retention = timedelta(seconds=pm["map_position_retention_time"])
|
||||
cutoff = datetime.now() - retention
|
||||
|
||||
to_be_removed = [callsign for (callsign, pos) in self.positions.items() if pos["updated"] < cutoff]
|
||||
for callsign in to_be_removed:
|
||||
self.removeLocation(callsign)
|
||||
|
||||
def rebuildPositions(self):
|
||||
with self.positionsLock:
|
||||
p = {key: value for key, value in self.positions.items()}
|
||||
self.positions = p
|
||||
|
||||
|
||||
class LatLngLocation(Location):
|
||||
def __init__(self, lat: float, lon: float):
|
||||
self.lat = lat
|
||||
self.lon = lon
|
||||
|
||||
def __dict__(self):
|
||||
res = {"type": "latlon", "lat": self.lat, "lon": self.lon}
|
||||
return res
|
||||
|
||||
|
||||
class LocatorLocation(Location):
|
||||
def __init__(self, locator: str):
|
||||
self.locator = locator
|
||||
|
||||
def __dict__(self):
|
||||
return {"type": "locator", "locator": self.locator}
|
108
owrx/meta.py
@ -1,108 +0,0 @@
|
||||
from owrx.config import PropertyManager
|
||||
from urllib import request
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import threading
|
||||
from owrx.map import Map, LatLngLocation
|
||||
from owrx.bands import Bandplan
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DmrCache(object):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if DmrCache.sharedInstance is None:
|
||||
DmrCache.sharedInstance = DmrCache()
|
||||
return DmrCache.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
self.cacheTimeout = timedelta(seconds=86400)
|
||||
|
||||
def isValid(self, key):
|
||||
if not key in self.cache:
|
||||
return False
|
||||
entry = self.cache[key]
|
||||
return entry["timestamp"] + self.cacheTimeout > datetime.now()
|
||||
|
||||
def put(self, key, value):
|
||||
self.cache[key] = {"timestamp": datetime.now(), "data": value}
|
||||
|
||||
def get(self, key):
|
||||
if not self.isValid(key):
|
||||
return None
|
||||
return self.cache[key]["data"]
|
||||
|
||||
|
||||
class DmrMetaEnricher(object):
|
||||
def __init__(self):
|
||||
self.threads = {}
|
||||
|
||||
def downloadRadioIdData(self, id):
|
||||
cache = DmrCache.getSharedInstance()
|
||||
try:
|
||||
logger.debug("requesting DMR metadata for id=%s", id)
|
||||
res = request.urlopen("https://www.radioid.net/api/dmr/user/?id={0}".format(id), timeout=30).read()
|
||||
data = json.loads(res.decode("utf-8"))
|
||||
cache.put(id, data)
|
||||
except json.JSONDecodeError:
|
||||
cache.put(id, None)
|
||||
del self.threads[id]
|
||||
|
||||
def enrich(self, meta):
|
||||
if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]:
|
||||
return None
|
||||
if not "source" in meta:
|
||||
return None
|
||||
id = meta["source"]
|
||||
cache = DmrCache.getSharedInstance()
|
||||
if not cache.isValid(id):
|
||||
if not id in self.threads:
|
||||
self.threads[id] = threading.Thread(target=self.downloadRadioIdData, args=[id], daemon=True)
|
||||
self.threads[id].start()
|
||||
return None
|
||||
data = cache.get(id)
|
||||
if "count" in data and data["count"] > 0 and "results" in data:
|
||||
return data["results"][0]
|
||||
return None
|
||||
|
||||
|
||||
class YsfMetaEnricher(object):
|
||||
def __init__(self, parser):
|
||||
self.parser = parser
|
||||
|
||||
def enrich(self, meta):
|
||||
if "source" in meta and "lat" in meta and "lon" in meta:
|
||||
# TODO parsing the float values should probably happen earlier
|
||||
loc = LatLngLocation(float(meta["lat"]), float(meta["lon"]))
|
||||
Map.getSharedInstance().updateLocation(meta["source"], loc, "YSF", self.parser.getBand())
|
||||
return None
|
||||
|
||||
|
||||
class MetaParser(object):
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.enrichers = {"DMR": DmrMetaEnricher(), "YSF": YsfMetaEnricher(self)}
|
||||
self.band = None
|
||||
|
||||
def setDialFrequency(self, freq):
|
||||
self.band = Bandplan.getSharedInstance().findBand(freq)
|
||||
|
||||
def getBand(self):
|
||||
return self.band
|
||||
|
||||
def parse(self, meta):
|
||||
fields = meta.split(";")
|
||||
meta = {v[0]: "".join(v[1:]) for v in map(lambda x: x.split(":"), fields) if v[0] != ""}
|
||||
|
||||
if "protocol" in meta:
|
||||
protocol = meta["protocol"]
|
||||
if protocol in self.enrichers:
|
||||
additional_data = self.enrichers[protocol].enrich(meta)
|
||||
if additional_data is not None:
|
||||
meta["additional"] = additional_data
|
||||
self.handler.write_metadata(meta)
|
@ -1,65 +0,0 @@
|
||||
import threading
|
||||
|
||||
|
||||
class Metric(object):
|
||||
def getValue(self):
|
||||
return 0
|
||||
|
||||
|
||||
class CounterMetric(Metric):
|
||||
def __init__(self):
|
||||
self.counter = 0
|
||||
|
||||
def inc(self, increment=1):
|
||||
self.counter += increment
|
||||
|
||||
def getValue(self):
|
||||
return {"count": self.counter}
|
||||
|
||||
|
||||
class DirectMetric(Metric):
|
||||
def __init__(self, getter):
|
||||
self.getter = getter
|
||||
|
||||
def getValue(self):
|
||||
return self.getter()
|
||||
|
||||
|
||||
class Metrics(object):
|
||||
sharedInstance = None
|
||||
creationLock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
with Metrics.creationLock:
|
||||
if Metrics.sharedInstance is None:
|
||||
Metrics.sharedInstance = Metrics()
|
||||
return Metrics.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.metrics = {}
|
||||
|
||||
def addMetric(self, name, metric):
|
||||
self.metrics[name] = metric
|
||||
|
||||
def hasMetric(self, name):
|
||||
return name in self.metrics
|
||||
|
||||
def getMetric(self, name):
|
||||
if not self.hasMetric(name):
|
||||
return None
|
||||
return self.metrics[name]
|
||||
|
||||
def getMetrics(self):
|
||||
result = {}
|
||||
|
||||
for (key, metric) in self.metrics.items():
|
||||
partial = result
|
||||
keys = key.split(".")
|
||||
for keypart in keys[0:-1]:
|
||||
if not keypart in partial:
|
||||
partial[keypart] = {}
|
||||
partial = partial[keypart]
|
||||
partial[keys[-1]] = metric.getValue()
|
||||
|
||||
return result
|
@ -1,225 +0,0 @@
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import socket
|
||||
from functools import reduce
|
||||
from operator import and_
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.version import openwebrx_version
|
||||
from owrx.locator import Locator
|
||||
from owrx.metrics import Metrics, CounterMetric
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PskReporterDummy(object):
|
||||
"""
|
||||
used in place of the PskReporter when reporting is disabled.
|
||||
does nothing.
|
||||
"""
|
||||
|
||||
def spot(self, spot):
|
||||
pass
|
||||
|
||||
def cancelTimer(self):
|
||||
pass
|
||||
|
||||
|
||||
class PskReporter(object):
|
||||
sharedInstance = None
|
||||
creationLock = threading.Lock()
|
||||
interval = 300
|
||||
supportedModes = ["FT8", "FT4", "JT9", "JT65"]
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
with PskReporter.creationLock:
|
||||
if PskReporter.sharedInstance is None:
|
||||
if PropertyManager.getSharedInstance()["pskreporter_enabled"]:
|
||||
PskReporter.sharedInstance = PskReporter()
|
||||
else:
|
||||
PskReporter.sharedInstance = PskReporterDummy()
|
||||
return PskReporter.sharedInstance
|
||||
|
||||
@staticmethod
|
||||
def stop():
|
||||
if PskReporter.sharedInstance:
|
||||
PskReporter.sharedInstance.cancelTimer()
|
||||
|
||||
def __init__(self):
|
||||
self.spots = []
|
||||
self.spotLock = threading.Lock()
|
||||
self.uploader = Uploader()
|
||||
self.timer = None
|
||||
metrics = Metrics.getSharedInstance()
|
||||
self.dupeCounter = CounterMetric()
|
||||
metrics.addMetric("pskreporter.duplicates", self.dupeCounter)
|
||||
self.spotCounter = CounterMetric()
|
||||
metrics.addMetric("pskreporter.spots", self.spotCounter)
|
||||
|
||||
def scheduleNextUpload(self):
|
||||
if self.timer:
|
||||
return
|
||||
delay = PskReporter.interval + random.uniform(0, 30)
|
||||
logger.debug("scheduling next pskreporter upload in %f seconds", delay)
|
||||
self.timer = threading.Timer(delay, self.upload)
|
||||
self.timer.start()
|
||||
|
||||
def spotEquals(self, s1, s2):
|
||||
keys = ["callsign", "timestamp", "locator", "mode", "msg"]
|
||||
|
||||
return reduce(and_, map(lambda key: s1[key] == s2[key], keys))
|
||||
|
||||
def spot(self, spot):
|
||||
if not spot["mode"] in PskReporter.supportedModes:
|
||||
return
|
||||
with self.spotLock:
|
||||
if any(x for x in self.spots if self.spotEquals(spot, x)):
|
||||
# dupe
|
||||
self.dupeCounter.inc()
|
||||
else:
|
||||
self.spotCounter.inc()
|
||||
self.spots.append(spot)
|
||||
self.scheduleNextUpload()
|
||||
|
||||
def upload(self):
|
||||
try:
|
||||
with self.spotLock:
|
||||
spots = self.spots
|
||||
self.spots = []
|
||||
|
||||
if spots:
|
||||
self.uploader.upload(spots)
|
||||
except Exception:
|
||||
logger.exception("Failed to upload spots")
|
||||
|
||||
self.timer = None
|
||||
self.scheduleNextUpload()
|
||||
|
||||
def cancelTimer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
|
||||
class Uploader(object):
|
||||
receieverDelimiter = [0x99, 0x92]
|
||||
senderDelimiter = [0x99, 0x93]
|
||||
|
||||
def __init__(self):
|
||||
self.sequence = 0
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
def upload(self, spots):
|
||||
logger.debug("uploading %i spots", len(spots))
|
||||
for packet in self.getPackets(spots):
|
||||
self.socket.sendto(packet, ("report.pskreporter.info", 4739))
|
||||
|
||||
def getPackets(self, spots):
|
||||
encoded = [self.encodeSpot(spot) for spot in spots]
|
||||
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i : i + n]
|
||||
|
||||
rHeader = self.getReceiverInformationHeader()
|
||||
rInfo = self.getReceiverInformation()
|
||||
sHeader = self.getSenderInformationHeader()
|
||||
|
||||
packets = []
|
||||
# 50 seems to be a safe bet
|
||||
for chunk in chunks(encoded, 50):
|
||||
sInfo = self.getSenderInformation(chunk)
|
||||
length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo)
|
||||
header = self.getHeader(length)
|
||||
packets.append(header + rHeader + sHeader + rInfo + sInfo)
|
||||
|
||||
return packets
|
||||
|
||||
def getHeader(self, length):
|
||||
self.sequence += 1
|
||||
return bytes(
|
||||
# protocol version
|
||||
[0x00, 0x0A]
|
||||
+ list(length.to_bytes(2, "big"))
|
||||
+ list(int(time.time()).to_bytes(4, "big"))
|
||||
+ list(self.sequence.to_bytes(4, "big"))
|
||||
+ list((id(self) & 0xFFFFFFFF).to_bytes(4, "big"))
|
||||
)
|
||||
|
||||
def encodeString(self, s):
|
||||
return [len(s)] + list(s.encode("utf-8"))
|
||||
|
||||
def encodeSpot(self, spot):
|
||||
return bytes(
|
||||
self.encodeString(spot["callsign"])
|
||||
+ list(spot["freq"].to_bytes(4, "big"))
|
||||
+ list(int(spot["db"]).to_bytes(1, "big", signed=True))
|
||||
+ self.encodeString(spot["mode"])
|
||||
+ self.encodeString(spot["locator"])
|
||||
# informationsource. 1 means "automatically extracted
|
||||
+ [0x01]
|
||||
+ list(int(spot["timestamp"] / 1000).to_bytes(4, "big"))
|
||||
)
|
||||
|
||||
def getReceiverInformationHeader(self):
|
||||
return bytes(
|
||||
# id, length
|
||||
[0x00, 0x03, 0x00, 0x24]
|
||||
+ Uploader.receieverDelimiter
|
||||
# number of fields
|
||||
+ [0x00, 0x03, 0x00, 0x00]
|
||||
# receiverCallsign
|
||||
+ [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
|
||||
# receiverLocator
|
||||
+ [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
|
||||
# decodingSoftware
|
||||
+ [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
|
||||
# padding
|
||||
+ [0x00, 0x00]
|
||||
)
|
||||
|
||||
def getReceiverInformation(self):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
callsign = pm["pskreporter_callsign"]
|
||||
locator = Locator.fromCoordinates(pm["receiver_gps"])
|
||||
decodingSoftware = "OpenWebRX " + openwebrx_version
|
||||
body = [b for s in [callsign, locator, decodingSoftware] for b in self.encodeString(s)]
|
||||
body = self.pad(body, 4)
|
||||
body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body)
|
||||
return body
|
||||
|
||||
def getSenderInformationHeader(self):
|
||||
return bytes(
|
||||
# id, length
|
||||
[0x00, 0x02, 0x00, 0x3C]
|
||||
+ Uploader.senderDelimiter
|
||||
# number of fields
|
||||
+ [0x00, 0x07]
|
||||
# senderCallsign
|
||||
+ [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
|
||||
# frequency
|
||||
+ [0x80, 0x05, 0x00, 0x04, 0x00, 0x00, 0x76, 0x8F]
|
||||
# sNR
|
||||
+ [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F]
|
||||
# mode
|
||||
+ [0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
|
||||
# senderLocator
|
||||
+ [0x80, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F]
|
||||
# informationSource
|
||||
+ [0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F]
|
||||
# flowStartSeconds
|
||||
+ [0x00, 0x96, 0x00, 0x04]
|
||||
)
|
||||
|
||||
def getSenderInformation(self, chunk):
|
||||
sInfo = self.padBytes(b"".join(chunk), 4)
|
||||
sInfoLength = len(sInfo) + 4
|
||||
return bytes(Uploader.senderDelimiter) + sInfoLength.to_bytes(2, "big") + sInfo
|
||||
|
||||
def pad(self, b, l):
|
||||
return b + [0x00 for _ in range(0, -1 * len(b) % l)]
|
||||
|
||||
def padBytes(self, b, l):
|
||||
return b + bytes([0x00 for _ in range(0, -1 * len(b) % l)])
|
@ -1,39 +0,0 @@
|
||||
import threading
|
||||
import subprocess
|
||||
import time
|
||||
from owrx.config import PropertyManager
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SdrHuUpdater(threading.Thread):
|
||||
def __init__(self):
|
||||
self.doRun = True
|
||||
super().__init__(daemon=True)
|
||||
|
||||
def update(self):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
cmd = 'wget --timeout=15 -4qO- https://sdr.hu/update --post-data "url=http://{server_hostname}:{web_port}&apikey={sdrhu_key}" 2>&1'.format(
|
||||
**pm.__dict__()
|
||||
)
|
||||
logger.debug(cmd)
|
||||
returned = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
|
||||
returned = returned[0].decode("utf-8")
|
||||
if "UPDATE:" in returned:
|
||||
retrytime_mins = 20
|
||||
value = returned.split("UPDATE:")[1].split("\n", 1)[0]
|
||||
if value.startswith("SUCCESS"):
|
||||
logger.info("Update succeeded!")
|
||||
else:
|
||||
logger.warning("Update failed, your receiver cannot be listed on sdr.hu! Reason: %s", value)
|
||||
else:
|
||||
retrytime_mins = 2
|
||||
logger.warning("wget failed while updating, your receiver cannot be listed on sdr.hu!")
|
||||
return retrytime_mins
|
||||
|
||||
def run(self):
|
||||
while self.doRun:
|
||||
retrytime_mins = self.update()
|
||||
time.sleep(60 * retrytime_mins)
|
402
owrx/service.py
@ -1,402 +0,0 @@
|
||||
import threading
|
||||
from owrx.socket import getAvailablePort
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from owrx.source import SdrService, SdrSource
|
||||
from owrx.bands import Bandplan
|
||||
from csdr import dsp, output
|
||||
from owrx.wsjt import WsjtParser
|
||||
from owrx.aprs import AprsParser
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.source import Resampler
|
||||
from owrx.feature import FeatureDetector
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServiceOutput(output):
|
||||
def __init__(self, frequency):
|
||||
self.frequency = frequency
|
||||
|
||||
def getParser(self):
|
||||
# abstract method; implement in subclasses
|
||||
pass
|
||||
|
||||
def receive_output(self, t, read_fn):
|
||||
parser = self.getParser()
|
||||
parser.setDialFrequency(self.frequency)
|
||||
target = self.pump(read_fn, parser.parse)
|
||||
threading.Thread(target=target).start()
|
||||
|
||||
|
||||
class WsjtServiceOutput(ServiceOutput):
|
||||
def getParser(self):
|
||||
return WsjtParser(WsjtHandler())
|
||||
|
||||
def supports_type(self, t):
|
||||
return t == "wsjt_demod"
|
||||
|
||||
|
||||
class AprsServiceOutput(ServiceOutput):
|
||||
def getParser(self):
|
||||
return AprsParser(AprsHandler())
|
||||
|
||||
def supports_type(self, t):
|
||||
return t == "packet_demod"
|
||||
|
||||
|
||||
class ScheduleEntry(object):
|
||||
def __init__(self, startTime, endTime, profile):
|
||||
self.startTime = startTime
|
||||
self.endTime = endTime
|
||||
self.profile = profile
|
||||
|
||||
def isCurrent(self, time):
|
||||
if self.startTime < self.endTime:
|
||||
return self.startTime <= time < self.endTime
|
||||
else:
|
||||
return self.startTime <= time or time < self.endTime
|
||||
|
||||
def getProfile(self):
|
||||
return self.profile
|
||||
|
||||
def getScheduledEnd(self):
|
||||
now = datetime.utcnow()
|
||||
end = now.combine(date=now.date(), time=self.endTime)
|
||||
while end < now:
|
||||
end += timedelta(days=1)
|
||||
return end
|
||||
|
||||
def getNextActivation(self):
|
||||
now = datetime.utcnow()
|
||||
start = now.combine(date=now.date(), time=self.startTime)
|
||||
while start < now:
|
||||
start += timedelta(days=1)
|
||||
return start
|
||||
|
||||
|
||||
class Schedule(object):
|
||||
@staticmethod
|
||||
def parse(scheduleDict):
|
||||
entries = []
|
||||
for time, profile in scheduleDict.items():
|
||||
if len(time) != 9:
|
||||
logger.warning("invalid schedule spec: %s", time)
|
||||
continue
|
||||
|
||||
startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time()
|
||||
endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time()
|
||||
entries.append(ScheduleEntry(startTime, endTime, profile))
|
||||
return Schedule(entries)
|
||||
|
||||
def __init__(self, entries):
|
||||
self.entries = entries
|
||||
|
||||
def getCurrentEntry(self):
|
||||
current = [p for p in self.entries if p.isCurrent(datetime.utcnow().time())]
|
||||
if current:
|
||||
return current[0]
|
||||
return None
|
||||
|
||||
def getNextEntry(self):
|
||||
s = sorted(self.entries, key=lambda e: e.getNextActivation())
|
||||
if s:
|
||||
return s[0]
|
||||
return None
|
||||
|
||||
|
||||
class ServiceScheduler(object):
|
||||
def __init__(self, source, schedule):
|
||||
self.source = source
|
||||
self.schedule = Schedule.parse(schedule)
|
||||
self.source.addClient(self)
|
||||
self.selectionTimer = None
|
||||
self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
|
||||
self.scheduleSelection()
|
||||
|
||||
def shutdown(self):
|
||||
self.cancelTimer()
|
||||
self.source.removeClient(self)
|
||||
|
||||
def scheduleSelection(self, time=None):
|
||||
seconds = 10
|
||||
if time is not None:
|
||||
delta = time - datetime.utcnow()
|
||||
seconds = delta.total_seconds()
|
||||
self.cancelTimer()
|
||||
self.selectionTimer = threading.Timer(seconds, self.selectProfile)
|
||||
self.selectionTimer.start()
|
||||
|
||||
def cancelTimer(self):
|
||||
if self.selectionTimer:
|
||||
self.selectionTimer.cancel()
|
||||
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_BACKGROUND
|
||||
|
||||
def onStateChange(self, state):
|
||||
if state == SdrSource.STATE_STOPPING:
|
||||
self.scheduleSelection()
|
||||
elif state == SdrSource.STATE_FAILED:
|
||||
self.cancelTimer()
|
||||
|
||||
def onBusyStateChange(self, state):
|
||||
if state == SdrSource.BUSYSTATE_IDLE:
|
||||
self.scheduleSelection()
|
||||
|
||||
def onFrequencyChange(self, name, value):
|
||||
self.scheduleSelection()
|
||||
|
||||
def selectProfile(self):
|
||||
if self.source.hasClients(SdrSource.CLIENT_USER):
|
||||
logger.debug("source has active users; not touching")
|
||||
return
|
||||
logger.debug("source seems to be idle, selecting profile for background services")
|
||||
entry = self.schedule.getCurrentEntry()
|
||||
|
||||
if entry is None:
|
||||
logger.debug("schedule did not return a profile. checking next entry...")
|
||||
nextEntry = self.schedule.getNextEntry()
|
||||
if nextEntry is not None:
|
||||
self.scheduleSelection(nextEntry.getNextActivation())
|
||||
return
|
||||
|
||||
logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd())
|
||||
self.scheduleSelection(entry.getScheduledEnd())
|
||||
|
||||
try:
|
||||
self.source.activateProfile(entry.getProfile())
|
||||
self.source.start()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class ServiceHandler(object):
|
||||
def __init__(self, source):
|
||||
self.lock = threading.Lock()
|
||||
self.services = []
|
||||
self.source = source
|
||||
self.startupTimer = None
|
||||
self.source.addClient(self)
|
||||
props = self.source.getProps()
|
||||
props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange)
|
||||
if self.source.isAvailable():
|
||||
self.scheduleServiceStartup()
|
||||
self.scheduler = None
|
||||
if "schedule" in props:
|
||||
self.scheduler = ServiceScheduler(self.source, props["schedule"])
|
||||
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_INACTIVE
|
||||
|
||||
def onStateChange(self, state):
|
||||
if state == SdrSource.STATE_RUNNING:
|
||||
self.scheduleServiceStartup()
|
||||
elif state == SdrSource.STATE_STOPPING:
|
||||
logger.debug("sdr source becoming unavailable; stopping services.")
|
||||
self.stopServices()
|
||||
elif state == SdrSource.STATE_FAILED:
|
||||
logger.debug("sdr source failed; stopping services.")
|
||||
self.stopServices()
|
||||
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
def isSupported(self, mode):
|
||||
# TODO this should be in a more central place (the frontend also needs this)
|
||||
requirements = {
|
||||
"ft8": "wsjt-x",
|
||||
"ft4": "wsjt-x",
|
||||
"jt65": "wsjt-x",
|
||||
"jt9": "wsjt-x",
|
||||
"wspr": "wsjt-x",
|
||||
"packet": "packet",
|
||||
}
|
||||
fd = FeatureDetector()
|
||||
|
||||
# this looks overly complicated... but i'd like modes with no requirements to be always available without
|
||||
# being listed in the hash above
|
||||
unavailable = [mode for mode, req in requirements.items() if not fd.is_available(req)]
|
||||
configured = PropertyManager.getSharedInstance()["services_decoders"]
|
||||
available = [mode for mode in configured if mode not in unavailable]
|
||||
|
||||
return mode in available
|
||||
|
||||
def shutdown(self):
|
||||
self.stopServices()
|
||||
self.source.removeClient(self)
|
||||
if self.scheduler:
|
||||
self.scheduler.shutdown()
|
||||
|
||||
def stopServices(self):
|
||||
with self.lock:
|
||||
services = self.services
|
||||
self.services = []
|
||||
|
||||
for service in services:
|
||||
service.stop()
|
||||
|
||||
def onFrequencyChange(self, key, value):
|
||||
self.stopServices()
|
||||
if not self.source.isAvailable():
|
||||
return
|
||||
self.scheduleServiceStartup()
|
||||
|
||||
def scheduleServiceStartup(self):
|
||||
if self.startupTimer:
|
||||
self.startupTimer.cancel()
|
||||
self.startupTimer = threading.Timer(10, self.updateServices)
|
||||
self.startupTimer.start()
|
||||
|
||||
def updateServices(self):
|
||||
logger.debug("re-scheduling services due to sdr changes")
|
||||
self.stopServices()
|
||||
if not self.source.isAvailable():
|
||||
logger.debug("sdr source is unavailable")
|
||||
return
|
||||
cf = self.source.getProps()["center_freq"]
|
||||
sr = self.source.getProps()["samp_rate"]
|
||||
srh = sr / 2
|
||||
frequency_range = (cf - srh, cf + srh)
|
||||
|
||||
dials = [
|
||||
dial
|
||||
for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range)
|
||||
if self.isSupported(dial["mode"])
|
||||
]
|
||||
|
||||
if not dials:
|
||||
logger.debug("no services available")
|
||||
return
|
||||
|
||||
with self.lock:
|
||||
self.services = []
|
||||
|
||||
groups = self.optimizeResampling(dials, sr)
|
||||
if groups is None:
|
||||
for dial in dials:
|
||||
self.services.append(self.setupService(dial["mode"], dial["frequency"], self.source))
|
||||
else:
|
||||
for group in groups:
|
||||
frequencies = sorted([f["frequency"] for f in group])
|
||||
min = frequencies[0]
|
||||
max = frequencies[-1]
|
||||
cf = (min + max) / 2
|
||||
bw = max - min
|
||||
logger.debug("group center frequency: {0}, bandwidth: {1}".format(cf, bw))
|
||||
resampler_props = PropertyManager()
|
||||
resampler_props["center_freq"] = cf
|
||||
# TODO the + 24000 is a temporary fix since the resampling optimizer does not account for required bandwidths
|
||||
resampler_props["samp_rate"] = bw + 24000
|
||||
resampler = Resampler(resampler_props, getAvailablePort(), self.source)
|
||||
resampler.start()
|
||||
|
||||
for dial in group:
|
||||
self.services.append(self.setupService(dial["mode"], dial["frequency"], resampler))
|
||||
|
||||
# resampler goes in after the services since it must not be shutdown as long as the services are still running
|
||||
self.services.append(resampler)
|
||||
|
||||
def optimizeResampling(self, freqs, bandwidth):
|
||||
freqs = sorted(freqs, key=lambda f: f["frequency"])
|
||||
distances = [
|
||||
{"frequency": freqs[i]["frequency"], "distance": freqs[i + 1]["frequency"] - freqs[i]["frequency"]}
|
||||
for i in range(0, len(freqs) - 1)
|
||||
]
|
||||
|
||||
distances = [d for d in distances if d["distance"] > 0]
|
||||
|
||||
distances = sorted(distances, key=lambda f: f["distance"], reverse=True)
|
||||
|
||||
def calculate_usage(num_splits):
|
||||
splits = sorted([f["frequency"] for f in distances[0:num_splits]])
|
||||
previous = 0
|
||||
groups = []
|
||||
for split in splits:
|
||||
groups.append([f for f in freqs if previous < f["frequency"] <= split])
|
||||
previous = split
|
||||
groups.append([f for f in freqs if previous < f["frequency"]])
|
||||
|
||||
def get_bandwitdh(group):
|
||||
freqs = sorted([f["frequency"] for f in group])
|
||||
# the group will process the full BW once, plus the reduced BW once for each group member
|
||||
return bandwidth + len(group) * (freqs[-1] - freqs[0] + 24000)
|
||||
|
||||
total_bandwidth = sum([get_bandwitdh(group) for group in groups])
|
||||
return {"num_splits": num_splits, "total_bandwidth": total_bandwidth, "groups": groups}
|
||||
|
||||
usages = [calculate_usage(i) for i in range(0, len(freqs))]
|
||||
# another possible outcome might be that it's best not to resample at all. this is a special case.
|
||||
usages += [{"num_splits": None, "total_bandwidth": bandwidth * len(freqs), "groups": [freqs]}]
|
||||
results = sorted(usages, key=lambda f: f["total_bandwidth"])
|
||||
|
||||
for r in results:
|
||||
logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"]))
|
||||
|
||||
best = results[0]
|
||||
if best["num_splits"] is None:
|
||||
return None
|
||||
return best["groups"]
|
||||
|
||||
def setupService(self, mode, frequency, source):
|
||||
logger.debug("setting up service {0} on frequency {1}".format(mode, frequency))
|
||||
# TODO selecting outputs will need some more intelligence here
|
||||
if mode == "packet":
|
||||
output = AprsServiceOutput(frequency)
|
||||
else:
|
||||
output = WsjtServiceOutput(frequency)
|
||||
d = dsp(output)
|
||||
d.nc_port = source.getPort()
|
||||
d.set_offset_freq(frequency - source.getProps()["center_freq"])
|
||||
if mode == "packet":
|
||||
d.set_demodulator("nfm")
|
||||
d.set_bpf(-4000, 4000)
|
||||
elif mode == "wspr":
|
||||
d.set_demodulator("usb")
|
||||
# WSPR only samples between 1400 and 1600 Hz
|
||||
d.set_bpf(1350, 1650)
|
||||
else:
|
||||
d.set_demodulator("usb")
|
||||
d.set_bpf(0, 3000)
|
||||
d.set_secondary_demodulator(mode)
|
||||
d.set_audio_compression("none")
|
||||
d.set_samp_rate(source.getProps()["samp_rate"])
|
||||
d.set_service()
|
||||
d.start()
|
||||
return d
|
||||
|
||||
|
||||
class WsjtHandler(object):
|
||||
def write_wsjt_message(self, msg):
|
||||
pass
|
||||
|
||||
|
||||
class AprsHandler(object):
|
||||
def write_aprs_data(self, data):
|
||||
pass
|
||||
|
||||
|
||||
class Services(object):
|
||||
handlers = []
|
||||
|
||||
@staticmethod
|
||||
def start():
|
||||
if not PropertyManager.getSharedInstance()["services_enabled"]:
|
||||
return
|
||||
for source in SdrService.getSources().values():
|
||||
Services.handlers.append(ServiceHandler(source))
|
||||
|
||||
@staticmethod
|
||||
def stop():
|
||||
for handler in Services.handlers:
|
||||
handler.shutdown()
|
||||
Services.handlers = []
|
||||
|
||||
|
||||
class Service(object):
|
||||
pass
|
||||
|
||||
|
||||
class WsjtService(Service):
|
||||
pass
|
@ -1,10 +0,0 @@
|
||||
import socket
|
||||
|
||||
|
||||
def getAvailablePort():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("", 0))
|
||||
s.listen(1)
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return port
|
957
owrx/source.py
@ -1,957 +0,0 @@
|
||||
import subprocess
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.feature import FeatureDetector, UnknownFeatureException
|
||||
from owrx.meta import MetaParser
|
||||
from owrx.wsjt import WsjtParser
|
||||
from owrx.aprs import AprsParser
|
||||
from owrx.metrics import Metrics, DirectMetric
|
||||
from owrx.socket import getAvailablePort
|
||||
import threading
|
||||
import csdr
|
||||
import time
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import socket
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SdrService(object):
|
||||
sdrProps = None
|
||||
sources = {}
|
||||
lastPort = None
|
||||
|
||||
@staticmethod
|
||||
def getNextPort():
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
(start, end) = pm["iq_port_range"]
|
||||
if SdrService.lastPort is None:
|
||||
SdrService.lastPort = start
|
||||
else:
|
||||
SdrService.lastPort += 1
|
||||
if SdrService.lastPort > end:
|
||||
raise IndexError("no more available ports to start more sdrs")
|
||||
return SdrService.lastPort
|
||||
|
||||
@staticmethod
|
||||
def loadProps():
|
||||
if SdrService.sdrProps is None:
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
featureDetector = FeatureDetector()
|
||||
|
||||
def loadIntoPropertyManager(dict: dict):
|
||||
propertyManager = PropertyManager()
|
||||
for (name, value) in dict.items():
|
||||
propertyManager[name] = value
|
||||
return propertyManager
|
||||
|
||||
def sdrTypeAvailable(value):
|
||||
try:
|
||||
if not featureDetector.is_available(value["type"]):
|
||||
logger.error(
|
||||
'The RTL source type "{0}" is not available. please check requirements.'.format(
|
||||
value["type"]
|
||||
)
|
||||
)
|
||||
return False
|
||||
return True
|
||||
except UnknownFeatureException:
|
||||
logger.error(
|
||||
'The RTL source type "{0}" is invalid. Please check your configuration'.format(value["type"])
|
||||
)
|
||||
return False
|
||||
|
||||
# transform all dictionary items into PropertyManager object, filtering out unavailable ones
|
||||
SdrService.sdrProps = {
|
||||
name: loadIntoPropertyManager(value) for (name, value) in pm["sdrs"].items() if sdrTypeAvailable(value)
|
||||
}
|
||||
logger.info(
|
||||
"SDR sources loaded. Availables SDRs: {0}".format(
|
||||
", ".join(map(lambda x: x["name"], SdrService.sdrProps.values()))
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def getSource(id=None):
|
||||
SdrService.loadProps()
|
||||
sources = SdrService.getSources()
|
||||
if not sources:
|
||||
return None
|
||||
if id is None:
|
||||
# TODO: configure default sdr in config? right now it will pick the first one off the list.
|
||||
id = list(sources.keys())[0]
|
||||
|
||||
if not id in sources:
|
||||
return None
|
||||
return sources[id]
|
||||
|
||||
@staticmethod
|
||||
def getSources():
|
||||
SdrService.loadProps()
|
||||
for id in SdrService.sdrProps.keys():
|
||||
if not id in SdrService.sources:
|
||||
props = SdrService.sdrProps[id]
|
||||
className = "".join(x for x in props["type"].title() if x.isalnum()) + "Source"
|
||||
cls = getattr(sys.modules[__name__], className)
|
||||
SdrService.sources[id] = cls(id, props, SdrService.getNextPort())
|
||||
return {key: s for key, s in SdrService.sources.items() if not s.isFailed()}
|
||||
|
||||
|
||||
class SdrSource(object):
|
||||
STATE_STOPPED = 0
|
||||
STATE_STARTING = 1
|
||||
STATE_RUNNING = 2
|
||||
STATE_STOPPING = 3
|
||||
STATE_TUNING = 4
|
||||
STATE_FAILED = 5
|
||||
|
||||
BUSYSTATE_IDLE = 0
|
||||
BUSYSTATE_BUSY = 1
|
||||
|
||||
CLIENT_INACTIVE = 0
|
||||
CLIENT_BACKGROUND = 1
|
||||
CLIENT_USER = 2
|
||||
|
||||
def __init__(self, id, props, port):
|
||||
self.id = id
|
||||
self.props = props
|
||||
self.profile_id = None
|
||||
self.activateProfile()
|
||||
self.rtlProps = self.props.collect(*self.getEventNames()).defaults(PropertyManager.getSharedInstance())
|
||||
self.wireEvents()
|
||||
|
||||
self.port = port
|
||||
self.monitor = None
|
||||
self.clients = []
|
||||
self.spectrumClients = []
|
||||
self.spectrumThread = None
|
||||
self.process = None
|
||||
self.modificationLock = threading.Lock()
|
||||
self.failed = False
|
||||
self.state = SdrSource.STATE_STOPPED
|
||||
self.busyState = SdrSource.BUSYSTATE_IDLE
|
||||
|
||||
def getEventNames(self):
|
||||
return [
|
||||
"samp_rate",
|
||||
"nmux_memory",
|
||||
"center_freq",
|
||||
"ppm",
|
||||
"rf_gain",
|
||||
"lna_gain",
|
||||
"rf_amp",
|
||||
"antenna",
|
||||
"if_gain",
|
||||
"lfo_offset",
|
||||
]
|
||||
|
||||
def wireEvents(self):
|
||||
def restart(name, value):
|
||||
logger.debug("restarting sdr source due to property change: {0} changed to {1}".format(name, value))
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
self.rtlProps.wire(restart)
|
||||
|
||||
# override this in subclasses
|
||||
def getCommand(self):
|
||||
pass
|
||||
|
||||
# override this in subclasses, if necessary
|
||||
def getFormatConversion(self):
|
||||
return None
|
||||
|
||||
def activateProfile(self, profile_id=None):
|
||||
profiles = self.props["profiles"]
|
||||
if profile_id is None:
|
||||
profile_id = list(profiles.keys())[0]
|
||||
if profile_id == self.profile_id:
|
||||
return
|
||||
logger.debug("activating profile {0}".format(profile_id))
|
||||
self.profile_id = profile_id
|
||||
profile = profiles[profile_id]
|
||||
self.props["profile_id"] = profile_id
|
||||
for (key, value) in profile.items():
|
||||
# skip the name, that would overwrite the source name.
|
||||
if key == "name":
|
||||
continue
|
||||
self.props[key] = value
|
||||
|
||||
def getId(self):
|
||||
return self.id
|
||||
|
||||
def getProfileId(self):
|
||||
return self.profile_id
|
||||
|
||||
def getProfiles(self):
|
||||
return self.props["profiles"]
|
||||
|
||||
def getName(self):
|
||||
return self.props["name"]
|
||||
|
||||
def getProps(self):
|
||||
return self.props
|
||||
|
||||
def getPort(self):
|
||||
return self.port
|
||||
|
||||
def useNmux(self):
|
||||
return True
|
||||
|
||||
def getCommandValues(self):
|
||||
dict = self.rtlProps.collect(*self.getEventNames()).__dict__()
|
||||
if "lfo_offset" in dict and dict["lfo_offset"] is not None:
|
||||
dict["tuner_freq"] = dict["center_freq"] + dict["lfo_offset"]
|
||||
else:
|
||||
dict["tuner_freq"] = dict["center_freq"]
|
||||
return dict
|
||||
|
||||
def start(self):
|
||||
self.modificationLock.acquire()
|
||||
if self.monitor:
|
||||
self.modificationLock.release()
|
||||
return
|
||||
|
||||
props = self.rtlProps
|
||||
|
||||
cmd = self.getCommand().format(**self.getCommandValues())
|
||||
|
||||
format_conversion = self.getFormatConversion()
|
||||
if format_conversion is not None:
|
||||
cmd += " | " + format_conversion
|
||||
|
||||
if self.useNmux():
|
||||
nmux_bufcnt = nmux_bufsize = 0
|
||||
while nmux_bufsize < props["samp_rate"] / 4:
|
||||
nmux_bufsize += 4096
|
||||
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
|
||||
nmux_bufcnt += 1
|
||||
if nmux_bufcnt == 0 or nmux_bufsize == 0:
|
||||
logger.error(
|
||||
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
|
||||
)
|
||||
self.modificationLock.release()
|
||||
return
|
||||
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
|
||||
cmd = cmd + " | nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (
|
||||
nmux_bufsize,
|
||||
nmux_bufcnt,
|
||||
self.port,
|
||||
)
|
||||
|
||||
self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
|
||||
logger.info("Started rtl source: " + cmd)
|
||||
|
||||
available = False
|
||||
|
||||
def wait_for_process_to_end():
|
||||
rc = self.process.wait()
|
||||
logger.debug("shut down with RC={0}".format(rc))
|
||||
self.monitor = None
|
||||
|
||||
self.monitor = threading.Thread(target=wait_for_process_to_end)
|
||||
self.monitor.start()
|
||||
|
||||
retries = 1000
|
||||
while retries > 0:
|
||||
retries -= 1
|
||||
if self.monitor is None:
|
||||
break
|
||||
testsock = socket.socket()
|
||||
try:
|
||||
testsock.connect(("127.0.0.1", self.getPort()))
|
||||
testsock.close()
|
||||
available = True
|
||||
break
|
||||
except:
|
||||
time.sleep(0.1)
|
||||
|
||||
if not available:
|
||||
self.failed = True
|
||||
|
||||
self.postStart()
|
||||
|
||||
self.modificationLock.release()
|
||||
|
||||
self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
|
||||
|
||||
def postStart(self):
|
||||
pass
|
||||
|
||||
def isAvailable(self):
|
||||
return self.monitor is not None
|
||||
|
||||
def isFailed(self):
|
||||
return self.failed
|
||||
|
||||
def stop(self):
|
||||
self.setState(SdrSource.STATE_STOPPING)
|
||||
|
||||
self.modificationLock.acquire()
|
||||
|
||||
if self.process is not None:
|
||||
try:
|
||||
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
# been killed by something else, ignore
|
||||
pass
|
||||
if self.monitor:
|
||||
self.monitor.join()
|
||||
self.sleepOnRestart()
|
||||
self.modificationLock.release()
|
||||
|
||||
self.setState(SdrSource.STATE_STOPPED)
|
||||
|
||||
def sleepOnRestart(self):
|
||||
pass
|
||||
|
||||
def hasClients(self, *args):
|
||||
clients = [c for c in self.clients if c.getClientClass() in args]
|
||||
return len(clients) > 0
|
||||
|
||||
def addClient(self, c):
|
||||
self.clients.append(c)
|
||||
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
|
||||
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
|
||||
if hasUsers or hasBackgroundTasks:
|
||||
self.start()
|
||||
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
|
||||
|
||||
def removeClient(self, c):
|
||||
try:
|
||||
self.clients.remove(c)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
hasUsers = self.hasClients(SdrSource.CLIENT_USER)
|
||||
hasBackgroundTasks = self.hasClients(SdrSource.CLIENT_BACKGROUND)
|
||||
self.setBusyState(SdrSource.BUSYSTATE_BUSY if hasUsers else SdrSource.BUSYSTATE_IDLE)
|
||||
if not hasUsers and not hasBackgroundTasks:
|
||||
self.stop()
|
||||
|
||||
def addSpectrumClient(self, c):
|
||||
self.spectrumClients.append(c)
|
||||
if self.spectrumThread is None:
|
||||
self.spectrumThread = SpectrumThread(self)
|
||||
self.spectrumThread.start()
|
||||
|
||||
def removeSpectrumClient(self, c):
|
||||
try:
|
||||
self.spectrumClients.remove(c)
|
||||
except ValueError:
|
||||
pass
|
||||
if not self.spectrumClients and self.spectrumThread is not None:
|
||||
self.spectrumThread.stop()
|
||||
self.spectrumThread = None
|
||||
|
||||
def writeSpectrumData(self, data):
|
||||
for c in self.spectrumClients:
|
||||
c.write_spectrum_data(data)
|
||||
|
||||
def setState(self, state):
|
||||
if state == self.state:
|
||||
return
|
||||
self.state = state
|
||||
for c in self.clients:
|
||||
c.onStateChange(state)
|
||||
|
||||
def setBusyState(self, state):
|
||||
if state == self.busyState:
|
||||
return
|
||||
self.busyState = state
|
||||
for c in self.clients:
|
||||
c.onBusyStateChange(state)
|
||||
|
||||
|
||||
class Resampler(SdrSource):
|
||||
def __init__(self, props, port, sdr):
|
||||
sdrProps = sdr.getProps()
|
||||
self.shift = (sdrProps["center_freq"] - props["center_freq"]) / sdrProps["samp_rate"]
|
||||
self.decimation = int(float(sdrProps["samp_rate"]) / props["samp_rate"])
|
||||
if_samp_rate = sdrProps["samp_rate"] / self.decimation
|
||||
self.transition_bw = 0.15 * (if_samp_rate / float(sdrProps["samp_rate"]))
|
||||
props["samp_rate"] = if_samp_rate
|
||||
|
||||
self.sdr = sdr
|
||||
super().__init__(None, props, port)
|
||||
|
||||
def start(self):
|
||||
if self.isFailed():
|
||||
return
|
||||
|
||||
self.modificationLock.acquire()
|
||||
if self.monitor:
|
||||
self.modificationLock.release()
|
||||
return
|
||||
|
||||
self.setState(SdrSource.STATE_STARTING)
|
||||
|
||||
props = self.rtlProps
|
||||
|
||||
resampler_command = [
|
||||
"nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()),
|
||||
"csdr shift_addition_cc {shift}".format(shift=self.shift),
|
||||
"csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format(
|
||||
decimation=self.decimation, ddc_transition_bw=self.transition_bw
|
||||
),
|
||||
]
|
||||
|
||||
nmux_bufcnt = nmux_bufsize = 0
|
||||
while nmux_bufsize < props["samp_rate"] / 4:
|
||||
nmux_bufsize += 4096
|
||||
while nmux_bufsize * nmux_bufcnt < props["nmux_memory"] * 1e6:
|
||||
nmux_bufcnt += 1
|
||||
if nmux_bufcnt == 0 or nmux_bufsize == 0:
|
||||
logger.error(
|
||||
"Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
|
||||
)
|
||||
self.modificationLock.release()
|
||||
return
|
||||
logger.debug("nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt))
|
||||
resampler_command += [
|
||||
"nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, self.port)
|
||||
]
|
||||
cmd = " | ".join(resampler_command)
|
||||
logger.debug("resampler command: %s", cmd)
|
||||
self.process = subprocess.Popen(cmd, shell=True, preexec_fn=os.setpgrp)
|
||||
logger.info("Started resampler source: " + cmd)
|
||||
|
||||
available = False
|
||||
|
||||
def wait_for_process_to_end():
|
||||
rc = self.process.wait()
|
||||
logger.debug("shut down with RC={0}".format(rc))
|
||||
self.monitor = None
|
||||
|
||||
self.monitor = threading.Thread(target=wait_for_process_to_end)
|
||||
self.monitor.start()
|
||||
|
||||
retries = 1000
|
||||
while retries > 0:
|
||||
retries -= 1
|
||||
if self.monitor is None:
|
||||
break
|
||||
testsock = socket.socket()
|
||||
try:
|
||||
testsock.connect(("127.0.0.1", self.getPort()))
|
||||
testsock.close()
|
||||
available = True
|
||||
break
|
||||
except:
|
||||
time.sleep(0.1)
|
||||
|
||||
if not available:
|
||||
self.failed = True
|
||||
|
||||
self.modificationLock.release()
|
||||
|
||||
self.setState(SdrSource.STATE_FAILED if self.failed else SdrSource.STATE_RUNNING)
|
||||
|
||||
def activateProfile(self, profile_id=None):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectorSource(SdrSource):
|
||||
def __init__(self, id, props, port):
|
||||
super().__init__(id, props, port)
|
||||
self.controlSocket = None
|
||||
self.controlPort = getAvailablePort()
|
||||
|
||||
def sendControlMessage(self, prop, value):
|
||||
logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
|
||||
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
|
||||
|
||||
def wireEvents(self):
|
||||
def reconfigure(prop, value):
|
||||
if self.monitor is None:
|
||||
return
|
||||
if (
|
||||
(prop == "center_freq" or prop == "lfo_offset")
|
||||
and "lfo_offset" in self.rtlProps
|
||||
and self.rtlProps["lfo_offset"] is not None
|
||||
):
|
||||
freq = self.rtlProps["center_freq"] + self.rtlProps["lfo_offset"]
|
||||
self.sendControlMessage("center_freq", freq)
|
||||
else:
|
||||
self.sendControlMessage(prop, value)
|
||||
|
||||
self.rtlProps.wire(reconfigure)
|
||||
|
||||
def postStart(self):
|
||||
logger.debug("opening control socket...")
|
||||
self.controlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.controlSocket.connect(("localhost", self.controlPort))
|
||||
|
||||
def stop(self):
|
||||
super().stop()
|
||||
if self.controlSocket:
|
||||
self.controlSocket.close()
|
||||
self.controlSocket = None
|
||||
|
||||
def getFormatConversion(self):
|
||||
return None
|
||||
|
||||
def useNmux(self):
|
||||
return False
|
||||
|
||||
|
||||
class RtlSdrConnectorSource(ConnectorSource):
|
||||
def getEventNames(self):
|
||||
return [
|
||||
"samp_rate",
|
||||
"center_freq",
|
||||
"ppm",
|
||||
"rf_gain",
|
||||
"device",
|
||||
"iqswap",
|
||||
"lfo_offset",
|
||||
]
|
||||
|
||||
def getCommand(self):
|
||||
cmd = (
|
||||
"rtl_connector -p {port} -c {controlPort}".format(port=self.port, controlPort=self.controlPort)
|
||||
+ " -s {samp_rate} -f {tuner_freq} -g {rf_gain} -P {ppm}"
|
||||
)
|
||||
if "device" in self.rtlProps and self.rtlProps["device"] is not None:
|
||||
cmd += ' -d "{device}"'
|
||||
if self.rtlProps["iqswap"]:
|
||||
cmd += " -i"
|
||||
return cmd
|
||||
|
||||
|
||||
class SdrplayConnectorSource(ConnectorSource):
|
||||
def getEventNames(self):
|
||||
return [
|
||||
"samp_rate",
|
||||
"center_freq",
|
||||
"ppm",
|
||||
"rf_gain",
|
||||
"antenna",
|
||||
"device",
|
||||
"iqswap",
|
||||
"lfo_offset",
|
||||
]
|
||||
|
||||
def getCommand(self):
|
||||
cmd = (
|
||||
"soapy_connector -p {port} -c {controlPort}".format(port=self.port, controlPort=self.controlPort)
|
||||
+ ' -s {samp_rate} -f {tuner_freq} -g "{rf_gain}" -P {ppm} -a "{antenna}"'
|
||||
)
|
||||
if "device" in self.rtlProps and self.rtlProps["device"] is not None:
|
||||
cmd += ' -d "{device}"'
|
||||
if self.rtlProps["iqswap"]:
|
||||
cmd += " -i"
|
||||
return cmd
|
||||
|
||||
|
||||
class AirspyConnectorSource(ConnectorSource):
|
||||
def getEventNames(self):
|
||||
return [
|
||||
"samp_rate",
|
||||
"center_freq",
|
||||
"ppm",
|
||||
"rf_gain",
|
||||
"device",
|
||||
"iqswap",
|
||||
"lfo_offset",
|
||||
"bias_tee",
|
||||
]
|
||||
|
||||
def getCommand(self):
|
||||
cmd = (
|
||||
"soapy_connector -p {port} -c {controlPort}".format(port=self.port, controlPort=self.controlPort)
|
||||
+ ' -s {samp_rate} -f {tuner_freq} -g "{rf_gain}" -P {ppm}'
|
||||
)
|
||||
if "device" in self.rtlProps and self.rtlProps["device"] is not None:
|
||||
cmd += ' -d "{device}"'
|
||||
if self.rtlProps["iqswap"]:
|
||||
cmd += " -i"
|
||||
if self.rtlProps["bias_tee"]:
|
||||
cmd += " -t biastee=true"
|
||||
return cmd
|
||||
|
||||
|
||||
class RtlSdrSource(SdrSource):
|
||||
def getCommand(self):
|
||||
return "rtl_sdr -s {samp_rate} -f {tuner_freq} -p {ppm} -g {rf_gain} -"
|
||||
|
||||
def getFormatConversion(self):
|
||||
return "csdr convert_u8_f"
|
||||
|
||||
|
||||
class HackrfSource(SdrSource):
|
||||
def getCommand(self):
|
||||
return "hackrf_transfer -s {samp_rate} -f {tuner_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-"
|
||||
|
||||
def getFormatConversion(self):
|
||||
return "csdr convert_s8_f"
|
||||
|
||||
|
||||
class SdrplaySource(SdrSource):
|
||||
def getCommand(self):
|
||||
command = "rx_sdr -F CF32 -s {samp_rate} -f {tuner_freq} -p {ppm}"
|
||||
gainMap = {"rf_gain": "RFGR", "if_gain": "IFGR"}
|
||||
gains = [
|
||||
"{0}={{{1}}}".format(gainMap[name], name)
|
||||
for (name, value) in self.rtlProps.collect("rf_gain", "if_gain").__dict__().items()
|
||||
if value is not None
|
||||
]
|
||||
if gains:
|
||||
command += " -g {gains}".format(gains=",".join(gains))
|
||||
if self.rtlProps["antenna"] is not None:
|
||||
command += ' -a "{antenna}"'
|
||||
command += " -"
|
||||
return command
|
||||
|
||||
def sleepOnRestart(self):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
class AirspySource(SdrSource):
|
||||
def getCommand(self):
|
||||
frequency = self.props["tuner_freq"] / 1e6
|
||||
command = "airspy_rx"
|
||||
command += " -f{0}".format(frequency)
|
||||
command += " -r /dev/stdout -a{samp_rate} -g {rf_gain}"
|
||||
return command
|
||||
|
||||
def getFormatConversion(self):
|
||||
return "csdr convert_s16_f"
|
||||
|
||||
|
||||
class SpectrumThread(csdr.output):
|
||||
def __init__(self, sdrSource):
|
||||
self.sdrSource = sdrSource
|
||||
super().__init__()
|
||||
|
||||
self.props = props = self.sdrSource.props.collect(
|
||||
"samp_rate",
|
||||
"fft_size",
|
||||
"fft_fps",
|
||||
"fft_voverlap_factor",
|
||||
"fft_compression",
|
||||
"csdr_dynamic_bufsize",
|
||||
"csdr_print_bufsizes",
|
||||
"csdr_through",
|
||||
"temporary_directory",
|
||||
).defaults(PropertyManager.getSharedInstance())
|
||||
|
||||
self.dsp = dsp = csdr.dsp(self)
|
||||
dsp.nc_port = self.sdrSource.getPort()
|
||||
dsp.set_demodulator("fft")
|
||||
|
||||
def set_fft_averages(key, value):
|
||||
samp_rate = props["samp_rate"]
|
||||
fft_size = props["fft_size"]
|
||||
fft_fps = props["fft_fps"]
|
||||
fft_voverlap_factor = props["fft_voverlap_factor"]
|
||||
|
||||
dsp.set_fft_averages(
|
||||
int(round(1.0 * samp_rate / fft_size / fft_fps / (1.0 - fft_voverlap_factor)))
|
||||
if fft_voverlap_factor > 0
|
||||
else 0
|
||||
)
|
||||
|
||||
self.subscriptions = [
|
||||
props.getProperty("samp_rate").wire(dsp.set_samp_rate),
|
||||
props.getProperty("fft_size").wire(dsp.set_fft_size),
|
||||
props.getProperty("fft_fps").wire(dsp.set_fft_fps),
|
||||
props.getProperty("fft_compression").wire(dsp.set_fft_compression),
|
||||
props.getProperty("temporary_directory").wire(dsp.set_temporary_directory),
|
||||
props.collect("samp_rate", "fft_size", "fft_fps", "fft_voverlap_factor").wire(set_fft_averages),
|
||||
]
|
||||
|
||||
set_fft_averages(None, None)
|
||||
|
||||
dsp.csdr_dynamic_bufsize = props["csdr_dynamic_bufsize"]
|
||||
dsp.csdr_print_bufsizes = props["csdr_print_bufsizes"]
|
||||
dsp.csdr_through = props["csdr_through"]
|
||||
logger.debug("Spectrum thread initialized successfully.")
|
||||
|
||||
def start(self):
|
||||
self.sdrSource.addClient(self)
|
||||
if self.sdrSource.isAvailable():
|
||||
self.dsp.start()
|
||||
|
||||
def supports_type(self, t):
|
||||
return t == "audio"
|
||||
|
||||
def receive_output(self, type, read_fn):
|
||||
if self.props["csdr_dynamic_bufsize"]:
|
||||
read_fn(8) # dummy read to skip bufsize & preamble
|
||||
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
|
||||
|
||||
threading.Thread(target=self.pump(read_fn, self.sdrSource.writeSpectrumData)).start()
|
||||
|
||||
def stop(self):
|
||||
self.dsp.stop()
|
||||
self.sdrSource.removeClient(self)
|
||||
for c in self.subscriptions:
|
||||
c.cancel()
|
||||
self.subscriptions = []
|
||||
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_USER
|
||||
|
||||
def onStateChange(self, state):
|
||||
if state in [SdrSource.STATE_STOPPING, SdrSource.STATE_FAILED]:
|
||||
self.dsp.stop()
|
||||
elif state == SdrSource.STATE_RUNNING:
|
||||
self.dsp.start()
|
||||
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class DspManager(csdr.output):
|
||||
def __init__(self, handler, sdrSource):
|
||||
self.handler = handler
|
||||
self.sdrSource = sdrSource
|
||||
self.metaParser = MetaParser(self.handler)
|
||||
self.wsjtParser = WsjtParser(self.handler)
|
||||
self.aprsParser = AprsParser(self.handler)
|
||||
|
||||
self.localProps = (
|
||||
self.sdrSource.getProps()
|
||||
.collect(
|
||||
"audio_compression",
|
||||
"fft_compression",
|
||||
"digimodes_fft_size",
|
||||
"csdr_dynamic_bufsize",
|
||||
"csdr_print_bufsizes",
|
||||
"csdr_through",
|
||||
"digimodes_enable",
|
||||
"samp_rate",
|
||||
"digital_voice_unvoiced_quality",
|
||||
"dmr_filter",
|
||||
"temporary_directory",
|
||||
"center_freq",
|
||||
)
|
||||
.defaults(PropertyManager.getSharedInstance())
|
||||
)
|
||||
|
||||
self.dsp = csdr.dsp(self)
|
||||
self.dsp.nc_port = self.sdrSource.getPort()
|
||||
|
||||
def set_low_cut(cut):
|
||||
bpf = self.dsp.get_bpf()
|
||||
bpf[0] = cut
|
||||
self.dsp.set_bpf(*bpf)
|
||||
|
||||
def set_high_cut(cut):
|
||||
bpf = self.dsp.get_bpf()
|
||||
bpf[1] = cut
|
||||
self.dsp.set_bpf(*bpf)
|
||||
|
||||
def set_dial_freq(key, value):
|
||||
freq = self.localProps["center_freq"] + self.localProps["offset_freq"]
|
||||
self.wsjtParser.setDialFrequency(freq)
|
||||
self.aprsParser.setDialFrequency(freq)
|
||||
self.metaParser.setDialFrequency(freq)
|
||||
|
||||
self.subscriptions = [
|
||||
self.localProps.getProperty("audio_compression").wire(self.dsp.set_audio_compression),
|
||||
self.localProps.getProperty("fft_compression").wire(self.dsp.set_fft_compression),
|
||||
self.localProps.getProperty("digimodes_fft_size").wire(self.dsp.set_secondary_fft_size),
|
||||
self.localProps.getProperty("samp_rate").wire(self.dsp.set_samp_rate),
|
||||
self.localProps.getProperty("output_rate").wire(self.dsp.set_output_rate),
|
||||
self.localProps.getProperty("offset_freq").wire(self.dsp.set_offset_freq),
|
||||
self.localProps.getProperty("squelch_level").wire(self.dsp.set_squelch_level),
|
||||
self.localProps.getProperty("low_cut").wire(set_low_cut),
|
||||
self.localProps.getProperty("high_cut").wire(set_high_cut),
|
||||
self.localProps.getProperty("mod").wire(self.dsp.set_demodulator),
|
||||
self.localProps.getProperty("digital_voice_unvoiced_quality").wire(self.dsp.set_unvoiced_quality),
|
||||
self.localProps.getProperty("dmr_filter").wire(self.dsp.set_dmr_filter),
|
||||
self.localProps.getProperty("temporary_directory").wire(self.dsp.set_temporary_directory),
|
||||
self.localProps.collect("center_freq", "offset_freq").wire(set_dial_freq),
|
||||
]
|
||||
|
||||
self.dsp.set_offset_freq(0)
|
||||
self.dsp.set_bpf(-4000, 4000)
|
||||
self.dsp.csdr_dynamic_bufsize = self.localProps["csdr_dynamic_bufsize"]
|
||||
self.dsp.csdr_print_bufsizes = self.localProps["csdr_print_bufsizes"]
|
||||
self.dsp.csdr_through = self.localProps["csdr_through"]
|
||||
|
||||
if self.localProps["digimodes_enable"]:
|
||||
|
||||
def set_secondary_mod(mod):
|
||||
if mod == False:
|
||||
mod = None
|
||||
self.dsp.set_secondary_demodulator(mod)
|
||||
if mod is not None:
|
||||
self.handler.write_secondary_dsp_config(
|
||||
{
|
||||
"secondary_fft_size": self.localProps["digimodes_fft_size"],
|
||||
"if_samp_rate": self.dsp.if_samp_rate(),
|
||||
"secondary_bw": self.dsp.secondary_bw(),
|
||||
}
|
||||
)
|
||||
|
||||
self.subscriptions += [
|
||||
self.localProps.getProperty("secondary_mod").wire(set_secondary_mod),
|
||||
self.localProps.getProperty("secondary_offset_freq").wire(self.dsp.set_secondary_offset_freq),
|
||||
]
|
||||
|
||||
self.sdrSource.addClient(self)
|
||||
|
||||
super().__init__()
|
||||
|
||||
def start(self):
|
||||
if self.sdrSource.isAvailable():
|
||||
self.dsp.start()
|
||||
|
||||
def receive_output(self, t, read_fn):
|
||||
logger.debug("adding new output of type %s", t)
|
||||
writers = {
|
||||
"audio": self.handler.write_dsp_data,
|
||||
"smeter": self.handler.write_s_meter_level,
|
||||
"secondary_fft": self.handler.write_secondary_fft,
|
||||
"secondary_demod": self.handler.write_secondary_demod,
|
||||
"meta": self.metaParser.parse,
|
||||
"wsjt_demod": self.wsjtParser.parse,
|
||||
"packet_demod": self.aprsParser.parse,
|
||||
}
|
||||
write = writers[t]
|
||||
|
||||
threading.Thread(target=self.pump(read_fn, write)).start()
|
||||
|
||||
def stop(self):
|
||||
self.dsp.stop()
|
||||
self.sdrSource.removeClient(self)
|
||||
for sub in self.subscriptions:
|
||||
sub.cancel()
|
||||
self.subscriptions = []
|
||||
|
||||
def setProperty(self, prop, value):
|
||||
self.localProps.getProperty(prop).setValue(value)
|
||||
|
||||
def getClientClass(self):
|
||||
return SdrSource.CLIENT_USER
|
||||
|
||||
def onStateChange(self, state):
|
||||
if state == SdrSource.STATE_RUNNING:
|
||||
logger.debug("received STATE_RUNNING, attempting DspSource restart")
|
||||
self.dsp.start()
|
||||
elif state == SdrSource.STATE_STOPPING:
|
||||
logger.debug("received STATE_STOPPING, shutting down DspSource")
|
||||
self.dsp.stop()
|
||||
elif state == SdrSource.STATE_FAILED:
|
||||
logger.debug("received STATE_FAILED, shutting down DspSource")
|
||||
self.dsp.stop()
|
||||
self.handler.handleSdrFailure("sdr device failed")
|
||||
|
||||
def onBusyStateChange(self, state):
|
||||
pass
|
||||
|
||||
|
||||
class CpuUsageThread(threading.Thread):
|
||||
sharedInstance = None
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
if CpuUsageThread.sharedInstance is None:
|
||||
CpuUsageThread.sharedInstance = CpuUsageThread()
|
||||
return CpuUsageThread.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
self.doRun = True
|
||||
self.last_worktime = 0
|
||||
self.last_idletime = 0
|
||||
self.endEvent = threading.Event()
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
while self.doRun:
|
||||
try:
|
||||
cpu_usage = self.get_cpu_usage()
|
||||
except:
|
||||
cpu_usage = 0
|
||||
for c in self.clients:
|
||||
c.write_cpu_usage(cpu_usage)
|
||||
self.endEvent.wait(timeout=3)
|
||||
logger.debug("cpu usage thread shut down")
|
||||
|
||||
def get_cpu_usage(self):
|
||||
try:
|
||||
f = open("/proc/stat", "r")
|
||||
except:
|
||||
return 0 # Workaround, possibly we're on a Mac
|
||||
line = ""
|
||||
while not "cpu " in line:
|
||||
line = f.readline()
|
||||
f.close()
|
||||
spl = line.split(" ")
|
||||
worktime = int(spl[2]) + int(spl[3]) + int(spl[4])
|
||||
idletime = int(spl[5])
|
||||
dworktime = worktime - self.last_worktime
|
||||
didletime = idletime - self.last_idletime
|
||||
rate = float(dworktime) / (didletime + dworktime)
|
||||
self.last_worktime = worktime
|
||||
self.last_idletime = idletime
|
||||
if self.last_worktime == 0:
|
||||
return 0
|
||||
return rate
|
||||
|
||||
def add_client(self, c):
|
||||
self.clients.append(c)
|
||||
if not self.is_alive():
|
||||
self.start()
|
||||
|
||||
def remove_client(self, c):
|
||||
try:
|
||||
self.clients.remove(c)
|
||||
except ValueError:
|
||||
pass
|
||||
if not self.clients:
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self):
|
||||
CpuUsageThread.sharedInstance = None
|
||||
self.doRun = False
|
||||
self.endEvent.set()
|
||||
|
||||
|
||||
class TooManyClientsException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ClientRegistry(object):
|
||||
sharedInstance = None
|
||||
creationLock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
with ClientRegistry.creationLock:
|
||||
if ClientRegistry.sharedInstance is None:
|
||||
ClientRegistry.sharedInstance = ClientRegistry()
|
||||
return ClientRegistry.sharedInstance
|
||||
|
||||
def __init__(self):
|
||||
self.clients = []
|
||||
Metrics.getSharedInstance().addMetric("openwebrx.users", DirectMetric(self.clientCount))
|
||||
super().__init__()
|
||||
|
||||
def broadcast(self):
|
||||
n = self.clientCount()
|
||||
for c in self.clients:
|
||||
c.write_clients(n)
|
||||
|
||||
def addClient(self, client):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
if len(self.clients) >= pm["max_clients"]:
|
||||
raise TooManyClientsException()
|
||||
self.clients.append(client)
|
||||
self.broadcast()
|
||||
|
||||
def clientCount(self):
|
||||
return len(self.clients)
|
||||
|
||||
def removeClient(self, client):
|
||||
try:
|
||||
self.clients.remove(client)
|
||||
except ValueError:
|
||||
pass
|
||||
self.broadcast()
|
@ -1 +0,0 @@
|
||||
openwebrx_version = "v0.18"
|
@ -1,247 +0,0 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from multiprocessing import Pipe
|
||||
import select
|
||||
import threading
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OPCODE_TEXT_MESSAGE = 0x01
|
||||
OPCODE_BINARY_MESSAGE = 0x02
|
||||
OPCODE_CLOSE = 0x08
|
||||
OPCODE_PING = 0x09
|
||||
OPCODE_PONG = 0x0A
|
||||
|
||||
|
||||
class WebSocketException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IncompleteRead(WebSocketException):
|
||||
pass
|
||||
|
||||
|
||||
class Drained(WebSocketException):
|
||||
pass
|
||||
|
||||
|
||||
class WebSocketClosed(WebSocketException):
|
||||
pass
|
||||
|
||||
|
||||
class WebSocketConnection(object):
|
||||
connections = []
|
||||
|
||||
@staticmethod
|
||||
def closeAll():
|
||||
for c in WebSocketConnection.connections:
|
||||
try:
|
||||
c.close()
|
||||
except:
|
||||
logger.exception("exception while shutting down websocket connections")
|
||||
|
||||
def __init__(self, handler, messageHandler):
|
||||
self.handler = handler
|
||||
self.handler.connection.setblocking(0)
|
||||
self.setMessageHandler(messageHandler)
|
||||
(self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False)
|
||||
self.open = True
|
||||
self.sendLock = threading.Lock()
|
||||
|
||||
headers = {key.lower(): value for key, value in self.handler.headers.items()}
|
||||
if not "upgrade" in headers:
|
||||
raise WebSocketException("Upgrade header not found")
|
||||
if headers["upgrade"].lower() != "websocket":
|
||||
raise WebSocketException("Upgrade header does not contain expected value")
|
||||
if not "sec-websocket-key" in headers:
|
||||
raise WebSocketException("Websocket key not provided")
|
||||
|
||||
ws_key = headers["sec-websocket-key"]
|
||||
shakey = hashlib.sha1()
|
||||
shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode())
|
||||
ws_key_toreturn = base64.b64encode(shakey.digest())
|
||||
self.handler.wfile.write(
|
||||
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format(
|
||||
ws_key_toreturn.decode()
|
||||
).encode()
|
||||
)
|
||||
self.pingTimer = None
|
||||
self.resetPing()
|
||||
|
||||
def setMessageHandler(self, messageHandler):
|
||||
self.messageHandler = messageHandler
|
||||
|
||||
def get_header(self, size, opcode):
|
||||
ws_first_byte = 0b10000000 | (opcode & 0x0F)
|
||||
if size > 2 ** 16 - 1:
|
||||
# frame size can be increased up to 2^64 by setting the size to 127
|
||||
# anything beyond that would need to be segmented into frames. i don't really think we'll need more.
|
||||
return bytes(
|
||||
[
|
||||
ws_first_byte,
|
||||
127,
|
||||
(size >> 56) & 0xFF,
|
||||
(size >> 48) & 0xFF,
|
||||
(size >> 40) & 0xFF,
|
||||
(size >> 32) & 0xFF,
|
||||
(size >> 24) & 0xFF,
|
||||
(size >> 16) & 0xFF,
|
||||
(size >> 8) & 0xFF,
|
||||
size & 0xFF,
|
||||
]
|
||||
)
|
||||
elif size > 125:
|
||||
# up to 2^16 can be sent using the extended payload size field by putting the size to 126
|
||||
return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF])
|
||||
else:
|
||||
# 125 bytes binary message in a single unmasked frame
|
||||
return bytes([ws_first_byte, size])
|
||||
|
||||
def send(self, data):
|
||||
if not self.open:
|
||||
raise WebSocketClosed()
|
||||
# convenience
|
||||
if type(data) == dict:
|
||||
# allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway.
|
||||
data = json.dumps(data, allow_nan=False)
|
||||
|
||||
# string-type messages are sent as text frames
|
||||
if type(data) == str:
|
||||
header = self.get_header(len(data), OPCODE_TEXT_MESSAGE)
|
||||
data_to_send = header + data.encode("utf-8")
|
||||
# anything else as binary
|
||||
else:
|
||||
header = self.get_header(len(data), OPCODE_BINARY_MESSAGE)
|
||||
data_to_send = header + data
|
||||
|
||||
self._sendBytes(data_to_send)
|
||||
|
||||
def _sendBytes(self, data_to_send):
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i : i + n]
|
||||
|
||||
try:
|
||||
with self.sendLock:
|
||||
for chunk in chunks(data_to_send, 1024):
|
||||
(_, write, _) = select.select([], [self.handler.wfile], [], 10)
|
||||
if self.handler.wfile in write:
|
||||
written = self.handler.wfile.write(chunk)
|
||||
if written != len(chunk):
|
||||
logger.error("incomplete write! closing socket!")
|
||||
self.close()
|
||||
else:
|
||||
logger.debug("socket not returned from select; closing")
|
||||
self.close()
|
||||
# these exception happen when the socket is closed
|
||||
except OSError:
|
||||
logger.exception("OSError while writing data")
|
||||
self.close()
|
||||
except ValueError:
|
||||
logger.exception("ValueError while writing data")
|
||||
self.close()
|
||||
|
||||
def interrupt(self):
|
||||
self.interruptPipeSend.send(bytes(0x00))
|
||||
|
||||
def handle(self):
|
||||
WebSocketConnection.connections.append(self)
|
||||
try:
|
||||
self.read_loop()
|
||||
finally:
|
||||
logger.debug("websocket loop ended; shutting down")
|
||||
|
||||
self.messageHandler.handleClose()
|
||||
self.cancelPing()
|
||||
|
||||
logger.debug("websocket loop ended; sending close frame")
|
||||
|
||||
header = self.get_header(0, OPCODE_CLOSE)
|
||||
self._sendBytes(header)
|
||||
|
||||
try:
|
||||
WebSocketConnection.connections.remove(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def read_loop(self):
|
||||
def protected_read(num):
|
||||
data = self.handler.rfile.read(num)
|
||||
if data is None:
|
||||
raise Drained()
|
||||
if len(data) != num:
|
||||
raise IncompleteRead()
|
||||
return data
|
||||
|
||||
self.open = True
|
||||
while self.open:
|
||||
(read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], [], 15)
|
||||
if self.handler.rfile in read:
|
||||
available = True
|
||||
self.resetPing()
|
||||
while self.open and available:
|
||||
try:
|
||||
header = protected_read(2)
|
||||
opcode = header[0] & 0x0F
|
||||
length = header[1] & 0x7F
|
||||
mask = (header[1] & 0x80) >> 7
|
||||
if length == 126:
|
||||
header = protected_read(2)
|
||||
length = (header[0] << 8) + header[1]
|
||||
if mask:
|
||||
masking_key = protected_read(4)
|
||||
data = protected_read(length)
|
||||
if mask:
|
||||
data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)])
|
||||
if opcode == OPCODE_TEXT_MESSAGE:
|
||||
message = data.decode("utf-8")
|
||||
self.messageHandler.handleTextMessage(self, message)
|
||||
elif opcode == OPCODE_BINARY_MESSAGE:
|
||||
self.messageHandler.handleBinaryMessage(self, data)
|
||||
elif opcode == OPCODE_PING:
|
||||
self.sendPong()
|
||||
elif opcode == OPCODE_PONG:
|
||||
# since every read resets the ping timer, there's nothing to do here.
|
||||
pass
|
||||
elif opcode == OPCODE_CLOSE:
|
||||
logger.debug("websocket close frame received; closing connection")
|
||||
self.open = False
|
||||
else:
|
||||
logger.warning("unsupported opcode: {0}".format(opcode))
|
||||
except Drained:
|
||||
available = False
|
||||
except IncompleteRead:
|
||||
logger.warning("incomplete read on websocket; closing connection")
|
||||
self.open = False
|
||||
except OSError:
|
||||
logger.exception("OSError while reading data; closing connection")
|
||||
self.open = False
|
||||
|
||||
def close(self):
|
||||
self.open = False
|
||||
self.interrupt()
|
||||
|
||||
def cancelPing(self):
|
||||
if self.pingTimer:
|
||||
self.pingTimer.cancel()
|
||||
|
||||
def resetPing(self):
|
||||
self.cancelPing()
|
||||
if not self.open:
|
||||
logger.debug("resetPing() while closed. passing...")
|
||||
return
|
||||
self.pingTimer = threading.Timer(30, self.sendPing)
|
||||
self.pingTimer.start()
|
||||
|
||||
def sendPing(self):
|
||||
header = self.get_header(0, OPCODE_PING)
|
||||
self._sendBytes(header)
|
||||
self.resetPing()
|
||||
|
||||
def sendPong(self):
|
||||
header = self.get_header(0, OPCODE_PONG)
|
||||
self._sendBytes(header)
|
389
owrx/wsjt.py
@ -1,389 +0,0 @@
|
||||
import threading
|
||||
import wave
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import subprocess
|
||||
import os
|
||||
from multiprocessing.connection import Pipe
|
||||
from owrx.map import Map, LocatorLocation
|
||||
import re
|
||||
from queue import Queue, Full
|
||||
from owrx.config import PropertyManager
|
||||
from owrx.bands import Bandplan
|
||||
from owrx.metrics import Metrics, CounterMetric, DirectMetric
|
||||
from owrx.pskreporter import PskReporter
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
class WsjtQueueWorker(threading.Thread):
|
||||
def __init__(self, queue):
|
||||
self.queue = queue
|
||||
self.doRun = True
|
||||
super().__init__(daemon=True)
|
||||
|
||||
def run(self) -> None:
|
||||
while self.doRun:
|
||||
(processor, file) = self.queue.get()
|
||||
try:
|
||||
logger.debug("processing file %s", file)
|
||||
processor.decode(file)
|
||||
except Exception:
|
||||
logger.exception("failed to decode job")
|
||||
self.queue.onError()
|
||||
self.queue.task_done()
|
||||
|
||||
|
||||
class WsjtQueue(Queue):
|
||||
sharedInstance = None
|
||||
creationLock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def getSharedInstance():
|
||||
with WsjtQueue.creationLock:
|
||||
if WsjtQueue.sharedInstance is None:
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"])
|
||||
return WsjtQueue.sharedInstance
|
||||
|
||||
def __init__(self, maxsize, workers):
|
||||
super().__init__(maxsize)
|
||||
metrics = Metrics.getSharedInstance()
|
||||
metrics.addMetric("wsjt.queue.length", DirectMetric(self.qsize))
|
||||
self.inCounter = CounterMetric()
|
||||
metrics.addMetric("wsjt.queue.in", self.inCounter)
|
||||
self.outCounter = CounterMetric()
|
||||
metrics.addMetric("wsjt.queue.out", self.outCounter)
|
||||
self.overflowCounter = CounterMetric()
|
||||
metrics.addMetric("wsjt.queue.overflow", self.overflowCounter)
|
||||
self.errorCounter = CounterMetric()
|
||||
metrics.addMetric("wsjt.queue.error", self.errorCounter)
|
||||
self.workers = [self.newWorker() for _ in range(0, workers)]
|
||||
|
||||
def put(self, item):
|
||||
self.inCounter.inc()
|
||||
try:
|
||||
super(WsjtQueue, self).put(item, block=False)
|
||||
except Full:
|
||||
self.overflowCounter.inc()
|
||||
raise
|
||||
|
||||
def get(self, **kwargs):
|
||||
# super.get() is blocking, so it would mess up the stats to inc() first
|
||||
out = super(WsjtQueue, self).get(**kwargs)
|
||||
self.outCounter.inc()
|
||||
return out
|
||||
|
||||
def newWorker(self):
|
||||
worker = WsjtQueueWorker(self)
|
||||
worker.start()
|
||||
return worker
|
||||
|
||||
def onError(self):
|
||||
self.errorCounter.inc()
|
||||
|
||||
|
||||
class WsjtChopper(threading.Thread):
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"]
|
||||
(self.wavefilename, self.wavefile) = self.getWaveFile()
|
||||
self.switchingLock = threading.Lock()
|
||||
self.timer = None
|
||||
(self.outputReader, self.outputWriter) = Pipe()
|
||||
self.doRun = True
|
||||
super().__init__()
|
||||
|
||||
def getWaveFile(self):
|
||||
filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format(
|
||||
tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.fileTimestampFormat)
|
||||
)
|
||||
wavefile = wave.open(filename, "wb")
|
||||
wavefile.setnchannels(1)
|
||||
wavefile.setsampwidth(2)
|
||||
wavefile.setframerate(12000)
|
||||
return filename, wavefile
|
||||
|
||||
def getNextDecodingTime(self):
|
||||
t = datetime.utcnow()
|
||||
zeroed = t.replace(minute=0, second=0, microsecond=0)
|
||||
delta = t - zeroed
|
||||
seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval
|
||||
t = zeroed + timedelta(seconds=seconds)
|
||||
logger.debug("scheduling: {0}".format(t))
|
||||
return t
|
||||
|
||||
def cancelTimer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
|
||||
def _scheduleNextSwitch(self):
|
||||
if self.doRun:
|
||||
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()
|
||||
try:
|
||||
WsjtQueue.getSharedInstance().put((self, filename))
|
||||
except Full:
|
||||
logger.warning("wsjt decoding queue overflow; dropping one file")
|
||||
os.unlink(filename)
|
||||
self._scheduleNextSwitch()
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
"""
|
||||
must be overridden in child classes
|
||||
"""
|
||||
return []
|
||||
|
||||
def decode(self, file):
|
||||
decoder = subprocess.Popen(
|
||||
self.decoder_commandline(file), stdout=subprocess.PIPE, cwd=self.tmp_dir, preexec_fn=lambda: os.nice(10)
|
||||
)
|
||||
while True:
|
||||
line = decoder.stdout.readline()
|
||||
if line is None or (isinstance(line, bytes) and len(line) == 0):
|
||||
break
|
||||
self.outputWriter.send(line)
|
||||
rc = decoder.wait()
|
||||
if rc != 0:
|
||||
logger.warning("decoder return code: %i", rc)
|
||||
os.unlink(file)
|
||||
|
||||
def run(self) -> None:
|
||||
logger.debug("WSJT chopper starting up")
|
||||
self._scheduleNextSwitch()
|
||||
while self.doRun:
|
||||
data = self.source.read(256)
|
||||
if data is None or (isinstance(data, bytes) and len(data) == 0):
|
||||
self.doRun = False
|
||||
else:
|
||||
self.switchingLock.acquire()
|
||||
self.wavefile.writeframes(data)
|
||||
self.switchingLock.release()
|
||||
|
||||
logger.debug("WSJT chopper shutting down")
|
||||
self.outputReader.close()
|
||||
self.outputWriter.close()
|
||||
self.cancelTimer()
|
||||
try:
|
||||
os.unlink(self.wavefilename)
|
||||
except Exception:
|
||||
logger.exception("error removing undecoded file")
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
return self.outputReader.recv()
|
||||
except EOFError:
|
||||
return None
|
||||
|
||||
def decoding_depth(self, mode):
|
||||
pm = PropertyManager.getSharedInstance()
|
||||
# mode-specific setting?
|
||||
if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]:
|
||||
return pm["wsjt_decoding_depths"][mode]
|
||||
# return global default
|
||||
if "wsjt_decoding_depth" in pm:
|
||||
return pm["wsjt_decoding_depth"]
|
||||
# default when no setting is provided
|
||||
return 3
|
||||
|
||||
|
||||
class Ft8Chopper(WsjtChopper):
|
||||
def __init__(self, source):
|
||||
self.interval = 15
|
||||
self.fileTimestampFormat = "%y%m%d_%H%M%S"
|
||||
super().__init__(source)
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file]
|
||||
|
||||
|
||||
class WsprChopper(WsjtChopper):
|
||||
def __init__(self, source):
|
||||
self.interval = 120
|
||||
self.fileTimestampFormat = "%y%m%d_%H%M"
|
||||
super().__init__(source)
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
cmd = ["wsprd"]
|
||||
if self.decoding_depth("wspr") > 1:
|
||||
cmd += ["-d"]
|
||||
cmd += [file]
|
||||
return cmd
|
||||
|
||||
|
||||
class Jt65Chopper(WsjtChopper):
|
||||
def __init__(self, source):
|
||||
self.interval = 60
|
||||
self.fileTimestampFormat = "%y%m%d_%H%M"
|
||||
super().__init__(source)
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file]
|
||||
|
||||
|
||||
class Jt9Chopper(WsjtChopper):
|
||||
def __init__(self, source):
|
||||
self.interval = 60
|
||||
self.fileTimestampFormat = "%y%m%d_%H%M"
|
||||
super().__init__(source)
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file]
|
||||
|
||||
|
||||
class Ft4Chopper(WsjtChopper):
|
||||
def __init__(self, source):
|
||||
self.interval = 7.5
|
||||
self.fileTimestampFormat = "%y%m%d_%H%M%S"
|
||||
super().__init__(source)
|
||||
|
||||
def decoder_commandline(self, file):
|
||||
return ["jt9", "--ft4", "-d", str(self.decoding_depth("ft4")), file]
|
||||
|
||||
|
||||
class WsjtParser(object):
|
||||
def __init__(self, handler):
|
||||
self.handler = handler
|
||||
self.dial_freq = None
|
||||
self.band = None
|
||||
|
||||
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"}
|
||||
|
||||
def parse(self, data):
|
||||
try:
|
||||
msg = data.decode().rstrip()
|
||||
# known debug messages we know to skip
|
||||
if msg.startswith("<DecodeFinished>"):
|
||||
return
|
||||
if msg.startswith(" EOF on input file"):
|
||||
return
|
||||
|
||||
modes = list(WsjtParser.modes.keys())
|
||||
if msg[21] in modes or msg[19] in modes:
|
||||
decoder = Jt9Decoder()
|
||||
else:
|
||||
decoder = WsprDecoder()
|
||||
out = decoder.parse(msg, self.dial_freq)
|
||||
if "mode" in out:
|
||||
self.pushDecode(out["mode"])
|
||||
if "callsign" in out and "locator" in out:
|
||||
Map.getSharedInstance().updateLocation(
|
||||
out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band
|
||||
)
|
||||
PskReporter.getSharedInstance().spot(out)
|
||||
|
||||
self.handler.write_wsjt_message(out)
|
||||
except ValueError:
|
||||
logger.exception("error while parsing wsjt message")
|
||||
|
||||
def pushDecode(self, mode):
|
||||
metrics = Metrics.getSharedInstance()
|
||||
band = "unknown"
|
||||
if self.band is not None:
|
||||
band = self.band.getName()
|
||||
if band is None:
|
||||
band = "unknown"
|
||||
|
||||
if mode is None:
|
||||
mode = "unknown"
|
||||
|
||||
name = "wsjt.decodes.{band}.{mode}".format(band=band, mode=mode)
|
||||
metric = metrics.getMetric(name)
|
||||
if metric is None:
|
||||
metric = CounterMetric()
|
||||
metrics.addMetric(name, metric)
|
||||
|
||||
metric.inc()
|
||||
|
||||
def setDialFrequency(self, freq):
|
||||
self.dial_freq = freq
|
||||
self.band = Bandplan.getSharedInstance().findBand(freq)
|
||||
|
||||
|
||||
class Decoder(object):
|
||||
def parse_timestamp(self, instring, dateformat):
|
||||
ts = datetime.strptime(instring, dateformat)
|
||||
return int(
|
||||
datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000
|
||||
)
|
||||
|
||||
|
||||
class Jt9Decoder(Decoder):
|
||||
locator_pattern = re.compile("[A-Z0-9]+\\s([A-Z0-9]+)\\s([A-R]{2}[0-9]{2})$")
|
||||
|
||||
def parse(self, msg, dial_freq):
|
||||
# ft8 sample
|
||||
# '222100 -15 -0.0 508 ~ CQ EA7MJ IM66'
|
||||
# jt65 sample
|
||||
# '2352 -7 0.4 1801 # R0WAS R2ABM KO85'
|
||||
# '0003 -4 0.4 1762 # CQ R2ABM KO85'
|
||||
modes = list(WsjtParser.modes.keys())
|
||||
if msg[19] in modes:
|
||||
dateformat = "%H%M"
|
||||
else:
|
||||
dateformat = "%H%M%S"
|
||||
timestamp = self.parse_timestamp(msg[0 : len(dateformat)], dateformat)
|
||||
msg = msg[len(dateformat) + 1 :]
|
||||
modeChar = msg[14:15]
|
||||
mode = WsjtParser.modes[modeChar] if modeChar in WsjtParser.modes else "unknown"
|
||||
wsjt_msg = msg[17:53].strip()
|
||||
|
||||
result = {
|
||||
"timestamp": timestamp,
|
||||
"db": float(msg[0:3]),
|
||||
"dt": float(msg[4:8]),
|
||||
"freq": dial_freq + int(msg[9:13]),
|
||||
"mode": mode,
|
||||
"msg": wsjt_msg,
|
||||
}
|
||||
result.update(self.parseMessage(wsjt_msg))
|
||||
return result
|
||||
|
||||
def parseMessage(self, msg):
|
||||
m = Jt9Decoder.locator_pattern.match(msg)
|
||||
if m is None:
|
||||
return {}
|
||||
# this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very
|
||||
# likely this just means roger roger goodbye.
|
||||
if m.group(2) == "RR73":
|
||||
return {"callsign": m.group(1)}
|
||||
return {"callsign": m.group(1), "locator": m.group(2)}
|
||||
|
||||
|
||||
class WsprDecoder(Decoder):
|
||||
wspr_splitter_pattern = re.compile("([A-Z0-9]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)")
|
||||
|
||||
def parse(self, msg, dial_freq):
|
||||
# wspr sample
|
||||
# '2600 -24 0.4 0.001492 -1 G8AXA JO01 33'
|
||||
# '0052 -29 2.6 0.001486 0 G02CWT IO92 23'
|
||||
wsjt_msg = msg[29:].strip()
|
||||
result = {
|
||||
"timestamp": self.parse_timestamp(msg[0:4], "%H%M"),
|
||||
"db": float(msg[5:8]),
|
||||
"dt": float(msg[9:13]),
|
||||
"freq": dial_freq + int(float(msg[14:24]) * 1e6),
|
||||
"drift": int(msg[25:28]),
|
||||
"mode": "WSPR",
|
||||
"msg": wsjt_msg,
|
||||
}
|
||||
result.update(self.parseMessage(wsjt_msg))
|
||||
return result
|
||||
|
||||
def parseMessage(self, msg):
|
||||
m = WsprDecoder.wspr_splitter_pattern.match(msg)
|
||||
if m is None:
|
||||
return {}
|
||||
return {"callsign": m.group(1), "locator": m.group(2)}
|
16
push.sh
@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euxo pipefail
|
||||
|
||||
ARCH=$(uname -m)
|
||||
|
||||
ALL_ARCHS="x86_64 armv7l"
|
||||
|
||||
for image in openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-full openwebrx; do
|
||||
docker push jketterl/$image:$ARCH
|
||||
IMAGE_LIST=""
|
||||
for a in $ALL_ARCHS; do
|
||||
IMAGE_LIST="$IMAGE_LIST jketterl/$image:$a"
|
||||
done
|
||||
docker manifest create --amend jketterl/$image:latest $IMAGE_LIST
|
||||
docker manifest push --purge jketterl/$image:latest
|
||||
done
|
171
rxws.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""
|
||||
rxws: WebSocket methods implemented for OpenWebRX
|
||||
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
published by the Free Software Foundation, either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
|
||||
import base64
|
||||
import sha
|
||||
import select
|
||||
import code
|
||||
|
||||
class WebSocketException(Exception):
|
||||
pass
|
||||
|
||||
def handshake(myself):
|
||||
my_client_id=myself.path[4:]
|
||||
my_headers=myself.headers.items()
|
||||
my_header_keys=map(lambda x:x[0],my_headers)
|
||||
h_key_exists=lambda x:my_header_keys.count(x)
|
||||
h_value=lambda x:my_headers[my_header_keys.index(x)][1]
|
||||
#print "The Lambdas(tm)"
|
||||
#print h_key_exists("upgrade")
|
||||
#print h_value("upgrade")
|
||||
#print h_key_exists("sec-websocket-key")
|
||||
if (not h_key_exists("upgrade")) or not (h_value("upgrade")=="websocket") or (not h_key_exists("sec-websocket-key")):
|
||||
raise WebSocketException
|
||||
ws_key=h_value("sec-websocket-key")
|
||||
ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest())
|
||||
#A sample list of keys we get: [('origin', 'http://localhost:8073'), ('upgrade', 'websocket'), ('sec-websocket-extensions', 'x-webkit-deflate-frame'), ('sec-websocket-version', '13'), ('host', 'localhost:8073'), ('sec-websocket-key', 't9J1rgy4fc9fg2Hshhnkmg=='), ('connection', 'Upgrade'), ('pragma', 'no-cache'), ('cache-control', 'no-cache')]
|
||||
myself.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ws_key_toreturn+"\r\nCQ-CQ-de: HA5KFU\r\n\r\n")
|
||||
|
||||
def get_header(size):
|
||||
#this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php
|
||||
ws_first_byte=0b10000010 # FIN=1, OP=2
|
||||
if(size>125):
|
||||
ws_second_byte=126 # The following two bytes will indicate frame size
|
||||
extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP
|
||||
else:
|
||||
ws_second_byte=size
|
||||
#256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data]
|
||||
extended_size=""
|
||||
return chr(ws_first_byte)+chr(ws_second_byte)+extended_size
|
||||
|
||||
def code_payload(data, masking_key=""):
|
||||
# both encode or decode
|
||||
if masking_key=="":
|
||||
key = (61, 84, 35, 6)
|
||||
else:
|
||||
key = [ord(i) for i in masking_key]
|
||||
encoded=""
|
||||
for i in range(0,len(data)):
|
||||
encoded+=chr(ord(data[i])^key[i%4])
|
||||
return encoded
|
||||
|
||||
def xxdg(data):
|
||||
output=""
|
||||
for i in range(0,len(data)/8):
|
||||
output+=xxd(data[i:i+8])
|
||||
if i%2: output+="\n"
|
||||
else: output+=" "
|
||||
return output
|
||||
|
||||
|
||||
def xxd(data):
|
||||
#diagnostic purposes only
|
||||
output=""
|
||||
for d in data:
|
||||
output+=hex(ord(d))[2:].zfill(2)+" "
|
||||
return output
|
||||
|
||||
#for R/W the WebSocket, use recv/send
|
||||
#for reading the TCP socket, use readsock
|
||||
#for writing the TCP socket, use myself.wfile.write and flush
|
||||
|
||||
def readsock(myself,size,blocking):
|
||||
#http://thenestofheliopolis.blogspot.hu/2011/01/how-to-implement-non-blocking-two-way.html
|
||||
if blocking:
|
||||
return myself.rfile.read(size)
|
||||
else:
|
||||
poll = select.poll()
|
||||
poll.register(myself.rfile.fileno(), select.POLLIN or select.POLLPRI)
|
||||
fd = poll.poll(0) #timeout is 0
|
||||
if len(fd):
|
||||
f = fd[0]
|
||||
if f[1] > 0:
|
||||
return myself.rfile.read(size)
|
||||
return ""
|
||||
|
||||
|
||||
def recv(myself, blocking=False, debug=False):
|
||||
bufsize=70000
|
||||
#myself.connection.setblocking(blocking) #umm... we cannot do that with rfile
|
||||
if debug: print "ws_recv begin"
|
||||
try:
|
||||
data=readsock(myself,6,blocking)
|
||||
#print "rxws.recv bytes:",xxd(data)
|
||||
except:
|
||||
if debug: print "ws_recv error"
|
||||
return ""
|
||||
if debug: print "ws_recv recved"
|
||||
if(len(data)==0): return ""
|
||||
fin=ord(data[0])&128!=0
|
||||
is_text_frame=ord(data[0])&15==1
|
||||
length=ord(data[1])&0x7f
|
||||
data+=readsock(myself,length,blocking)
|
||||
#print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data)
|
||||
has_one_byte_length=length<125
|
||||
masked=ord(data[1])&0x80!=0
|
||||
#print "len=", length, len(data)-2
|
||||
#print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked)
|
||||
#print xxd(data)
|
||||
if fin and is_text_frame and has_one_byte_length:
|
||||
if masked:
|
||||
return code_payload(data[6:], data[2:6])
|
||||
else:
|
||||
return data[2:]
|
||||
|
||||
#Useful links for ideas on WebSockets:
|
||||
# http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side
|
||||
# https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server
|
||||
# http://tools.ietf.org/html/rfc6455#section-5.2
|
||||
|
||||
|
||||
def flush(myself):
|
||||
myself.wfile.flush()
|
||||
#or the socket, not the rfile:
|
||||
#lR,lW,lX = select.select([],[myself.connection,],[],60)
|
||||
|
||||
|
||||
def send(myself, data, begin_id="", debug=0):
|
||||
base_frame_size=35000 #could guess by MTU?
|
||||
debug=0
|
||||
#try:
|
||||
while True:
|
||||
counter=0
|
||||
from_end=len(data)-counter
|
||||
if from_end+len(begin_id)>base_frame_size:
|
||||
data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)]
|
||||
header=get_header(len(data_to_send))
|
||||
flush(myself)
|
||||
myself.wfile.write(header+data_to_send)
|
||||
flush(myself)
|
||||
if debug: print "rxws.send ==================== #1 if branch :: from={0} to={1} dlen={2} hlen={3}".format(counter,counter+base_frame_size-len(begin_id),len(data_to_send),len(header))
|
||||
else:
|
||||
data_to_send=begin_id+data[counter:]
|
||||
header=get_header(len(data_to_send))
|
||||
flush(myself)
|
||||
myself.wfile.write(header+data_to_send)
|
||||
flush(myself)
|
||||
if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header))
|
||||
#if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send)
|
||||
break
|
||||
counter+=base_frame_size-len(begin_id)
|
||||
#except:
|
||||
# pass
|
32
sdrhu.py
@ -4,7 +4,6 @@
|
||||
This file is part of OpenWebRX,
|
||||
an open-source SDR receiver software with a web UI.
|
||||
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
||||
Copyright (c) 2019 by Jakob Ketterl <dd5jfk@darc.de>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as
|
||||
@ -21,12 +20,31 @@
|
||||
|
||||
"""
|
||||
|
||||
from owrx.sdrhu import SdrHuUpdater
|
||||
from owrx.config import PropertyManager
|
||||
import config_webrx as cfg, time, subprocess
|
||||
|
||||
def run(continuously=True):
|
||||
if not cfg.sdrhu_key: return
|
||||
firsttime="(Your receiver is soon getting listed on sdr.hu!)"
|
||||
while True:
|
||||
cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1"
|
||||
print "[openwebrx-sdrhu]", cmd
|
||||
returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()
|
||||
returned=returned[0]
|
||||
#print returned
|
||||
if "UPDATE:" in returned:
|
||||
retrytime_mins = 20
|
||||
value=returned.split("UPDATE:")[1].split("\n",1)[0]
|
||||
if value.startswith("SUCCESS"):
|
||||
print "[openwebrx-sdrhu] Update succeeded! "+firsttime
|
||||
firsttime=""
|
||||
else:
|
||||
print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value
|
||||
else:
|
||||
retrytime_mins = 2
|
||||
print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!"
|
||||
if not continuously: break
|
||||
time.sleep(60*retrytime_mins)
|
||||
|
||||
if __name__=="__main__":
|
||||
pm = PropertyManager.getSharedInstance().loadConfig("config_webrx")
|
||||
run(False)
|
||||
|
||||
if not "sdrhu_key" in pm:
|
||||
exit(1)
|
||||
SdrHuUpdater().update()
|
||||
|