6 Commits

Author SHA1 Message Date
4fb99c05e0 fix /status http response 2020-06-21 19:39:57 +02:00
d84e672e6e README fix 2019-12-29 23:31:23 +01:00
a687d7f163 README fix 2019-12-29 23:30:40 +01:00
9f1c9a0a09 README fix 2019-12-29 23:30:01 +01:00
f531f3c464 README fix 2019-12-29 23:25:39 +01:00
d3161b6fc1 final version 2019-12-29 23:21:44 +01:00
147 changed files with 17777 additions and 13015 deletions

View File

@ -1,7 +0,0 @@
.git
.gitignore
.idea
**/*.pyc
**/*.swp
black-env
debian

6
.gitignore vendored
View File

@ -1,5 +1,3 @@
**/*.pyc
**/*.swp
*.pyc
*.swp
tags
.idea
packages

View File

@ -1,118 +0,0 @@
**unreleased**
- Support for SoapyRemote
**2020-02-08**
- Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed
- Decoding of Pocsag modulation is now possible
- Removed the 3D waterfall since it had no real application and required ~1MB of javascript code to be downloaded
- Improved the frontend handling of the "too many users" scenario
- PSK63 digimode is now available (same decoding pipeline as PSK31, but with adopted parameters)
- The frequency can now be manipulated with the mousewheel, which should allow the user to tune more precise. The tuning
step size is determined by the digit the mouse cursor is hovering over.
- Clicking on the frequency now opens an input for direct frequency selection
- URL hashes have been fixed and improved: They are now updated automatically, so a shared URL will include frequency
and demodulator, which allows for improved sharing and linking.
- New daylight scheduler for background decoding, allows profiles to be selected by local sunrise / sunset times
- New devices supported:
- LimeSDR (`"type": "lime_sdr"`)
- PlutoSDR (`"type": "pluto_sdr"`)
- RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow use of the direct sampling mode
**2020-01-04**
- The [owrx_connector](https://github.com/jketterl/owrx_connector) is now the default way of communicating with sdr
devices. The old sdr types have been replaced, all `_connector` suffixes on the type must be removed!
- The sources have been refactored, making it a lot easier to add support for other devices
- SDR device failure handling has been improved, including user feedback
- New devices supported:
- FiFiSDR (`"type": "fifi_sdr"`)
**2019-12-15**
- wsjt-x updated to 2.1.2
- The rtl_tcp compatibility mode of the owrx_connector is now configurable using the `rtltcp_compat` flag
**2019-12-10**
- added support for airspyhf devices (Airspy HF+ / Discovery)
**2019-12-05**
- explicit device filter for soapy devices for multi-device setups
**2019-12-03**
- compatibility fixes for safari browsers (ios and mac)
**2019-11-24**
- There is now a new way to interface with SDR hardware, .
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.
**2019-10-27**
- 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
**2019-09-29**
- 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.
**2019-09-25**
- 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.
**2019-09-13**
- 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.
**2019-07-21**
- 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
**2019-07-13**
- 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)
**2019-06-30**
- 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!

15
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,15 @@
First of all, thank you for taking the time to contribute to this project!
Before I can accept your contributions, I need a signed copy of the Individual Contributor License Agreement (ICLA) from you, which is available <a href="ICLA.txt">here</a>.
The ICLA is needed because it will allow me to dual license the OpenWebRX project under AGPL and a commercial license.
I will also apply dual licensing to csdr, but only those parts that are original work (e.g. without the parts enabled by `-DUSE_IMA_ADPCM`; code taken from other projects is clearly separable).
However, even if there is commercial interest in the projects, I promise to keep them as open as possible, keeping my original intention to provide an open-source web-based SDR receiver software to the amateur radio operators and SDR enthusiasts.
This contributor agreement is based on the one of Apache Software Foundation, with some modifications. (You can review differences <a href="https://gist.github.com/ha7ilm/9e981006d24659e336c7/revisions">here</a>).
When you contribute for the first time, I will send you the ICLA. Replying with only the information requested and the text "I Agree" is sufficient.
Thanks,
Andras, HA7ILM

5
CONTRIBUTORS Normal file
View File

@ -0,0 +1,5 @@
This is a list of the great people who contributed code to the OpenWebRX repository. (Names are sorted alphabetically.)
Gnoxter <gnoxter@linuxlounge.net>
John Seamons, ZL/KF6VO <jks@jks.com>

128
ICLA.txt Normal file
View File

@ -0,0 +1,128 @@
Individual Contributor License Agreement ("Agreement")
In order to clarify the intellectual property license granted
with Contributions from any person or entity, Retzler András
(hereinafter referred to as "Project Owner") must have a
Contributor License Agreement ("CLA") on file that has
been signed by each Contributor, indicating agreement to the license
terms below. This license is for your protection as a Contributor as
well as the protection of the Project Owner; it does not change your
rights to use your own Contributions for any other purpose.
Please read this document carefully before signing and keep a copy
for your records.
Full name: ______________________________________________________
(optional) Public name: _________________________________________
Mailing Address: ________________________________________________
________________________________________________
Country: ______________________________________________________
(optional) Telephone: ___________________________________________
E-Mail: ______________________________________________________
You accept and agree to the following terms and conditions for Your
present and future Contributions submitted to the Project Owner.
Except for the license granted herein to the Project Owner and recipients
of software distributed by the Project Owner, You reserve all right, title,
and interest in and to Your Contributions.
1. Definitions.
"You" (or "Your") shall mean the copyright owner or legal entity
authorized by the copyright owner that is making this Agreement
with the Project Owner. For legal entities, the entity making a
Contribution and all other entities that control, are controlled
by, or are under common control with that entity are considered to
be a single Contributor. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"Contribution" shall mean any original work of authorship,
including any modifications or additions to an existing work, that
is intentionally submitted by You to the Project Owner for inclusion
in, or documentation of, any of the products owned or managed by
the Project Owner (the "Work"). For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written
communication sent to the Project Owner or its representatives,
including but not limited to communication on electronic mailing
lists, source code control systems, and issue tracking systems that
are managed by, or on behalf of, the Project Owner for the purpose of
discussing and improving the Work, but excluding communication that
is conspicuously marked or otherwise designated in writing by You
as "Not a Contribution."
2. Grant of Copyright License. Subject to the terms and conditions of
this Agreement, You hereby grant to the Project Owner and to
recipients of software distributed by the Project Owner a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare derivative works of,
publicly display, publicly perform, sublicense, and distribute Your
Contributions and such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of
this Agreement, You hereby grant to the Project Owner and to
recipients of software distributed by the Project Owner a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the
Work, where such license applies only to those patent claims
licensable by You that are necessarily infringed by Your
Contribution(s) alone or by combination of Your Contribution(s)
with the Work to which such Contribution(s) was submitted. If any
entity institutes patent litigation against You or any other entity
(including a cross-claim or counterclaim in a lawsuit) alleging
that your Contribution, or the Work to which you have contributed,
constitutes direct or contributory patent infringement, then any
patent licenses granted to that entity under this Agreement for
that Contribution or Work shall terminate as of the date such
litigation is filed.
4. You represent that you are legally entitled to grant the above
license. If your employer(s) has rights to intellectual property
that you create that includes your Contributions, you represent
that you have received permission to make Contributions on behalf
of that employer, that your employer has waived such rights for
your Contributions to the Project Owner, or that your employer has
executed a separate Corporate CLA with the Project Owner.
5. You represent that each of Your Contributions is Your original
creation (see section 7 for submissions on behalf of others). You
represent that Your Contribution submissions include complete
details of any third-party license or other restriction (including,
but not limited to, related patents and trademarks) of which you
are personally aware and which are associated with any part of Your
Contributions.
6. You are not expected to provide support for Your Contributions,
except to the extent You desire to provide support. You may provide
support for free, for a fee, or not at all. Unless required by
applicable law or agreed to in writing, You provide Your
Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
OF ANY KIND, either express or implied, including, without
limitation, any warranties or conditions of TITLE, NON-
INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
7. Should You wish to submit work that is not Your original creation,
You may submit it to the Project Owner separately from any
Contribution, identifying the complete details of its source and of
any license or other restriction (including, but not limited to,
related patents, trademarks, and license agreements) of which you
are personally aware, and conspicuously marking the work as
"Submitted on behalf of a third-party: [named here]".
8. You agree to notify the Project Owner of any facts or circumstances of
which you become aware that would make these representations
inaccurate in any respect.
Please sign: __________________________________ Date: ________________
Text derived from the Apache Individual Contributor License Agreement
("Agreement") V2.0, available at http://apache.org/licenses/icla.txt

136
README.md
View File

@ -1,76 +1,94 @@
OpenWebRX
=========
# OpenWebRX
OpenWebRX is a multi-user SDR receiver software with a web interface.
----
### ⚠️ From 2019-12-29 OpenWebRX development is discontinued. ⚠️
I'm would like to say a big thanks to everyone who supported me during this project, including those who contributed either code or donations. It has been a very fruitful 6 years, but now it's time to move on to other projects. See also my [blog](https://blog.sdr.hu) about that.
(@simonyiszk, please keep this GitHub repo for historic purposes.)
Know limitations of the last version:
- Python 2.7, a main dependency of the project, will be not be officially maintained from 1 January 2020. By time, probably it will not be secure to use this version on public servers, unless someone still provides security patches for Python 2.
- Some specific parts of the DSP code could be improved for better SNR.
Even though these limitations are probably acceptable in an amateur radio project, I would not build critical infrastructure on it.
For commercial inquiries (e.g. if someone wants me to develop an improved version without these limitations), I'm still open, [drop me an e-mail](mailto:randras@sdr.hu).
----
[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/)
![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png)
It has the following features:
- [csdr](https://github.com/jketterl/csdr) based demodulators (AM/FM/SSB/CW/BPSK31/BPSK63)
- filter passband can be set from GUI
- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
- it works in Google Chrome, Chromium and Mozilla Firefox
- currently supports RTL-SDR, HackRF, SDRplay, AirSpy, LimeSDR, PlutoSDR
- Multiple SDR devices can be used simultaneously
- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag)
- [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN)
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9)
- <a href="https://github.com/simonyiszk/csdr">csdr</a> based demodulators (AM/FM/SSB/CW/BPSK31),
- filter passband can be set from GUI,
- waterfall display can be shifted back in time,
- it extensively uses HTML5 features like WebSocket, Web Audio API, and &lt;canvas&gt;,
- 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:
![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif)
**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 (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 (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 (2017-07-12)**
- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display.
> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*!
## OpenWebRX servers on SDR.hu
[SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want.
![sdr.hu](http://blog.sdr.hu/images/openwebrx/screenshot-sdrhu.png)
## Setup
### Raspberry Pi SD Card Images
Probably the quickest way to get started is to download the latest Raspberry Pi SD Card Image. It contains all the
depencencies out of the box, and should work on all Raspberry Pis. It is based off the Raspbian Lite distribution,
so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply.
You can find the latest images [here](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/index.html). You can
also checkout the `nightly` folder, which has the most recent builds, albeit untested.
Once you have booted a Raspberry with the SD Card, it will appear in your network with the hostname "openwebrx", which
should make it available as https://openwebrx/ on most networks. This may vary depending on your specific setup.
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/jketterl/csdr)
- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
- [owrx_connector](https://github.com/jketterl/owrx_connector)
Optional dependencies if you want to be able to listen do digital voice:
- [digiham](https://github.com/jketterl/digiham)
- [dsd](https://github.com/f4exb/dsdcc)
Optional dependency if you want to decode WSJT-X modes:
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html)
[Detailed installation instructions in the Wiki](https://github.com/jketterl/openwebrx/wiki/Manual-Package-installation-(including-digital-voice))
- <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):
- 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 &lt;dd5jfk@darc.de&gt;*
*Andras Retzler, HA7ILM &lt;randras@sdr.hu&gt;*
## Usage tips
@ -80,10 +98,16 @@ The filter envelope can be dragged at its ends and moved around to set the passb
However, if you hold down the shift key, you can drag the center line (BFO) or the whole passband (PBS).
## Setup tips
If you have any problems installing OpenWebRX, you should check out the <a href="https://github.com/simonyiszk/openwebrx/wiki">Wiki</a> about it, which has a page on the <a href="https://github.com/simonyiszk/openwebrx/wiki/Common-problems-and-their-solutions">common problems and their solutions</a>.
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
([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)).
OpenWebRX is available under Affero GPL v3 license (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">summary</a>).
OpenWebRX is also available under a commercial license on request. Please contact me at the address
*&lt;randras@sdr.hu&gt;* for licensing options.
OpenWebRX is also available under a commercial license on request. Please contact me at the address *&lt;randras@sdr.hu&gt;* for licensing options.

View File

@ -1,193 +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,
"frequencies": {
"pocsag": 439987500
}
},
{
"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
}
]

View File

@ -1,217 +0,0 @@
[
{
"name": "DB0ZU",
"frequency": 145725000,
"modulation": "nfm"
},
{
"name": "DB0ZM",
"frequency": 145750000,
"modulation": "nfm"
},
{
"name": "DM0ULR",
"frequency": 145787500,
"modulation": "nfm"
},
{
"name": "DB0EL",
"frequency": 439275000,
"modulation": "nfm"
},
{
"name": "DB0NJ",
"frequency": 438775000,
"modulation": "nfm"
},
{
"name": "DB0NJ",
"frequency": 439437500,
"modulation": "dmr"
},
{
"name": "DB0UFO",
"frequency": 438312500,
"modulation": "dmr"
},
{
"name": "DB0PV",
"frequency": 438525000,
"modulation": "ysf"
},
{
"name": "DB0BZA",
"frequency": 438412500,
"modulation": "ysf"
},
{
"name": "DB0OSH",
"frequency": 438250000,
"modulation": "ysf"
},
{
"name": "DB0ULR",
"frequency": 439325000,
"modulation": "nfm"
},
{
"name": "DB0ZU",
"frequency": 438850000,
"modulation": "nfm"
},
{
"name": "DB0ISW",
"frequency": 438650000,
"modulation": "nfm"
},
{
"name": "Radio DARC",
"frequency": 6070000,
"modulation": "am"
},
{
"name": "DB0TVM",
"frequency": 439575000,
"modulation": "dstar"
},
{
"name": "DB0TVM",
"frequency": 439800000,
"modulation": "dmr"
},
{
"name": "DB0TR",
"frequency": 438700000,
"modulation": "nfm"
},
{
"name": "DB0PME",
"frequency": 439825000,
"modulation": "dmr"
},
{
"name": "DB0HKN",
"frequency": 438300000,
"modulation": "dmr"
},
{
"name": "OE2XHM",
"frequency": 438825000,
"modulation": "nfm"
},
{
"name": "DM0WW",
"frequency": 438962500,
"modulation": "dmr"
},
{
"name": "OE7XXR",
"frequency": 438200000,
"modulation": "dstar"
},
{
"name": "OE2XZR",
"frequency": 439000000,
"modulation": "dstar"
},
{
"name": "DB0OAL",
"frequency": 439912500,
"modulation": "dmr"
},
{
"name": "DB0AAT",
"frequency": 439550000,
"modulation": "dmr"
},
{
"name": "DB0FSG",
"frequency": 439937500,
"modulation": "dmr"
},
{
"name": "DB0ULR",
"frequency": 145575000,
"modulation": "nfm"
},
{
"name": "DB0RDH",
"frequency": 145737500,
"modulation": "dstar"
},
{
"name": "DM0GAP",
"frequency": 145612500,
"modulation": "nfm"
},
{
"name": "DB0XF",
"frequency": 145600000,
"modulation": "nfm"
},
{
"name": "DB0TOL",
"frequency": 145712500,
"modulation": "nfm"
},
{
"name": "DB0TTB",
"frequency": 439587500,
"modulation": "dmr"
},
{
"name": "DB0TRS",
"frequency": 439125000,
"modulation": "nfm"
},
{
"name": "DB0OAL",
"frequency": 438937500,
"modulation": "nfm"
},
{
"name": "DM0ULR",
"frequency": 439337500,
"modulation": "nxdn"
},
{
"name": "DB0MIR",
"frequency": 439300000,
"modulation": "nfm"
},
{
"name": "DB0PM",
"frequency": 439075000,
"modulation": "nfm"
},
{
"name": "DB0CP",
"frequency": 439025000,
"modulation": "nfm"
},
{
"name": "OE7XGR",
"frequency": 438925000,
"modulation": "dmr"
},
{
"name": "DB0TOL",
"frequency": 438725000,
"modulation": "nfm"
},
{
"name": "DB0OAL",
"frequency": 438325000,
"modulation": "dstar"
},
{
"name": "DB0ROL",
"frequency": 439237500,
"modulation": "nfm"
},
{
"name": "DB0ABX",
"frequency": 439137500,
"modulation": "nfm"
}
]

View File

@ -1,15 +0,0 @@
#!/bin/bash
set -euxo pipefail
. docker/env
docker build --pull -t openwebrx-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-base .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr .
docker build --build-arg ARCHTAG=$ARCHTAG -t openwebrx-soapysdr-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-soapysdr .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-sdrplay:$ARCHTAG -f docker/Dockerfiles/Dockerfile-sdrplay .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-hackrf:$ARCHTAG -f docker/Dockerfiles/Dockerfile-hackrf .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-airspy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-airspy .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr-soapy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr-soapy .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-plutosdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-plutosdr .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-limesdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-limesdr .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-soapyremote:$ARCHTAG -f docker/Dockerfiles/Dockerfile-soapyremote .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-full:$ARCHTAG -t jketterl/openwebrx:$ARCHTAG -f docker/Dockerfiles/Dockerfile-full .

View File

@ -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-2020 by Jakob Ketterl <dd5jfk@darc.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
@ -36,17 +35,22 @@ config_webrx: configuration options for OpenWebRX
# https://github.com/simonyiszk/openwebrx/wiki
# ==== Server settings ====
web_port = 8073
max_clients = 20
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_asl = 200
receiver_admin = "example@example.com"
receiver_gps = (47.000000, 19.000000)
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
photo_desc = """
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 />
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/>
Device: %[RX_DEVICE]<br />
@ -61,26 +65,25 @@ 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_fps=9
fft_size=4096 #Should be power of 2
fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram.
audio_compression = "adpcm" # valid values: "adpcm", "none"
fft_compression = "adpcm" # valid values: "adpcm", "none"
# 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
digimodes_enable = True # Decoding digimodes come with higher CPU usage.
digimodes_fft_size = 1024
audio_compression="adpcm" #valid values: "adpcm", "none"
fft_compression="adpcm" #valid values: "adpcm", "none"
# 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
digimodes_enable=True #Decoding digimodes come with higher CPU usage.
digimodes_fft_size=1024
start_rtl_thread=True
"""
Note: if you experience audio underruns while CPU usage is 100%, you can:
@ -98,162 +101,92 @@ 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", "airspyhf", "fifi_sdr"
#
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
# connector.
#
# https://github.com/jketterl/owrx_connector
#
# NOTE: The connector sources have replaced the old piped nmux style of reading input. If you still have any sdrs
# configured that have type endin in "_connector", simply remove that suffix.
# 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",
"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",
},
},
},
"airspy": {
"name": "Airspy HF+",
"type": "airspyhf",
"ppm": 0,
"profiles": {
"20m": {
"name": "20m",
"center_freq": 14150000,
"rf_gain": 10,
"samp_rate": 768000,
"start_freq": 14070000,
"start_mod": "usb",
},
"30m": {
"name": "30m",
"center_freq": 10125000,
"rf_gain": 10,
"samp_rate": 192000,
"start_freq": 10142000,
"start_mod": "usb",
},
"40m": {
"name": "40m",
"center_freq": 7100000,
"rf_gain": 10,
"samp_rate": 256000,
"start_freq": 7070000,
"start_mod": "usb",
},
"80m": {
"name": "80m",
"center_freq": 3650000,
"rf_gain": 10,
"samp_rate": 768000,
"start_freq": 3570000,
"start_mod": "usb",
},
"49m": {
"name": "49m Broadcast",
"center_freq": 6000000,
"rf_gain": 10,
"samp_rate": 768000,
"start_freq": 6070000,
"start_mod": "am",
},
},
},
"sdrplay": {
"name": "SDRPlay RSP2",
"type": "sdrplay",
"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 ====
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
#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_min_level = -88 # in dB
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)
### old theme by HA7ILM:
# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
# waterfall_min_level = -115 #in dB
# waterfall_max_level = 0
# waterfall_auto_level_margin = (20, 30)
#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
#waterfall_min_level = -115 #in dB
#waterfall_max_level = 0
#waterfall_auto_level_margin = (20, 30)
##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
# Note: When the auto waterfall level button is clicked, the following happens:
#Note: When the auto waterfall level button is clicked, the following happens:
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]]
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]]
#
@ -261,56 +194,23 @@ waterfall_auto_level_margin = (5, 40)
# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/
# current_max_power_level __|
# 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]"
# === Experimental settings ===
# Warning! The settings below are very experimental.
csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr.
#Warning! The settings below are very experimental.
csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr.
csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes.
csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
csdr_through = False # Setting this True will print out how much data is going into the DSP chains.
nmux_memory = 50 # in megabytes. This sets the approximate size of the circular buffer used by nmux.
nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux.
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
"""

424
csdr.py Executable file
View File

@ -0,0 +1,424 @@
"""
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>
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 subprocess
import time
import os
import code
import signal
import fcntl
class dsp:
def __init__(self):
self.samp_rate = 250000
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
self.low_cut = -4000
self.high_cut = 4000
self.bpf_transition_bw = 320 #Hz, and this is a constant
self.ddc_transition_bw_rate = 0.15 # of the IF sample rate
self.running = False
self.secondary_processes_running = False
self.audio_compression = "none"
self.fft_compression = "none"
self.demodulator = "nfm"
self.name = "csdr"
self.format_conversion = "csdr convert_u8_f"
self.base_bufsize = 512
self.nc_port = 4951
self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False
self.csdr_through = False
self.squelch_level = 0
self.fft_averages = 50
self.iqtee = False
self.iqtee2 = False
self.secondary_demodulator = None
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", "iqtee_pipe", "iqtee2_pipe"]
self.secondary_pipe_names=["secondary_shift_pipe"]
self.secondary_offset_freq = 1000
def chain(self,which):
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":
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":
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:
chain_begin+="csdr tee {iqtee_pipe} | "
chain_begin+="csdr tee {iqtee2_pipe} | "
chain_end = ""
if self.audio_compression=="adpcm":
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):
secondary_chain_base="cat {input_pipe} | "
if which == "fft":
return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "")
elif which == "bpsk31":
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):
self.secondary_demodulator = what
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
def secondary_decimation(self):
return 1 #currently unused
def secondary_bpf_cutoff(self):
if self.secondary_demodulator == "bpsk31":
return (31.25/2) / self.if_samp_rate()
return 0
def secondary_bpf_transition_bw(self):
if self.secondary_demodulator == "bpsk31":
return (31.25/2) / self.if_samp_rate()
return 0
def secondary_samples_per_bits(self):
if self.secondary_demodulator == "bpsk31":
return int(round(self.if_samp_rate()/31.25))&~3
return 0
def secondary_bw(self):
if self.secondary_demodulator == "bpsk31":
return 31.25
def start_secondary_demodulator(self):
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_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()
)
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";
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
#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:
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
self.try_delete_pipes(self.secondary_pipe_names)
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
def set_secondary_fft_size(self,secondary_fft_size):
#to change this, restart is required
self.secondary_fft_size=secondary_fft_size
def set_audio_compression(self,what):
self.audio_compression = what
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 (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)
def set_samp_rate(self,samp_rate):
#to change this, restart is required
self.samp_rate=samp_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
def get_name(self):
return self.name
def get_output_rate(self):
return self.output_rate
def set_output_rate(self,output_rate):
self.output_rate=output_rate
self.set_samp_rate(self.samp_rate) #as it depends on output_rate
def set_demodulator(self,demodulator):
#to change this, restart is required
self.demodulator=demodulator
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
def set_fft_fps(self,fft_fps):
#to change this, restart is required
self.fft_fps=fft_fps
def set_fft_averages(self,fft_averages):
#to change this, restart is required
self.fft_averages=fft_averages
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
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.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate))
self.shift_pipe_file.flush()
def set_bpf(self,low_cut,high_cut):
self.low_cut=low_cut
self.high_cut=high_cut
if self.running:
self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) )
self.bpf_pipe_file.flush()
def get_bpf(self):
return [self.low_cut, self.high_cut]
def set_squelch_level(self, squelch_level):
self.squelch_level=squelch_level
if self.running:
self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) )
self.squelch_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:
os.unlink(path)
except:
pass
os.mkfifo(path)
def ddc_transition_bw(self):
return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base):
# 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))
else:
setattr(self, pipe_name, None)
def try_delete_pipes(self, pipe_names):
for pipe_name in pipe_names:
pipe_path = getattr(self,pipe_name,None)
if pipe_path:
try: os.unlink(pipe_path)
except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e
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):
command_base=self.chain(self.demodulator)
#create control pipes for csdr
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, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe )
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";
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env)
self.running = True
#open control pipes for csdr and send initialization data
if self.bpf_pipe != None:
self.bpf_pipe_file=open(self.bpf_pipe,"w")
self.set_bpf(self.low_cut,self.high_cut)
if self.shift_pipe != None:
self.shift_pipe_file=open(self.shift_pipe,"w")
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()
def read(self,size):
return self.process.stdout.read(size)
def stop(self):
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
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)
# 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):
self.stop()
self.start()
def __del__(self):
self.stop()
del(self.process)

View File

@ -1,760 +0,0 @@
"""
OpenWebRX csdr plugin: do the signal processing with csdr
This file is part of OpenWebRX,
an open-source SDR receiver software with a web UI.
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
Copyright (c) 2019-2020 by Jakob Ketterl <dd5jfk@darc.de>
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 subprocess
import os
import signal
import threading
import math
from functools import partial
from owrx.kiss import KissClient, DirewolfConfig
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper
import logging
logger = logging.getLogger(__name__)
class output(object):
def send_output(self, t, read_fn):
if not self.supports_type(t):
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data
logger.warning("dumping output of type %s since it is not supported.", t)
threading.Thread(target=self.pump(read_fn, lambda x: None)).start()
return
self.receive_output(t, read_fn)
def receive_output(self, t, read_fn):
pass
def pump(self, read, write):
def copy():
run = True
while run:
data = read()
if data is None or (isinstance(data, bytes) and len(data) == 0):
run = False
else:
write(data)
return copy
def supports_type(self, t):
return True
class dsp(object):
def __init__(self, output):
self.samp_rate = 250000
self.output_rate = 11025
self.fft_size = 1024
self.fft_fps = 5
self.offset_freq = 0
self.low_cut = -4000
self.high_cut = 4000
self.bpf_transition_bw = 320 # Hz, and this is a constant
self.ddc_transition_bw_rate = 0.15 # of the IF sample rate
self.running = False
self.secondary_processes_running = False
self.audio_compression = "none"
self.fft_compression = "none"
self.demodulator = "nfm"
self.name = "csdr"
self.base_bufsize = 512
self.nc_port = None
self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False
self.csdr_through = False
self.squelch_level = -150
self.fft_averages = 50
self.iqtee = False
self.iqtee2 = False
self.secondary_demodulator = None
self.secondary_fft_size = 1024
self.secondary_process_fft = None
self.secondary_process_demod = None
self.pipe_names = [
"bpf_pipe",
"shift_pipe",
"squelch_pipe",
"smeter_pipe",
"meta_pipe",
"iqtee_pipe",
"iqtee2_pipe",
"dmr_control_pipe",
]
self.secondary_pipe_names = ["secondary_shift_pipe"]
self.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"]
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}",
]
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}"
]
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"]
if self.audio_compression == "adpcm":
chain += ["csdr encode_ima_adpcm_i16_u8"]
return chain
def secondary_chain(self, which):
chain = ["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
elif which == "bpsk31" or which == "bpsk63":
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"]
elif which == "pocsag":
chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"]
return chain + ["fsk_demodulator -i", "pocsag_decoder"]
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
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()
elif self.secondary_demodulator == "bpsk63":
return 62.5 / self.if_samp_rate()
return 0
def secondary_bpf_transition_bw(self):
if self.secondary_demodulator == "bpsk31":
return 31.25 / self.if_samp_rate()
elif self.secondary_demodulator == "bpsk63":
return 62.5 / self.if_samp_rate()
return 0
def secondary_samples_per_bits(self):
if self.secondary_demodulator == "bpsk31":
return int(round(self.if_samp_rate() / 31.25)) & ~3
elif self.secondary_demodulator == "bpsk63":
return int(round(self.if_samp_rate() / 62.5)) & ~3
return 0
def secondary_bw(self):
if self.secondary_demodulator == "bpsk31":
return 31.25
elif self.secondary_demodulator == "bpsk63":
return 62.5
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)
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,
)
logger.debug("secondary command (demod) = %s", secondary_command_demod)
my_env = os.environ.copy()
# if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1";
if self.csdr_print_bufsizes:
my_env["CSDR_PRINT_BUFSIZES"] = "1"
if self.output.supports_type("secondary_fft"):
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
secondary_command_fft = secondary_command_fft.format(
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, start_new_session=True, 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, start_new_session=True, env=my_env
)
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)
elif self.isPocsag():
self.output.send_output("pocsag_demod", self.secondary_process_demod.stdout.readline)
else:
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
# open control pipes for csdr and send initialization data
if self.secondary_shift_pipe != None: # TODO digimodes
self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
def set_secondary_offset_freq(self, value):
self.secondary_offset_freq = value
if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"):
self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
self.secondary_shift_pipe_file.flush()
def stop_secondary_demodulator(self):
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
self.secondary_processes_running = False
def get_secondary_demodulator(self):
return self.secondary_demodulator
def set_secondary_fft_size(self, secondary_fft_size):
# to change this, restart is required
self.secondary_fft_size = secondary_fft_size
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))
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)
def set_samp_rate(self, samp_rate):
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)
def if_samp_rate(self):
return self.samp_rate / self.decimation
def get_name(self):
return self.name
def get_output_rate(self):
return self.output_rate
def get_audio_rate(self):
if self.isDigitalVoice() or self.isPacket() or self.isPocsag():
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 isPocsag(self, demodulator=None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "pocsag"
def set_output_rate(self, output_rate):
if self.output_rate == output_rate:
return
self.output_rate = output_rate
self.calculate_decimation()
self.restart()
def set_demodulator(self, demodulator):
if self.demodulator == demodulator:
return
self.demodulator = demodulator
self.calculate_decimation()
self.restart()
def get_demodulator(self):
return self.demodulator
def set_fft_size(self, fft_size):
self.fft_size = fft_size
self.restart()
def set_fft_fps(self, fft_fps):
self.fft_fps = fft_fps
self.restart()
def set_fft_averages(self, fft_averages):
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
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.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() or self.isPocsag() else self.squelch_level
if self.running:
self.modification_lock.acquire()
self.squelch_pipe_file.write("%g\n" % (self.convertToLinear(actual_squelch)))
self.squelch_pipe_file.flush()
self.modification_lock.release()
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 mkfifo(self, path):
try:
os.unlink(path)
except:
pass
os.mkfifo(path)
def ddc_transition_bw(self):
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base):
for pipe_name in pipe_names:
if "{" + pipe_name + "}" in command_base:
setattr(self, pipe_name, self.pipe_base_path + pipe_name)
self.mkfifo(getattr(self, pipe_name))
else:
setattr(self, pipe_name, None)
def try_delete_pipes(self, pipe_names):
for pipe_name in pipe_names:
pipe_path = getattr(self, pipe_name, None)
if pipe_path:
try:
os.unlink(pipe_path)
except FileNotFoundError:
# it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
pass
except Exception:
logger.exception("try_delete_pipes()")
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 start(self):
self.modification_lock.acquire()
if self.running:
self.modification_lock.release()
return
self.running = True
command_base = " | ".join(self.chain(self.demodulator))
# create control pipes for csdr
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self))
self.try_create_pipes(self.pipe_names, command_base)
# run the command
command = command_base.format(
bpf_pipe=self.bpf_pipe,
shift_pipe=self.shift_pipe,
decimation=self.decimation,
last_decimation=self.last_decimation,
fft_size=self.fft_size,
fft_block_size=self.fft_block_size(),
fft_averages=self.fft_averages,
bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
ddc_transition_bw=self.ddc_transition_bw(),
flowcontrol=int(self.samp_rate * 2),
start_bufsize=self.base_bufsize * self.decimation,
nc_port=self.nc_port,
squelch_pipe=self.squelch_pipe,
smeter_pipe=self.smeter_pipe,
meta_pipe=self.meta_pipe,
iqtee_pipe=self.iqtee_pipe,
iqtee2_pipe=self.iqtee2_pipe,
output_rate=self.get_output_rate(),
smeter_report_every=int(self.if_samp_rate() / 6000),
unvoiced_quality=self.get_unvoiced_quality(),
dmr_control_pipe=self.dmr_control_pipe,
audio_rate=self.get_audio_rate(),
)
logger.debug("Command = %s", command)
my_env = os.environ.copy()
if self.csdr_dynamic_bufsize:
my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
if self.csdr_print_bufsizes:
my_env["CSDR_PRINT_BUFSIZES"] = "1"
out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True, env=my_env)
def watch_thread():
rc = self.process.wait()
logger.debug("dsp thread ended with rc=%d", rc)
if rc == 0 and self.running and not self.modification_lock.locked():
logger.debug("restarting since rc = 0, self.running = true, and no modification")
self.restart()
threading.Thread(target=watch_thread).start()
if self.output.supports_type("audio"):
self.output.send_output(
"audio",
partial(
self.process.stdout.read,
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
),
)
# open control pipes for csdr
if self.bpf_pipe:
self.bpf_pipe_file = open(self.bpf_pipe, "w")
if self.shift_pipe:
self.shift_pipe_file = open(self.shift_pipe, "w")
if self.squelch_pipe:
self.squelch_pipe_file = open(self.squelch_pipe, "w")
self.start_secondary_demodulator()
self.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 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()
self.try_delete_pipes(self.pipe_names)
self.modification_lock.release()
def restart(self):
if not self.running:
return
self.stop()
self.start()
def __del__(self):
self.stop()
del self.process

97
debian/changelog vendored
View File

@ -1,97 +0,0 @@
openwebrx (0.18.0) buster; urgency=low
* Compression, resampling and filtering in the frontend have been rewritten
in javascript, sdr.js has been removed
* Decoding of Pocsag modulation is now possible
* Removed the 3D waterfall since it had no real application and required ~1MB
of javascript code to be downloaded
* Improved the frontend handling of the "too many users" scenario
* PSK63 digimode is now available (same decoding pipeline as PSK31, but with
adopted parameters)
* The frequency can now be manipulated with the mousewheel, which should
allow the user to tune more precise. The tuning step size is determined by
the digit the mouse cursor is hovering over.
* Clicking on the frequency now opens an input for direct frequency selection
* URL hashes have been fixed and improved: They are now updated
automatically, so a shared URL will include frequency and demodulator,
which allows for improved sharing and linking.
* New daylight scheduler for background decoding, allows profiles to be
selected by local sunrise / sunset times
* The owrx_connector is now the default way of communicating with sdr
devices. The old sdr types have been replaced, all `_connector` suffixes on
the type must be removed!
* The sources have been refactored, making it a lot easier to add support for
other devices
* SDR device failure handling has been improved, including user feedback
* New devices supported:
* wsjt-x updated to 2.1.2
* The rtl_tcp compatibility mode of the owrx_connector is now configurable
using the `rtltcp_compat` flag
* explicit device filter for soapy devices for multi-device setups
* compatibility fixes for safari browsers (ios and mac)
* Offset tuning using the `lfo_offset` has been reworked in a way that
`center_freq` has to be set to the frequency you actually want to listen
to. If you're using an `lfo_offset` already, you will probably need to
change its sign.
* `initial_squelch_level` can now be set on each profile.
* Part of the frontend code has been reworked
- Audio buffer minimums have been completely stripped. As a result, you
should get better latency. Unfortunately, this also means there will be
some skipping when audio starts.
- Now also supports AudioWorklets (for those browser that have it).
- Mousewheel controls for the receiver sliders
* Error handling for failed SDR devices
* One of the most-requested features is finally coming to OpenWebRX:
Bookmarks (sometimes also referred to as labels).
There's two kinds of bookmarks available:
- Serverside bookmarks that are set up by the receiver administrator.
Check the file `bookmarks.json` for examples!
- Clientside bookmarks which every user can store for themselves. They are
stored in the browser's localStorage.
* Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is
now possible. Please have a look at the configuration on how to set it up.
* Websocket communication has been overhauled in large parts. It should now
be more reliable, and failing connections should now have no impact on
other users.
* Profile scheduling allows to set up band-hopping if you are running
background services.
* APRS now has the ability to show symbols on the map, if a corresponding
symbol set has been installed. Check the config!
* Debug logging has been disabled in a handful of modules, expect vastly
reduced output on the shell.
* New set of APRS-related features
- Decode Packet transmissions using direwolf (1k2 only for now)
- APRS packets are mostly decoded and shown both in a new panel and on the
map
- APRS is also available as a background service
- direwolfs I-gate functionality can be enabled, which allows your receiver
to work as a receive-only I-gate for the APRS network in the background
* Demodulation for background services has been optimized to use less total
bandwidth, saving CPU
* More metrics have been added; they can be used together with collectd and
its curl_json plugin for now, with some limitations.
* New bandplan feature, the first thing visible is the "dial" indicator that
brings you right to the dial frequency for digital modes
* fixed some bugs in the websocket communication which broke the map
* WSJT-X integration (FT8, FT4, WSPR, JT65, JT9 using wsjt-x demodulators)
* New Map Feature that shows both decoded grid squares from FT8 and Locations
decoded from YSF digital voice
* New Feature report that will show what functionality is available
* major rework on the openwebrx core
* Support of multiple SDR devices simultaneously
* Support for multiple profiles per SDR that allow the user to listen to
different frequencies
* Support for digital voice decoding
* Feature detection that will disable functionality when dependencies are not
available (if you're missing the digital
buttons, this is probably why)
* Support added for the following SDR sources:
- LimeSDR (`"type": "lime_sdr"`)
- PlutoSDR (`"type": "pluto_sdr"`)
- RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow
use of the direct sampling mode
- SoapyRemote (`"type": "soapy_remote"`)
- FiFiSDR (`"type": "fifi_sdr"`)
- airspyhf devices (Airspy HF+ / Discovery) (`"type": "airspyhf"`)
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 18 Feb 2020 20:09:00 +0000

1
debian/compat vendored
View File

@ -1 +0,0 @@
10

13
debian/control vendored
View File

@ -1,13 +0,0 @@
Source: openwebrx
Maintainer: Jakob Ketterl <jakob.ketterl@gmx.de>
Section: hamradio
Priority: optional
Standards-Version: 4.2.0
Build-Depends: debhelper (>= 10), dh-python, python3 (>= 3.5)
Package: openwebrx
Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface

View File

@ -1,4 +0,0 @@
config_webrx.py etc/openwebrx/
bands.json etc/openwebrx/
bookmarks.json etc/openwebrx/
systemd/openwebrx.service lib/systemd/system/

7
debian/postinst vendored
View File

@ -1,7 +0,0 @@
#!/bin/bash
set -euxo pipefail
adduser --system --group --no-create-home --home /nonexistant openwebrx
usermod -aG plugdev openwebrx
#DEBHELPER#

5
debian/rules vendored
View File

@ -1,5 +0,0 @@
#!/usr/bin/make -f
export PYBUILD_NAME=openwebrx
%:
dh $@ --with python3 --buildsystem=pybuild --with systemd

View File

@ -1 +0,0 @@
3.0 (native)

View File

@ -1,10 +0,0 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-airspy.sh /
RUN /install-dependencies-airspy.sh
RUN rm /install-dependencies-airspy.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -1,19 +0,0 @@
FROM alpine:3.10
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
RUN rm /install-dependencies.sh
ADD . /opt/openwebrx
WORKDIR /opt/openwebrx
VOLUME /etc/openwebrx
ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ]
EXPOSE 8073

View File

@ -1,20 +0,0 @@
ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG
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
RUN /install-dependencies-rtlsdr-soapy.sh
RUN /install-dependencies-plutosdr.sh
RUN /install-dependencies-limesdr.sh
RUN /install-dependencies-soapyremote.sh
RUN rm /install-dependencies-*.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

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

View File

@ -1,10 +0,0 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-limesdr.sh /
RUN /install-dependencies-limesdr.sh
RUN rm /install-dependencies-limesdr.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -1,10 +0,0 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-plutosdr.sh /
RUN /install-dependencies-plutosdr.sh
RUN rm /install-dependencies-plutosdr.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -1,10 +0,0 @@
ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-rtlsdr.sh /
RUN /install-dependencies-rtlsdr.sh
RUN rm /install-dependencies-rtlsdr.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -1,10 +0,0 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh /
RUN /install-dependencies-rtlsdr-soapy.sh
RUN rm /install-dependencies-rtlsdr-soapy.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

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

View File

@ -1,10 +0,0 @@
ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-soapyremote.sh /
RUN /install-dependencies-soapyremote.sh
RUN rm /install-dependencies-soapyremote.sh
ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

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

View File

@ -1,5 +0,0 @@
ARCH=$(uname -m)
IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-full openwebrx"
ALL_ARCHS="x86_64 armv7l aarch64"
TAG=${TAG:-"latest"}
ARCHTAG="$TAG-$ARCH"

View File

@ -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"

View File

@ -1,29 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
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 22a34fe649a0121a79262f54e99e9aa864b1536f
apk del .build-deps

View File

@ -1,39 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="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 bceca18f9e3a5f89cff78c4d949c71771d92dfd3
git clone https://github.com/pothosware/SoapyAirspy.git
cmakebuild SoapyAirspy 99756be5c3413a2d447baf70cb5a880662452655
git clone https://github.com/airspy/airspyhf.git
cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
git clone https://github.com/pothosware/SoapyAirspyHF.git
cmakebuild SoapyAirspyHF 54f5487dd96207540b2dd562ff9e718e0588770b
apk del .build-deps

View File

@ -1,34 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="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
git checkout 06eb9192cd348083f5f7de9c0da9ead276020011
cmakebuild host
cd ..
rm -rf hackrf
apk del .build-deps

View File

@ -1,24 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
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/myriadrf/LimeSuite.git
cd LimeSuite
git checkout 1c1c202f9a6ae4bb34068b6f3f576f7f8e74c7f1
mkdir builddir
cd builddir
cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic"
make
make install
cd ../..
rm -rf LimeSuite
apk del .build-deps

View File

@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake .. ${3:-}
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb libxml2"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers libxml2-dev flex bison"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/analogdevicesinc/libiio.git
cmakebuild libiio 4e22517c60f3c5e691320871956edede15459ae3 -DCMAKE_INSTALL_PREFIX=/usr/local
git clone https://github.com/analogdevicesinc/libad9361-iio.git
cmakebuild libad9361-iio 8ac95f3325c18c2e34cd9cfd49c7b63d69a0a9d2
git clone https://github.com/pothosware/SoapyPlutoSDR.git
cmakebuild SoapyPlutoSDR e28e4f5c68c16a38c0b50b9606035f3267a135c8
apk del .build-deps

View File

@ -1,33 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="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 b5af355b1d833b3c898a61cf1e072b59b0ea3440
git clone https://github.com/pothosware/SoapyRTLSDR.git
cmakebuild SoapyRTLSDR 5c5d9503337c6d1c34b496dec6f908aab9478b0f
apk del .build-deps

View File

@ -1,30 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="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 b5af355b1d833b3c898a61cf1e072b59b0ea3440
apk del .build-deps

View File

@ -1,54 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="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
;;
aarch64)
BINARY=SDRplay_RSP_API-ARM64-2.13.1.run
;;
esac
wget https://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 14ec39e4ff0dab7ae7fdf1afbbd2d28b49b0ffae
apk del .build-deps

View File

@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="avahi"
BUILD_PACKAGES="git cmake make gcc musl-dev g++ linux-headers avahi-dev"
apk add --no-cache $STATIC_PACKAGES
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapyRemote.git
cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f
apk del .build-deps

View File

@ -1,30 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="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 f722f9ce5b629c3c44401a9bf628b3f8e67a9695
apk del .build-deps

View File

@ -1,66 +0,0 @@
#!/bin/bash
set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="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 bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
git clone https://github.com/jketterl/csdr.git
cd csdr
git checkout fe0b042d9cdc2605a817ca7fdd3a23c48bf07563
make
make install
cd ..
rm -rf csdr
git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
git clone https://github.com/jketterl/digiham.git
cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f
git clone https://github.com/f4exb/dsd.git
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
WSJT_DIR=wsjtx-2.1.2
WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ
tar xvfz $WSJT_TGZ
cmakebuild $WSJT_DIR
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
cd direwolf
patch -Np1 < /direwolf-1.5.patch
make
make install
cd ..
rm -rf direwolf
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
pushd /opt/aprs-symbols
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
popd
apk del .build-deps

View File

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

View File

@ -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

View File

@ -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..."

View File

@ -1,28 +0,0 @@
#!/bin/bash
set -euo pipefail
mkdir -p /etc/openwebrx/
mkdir -p /tmp/openwebrx/
if [[ ! -f /etc/openwebrx/config_webrx.py ]] ; then
sed 's/temporary_directory = "\/tmp"/temporary_directory = "\/tmp\/openwebrx"/' < "/opt/openwebrx/config_webrx.py" > "/etc/openwebrx/config_webrx.py"
fi
if [[ ! -f /etc/openwebrx/bands.json ]] ; then
cp bands.json /etc/openwebrx/
fi
if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then
cp bookmarks.json /etc/openwebrx/
fi
_term() {
echo "Caught signal!"
kill -TERM "$child" 2>/dev/null
}
trap _term SIGTERM SIGINT
python3 openwebrx.py $@ &
child=$!
wait "$child"

View File

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +0,0 @@
<HTML><HEAD>
<TITLE>OpenWebRX Feature report</TITLE>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<link rel="stylesheet" href="static/css/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>

View File

@ -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("")
);
})
});
});

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,93 @@
Copyright (c) 2011, Jasper de Waard (jasper@designtown.nl),
with Reserved Font Name "Expletus Sans".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 797 B

85
htdocs/inactive.html Normal file
View 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>

View File

@ -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>

View File

@ -1,268 +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-2020 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>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/openwebrx.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/jquery.nanoscroller.js"></script>
<script src="static/lib/BookmarkBar.js"></script>
<script src="static/lib/AudioEngine.js"></script>
<script src="static/lib/ProgressBar.js"></script>
<script src="static/lib/Measurement.js"></script>
<script src="static/lib/FrequencyDisplay.js"></script>
<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="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" 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>
<table class="openwebrx-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
<thead><tr>
<th class="address">Address</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<div class="openwebrx-panel openwebrx-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://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"></div>
<div id="webrx-mouse-freq"></div>
</div>
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
<img src="static/gfx/openwebrx-bookmark.png">
</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="bpsk63">BPSK63</option>
<option value="ft8" data-feature="wsjt-x">FT8</option>
<option value="wspr" data-feature="wsjt-x">WSPR</option>
<option value="jt65" data-feature="wsjt-x">JT65</option>
<option value="jt9" data-feature="wsjt-x">JT9</option>
<option value="ft4" data-feature="wsjt-x">FT4</option>
<option value="packet" data-feature="packet">Packet</option>
<option value="pocsag" data-feature="pocsag">Pocsag</option>
</select>
</div>
<div class="openwebrx-panel-line">
<div 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 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" class="openwebrx-overlay" style="display:none;">
<div class="overlay-content">
<img id="openwebrx-play-button" src="static/gfx/openwebrx-play-button.png" />
<div>Start OpenWebRX</div>
</div>
</div>
<div id="openwebrx-error-overlay" class="openwebrx-overlay" style="display:none;">
<div class="overlay-content">
<div>This receiver is currently unavailable due to technical issues.</div>
<div>Error Message:</div>
<div class="errormessage"></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
View 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>

View File

@ -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);
};

View File

@ -1,356 +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 ImaAdpcmCodec();
this.compression = 'none';
this.setupResampling();
this.resampler = new Interpolator(this.resamplingFactor);
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(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);
};
function ImaAdpcmCodec() {
this.reset();
}
ImaAdpcmCodec.prototype.reset = function() {
this.stepIndex = 0;
this.predictor = 0;
this.step = 0;
};
ImaAdpcmCodec.imaIndexTable = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ];
ImaAdpcmCodec.imaStepTable = [
7, 8, 9, 10, 11, 12, 13, 14, 16, 17,
19, 21, 23, 25, 28, 31, 34, 37, 41, 45,
50, 55, 60, 66, 73, 80, 88, 97, 107, 118,
130, 143, 157, 173, 190, 209, 230, 253, 279, 307,
337, 371, 408, 449, 494, 544, 598, 658, 724, 796,
876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066,
2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358,
5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767
];
ImaAdpcmCodec.prototype.decode = function(data) {
var output = new Int16Array(data.length * 2);
for (var i = 0; i < data.length; i++) {
output[i * 2] = this.decodeNibble(data[i] & 0x0F);
output[i * 2 + 1] = this.decodeNibble((data[i] >> 4) & 0x0F);
}
return output;
};
ImaAdpcmCodec.prototype.decodeNibble = function(nibble) {
this.stepIndex += ImaAdpcmCodec.imaIndexTable[nibble];
this.stepIndex = Math.min(Math.max(this.stepIndex, 0), 88);
var diff = this.step >> 3;
if (nibble & 1) diff += this.step >> 2;
if (nibble & 2) diff += this.step >> 1;
if (nibble & 4) diff += this.step;
if (nibble & 8) diff = -diff;
this.predictor += diff;
this.predictor = Math.min(Math.max(this.predictor, -32768), 32767);
this.step = ImaAdpcmCodec.imaStepTable[this.stepIndex];
return this.predictor;
};
function Interpolator(factor) {
this.factor = factor;
this.lowpass = new Lowpass(factor)
}
Interpolator.prototype.process = function(data) {
var output = new Float32Array(data.length * this.factor);
for (var i = 0; i < data.length; i++) {
output[i * this.factor] = (data[i] + 0.5) / 32768;
}
return this.lowpass.process(output);
};
function Lowpass(interpolation) {
this.interpolation = interpolation;
var transitionBandwidth = 0.05;
this.numtaps = Math.round(4 / transitionBandwidth);
if (this.numtaps % 2 == 0) this.numtaps += 1;
var cutoff = 1 / interpolation;
this.coefficients = this.getCoefficients(cutoff / 2);
this.delay = new Float32Array(this.numtaps);
for (var i = 0; i < this.numtaps; i++){
this.delay[i] = 0;
}
this.delayIndex = 0;
}
Lowpass.prototype.getCoefficients = function(cutoffRate) {
var middle = Math.floor(this.numtaps / 2);
// hamming window
var window_function = function(r){
var rate = 0.5 + r / 2;
return 0.54 - 0.46 * Math.cos(2 * Math.PI * rate);
}
var output = [];
output[middle] = 2 * Math.PI * cutoffRate * window_function(0);
for (var i = 1; i <= middle; i++) {
output[middle - i] = output[middle + i] = (Math.sin(2 * Math.PI * cutoffRate * i) / i) * window_function(i / middle);
}
return this.normalizeCoefficients(output);
};
Lowpass.prototype.normalizeCoefficients = function(input) {
var sum = 0;
var output = [];
for (var i = 0; i < input.length; i++) {
sum += input[i];
}
for (var i = 0; i < input.length; i++) {
output[i] = input[i] / sum;
}
return output;
};
Lowpass.prototype.process = function(input) {
output = new Float32Array(input.length);
for (var oi = 0; oi < input.length; oi++) {
this.delay[this.delayIndex] = input[oi];
this.delayIndex = (this.delayIndex + 1) % this.numtaps;
var acc = 0;
var index = this.delayIndex;
for (var i = 0; i < this.numtaps; ++i) {
var index = index != 0 ? index - 1 : this.numtaps - 1;
acc += this.delay[index] * this.coefficients[i];
if (isNaN(acc)) debugger;
}
// gain by interpolation
output[oi] = this.interpolation * acc;
}
return output;
};

View File

@ -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);

View File

@ -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;
demodulators[0].set_offset_frequency(b.frequency - center_freq);
if (b.modulation) {
demodulator_analog_replace(b.modulation);
} else if (b.digital_modulation) {
demodulator_digital_replace(b.digital_modulation);
}
$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);
};

View File

@ -1,101 +0,0 @@
function FrequencyDisplay(element) {
this.element = $(element);
this.digits = [];
this.setupElements();
this.setFrequency(0);
}
FrequencyDisplay.prototype.setupElements = function() {
this.displayContainer = $('<div>');
this.digitContainer = $('<span>');
this.displayContainer.html([this.digitContainer, $('<span> MHz</span>')]);
this.element.html(this.displayContainer);
};
FrequencyDisplay.prototype.setFrequency = function(freq) {
this.frequency = freq;
var formatted = (freq / 1e6).toLocaleString(undefined, {maximumFractionDigits: 4, minimumFractionDigits: 4});
var children = this.digitContainer.children();
for (var i = 0; i < formatted.length; i++) {
if (!this.digits[i]) {
this.digits[i] = $('<span>');
var before = children[i];
if (before) {
$(before).after(this.digits[i]);
} else {
this.digitContainer.append(this.digits[i]);
}
}
this.digits[i][(isNaN(formatted[i]) ? 'remove' : 'add') + 'Class']('digit');
this.digits[i].html(formatted[i]);
}
while (this.digits.length > formatted.length) {
this.digits.pop().remove();
}
};
function TuneableFrequencyDisplay(element) {
FrequencyDisplay.call(this, element);
this.setupEvents();
}
TuneableFrequencyDisplay.prototype = new FrequencyDisplay();
TuneableFrequencyDisplay.prototype.setupElements = function() {
FrequencyDisplay.prototype.setupElements.call(this);
this.input = $('<input>');
this.input.hide();
this.element.append(this.input);
};
TuneableFrequencyDisplay.prototype.setupEvents = function() {
var me = this;
me.listeners = [];
me.element.on('wheel', function(e){
e.preventDefault();
e.stopPropagation();
var index = me.digitContainer.find('.digit').index(e.target);
if (index < 0) return;
var delta = 10 ** (Math.floor(Math.log10(me.frequency)) - index);
if (e.originalEvent.deltaY > 0) delta *= -1;
var newFrequency = me.frequency + delta;
me.listeners.forEach(function(l) {
l(newFrequency);
});
});
var submit = function(){
var freq = parseInt(me.input.val());
if (!isNaN(freq)) {
me.listeners.forEach(function(l) {
l(freq);
});
}
me.input.hide();
me.displayContainer.show();
};
me.input.on('blur', submit).on('keyup', function(e){
if (e.keyCode == 13) return submit();
if (e.keyCode == 27) {
me.input.hide();
me.displayContainer.show();
}
});
me.input.on('click', function(e){
e.stopPropagation();
});
me.element.on('click', function(){
me.input.val(me.frequency);
me.input.show();
me.displayContainer.hide();
me.input.focus();
});
};
TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){
this.listeners.push(listener);
};

View File

@ -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);
};

View File

@ -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);
};

File diff suppressed because one or more lines are too long

View File

@ -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();
}
}

View File

@ -1,25 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Map</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/chroma.min.js"></script>
<script src="static/map.js"></script>
<script src="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>

View File

@ -1,362 +0,0 @@
(function(){
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 protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws';
var href = window.location.href;
var index = href.lastIndexOf('/');
if (index > 0) {
href = href.substr(0, index + 1);
}
href = href.split("://")[1];
href = protocol + "://" + href;
if (!href.endsWith('/')) {
href += '/';
}
var ws_url = href + "ws/";
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);
})();

33
htdocs/mathbox-bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

461
htdocs/mathbox.css Normal file
View File

@ -0,0 +1,461 @@
.shadergraph-graph {
font: 12px sans-serif;
line-height: 25px;
position: relative;
}
.shadergraph-graph:after {
content: ' ';
display: block;
height: 0;
font-size: 0;
clear: both;
}
.shadergraph-graph svg {
pointer-events: none;
}
.shadergraph-clear {
clear: both;
}
.shadergraph-graph svg {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: auto;
height: auto;
}
.shadergraph-column {
float: left;
}
.shadergraph-node .shadergraph-graph {
float: left;
clear: both;
overflow: visible;
}
.shadergraph-node .shadergraph-graph .shadergraph-node {
margin: 5px 15px 15px;
}
.shadergraph-node {
margin: 5px 15px 25px;
background: rgba(0, 0, 0, .1);
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .2),
0 1px 10px rgba(0, 0, 0, .2);
min-height: 35px;
float: left;
clear: left;
position: relative;
}
.shadergraph-type {
font-weight: bold;
}
.shadergraph-header {
font-weight: bold;
text-align: center;
height: 25px;
background: rgba(0, 0, 0, .3);
text-shadow: 0 1px 2px rgba(0, 0, 0, .25);
color: #fff;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
margin-bottom: 5px;
padding: 0 10px;
}
.shadergraph-outlet div {
}
.shadergraph-outlet-in .shadergraph-name {
margin-right: 7px;
}
.shadergraph-outlet-out .shadergraph-name {
margin-left: 7px;
}
.shadergraph-name {
margin: 0 4px;
}
.shadergraph-point {
margin: 6px;
width: 11px;
height: 11px;
border-radius: 7.5px;
background: rgba(255, 255, 255, 1);
}
.shadergraph-outlet-in {
float: left;
clear: left;
}
.shadergraph-outlet-in div {
float: left;
}
.shadergraph-outlet-out {
float: right;
clear: right;
}
.shadergraph-outlet-out div {
float: right;
}
.shadergraph-node-callback {
background: rgba(205, 209, 221, .5);
box-shadow: 0 1px 2px rgba(0, 10, 40, .2),
0 1px 10px rgba(0, 10, 40, .2);
}
.shadergraph-node-callback > .shadergraph-header {
background: rgba(0, 20, 80, .3);
}
.shadergraph-graph .shadergraph-graph .shadergraph-node-callback {
background: rgba(0, 20, 80, .1);
}
.shadergraph-node-call {
background: rgba(209, 221, 205, .5);
box-shadow: 0 1px 2px rgba(10, 40, 0, .2),
0 1px 10px rgba(10, 40, 0, .2);
}
.shadergraph-node-call > .shadergraph-header {
background: rgba(20, 80, 0, .3);
}
.shadergraph-graph .shadergraph-graph .shadergraph-node-call {
background: rgba(20, 80, 0, .1);
}
.shadergraph-node-isolate {
background: rgba(221, 205, 209, .5);
box-shadow: 0 1px 2px rgba(40, 0, 10, .2),
0 1px 10px rgba(40, 0, 10, .2);
}
.shadergraph-node-isolate > .shadergraph-header {
background: rgba(80, 0, 20, .3);
}
.shadergraph-graph .shadergraph-graph .shadergraph-node-isolate {
background: rgba(80, 0, 20, .1);
}
.shadergraph-node.shadergraph-has-code {
cursor: pointer;
}
.shadergraph-node.shadergraph-has-code::before {
position: absolute;
content: ' ';
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
border: 2px solid rgba(0, 0, 0, .25);
border-radius: 5px;
}
.shadergraph-node.shadergraph-has-code:hover::before {
display: block;
}
.shadergraph-code {
z-index: 10000;
display: none;
position: absolute;
background: #fff;
color: #000;
white-space: pre;
padding: 10px;
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, .2),
0 1px 10px rgba(0, 0, 0, .2);
font-family: monospace;
font-size: 10px;
line-height: 12px;
}
.shadergraph-overlay {
position: fixed;
top: 50%;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-top: 1px solid #CCC;
}
.shadergraph-overlay .shadergraph-view {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
overflow: auto;
}
.shadergraph-overlay .shadergraph-inside {
width: 4000px;
min-height: 100%;
box-sizing: border-box;
}
.shadergraph-overlay .shadergraph-close {
position: absolute;
top: 5px;
right: 5px;
padding: 4px;
border-radius: 16px;
background: rgba(255,255,255,.3);
color: rgba(0, 0, 0, .3);
cursor: pointer;
font-size: 24px;
line-height: 24px;
width: 24px;
text-align: center;
vertical-align: middle;
}
.shadergraph-overlay .shadergraph-close:hover {
background: rgba(255,255,255,1);
color: rgba(0, 0, 0, 1);
}
.shadergraph-overlay .shadergraph-graph {
padding-top: 10px;
overflow: visible;
min-height: 100%;
}
.shadergraph-overlay span {
display: block;
padding: 5px 15px;
margin: 0;
background: rgba(0, 0, 0, .1);
font-weight: bold;
font-family: sans-serif;
}
.mathbox-loader {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
padding: 10px;
border-radius: 50%;
background: #fff;
}
.mathbox-loader.mathbox-exit {
opacity: 0;
-webkit-transition:
opacity .15s ease-in-out;
transition:
opacity .15s ease-in-out;
}
.mathbox-progress {
height: 10px;
border-radius: 5px;
width: 80px;
margin: 0 auto 20px;
box-shadow:
1px 1px 1px rgba(255, 255, 255, .2),
1px -1px 1px rgba(255, 255, 255, .2),
-1px 1px 1px rgba(255, 255, 255, .2),
-1px -1px 1px rgba(255, 255, 255, .2);
background: #ccc;
overflow: hidden;
}
.mathbox-progress > div {
display: block;
width: 0px;
height: 10px;
background: #888;
}
.mathbox-logo {
position: relative;
width: 140px;
height: 100px;
margin: 0 auto 10px;
-webkit-perspective: 200px;
perspective: 200px;
}
.mathbox-logo > div {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
}
.mathbox-logo > :nth-child(1) {
-webkit-transform: rotateZ(22deg) rotateX(24deg) rotateY(30deg);
transform: rotateZ(22deg) rotateX(24deg) rotateY(30deg);
}
.mathbox-logo > :nth-child(2) {
-webkit-transform: rotateZ(11deg) rotateX(12deg) rotateY(15deg) scale3d(.6, .6, .6);
transform: rotateZ(11deg) rotateX(12deg) rotateY(15deg) scale3d(.6, .6, .6);
}
.mathbox-logo > div > div {
position: absolute;
top: 50%;
left: 50%;
margin-left: -100px;
margin-top: -100px;
width: 200px;
height: 200px;
box-sizing: border-box;
border-radius: 50%;
}
.mathbox-logo > div > :nth-child(1) {
-webkit-transform: scale(0.5, 0.5);
transform: rotateX(30deg) scale(0.5, 0.5);
}
.mathbox-logo > div > :nth-child(2) {
-webkit-transform: rotateX(90deg) scale(0.42, 0.42);
transform: rotateX(90deg) scale(0.42, 0.42);
}
.mathbox-logo > div > :nth-child(3) {
-webkit-transform: rotateY(90deg) scale(0.35, 0.35);
transform: rotateY(90deg) scale(0.35, 0.35);
}
.mathbox-logo > :nth-child(1) > :nth-child(1) {
border: 16px solid #808080;
}
.mathbox-logo > :nth-child(1) > :nth-child(2) {
border: 19px solid #A0A0A0;
}
.mathbox-logo > :nth-child(1) > :nth-child(3) {
border: 23px solid #C0C0C0;
}
.mathbox-logo > :nth-child(2) > :nth-child(1) {
border: 27px solid #808080;
}
.mathbox-logo > :nth-child(2) > :nth-child(2) {
border: 32px solid #A0A0A0;
}
.mathbox-logo > :nth-child(2) > :nth-child(3) {
border: 38px solid #C0C0C0;
}
.mathbox-splash-blue .mathbox-progress {
background: #def;
}
.mathbox-splash-blue .mathbox-progress > div {
background: #1979e7;
}
.mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(1) {
border-color: #1979e7;
}
.mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(2) {
border-color: #33b0ff;
}
.mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(3) {
border-color: #75eaff;
}
.mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(1) {
border-color: #18487F;
}
.mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(2) {
border-color: #33b0ff;
}
.mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(3) {
border-color: #75eaff;
}
.mathbox-overlays {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
pointer-events: none;
transform-style: preserve-3d;
overflow: hidden;
}
.mathbox-overlays > div {
transform-style: preserve-3d;
}
.mathbox-overlay > div {
position: absolute;
will-change: transform, opacity;
}
.mathbox-label {
font-family: sans-serif;
}
.mathbox-outline-1 {
text-shadow:
-1px -1px 0px rgb(255, 255, 255),
1px 1px 0px rgb(255, 255, 255),
-1px 1px 0px rgb(255, 255, 255),
1px -1px 0px rgb(255, 255, 255),
1px 0px 1px rgb(255, 255, 255),
-1px 0px 1px rgb(255, 255, 255),
0px -1px 1px rgb(255, 255, 255),
0px 1px 1px rgb(255, 255, 255);
}
.mathbox-outline-2 {
text-shadow:
0px -2px 0px rgb(255, 255, 255),
0px 2px 0px rgb(255, 255, 255),
-2px 0px 0px rgb(255, 255, 255),
2px 0px 0px rgb(255, 255, 255),
-1px -2px 0px rgb(255, 255, 255),
-2px -1px 0px rgb(255, 255, 255),
-1px 2px 0px rgb(255, 255, 255),
-2px 1px 0px rgb(255, 255, 255),
1px 2px 0px rgb(255, 255, 255),
2px 1px 0px rgb(255, 255, 255),
1px -2px 0px rgb(255, 255, 255),
2px -1px 0px rgb(255, 255, 255);
}
.mathbox-outline-3 {
text-shadow:
3px 0px 0px rgb(255, 255, 255),
-3px 0px 0px rgb(255, 255, 255),
0px 3px 0px rgb(255, 255, 255),
0px -3px 0px rgb(255, 255, 255),
-2px -2px 0px rgb(255, 255, 255),
-2px 2px 0px rgb(255, 255, 255),
2px 2px 0px rgb(255, 255, 255),
2px -2px 0px rgb(255, 255, 255),
-1px -2px 1px rgb(255, 255, 255),
-2px -1px 1px rgb(255, 255, 255),
-1px 2px 1px rgb(255, 255, 255),
-2px 1px 1px rgb(255, 255, 255),
1px 2px 1px rgb(255, 255, 255),
2px 1px 1px rgb(255, 255, 255),
1px -2px 1px rgb(255, 255, 255),
2px -1px 1px rgb(255, 255, 255);
}
.mathbox-outline-4 {
text-shadow:
4px 0px 0px rgb(255, 255, 255),
-4px 0px 0px rgb(255, 255, 255),
0px 4px 0px rgb(255, 255, 255),
0px -4px 0px rgb(255, 255, 255),
-3px -2px 0px rgb(255, 255, 255),
-3px 2px 0px rgb(255, 255, 255),
3px 2px 0px rgb(255, 255, 255),
3px -2px 0px rgb(255, 255, 255),
-2px -3px 0px rgb(255, 255, 255),
-2px 3px 0px rgb(255, 255, 255),
2px 3px 0px rgb(255, 255, 255),
2px -3px 0px rgb(255, 255, 255),
-1px -2px 1px rgb(255, 255, 255),
-2px -1px 1px rgb(255, 255, 255),
-1px 2px 1px rgb(255, 255, 255),
-2px 1px 1px rgb(255, 255, 255),
1px 2px 1px rgb(255, 255, 255),
2px 1px 1px rgb(255, 255, 255),
1px -2px 1px rgb(255, 255, 255),
2px -1px 1px rgb(255, 255, 255);
}
.mathbox-outline-fill, .mathbox-outline-fill * {
color: #fff !important;
}

973
htdocs/openwebrx.css Normal file
View File

@ -0,0 +1,973 @@
/*
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, body
{
margin: 0;
padding: 0;
height: 100%;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
overflow: hidden;
}
select
{
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
}
input
{
vertical-align:middle;
}
input[type=range]
{
-webkit-appearance: none;
margin: 0 0;
}
input[type=range]:focus
{
outline: none;
}
input[type=range]::-webkit-slider-runnable-track
{
height: 5px;
cursor: pointer;
animate: 0.2s;
box-shadow: 0px 0px 0px #000000;
background: #B6B6B6;
/*border-radius: 11px;*/
border: 1px solid #8A8A8A;
}
input[type=range]::-webkit-slider-thumb
{
box-shadow: 1px 1px 1px #828282;
border: 1px solid #8A8A8A;
height: 15px;
width: 15px;
border-radius: 10px;
background: #FFFFFF;
cursor: pointer;
-webkit-appearance: none;
margin-top: -7px;
}
input[type=range]:focus::-webkit-slider-runnable-track
{
background: #B6B6B6;
}
input[type=range]::-moz-range-track
{
height: 3px;
cursor: pointer;
animate: 0.2s;
box-shadow: 0px 0px 0px #000000;
background: #B6B6B6;
border-radius: 11px;
border: 1px solid #8A8A8A;
}
input[type=range]::-moz-range-thumb
{
box-shadow: 1px 1px 1px #828282;
border: 1px solid #8A8A8A;
height: 12px;
width: 12px;
border-radius: 10px;
background: #FFFFFF;
cursor: pointer;
}
input[type=range]::-ms-track
{
width: 100%;
height: 7px;
cursor: pointer;
animate: 0.2s;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type=range]::-ms-fill-lower
{
background: #B6B6B6;
border: 1px solid #8A8A8A;
border-radius: 22px;
box-shadow: 0px 0px 0px #000000;
}
input[type=range]::-ms-fill-upper
{
background: #B6B6B6;
border: 1px solid #8A8A8A;
border-radius: 22px;
box-shadow: 0px 0px 0px #000000;
}
input[type=range]::-ms-thumb
{
box-shadow: 1px 1px 1px #828282;
border: 1px solid #8A8A8A;
height: 24px;
width: 7px;
border-radius: 0px;
background: #FFFFFF;
cursor: pointer;
}
input[type=range]:focus::-ms-fill-lower
{
background: #B6B6B6;
}
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
{
min-height:100%;
position:relative;
}
/*#webrx-photo-gradient-left
{
position: absolute;
bottom: 0px;
left: 0px;
background-image:url(gfx/webrx-photo-gradient-corner.png);
width: 59px;
height: 92px;
}
#webrx-photo-gradient-middle
{
position: absolute;
bottom: 0px;
left: 59px;
right: 59px;
height: 92px;
background-image:url(gfx/webrx-photo-gradient-middle.png);
}
#webrx-photo-gradient-right
{
position: absolute;
bottom: 0px;
right: 0px;
background-image:url(gfx/webrx-photo-gradient-corner.png);
width: 59px;
height: 92px;
-webkit-transform:scaleX(-1);
-moz-transform:scaleX(-1);
-ms-transform:scaleX(-1);
-o-transform:scaleX(-1);
transform:scaleX(-1);
}*/
#webrx-rx-photo-title
{
position: absolute;
left: 15px;
top: 78px;
color: White;
font-size: 16pt;
text-shadow: 1px 1px 4px #444;
opacity: 1;
}
#webrx-rx-photo-desc
{
position: absolute;
left: 15px;
top: 109px;
color: White;
font-size: 10pt;
font-weight: bold;
text-shadow: 0px 0px 6px #444;
opacity: 1;
line-height: 1.5em;
}
#webrx-rx-photo-desc a
{
/*color: #007df1;*/
color: #5ca8ff;
text-shadow: none;
/*text-shadow: 0px 0px 7px #fff;*/
}
#webrx-rx-title
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
position: absolute;
left: 350px;
top: 13px;
font-family: "DejaVu Sans", Verdana, Geneva, sans-serif;
color: #909090;
font-size: 11pt;
font-weight: bold;
}
#webrx-rx-desc
{
white-space:nowrap;
overflow: hidden;
cursor:pointer;
font-size: 10pt;
color: #909090;
position: absolute;
left: 350px;
top: 34px;
}
#webrx-rx-desc a
{
color: #909090;
/*text-decoration: none;*/
}
#openwebrx-rx-details-arrow
{
cursor:pointer;
position: absolute;
left: 470px;
top: 51px;
}
#openwebrx-rx-details-arrow a
{
margin: 0;
padding: 0;
}
#openwebrx-rx-details-arrow-down
{
display:none;
}
/*canvas#waterfall-canvas
{
border-style: none;
border-width: 1px;
height: 150px;
width: 100%;
}*/
#openwebrx-scale-container
{
height: 47px;
background-image: url("gfx/openwebrx-scale-background.png");
background-repeat: repeat-x;
overflow: hidden;
z-index:1000;
position: relative;
}
#webrx-canvas-container
{
/*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/
position: relative;
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;
}
#webrx-canvas-container canvas
{
position: absolute;
border-style: none;
image-rendering: crisp-edges;
image-rendering: -webkit-optimize-contrast;
/*transition: left 200ms, width 200ms;*/
}
#openwebrx-mathbox-container
{
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;*/
height: 125px;
width: 619px
}
.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;
position: absolute;
top: 0px;
left: 0px;
padding: 5px;
font-weight: bold;
border-radius: 10px;
-moz-border-radius: 10px;
background-color: #999999;
color: White;
z-index:9999; /*should be higher?
}*/
/* 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');
font-weight: normal;
font-style: normal;
}*/
@font-face {
font-family: 'expletus-sans-medium';
src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf');
font-weight: normal;
font-style: normal;
}
#webrx-actual-freq
{
width: 100%;
text-align: left;
font-size: 16pt;
font-family: 'expletus-sans-medium';
padding: 0;
margin: 0;
line-height:22px;
}
#webrx-mouse-freq
{
width: 100%;
text-align: left;
font-size: 10pt;
color: #AAA;
font-family: 'expletus-sans-medium';
margin-bottom: 5px;
}
.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;
}
.openwebrx-panel a
{
color: #5ca8ff;
text-shadow: none;
}
.openwebrx-panel-inner
{
overflow-y: auto;
overflow-x: hidden;
height: 100%;
}
.openwebrx-button
{
background-color: #373737;
padding: 4.2px;
border-radius: 5px;
-moz-border-radius: 5px;
color: White;
font-weight: bold;
margin-right: 1px;
cursor: pointer;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
display: inline-block;
}
.openwebrx-button:hover, .openwebrx-demodulator-button.highlighted
{
/*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #3F3F3F), color-stop(1, #777777) );
background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/
background: #474747;
color: #FFFF50;
}
.openwebrx-button:active
{
background: #777777;
color: #FFFF50;
}
.openwebrx-demodulator-button
{
width: 38px;
height: 19px;
font-size: 12pt;
text-align: center;
}
.openwebrx-square-button img
{
height: 27px;
}
.openwebrx-round-button
{
margin-right: -2px;
width: 35px;
height: 35px;
border-radius: 25px;
}
.openwebrx-round-button img
{
height: 30px;
}
.openwebrx-round-button-small
{
margin-right: -3px;
width: 20px;
height: 20px;
border-radius: 25px;
}
.openwebrx-round-button-small img
{
height: 20px;
}
img.openwebrx-mirror-img
{
transform: scale(-1, 1);
}
.openwebrx-round-rightarrow img
{
position: relative;
left: 12px;
top: 3px;
}
.openwebrx-round-leftarrow img
{
position: relative;
left: 7px;
top: 3px;
}
#openwebrx-client-log-title
{
margin-bottom: 5px;
font-weight: bold;
}
.openwebrx-progressbar
{
position: relative;
border-radius: 5px;
background-color: #003850; /*#006235;*/
display: inline-block;
text-align: center;
font-size: 8pt;
font-weight: bold;
text-shadow: 0px 0px 4px #000000;
cursor: default;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.openwebrx-progressbar-bar
{
border-radius: 5px;
height: 100%;
width: 100%;
}
.openwebrx-progressbar-text
{
position: absolute;
left:0px;
top:4px;
width: inherit;
}
#openwebrx-panel-status
{
margin: 0px;
padding: 0px;
background-color:rgba(0, 0, 0, 0);
}
#openwebrx-panel-status div.openwebrx-progressbar
{
width: 200px;
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-mute-on
{
color: lime;
}
#openwebrx-mute-off
{
color: white;
}
.openwebrx-panel-slider
{
position: relative;
top: -2px;
width: 95px;
}
.openwebrx-sliderbtn-img
{
width: 14px;
position:relative;
top: 1px;
}
.openwebrx-panel-line
{
padding-top: 5px;
}
#openwebrx-smeter-outer
{
border-color: #888;
border-style: solid;
border-width: 0px;
width: 255px;
height: 7px;
background-color: #373737;
border-radius: 3px;
position: relative;
}
#openwebrx-smeter-bar
{
transition: all 0.2s linear;
width: 0px;
height: 7px;
background: linear-gradient(to top, #ff5939 , #961700);
position: absolute;
margin: 0; padding: 0; left: 0;
border-radius: 3px;
}
#openwebrx-smeter-db
{
color: #aaa;
display: inline-block;
font-size: 10pt;
float: right;
margin-right: 5px;
margin-top: 24px;
font-family: 'expletus-sans-medium';
}
#openwebrx-big-grey
{
position: fixed;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
opacity: 0.8;
background-color: #777;
left: 0;
top: 0;
z-index: 1001;
display: none;
vertical-align: middle;
text-align: center;
color: white;
font-weight: bold;
font-size: 20pt;
cursor: pointer;
transition: opacity 0.3s linear;
}
#openwebrx-big-grey img
{
width: 150px;
}
#openwebrx-digimode-canvas-container
{
/*margin: -10px -10px 10px -10px;*/
margin: -10px -10px 0px -10px;
border-radius: 15px;
height: 150px;
background-color: #333;
position: relative;
overflow: hidden;
}
#openwebrx-digimode-canvas-container canvas
{
position: absolute;
pointer-events: none;
transition: width 500ms, left 500ms;
}
#openwebrx-secondary-demod-listbox
{
width: 201px;
height: 27px;
border-radius: 5px;
background-color: #373737;
color: White;
font-weight: normal;
font-size: 13pt;
margin-right: 1px;
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) );
background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% );
border-color: transparent;
border-width: 0px;
-moz-appearance: none;
padding-left:3px;
}
#openwebrx-secondary-demod-listbox option
{
border-width: 0px;
background-color: #373737;
color: White;
}
#openwebrx-cursor-blink
{
animation: cursor-blink 1s infinite;
/*animation: cursor-3d 2s infinite;*/
animation-timing-function: linear;
animation-direction: alternate;
height: 1em;
width: 8px;
background-color: White;
display: inline-block;
position: relative;
top: 1px;
/*perspective: 60px;*/
}
@keyframes cursor-blink
{
0%{ opacity: 0; }
50% { opacity: 1; }
100%{ opacity: 0; }
}
@keyframes cursor-3d
{
0%{ transform: rotateX(0deg) rotateX(Ydeg); }
50% { transform: rotateX(180deg) rotateY(360deg); opacity: 0.1; }
100%{ transform: rotateX(360deg) rotateY(720deg); }
}
#openwebrx-digimode-content
{
word-wrap: break-word;
position: absolute;
bottom: 0;
width: 100%;
}
#openwebrx-digimode-content-container
{
overflow-y: hidden;
display: block;
height: 50px;
position: relative;
}
#openwebrx-digimode-content-container .gradient
{
width: 100%;
height: 20px;
background: linear-gradient(to top, rgba(87,87,87,0) 0%,rgba(87,87,87,1) 100%);
position: absolute;
top: 0;
z-index: 10;
}
#openwebrx-digimode-content .part
{
perspective: 700px;
}
#openwebrx-digimode-content .part
{
animation: new-digimode-data-3d 100ms;
animation-timing-function: linear;
display: inline-block;
perspective-origin: 50% 50%;
transform-origin: 0% 50%;
}
#openwebrx-digimode-content .part .subpart
{
}
@keyframes new-digimode-data
{
0%{ opacity: 0; }
100%{ opacity: 1; }
}
@keyframes new-digimode-data-3d
{
0%{ transform: rotateX(0deg) rotateY(-90deg) translateX(-5px) scale(1.3); }
100%{ transform: rotateX(0deg) rotateY(0deg) translateX(0) scale(1); }
}
#openwebrx-digimode-select-channel
{
transition: all 500ms;
background-color: Yellow;
display: block;
position: absolute;
pointer-events: none;
height: 100%;
width: 0px;
top: 0px;
left: 0px;
opacity: 0.7;
border-style: solid;
border-width: 0px;
border-color: Red;
}

File diff suppressed because it is too large Load Diff

94
htdocs/retry.html Normal file
View 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>

11679
htdocs/sdr.js Normal file

File diff suppressed because one or more lines are too long

95
htdocs/upgrade.html Normal file
View 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>

View File

@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
. docker/env
for image in ${IMAGES}; do
# there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually
rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TAG}"
IMAGE_LIST=""
for a in $ALL_ARCHS; do
IMAGE_LIST="$IMAGE_LIST jketterl/$image:$TAG-$a"
done
docker manifest create jketterl/$image:$TAG $IMAGE_LIST
docker manifest push --purge jketterl/$image:$TAG
docker pull jketterl/$image:$TAG
done

View File

@ -1,6 +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 owrx.__main__ import main
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>
if __name__ == "__main__":
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/>.
"""
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():
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
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
#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)
#Pypy
if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)"
#Change process name to "openwebrx" (to be seen in ps)
try:
for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]:
if os.path.exists(libcpath):
libc = dl.open(libcpath)
libc.call("prctl", 15, "openwebrx", 0, 0, 0)
break
except:
pass
#Start rtl thread
if os.system("csdr 2> /dev/null") == 32512: #check for csdr
print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n"
return
if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux
print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n"
return
if cfg.start_rtl_thread:
nmux_bufcnt = nmux_bufsize = 0
while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096
while nmux_bufsize * nmux_bufcnt < cfg.nmux_memory * 1e6: nmux_bufcnt += 1
if nmux_bufcnt == 0 or nmux_bufsize == 0:
print "[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py"
return
print "[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt)
cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, cfg.iq_server_port)
rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=())
rtl_thread.start()
print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command
print "[openwebrx-main] Waiting for I/Q server to start..."
while True:
testsock=socket.socket()
try: testsock.connect(("127.0.0.1", cfg.iq_server_port))
except:
time.sleep(0.1)
continue
testsock.close()
break
print "[openwebrx-main] I/Q server started."
#Initialize clients
clients=[]
clients_mutex=threading.Lock()
lock_try_time=0
#Start watchdog thread
print "[openwebrx-main] Starting watchdog threads."
mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ())
mutex_test_thread.start()
mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ())
mutex_watchdog_thread.start()
#Start spectrum thread
print "[openwebrx-main] Starting spectrum thread."
spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ())
spectrum_thread.start()
#spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ())
#spectrum_watchdog_thread.start()
get_cpu_usage()
bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ())
bcastmsg_thread.start()
#threading.Thread(target = measure_thread_function, args = ()).start()
#Start sdr.hu update thread
if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing:
print "[openwebrx-main] Starting sdr.hu update thread..."
avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png"))
sdrhu_thread=threading.Thread(target = sdrhu.run, args = ())
sdrhu_thread.start()
#Start HTTP thread
httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler)
print('[openwebrx-main] Starting HTTP server.')
access_log("Starting OpenWebRX...")
httpd.serve_forever()
# This is a debug function below:
measure_value=0
def measure_thread_function():
global measure_value
while True:
print "[openwebrx-measure] value is",measure_value
measure_value=0
time.sleep(1)
def bcastmsg_thread_function():
global clients
while True:
time.sleep(3)
try: cpu_usage=get_cpu_usage()
except: cpu_usage=0
cma("bcastmsg_thread")
for i in range(0,len(clients)):
clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients))
cmr()
def mutex_test_thread_function():
global clients_mutex, lock_try_time
while True:
time.sleep(0.5)
lock_try_time=time.time()
clients_mutex.acquire()
clients_mutex.release()
lock_try_time=0
def cma(what): #clients_mutex acquire
global clients_mutex
global clients_mutex_locker
if not clients_mutex.locked(): clients_mutex_locker = what
clients_mutex.acquire()
def cmr():
global clients_mutex
global clients_mutex_locker
clients_mutex_locker = None
clients_mutex.release()
def mutex_watchdog_thread_function():
global lock_try_time
global clients_mutex_locker
global clients_mutex
while True:
if lock_try_time != 0 and time.time()-lock_try_time > 3.0:
#if 3 seconds pass without unlock
print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..."
clients_mutex.release()
time.sleep(0.5)
def spectrum_watchdog_thread_function():
global spectrum_thread_watchdog_last_tick, receiver_failed
while True:
time.sleep(60)
if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0:
print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port."
print "[openwebrx-spectrum-watchdog] Deactivating receiver."
receiver_failed="spectrum"
return
def check_server():
global spectrum_dsp, server_fail, rtl_thread
if server_fail: return server_fail
#print spectrum_dsp.process.poll()
if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed"
#if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed"
if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail
return server_fail
def apply_csdr_cfg_to_dsp(dsp):
dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize
dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes
dsp.csdr_through = cfg.csdr_through
def spectrum_thread_function():
global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick
spectrum_dsp=dsp=csdr.dsp()
dsp.nc_port=cfg.iq_server_port
dsp.set_demodulator("fft")
dsp.set_samp_rate(cfg.samp_rate)
dsp.set_fft_size(cfg.fft_size)
dsp.set_fft_fps(cfg.fft_fps)
dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0)
dsp.set_fft_compression(cfg.fft_compression)
dsp.set_format_conversion(cfg.format_conversion)
apply_csdr_cfg_to_dsp(dsp)
sleep_sec=0.87/cfg.fft_fps
print "[openwebrx-spectrum] Spectrum thread initialized successfully."
dsp.start()
if cfg.csdr_dynamic_bufsize:
dsp.read(8) #dummy read to skip bufsize & preamble
print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1"
print "[openwebrx-spectrum] Spectrum thread started."
bytes_to_read=int(dsp.get_fft_bytes_to_read())
spectrum_thread_counter=0
while True:
data=dsp.read(bytes_to_read)
#print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()"
if spectrum_thread_counter >= cfg.fft_fps:
spectrum_thread_counter=0
spectrum_thread_watchdog_last_tick = time.time() #once every second
else: spectrum_thread_counter+=1
cma("spectrum_thread")
correction=0
for i in range(0,len(clients)):
i-=correction
if (clients[i].ws_started):
if clients[i].spectrum_queue.full():
print "[openwebrx-spectrum] client spectrum queue full, closing it."
close_client(i, False)
correction+=1
else:
clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients
cmr()
def get_client_by_id(client_id, use_mutex=True):
global clients
output=-1
if use_mutex: cma("get_client_by_id")
for i in range(0,len(clients)):
if(clients[i].id==client_id):
output=i
break
if use_mutex: cmr()
if output==-1:
raise ClientNotFoundException
else:
return output
def log_client(client, what):
print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what)
def cleanup_clients(end_all=False):
# - if a client doesn't open websocket for too long time, we drop it
# - or if end_all is true, we drop all clients
global clients
cma("cleanup_clients")
correction=0
for i in range(0,len(clients)):
i-=correction
#print "cleanup_clients:: len(clients)=", len(clients), "i=", i
if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45):
if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket"
close_client(i, False)
correction+=1
cmr()
def generate_client_id(ip):
#add a client
global clients
new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat")
new_client.id=md5.md5(str(random.random())).hexdigest()
new_client.gen_time=time.time()
new_client.ws_started=False # to check whether client has ever tried to open the websocket
new_client.spectrum_queue=Queue.Queue(1000)
new_client.ip=ip
new_client.bcastmsg=""
new_client.closed=[False] #byref, not exactly sure if required
new_client.dsp=None
cma("generate_client_id")
clients.append(new_client)
log_client(new_client,"client added. Clients now: {0}".format(len(clients)))
cmr()
cleanup_clients()
return new_client.id
def close_client(i, use_mutex=True):
global clients
log_client(clients[i],"client being closed.")
if use_mutex: cma("close_client")
try:
clients[i].dsp.stop()
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value
traceback.print_tb(exc_traceback)
clients[i].closed[0]=True
access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")")
del clients[i]
if use_mutex: cmr()
# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python
# some ideas are used from the artice above
class WebRXHandler(BaseHTTPRequestHandler):
def proc_read_thread():
pass
def send_302(self,what):
self.send_response(302)
self.send_header('Content-type','text/html')
self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what))
self.end_headers()
self.wfile.write("<html><body><h1>Object moved</h1>Please <a href=\"/{0}\">click here</a> to continue.</body></html>".format(what))
def do_GET(self):
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version, receiver_failed
rootdir = 'htdocs'
self.path=self.path.replace("..","")
path_temp_parts=self.path.split("?")
self.path=path_temp_parts[0]
request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else ""
access_log("GET "+self.path+" from "+self.client_address[0])
try:
if self.path=="/":
self.path="/index.wrx"
# there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python
#if self.path[:5]=="/lock": cma("do_GET /lock/") # to test mutex_watchdog_thread. Do not uncomment in production environment!
if self.path[:4]=="/ws/":
print "[openwebrx-ws] Client requested WebSocket connection"
if receiver_failed: self.send_error(500,"Internal server error")
try:
# ========= WebSocket handshake =========
ws_success=True
try:
rxws.handshake(self)
cma("do_GET /ws/")
client_i=get_client_by_id(self.path[4:], False)
myclient=clients[client_i]
except rxws.WebSocketException: ws_success=False
except ClientNotFoundException: ws_success=False
finally:
if clients_mutex.locked(): cmr()
if not ws_success:
self.send_error(400, 'Bad request.')
return
# ========= Client handshake =========
if myclient.ws_started:
print "[openwebrx-httpd] error: second WS connection with the same client id, throwing it."
self.send_error(400, 'Bad request.') #client already started
return
rxws.send(self, "CLIENT DE SERVER openwebrx.py")
client_ans=rxws.recv(self, True)
if client_ans[:16]!="SERVER DE CLIENT":
rxws.send("ERR Bad answer.")
return
myclient.ws_started=True
#send default parameters
rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} audio_compression={4} fft_compression={5} max_clients={6} setup".format(str(cfg.shown_center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps,cfg.audio_compression,cfg.fft_compression,cfg.max_clients))
# ========= Initialize DSP =========
dsp=csdr.dsp()
dsp_initialized=False
dsp.set_audio_compression(cfg.audio_compression)
dsp.set_fft_compression(cfg.fft_compression) #used by secondary chains
dsp.set_format_conversion(cfg.format_conversion)
dsp.set_offset_freq(0)
dsp.set_bpf(-4000,4000)
dsp.set_secondary_fft_size(cfg.digimodes_fft_size)
dsp.nc_port=cfg.iq_server_port
apply_csdr_cfg_to_dsp(dsp)
myclient.dsp=dsp
do_secondary_demod=False
access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")")
while True:
myclient.loopstat=0
if myclient.closed[0]:
print "[openwebrx-httpd:ws] client closed by other thread"
break
# ========= send audio =========
if dsp_initialized:
myclient.loopstat=10
temp_audio_data=dsp.read(256)
myclient.loopstat=11
rxws.send(self, temp_audio_data, "AUD ")
# ========= send spectrum =========
while not myclient.spectrum_queue.empty():
myclient.loopstat=20
spectrum_data=myclient.spectrum_queue.get()
#spectrum_data_mid=len(spectrum_data[0])/2
#rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ")
# (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it)
myclient.loopstat=21
rxws.send(self, spectrum_data[0],"FFT ")
# ========= send smeter_level =========
smeter_level=None
while True:
try:
myclient.loopstat=30
smeter_level=dsp.get_smeter_level()
if smeter_level == None: break
except:
break
if smeter_level!=None:
myclient.loopstat=31
rxws.send(self, "MSG s={0}".format(smeter_level))
# ========= send bcastmsg =========
if myclient.bcastmsg!="":
myclient.loopstat=40
rxws.send(self,myclient.bcastmsg)
myclient.bcastmsg=""
# ========= send secondary =========
if do_secondary_demod:
myclient.loopstat=41
while True:
try:
secondary_spectrum_data=dsp.read_secondary_fft(dsp.get_secondary_fft_bytes_to_read())
if len(secondary_spectrum_data) == 0: break
# print "len(secondary_spectrum_data)", len(secondary_spectrum_data) #TODO digimodes
rxws.send(self, secondary_spectrum_data, "FFTS")
except: break
myclient.loopstat=42
while True:
try:
myclient.loopstat=422
secondary_demod_data=dsp.read_secondary_demod(1)
myclient.loopstat=423
if len(secondary_demod_data) == 0: break
# print "len(secondary_demod_data)", len(secondary_demod_data), secondary_demod_data #TODO digimodes
rxws.send(self, secondary_demod_data, "DAT ")
except: break
# ========= process commands =========
while True:
myclient.loopstat=50
rdata=rxws.recv(self, False)
myclient.loopstat=51
#try:
if not rdata: break
elif rdata[:3]=="SET":
print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata)
pairs=rdata[4:].split(" ")
bpf_set=False
new_bpf=dsp.get_bpf()
filter_limit=dsp.get_output_rate()/2
for pair in pairs:
param_name, param_value = pair.split("=")
if param_name == "low_cut" and -filter_limit <= int(param_value) <= filter_limit:
bpf_set=True
new_bpf[0]=int(param_value)
elif param_name == "high_cut" and -filter_limit <= int(param_value) <= filter_limit:
bpf_set=True
new_bpf[1]=int(param_value)
elif param_name == "offset_freq" and -cfg.samp_rate/2 <= int(param_value) <= cfg.samp_rate/2:
myclient.loopstat=510
dsp.set_offset_freq(int(param_value))
elif param_name == "squelch_level" and float(param_value) >= 0:
myclient.loopstat=520
dsp.set_squelch_level(float(param_value))
elif param_name=="mod":
if (dsp.get_demodulator()!=param_value):
myclient.loopstat=530
if dsp_initialized: dsp.stop()
dsp.set_demodulator(param_value)
if dsp_initialized: dsp.start()
elif param_name == "output_rate":
if not dsp_initialized:
myclient.loopstat=540
dsp.set_output_rate(int(param_value))
myclient.loopstat=541
dsp.set_samp_rate(cfg.samp_rate)
elif param_name=="action" and param_value=="start":
if not dsp_initialized:
myclient.loopstat=550
dsp.start()
dsp_initialized=True
elif param_name=="secondary_mod" and cfg.digimodes_enable:
if (dsp.get_secondary_demodulator() != param_value):
if dsp_initialized: dsp.stop()
if param_value == "off":
dsp.set_secondary_demodulator(None)
do_secondary_demod = False
else:
dsp.set_secondary_demodulator(param_value)
do_secondary_demod = True
rxws.send(self, "MSG secondary_fft_size={0} if_samp_rate={1} secondary_bw={2} secondary_setup".format(cfg.digimodes_fft_size, dsp.if_samp_rate(), dsp.secondary_bw()))
if dsp_initialized: dsp.start()
elif param_name=="secondary_offset_freq" and 0 <= int(param_value) <= dsp.if_samp_rate()/2 and cfg.digimodes_enable:
dsp.set_secondary_offset_freq(int(param_value))
else:
print "[openwebrx-httpd:ws] invalid parameter"
if bpf_set:
myclient.loopstat=560
dsp.set_bpf(*new_bpf)
#code.interact(local=locals())
except:
myclient.loopstat=990
exc_type, exc_value, exc_traceback = sys.exc_info()
print "[openwebrx-httpd:ws] exception: ",exc_type,exc_value
traceback.print_tb(exc_traceback) #TODO digimodes
#if exc_value[0]==32: #"broken pipe", client disconnected
# pass
#elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected
# pass
#else:
# print "[openwebrx-httpd] error in /ws/ handler: ",exc_type,exc_value
# traceback.print_tb(exc_traceback)
#stop dsp for the disconnected client
myclient.loopstat=991
try:
dsp.stop()
del dsp
except:
print "[openwebrx-httpd] error in dsp.stop()"
#delete disconnected client
myclient.loopstat=992
try:
cma("do_GET /ws/ delete disconnected")
id_to_close=get_client_by_id(myclient.id,False)
close_client(id_to_close,False)
except:
exc_type, exc_value, exc_traceback = sys.exc_info()
print "[openwebrx-httpd] client cannot be closed: ",exc_type,exc_value
traceback.print_tb(exc_traceback)
finally:
cmr()
myclient.loopstat=1000
return
elif self.path in ("/status", "/status/"):
#self.send_header('Content-type','text/plain')
self.send_response(200)
self.end_headers()
getbands=lambda: str(int(cfg.shown_center_freq-cfg.samp_rate/2))+"-"+str(int(cfg.shown_center_freq+cfg.samp_rate/2))
self.wfile.write("status="+("inactive" if receiver_failed else "active")+"\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\nusers_max="+str(cfg.max_clients)+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n")
print "[openwebrx-httpd] GET /status/ from",self.client_address[0]
else:
f=open(rootdir+self.path)
data=f.read()
extension=self.path[(len(self.path)-4):len(self.path)]
extension=extension[2:] if extension[1]=='.' else extension[1:]
checkresult=check_server()
if extension == "wrx" and (checkresult or receiver_failed):
self.send_302("inactive.html")
return
anyStringsPresentInUserAgent=lambda a: reduce(lambda x,y:x or y, map(lambda b:self.headers['user-agent'].count(b), a), False)
if extension == "wrx" and ( (not anyStringsPresentInUserAgent(("Chrome","Firefox","Googlebot","iPhone","iPad","iPod"))) if 'user-agent' in self.headers.keys() else True ) and (not request_param.count("unsupported")):
self.send_302("upgrade.html")
return
if extension == "wrx":
cleanup_clients(False)
if cfg.max_clients<=len(clients):
self.send_302("retry.html")
return
self.send_response(200)
if(("wrx","html","htm").count(extension)):
self.send_header('Content-type','text/html')
elif(extension=="js"):
self.send_header('Content-type','text/javascript')
elif(extension=="css"):
self.send_header('Content-type','text/css')
self.end_headers()
if extension == "wrx":
replace_dictionary=(
("%[RX_PHOTO_DESC]",cfg.photo_desc),
("%[CLIENT_ID]", generate_client_id(self.client_address[0])) if "%[CLIENT_ID]" in data else "",
("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"),
("%[RX_TITLE]",cfg.receiver_name),
("%[RX_LOC]",cfg.receiver_location),
("%[RX_QRA]",cfg.receiver_qra),
("%[RX_ASL]",str(cfg.receiver_asl)),
("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])),
("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title),
("%[RX_ADMIN]",cfg.receiver_admin),
("%[RX_ANT]",cfg.receiver_ant),
("%[RX_DEVICE]",cfg.receiver_device),
("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)),
("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)),
("%[START_MOD]",cfg.start_mod),
("%[WATERFALL_COLORS]",cfg.waterfall_colors),
("%[WATERFALL_MIN_LEVEL]",str(cfg.waterfall_min_level)),
("%[WATERFALL_MAX_LEVEL]",str(cfg.waterfall_max_level)),
("%[WATERFALL_AUTO_LEVEL_MARGIN]","[%d,%d]"%cfg.waterfall_auto_level_margin),
("%[DIGIMODES_ENABLE]",("true" if cfg.digimodes_enable else "false")),
("%[MATHBOX_WATERFALL_FRES]",str(cfg.mathbox_waterfall_frequency_resolution)),
("%[MATHBOX_WATERFALL_THIST]",str(cfg.mathbox_waterfall_history_length)),
("%[MATHBOX_WATERFALL_COLORS]",cfg.mathbox_waterfall_colors)
)
for rule in replace_dictionary:
while data.find(rule[0])!=-1:
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__":
main()

View File

@ -1,59 +0,0 @@
from http.server import HTTPServer
from owrx.http import RequestHandler
from owrx.config import PropertyManager
from owrx.feature import FeatureDetector
from owrx.sdr import SdrService
from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater
from owrx.service import Services
from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter
import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
pass
def main():
print(
"""
OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package
_________________________________________________________________________________________________
Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
"""
)
pm = PropertyManager.getSharedInstance().loadConfig()
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")))
return
# Get error messages about unknown / unavailable features as soon as possible
SdrService.loadProps()
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
updater = SdrHuUpdater()
updater.start()
Services.start()
try:
server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler)
server.serve_forever()
except KeyboardInterrupt:
WebSocketConnection.closeAll()
Services.stop()
PskReporter.stop()

View File

@ -1,575 +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 owrx.parser import Parser
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(Parser):
def __init__(self, handler):
super().__init__(handler)
self.ax25parser = Ax25Parser()
self.deframer = KissDeframer()
self.metric = self.getMetric()
def setDialFrequency(self, freq):
super().setDialFrequency(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])),
}

View File

@ -1,79 +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):
self.bands = self.loadBands()
def loadBands(self):
for file in ["/etc/openwebrx/bands.json", "bands.json"]:
try:
f = open(file, "r")
bands_json = json.load(f)
f.close()
return [Band(d) for d in bands_json]
except FileNotFoundError:
pass
except json.JSONDecodeError:
logger.exception("error while parsing bandplan file %s", file)
return []
except Exception:
logger.exception("error while processing bandplan from %s", file)
return []
return []
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)]

Some files were not shown because too many files have changed in this diff Show More