Merge branch 'develop' into agc_work

This commit is contained in:
Jakob Ketterl 2020-08-24 00:03:55 +02:00
commit 4204e4d9e2
164 changed files with 7787 additions and 3215 deletions

View File

@ -4,3 +4,4 @@
**/*.pyc **/*.pyc
**/*.swp **/*.swp
black-env black-env
debian

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Installation method**
How did you install OpenWebRX? (Raspberry Pi SD card image, Debian / Ubuntu packages, Docker image, manually?)
**Versions**
What version of OpenWebRX are you running? (Check on startup, or see `owrx/version.py`. If a `-dev` version is used, ideally state the commit the issue is appearing on)
**Log messages**
Are there any relevant messages relating to the bug in the output / log of OpenWebRX? (On most installations, the log should be available using the command `sudo journalctl -u openwebrx`)
**Additional context**
Add any other context about the problem here.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: General support or other project-relasted question
url: https://groups.io/g/openwebrx
about: Request help on the community mailing list

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
Before posting a new feature request, please check if a similar idea has already been listed
* on the issue tracker
* on the [OpenWebRX github project](https://github.com/users/jketterl/projects/1).
In the latter case, please only proceed if you have additional information about the feature, and please let us know that there's already a card there.
**Feature description**
Please describe in plain words what functionality you'd like to see in OpenWebRX, and why you think it's useful.
**Target audience**
Please let us know if you think that this feature is of particular interest for a particular group of users (e.g. hams, SWLs, DXers, ...)

160
CHANGELOG.md Normal file
View File

@ -0,0 +1,160 @@
**unreleased**
- Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver
on receiverbook.de
- Fixed file descriptor leaks to prevent "too many open files" errors
- Add new demodulator chain for FreeDV
- Added new HD audio streaming mode along with a new WFM demodulator
- New devices supported:
- FunCube Dongle Pro+ (`"type": "fcdpp"`)
- Support for connections to rtl_tcp
**0.19.1**
- Added ability to authenticate receivers with listing sites using "receiver id" tokens
**0.19.0**
- Fix direwolf connection setup by implementing a retry loop
- Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
- OSM maps instead of Google when google_maps_api_key is not set (thanks @jquagga)
- Improved logic to pass parameters to soapy devices.
- `rtl_sdr_soapy`: added support for `bias_tee`
- `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
- `airspy`: added support for `bitpack`
- Added support for Perseus-SDR devices, (thanks @amontefusco)
- Property System has been rewritten so that defaults on sdr behave as expected
- Waterfall range auto-adjustment now only takes the center 80% of the spectrum into account, which should work better
with SDRs that oversample or have rather flat filter curves towards the spectrum edges
- Bugfix for negative network usage
- FiFi SDR: prevent arecord from shutting down after 2GB of data has been sent
- Added support for bias tee control on rtl_sdr devices
- All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
- `rtl_sdr` type now also supports the `direct_sampling` option
- Added decoding implementation for for digimode "JS8Call"
(requires an installation of [js8call](http://js8call.com/) and
[the js8py library](https://github.com/jketterl/js8py))
- Reorganization of the frontend demodulator code
- Improve receiver load time by concatenating javascript assets
- Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in
csdr and owrx_connector to allow the images to run on a wider range of CPUs
- Docker containers have been updated to include the SDRplay driver version 3
- HackRF support is now based on SoapyHackRF
- Removed sdr.hu server listing support since the site has been shut down
- Added support for Radioberry 2 Rasbperry Pi SDR Cape
**0.18.0**
- Support for SoapyRemote
**2020-02-08**
- Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed
- Decoding of Pocsag modulation is now possible
- 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!

156
README.md
View File

@ -1,151 +1,39 @@
OpenWebRX OpenWebRX
========= =========
[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/)
OpenWebRX is a multi-user SDR receiver software with a web interface. OpenWebRX is a multi-user SDR receiver software with a web interface.
![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png) ![OpenWebRX](https://www.openwebrx.de/gfx/openwebrx-screenshot.png)
It has the following features: It has the following features:
- [csdr](https://github.com/jketterl/csdr) based demodulators (AM/FM/SSB/CW/BPSK31), - [csdr](https://github.com/jketterl/csdr) based demodulators (AM/FM/SSB/CW/BPSK31/BPSK63)
- filter passband can be set from GUI, - filter passband can be set from GUI
- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas - it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas
- it works in Google Chrome, Chromium and Mozilla Firefox - it works in Google Chrome, Chromium and Mozilla Firefox
- currently supports RTL-SDR, HackRF, SDRplay, AirSpy - currently supports RTL-SDR, HackRF, SDRplay, AirSpy, LimeSDR, PlutoSDR
- Multiple SDR devices can be used simultaneously - Multiple SDR devices can be used simultaneously
- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF) - [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag)
- [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN) - [dsd](https://github.com/f4exb/dsdcc) based demodulators (D-Star, NXDN)
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9) - [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html) based demodulators (FT8, FT4, WSPR, JT65, JT9)
**News (2019-11-24 by DD5JFK)**
- There is now a new way to interface with SDR hardware, [owrx_connectors](https://github.com/jketterl/owrx_connector).
They talk directly to the hardware (no rtl_sdr / rx_sdr necessary) and offer I/Q data on a socket, just like nmux
did before. They additionally offer a control socket that allows openwebrx to control the SDR parameters directly,
without the need for repeated restarts. This allows for quicker profile changes, and also reduces the risk of your
SDR hardware from failing during the switchover. See `config_webrx.py` for further information and instructions.
- Offset tuning using the `lfo_offset` has been reworked in a way that `center_freq` has to be set to the frequency you
actually want to listen to. If you're using an `lfo_offset` already, you will probably need to change its sign.
- `initial_squelch_level` can now be set on each profile.
- As usual, plenty of fixes and improvements.
**News (2019-10-27 by DD5JFK)**
- Part of the frontend code has been reworked
- Audio buffer minimums have been completely stripped. As a result, you should get better latency. Unfortunately, this also means there will be some skipping when audio starts.
- Now also supports AudioWorklets (for those browser that have it). The Raspberry Pi image has been updated to include https due to the SecureContext requirement.
- Mousewheel controls for the receiver sliders
- Error handling for failed SDR devices
**News (2019-09-29 by DD5FJK)**
- One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels). There's two kinds of bookmarks available:
- Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples!
- Clientside bookmarks which every user can store for themselves. They are stored in the browser's localStorage.
- Some more bugs in the websocket handling have been fixed.
**News (2019-09-25 by DD5JFK)**
- Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is now possible. Please have a look at the configuration on how to set it up.
- Websocket communication has been overhauled in large parts. It should now be more reliable, and failing connections should now have no impact on other users.
- Profile scheduling allows to set up band-hopping if you are running background services.
- APRS now has the ability to show symbols on the map, if a corresponding symbol set has been installed. Check the config!
- Debug logging has been disabled in a handful of modules, expect vastly reduced output on the shell.
**News (2019-09-13 by DD5JFK)**
- New set of APRS-related features
- Decode Packet transmissions using [direwolf](https://github.com/wb2osz/direwolf) (1k2 only for now)
- APRS packets are mostly decoded and shown both in a new panel and on the map
- APRS is also available as a background service
- direwolfs I-gate functionality can be enabled, which allows your receiver to work as a receive-only I-gate for the APRS network in the background
- Demodulation for background services has been optimized to use less total bandwidth, saving CPU
- More metrics have been added; they can be used together with collectd and its curl_json plugin for now, with some limitations.
**News (2019-07-21 by DD5JFK)**
- Latest Features:
- More WSJT-X modes have been added, including the new FT4 mode
- I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the dial frequency for digital modes
- fixed some bugs in the websocket communication which broke the map
**News (2019-07-13 by DD5JFK)**
- Latest Features:
- FT8 Integration (using wsjt-x demodulators)
- New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice
- New Feature report that will show what functionality is available
- There's a new Raspbian SD Card image available (see below)
**News (2019-06-30 by DD5JFK)**
- I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near future. Please check this place for updates.
- My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official version.
- I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there!
- This version sports the following new and amazing features:
- Support of multiple SDR devices simultaneously
- Support for multiple profiles per SDR that allow the user to listen to different frequencies
- Support for digital voice decoding
- Feature detection that will disable functionality when dependencies are not available (if you're missing the digital buttons, this is probably why)
- Raspbian SD Card Images and Docker builds available (see below)
- I am currently working on the feature set for a stable release, but you are more than welcome to test development versions!
> When upgrading OpenWebRX, please make sure that you also upgrade *csdr* and *digiham*!
## 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 ## Setup
### Raspberry Pi SD Card Images The following methods of setting up a receiver are currently available:
Probably the quickest way to get started is to download the [latest Raspberry Pi SD Card Image](https://s3.eu-central-1.amazonaws.com/de.dd5jfk.openwebrx/2019-11-24-OpenWebRX-full.zip). It contains all the depencencies out of the box, and should work on all Raspberries up to the 3B+. - Raspberry Pi SD card images
- Debian repository
- Docker images
- Manual installation
This is based off the Raspbian Lite distribution, so [their installation instructions](https://www.raspberrypi.org/documentation/installation/installing-images/) apply. Please checkout the [setup guide on the wiki](https://github.com/jketterl/openwebrx/wiki/Setup-Guide) for more details
on the respective methods.
Please note: I have not updated this to include the Raspberry Pi 4 yet. (It seems to be impossible to build Rasbpian Buster images on x86 hardware right now. Stay tuned!) ## Community
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. If you have trouble setting up or configuring your receiver, you have some great idea you want to see implemented, or
you just generally want to have some OpenWebRX-related chat, come visit us over on
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. [our groups.io group](https://groups.io/g/openwebrx).
### Docker Images
For those familiar with docker, I am providing [recent builds and Releases for both x86 and arm processors on the Docker hub](https://hub.docker.com/r/jketterl/openwebrx). You can find a short introduction there.
### Manual Installation
OpenWebRX currently requires Linux and python >= 3.6 to run.
First you will need to install the dependencies:
- [csdr](https://github.com/jketterl/csdr)
- [rtl-sdr](http://sdr.osmocom.org/trac/wiki/rtl-sdr)
Optional dependency for improved hardware access (to become mandatory at some point):
- [owrx_connector](https://github.com/jketterl/owrx_connector)
Optional dependencies if you want to be able to listen do digital voice:
- [digiham](https://github.com/jketterl/digiham)
- [dsd](https://github.com/f4exb/dsdcc)
Optional dependency if you want to decode WSJT-X modes:
- [wsjt-x](https://physics.princeton.edu/pulsar/k1jt/wsjtx.html)
After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server:
./openwebrx.py
You can now open the GUI at <a href="http://localhost:8073">http://localhost:8073</a>.
Please note that the server is also listening on the following ports (on localhost only):
- ports 4950 to 4960 for the multi-user I/Q servers.
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;*
## Usage tips ## Usage tips
@ -155,14 +43,10 @@ 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). 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.
## Licensing ## Licensing
OpenWebRX is available under Affero GPL v3 license (<a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">summary</a>). OpenWebRX is available under Affero GPL v3 license
([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0))).
OpenWebRX is also available under a commercial license on request. Please contact me at the address *&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

@ -8,7 +8,8 @@
"ft8": 1840000, "ft8": 1840000,
"wspr": 1836600, "wspr": 1836600,
"jt65": 1838000, "jt65": 1838000,
"jt9": 1839000 "jt9": 1839000,
"js8": 1842000
} }
}, },
{ {
@ -21,7 +22,8 @@
"wspr": 3592600, "wspr": 3592600,
"jt65": 3570000, "jt65": 3570000,
"jt9": 3572000, "jt9": 3572000,
"ft4": [3568000, 3575000] "ft4": [3568000, 3575000],
"js8": 3578000
} }
}, },
{ {
@ -43,7 +45,8 @@
"wspr": 7038600, "wspr": 7038600,
"jt65": 7076000, "jt65": 7076000,
"jt9": 7078000, "jt9": 7078000,
"ft4": 7047500 "ft4": 7047500,
"js8": 7078000
} }
}, },
{ {
@ -56,7 +59,8 @@
"wspr": 10138700, "wspr": 10138700,
"jt65": 10138000, "jt65": 10138000,
"jt9": 10140000, "jt9": 10140000,
"ft4": 10140000 "ft4": 10140000,
"js8": 10130000
} }
}, },
{ {
@ -69,7 +73,8 @@
"wspr": 14095600, "wspr": 14095600,
"jt65": 14076000, "jt65": 14076000,
"jt9": 14078000, "jt9": 14078000,
"ft4": 14080000 "ft4": 14080000,
"js8": 14078000
} }
}, },
{ {
@ -82,7 +87,8 @@
"wspr": 18104600, "wspr": 18104600,
"jt65": 18102000, "jt65": 18102000,
"jt9": 18104000, "jt9": 18104000,
"ft4": 18104000 "ft4": 18104000,
"js8": 18104000
} }
}, },
{ {
@ -95,7 +101,8 @@
"wspr": 21094600, "wspr": 21094600,
"jt65": 21076000, "jt65": 21076000,
"jt9": 21078000, "jt9": 21078000,
"ft4": 21140000 "ft4": 21140000,
"js8": 21078000
} }
}, },
{ {
@ -108,7 +115,8 @@
"wspr": 24924600, "wspr": 24924600,
"jt65": 24917000, "jt65": 24917000,
"jt9": 24919000, "jt9": 24919000,
"ft4": 24919000 "ft4": 24919000,
"js8": 24922000
} }
}, },
{ {
@ -121,7 +129,8 @@
"wspr": 28124600, "wspr": 28124600,
"jt65": 28076000, "jt65": 28076000,
"jt9": 28078000, "jt9": 28078000,
"ft4": 28180000 "ft4": 28180000,
"js8": 28078000
} }
}, },
{ {
@ -134,7 +143,8 @@
"wspr": 50293000, "wspr": 50293000,
"jt65": 50310000, "jt65": 50310000,
"jt9": 50312000, "jt9": 50312000,
"ft4": 50318000 "ft4": 50318000,
"js8": 50318000
} }
}, },
{ {
@ -189,5 +199,75 @@
"name": "3cm", "name": "3cm",
"lower_bound": 10000000000, "lower_bound": 10000000000,
"upper_bound": 10500000000 "upper_bound": 10500000000
},
{
"name": "120m Broadcast",
"lower_bound": 2300000,
"upper_bound": 2495000
},
{
"name": "90m Broadcast",
"lower_bound": 3200000,
"upper_bound": 3400000
},
{
"name": "75m Broadcast",
"lower_bound": 3900000,
"upper_bound": 4000000
},
{
"name": "60m Broadcast",
"lower_bound": 4750000,
"upper_bound": 4995000
},
{
"name": "49m Broadcast",
"lower_bound": 5900000,
"upper_bound": 6200000
},
{
"name": "41m Broadcast",
"lower_bound": 7200000,
"upper_bound": 7450000
},
{
"name": "31m Broadcast",
"lower_bound": 9400000,
"upper_bound": 9900000
},
{
"name": "25m Broadcast",
"lower_bound": 11600000,
"upper_bound": 12100000
},
{
"name": "22m Broadcast",
"lower_bound": 13570000,
"upper_bound": 13870000
},
{
"name": "19m Broadcast",
"lower_bound": 15100000,
"upper_bound": 15830000
},
{
"name": "16m Broadcast",
"lower_bound": 17480000,
"upper_bound": 17900000
},
{
"name": "15m Broadcast",
"lower_bound": 18900000,
"upper_bound": 19020000
},
{
"name": "13m Broadcast",
"lower_bound": 21450000,
"upper_bound": 21850000
},
{
"name": "11m Broadcast",
"lower_bound": 25670000,
"upper_bound": 26100000
} }
] ]

View File

@ -1,10 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
. docker/env
ARCH=$(uname -m)
TAG="latest"
ARCHTAG="$TAG-$ARCH"
docker build --pull -t openwebrx-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-base . docker build --pull -t openwebrx-base:$ARCHTAG -f docker/Dockerfiles/Dockerfile-base .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr . docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr .
@ -13,4 +9,10 @@ docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-sdrplay:$ARCHTAG
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-hackrf:$ARCHTAG -f docker/Dockerfiles/Dockerfile-hackrf . docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-hackrf:$ARCHTAG -f docker/Dockerfiles/Dockerfile-hackrf .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-airspy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-airspy . docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-airspy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-airspy .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr-soapy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr-soapy . docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-rtlsdr-soapy:$ARCHTAG -f docker/Dockerfiles/Dockerfile-rtlsdr-soapy .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-plutosdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-plutosdr .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-limesdr:$ARCHTAG -f docker/Dockerfiles/Dockerfile-limesdr .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-soapyremote:$ARCHTAG -f docker/Dockerfiles/Dockerfile-soapyremote .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-perseus:$ARCHTAG -f docker/Dockerfiles/Dockerfile-perseus .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-fcdpp:$ARCHTAG -f docker/Dockerfiles/Dockerfile-fcdpp .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-radioberry:$ARCHTAG -f docker/Dockerfiles/Dockerfile-radioberry .
docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-full:$ARCHTAG -t jketterl/openwebrx:$ARCHTAG -f docker/Dockerfiles/Dockerfile-full . docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/openwebrx-full:$ARCHTAG -t jketterl/openwebrx:$ARCHTAG -f docker/Dockerfiles/Dockerfile-full .

View File

@ -32,8 +32,11 @@ config_webrx: configuration options for OpenWebRX
and use them for running your web service with OpenWebRX.) and use them for running your web service with OpenWebRX.)
""" """
# configuration version. please only modify if you're able to perform the associated migration steps.
version = 2
# NOTE: you can find additional information about configuring OpenWebRX in the Wiki: # NOTE: you can find additional information about configuring OpenWebRX in the Wiki:
# https://github.com/simonyiszk/openwebrx/wiki # https://github.com/jketterl/openwebrx/wiki/Configuration-guide
# ==== Server settings ==== # ==== Server settings ====
web_port = 8073 web_port = 8073
@ -44,24 +47,30 @@ receiver_name = "[Callsign]"
receiver_location = "Budapest, Hungary" receiver_location = "Budapest, Hungary"
receiver_asl = 200 receiver_asl = 200
receiver_admin = "example@example.com" receiver_admin = "example@example.com"
receiver_gps = (47.000000, 19.000000) receiver_gps = {"lat": 47.000000, "lon": 19.000000}
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory"
# photo_desc allows you to put pretty much any HTML you like into the receiver description.
# The lines below should give you some examples of what's possible.
photo_desc = """ photo_desc = """
You can add your own background photo and receiver information.<br /> You can add your own background photo and receiver information.<br />
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/> Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
Device: %[RX_DEVICE]<br /> Device: Receiver Device<br />
Antenna: %[RX_ANT]<br /> Antenna: Receiver Antenna<br />
Website: <a href="http://localhost" target="_blank">http://localhost</a> Website: <a href="http://localhost" target="_blank">http://localhost</a>
""" """
# ==== sdr.hu listing ==== # ==== Public receiver listings ====
# If you want your ham receiver to be listed publicly on sdr.hu, then take the following steps: # You can publish your receiver on online receiver directories, like https://www.receiverbook.de
# 1. Register at: http://sdr.hu/register # You will receive a receiver key from the directory that will authenticate you as the operator of this receiver.
# 2. You will get an unique key by email. Copy it and paste here: # Please note that you not share your receiver keys publicly since anyone that obtains your receiver key can take over
sdrhu_key = "" # your public listing.
# 3. Set this setting to True to enable listing: # Your receiver keys should be placed into this array:
sdrhu_public_listing = False receiver_keys = []
server_hostname = "localhost" # If you list your receiver on multiple sites, you can place all your keys into the array above, or you can append
# keys to the arraylike this:
# receiver_keys += ["my-receiver-key"]
# If you're not sure, simply copy & paste the code you received from your listing site below this line:
# ==== DSP/RX settings ==== # ==== DSP/RX settings ====
fft_fps = 9 fft_fps = 9
@ -93,13 +102,14 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# ==== I/Q sources ==== # ==== I/Q sources ====
# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.) # (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.)
################################################################################################# ###############################################################################
# Is my SDR hardware supported? # # Is my SDR hardware supported? #
# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # # Check here: https://github.com/jketterl/openwebrx/wiki/Supported-Hardware #
################################################################################################# ###############################################################################
# Currently supported types of sdr receivers: # Currently supported types of sdr receivers:
# "rtl_sdr", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr" # "rtl_sdr", "rtl_sdr_soapy", "sdrplay", "hackrf", "airspy", "airspyhf", "fifi_sdr",
# "perseussdr", "lime_sdr", "pluto_sdr", "soapy_remote"
# #
# In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector. # In order to use rtl_sdr, you will need to install librtlsdr-dev and the connector.
# In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the # In order to use sdrplay, airspy or airspyhf, you will need to install soapysdr, the corresponding driver, and the
@ -107,8 +117,13 @@ Note: if you experience audio underruns while CPU usage is 100%, you can:
# #
# https://github.com/jketterl/owrx_connector # https://github.com/jketterl/owrx_connector
# #
# NOTE: The connector sources have replaced the old piped nmux style of reading input. If you still have any sdrs # In order to use Perseus HF you need to install the libperseus-sdr
# configured that have type endin in "_connector", simply remove that suffix. #
# https://github.com/Microtelecom/libperseus-sdr
#
# and do the proper changes to the sdrs object below
# (see also Wiki in https://github.com/jketterl/openwebrx/wiki/Sample-configuration-for-Perseus-HF-receiver).
#
sdrs = { sdrs = {
"rtlsdr": { "rtlsdr": {
@ -141,19 +156,18 @@ sdrs = {
"name": "Airspy HF+", "name": "Airspy HF+",
"type": "airspyhf", "type": "airspyhf",
"ppm": 0, "ppm": 0,
"rf_gain": "auto",
"profiles": { "profiles": {
"20m": { "20m": {
"name": "20m", "name": "20m",
"center_freq": 14150000, "center_freq": 14150000,
"rf_gain": 10, "samp_rate": 384000,
"samp_rate": 768000,
"start_freq": 14070000, "start_freq": 14070000,
"start_mod": "usb", "start_mod": "usb",
}, },
"30m": { "30m": {
"name": "30m", "name": "30m",
"center_freq": 10125000, "center_freq": 10125000,
"rf_gain": 10,
"samp_rate": 192000, "samp_rate": 192000,
"start_freq": 10142000, "start_freq": 10142000,
"start_mod": "usb", "start_mod": "usb",
@ -161,24 +175,21 @@ sdrs = {
"40m": { "40m": {
"name": "40m", "name": "40m",
"center_freq": 7100000, "center_freq": 7100000,
"rf_gain": 10,
"samp_rate": 256000, "samp_rate": 256000,
"start_freq": 7070000, "start_freq": 7070000,
"start_mod": "usb", "start_mod": "lsb",
}, },
"80m": { "80m": {
"name": "80m", "name": "80m",
"center_freq": 3650000, "center_freq": 3650000,
"rf_gain": 10, "samp_rate": 384000,
"samp_rate": 768000,
"start_freq": 3570000, "start_freq": 3570000,
"start_mod": "usb", "start_mod": "lsb",
}, },
"49m": { "49m": {
"name": "49m Broadcast", "name": "49m Broadcast",
"center_freq": 6000000, "center_freq": 6050000,
"rf_gain": 10, "samp_rate": 384000,
"samp_rate": 768000,
"start_freq": 6070000, "start_freq": 6070000,
"start_mod": "am", "start_mod": "am",
}, },
@ -188,6 +199,7 @@ sdrs = {
"name": "SDRPlay RSP2", "name": "SDRPlay RSP2",
"type": "sdrplay", "type": "sdrplay",
"ppm": 0, "ppm": 0,
"antenna": "Antenna A",
"profiles": { "profiles": {
"20m": { "20m": {
"name": "20m", "name": "20m",
@ -196,7 +208,6 @@ sdrs = {
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 14070000, "start_freq": 14070000,
"start_mod": "usb", "start_mod": "usb",
"antenna": "Antenna A",
}, },
"30m": { "30m": {
"name": "30m", "name": "30m",
@ -212,8 +223,7 @@ sdrs = {
"rf_gain": 0, "rf_gain": 0,
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 7070000, "start_freq": 7070000,
"start_mod": "usb", "start_mod": "lsb",
"antenna": "Antenna A",
}, },
"80m": { "80m": {
"name": "80m", "name": "80m",
@ -221,8 +231,7 @@ sdrs = {
"rf_gain": 0, "rf_gain": 0,
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 3570000, "start_freq": 3570000,
"start_mod": "usb", "start_mod": "lsb",
"antenna": "Antenna A",
}, },
"49m": { "49m": {
"name": "49m Broadcast", "name": "49m Broadcast",
@ -231,7 +240,6 @@ sdrs = {
"samp_rate": 500000, "samp_rate": 500000,
"start_freq": 6070000, "start_freq": 6070000,
"start_mod": "am", "start_mod": "am",
"antenna": "Antenna A",
}, },
}, },
}, },
@ -239,27 +247,25 @@ sdrs = {
# ==== Color themes ==== # ==== Color themes ====
# A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels
### default theme by teejez: ### default theme by teejez:
waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF] waterfall_colors = [0x000000FF, 0x0000FFFF, 0x00FFFFFF, 0x00FF00FF, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFFFFFFFF]
waterfall_min_level = -88 # in dB waterfall_min_level = -88 # in dB
waterfall_max_level = -20 waterfall_max_level = -20
waterfall_auto_level_margin = (5, 40) waterfall_auto_level_margin = {"min": 5, "max": 40}
### old theme by HA7ILM: ### old theme by HA7ILM:
# waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" # waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]"
# waterfall_min_level = -115 #in dB # waterfall_min_level = -115 #in dB
# waterfall_max_level = 0 # waterfall_max_level = 0
# waterfall_auto_level_margin = (20, 30) # waterfall_auto_level_margin = {"min": 20, "max": 30}
##For the old colors, you might also want to set [fft_voverlap_factor] to 0. ##For the old colors, you might also want to set [fft_voverlap_factor] to 0.
# Note: When the auto waterfall level button is clicked, the following happens: # Note: When the auto waterfall level button is clicked, the following happens:
# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] # [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin["min"]]
# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] # [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin["max"]]
# #
# ___|____________________________________|____________________________________|____________________________________|___> signal power # ___|________________________________________|____________________________________|________________________________________|___> signal power
# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/ # \_waterfall_auto_level_margin["min"]_/ |__ current_min_power_level | \_waterfall_auto_level_margin["max"]_/
# current_max_power_level __| # current_max_power_level __|
# === Experimental settings === # === Experimental settings ===
# Warning! The settings below are very experimental. # Warning! The settings below are very experimental.
@ -276,22 +282,28 @@ google_maps_api_key = ""
# in seconds; default: 2 hours # in seconds; default: 2 hours
map_position_retention_time = 2 * 60 * 60 map_position_retention_time = 2 * 60 * 60
# wsjt decoder queue configuration # decoder queue configuration
# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount # due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount
# of time (6.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads. # of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads.
# to mitigate this, the recordings will be queued and processed in sequence. # to mitigate this, the recordings will be queued and processed in sequence.
# the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread) # the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
wsjt_queue_workers = 2 decoding_queue_workers = 2
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up # the maximum queue length will cause decodes to be dumped if the workers cannot keep up
# if you are running background services, make sure this number is high enough to accept the task influx during peaks # if you are running background services, make sure this number is high enough to accept the task influx during peaks
# i.e. this should be higher than the number of wsjt services running at the same time # i.e. this should be higher than the number of decoding services running at the same time
wsjt_queue_length = 10 decoding_queue_length = 10
# wsjt decoding depth will allow more results, but will also consume more cpu # wsjt decoding depth will allow more results, but will also consume more cpu
wsjt_decoding_depth = 3 wsjt_decoding_depth = 3
# can also be set for each mode separately # can also be set for each mode separately
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent # jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
wsjt_decoding_depths = {"jt65": 1} wsjt_decoding_depths = {"jt65": 1}
# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled.
js8_enabled_profiles = ["normal", "slow"]
# JS8 decoding depth; higher value will get more results, but will also consume more cpu
js8_decoding_depth = 3
temporary_directory = "/tmp" temporary_directory = "/tmp"
services_enabled = False services_enabled = False
@ -314,3 +326,8 @@ aprs_symbols_path = "/opt/aprs-symbols/png"
# this also uses the receiver_gps setting from above, so make sure it contains a correct locator # this also uses the receiver_gps setting from above, so make sure it contains a correct locator
pskreporter_enabled = False pskreporter_enabled = False
pskreporter_callsign = "N0CALL" pskreporter_callsign = "N0CALL"
# === Web admin settings ===
# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote
# changes to the receiver settings. enable for testing in controlled environment only.
# webadmin_enabled = False

View File

@ -29,7 +29,11 @@ import math
from functools import partial from functools import partial
from owrx.kiss import KissClient, DirewolfConfig from owrx.kiss import KissClient, DirewolfConfig
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile
from owrx.js8 import Js8Profiles
from owrx.audio import AudioChopper
from csdr.pipe import Pipe
import logging import logging
@ -41,7 +45,7 @@ class output(object):
if not self.supports_type(t): if not self.supports_type(t):
# TODO rewrite the output mechanism in a way that avoids producing unnecessary data # 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) logger.warning("dumping output of type %s since it is not supported.", t)
threading.Thread(target=self.pump(read_fn, lambda x: None)).start() threading.Thread(target=self.pump(read_fn, lambda x: None), name="csdr_pump_thread").start()
return return
self.receive_output(t, read_fn) self.receive_output(t, read_fn)
@ -52,7 +56,11 @@ class output(object):
def copy(): def copy():
run = True run = True
while run: while run:
data = read() data = None
try:
data = read()
except ValueError:
pass
if data is None or (isinstance(data, bytes) and len(data) == 0): if data is None or (isinstance(data, bytes) and len(data) == 0):
run = False run = False
else: else:
@ -68,8 +76,10 @@ class dsp(object):
def __init__(self, output): def __init__(self, output):
self.samp_rate = 250000 self.samp_rate = 250000
self.output_rate = 11025 self.output_rate = 11025
self.hd_output_rate = 44100
self.fft_size = 1024 self.fft_size = 1024
self.fft_fps = 5 self.fft_fps = 5
self.center_freq = 0
self.offset_freq = 0 self.offset_freq = 0
self.low_cut = -4000 self.low_cut = -4000
self.high_cut = 4000 self.high_cut = 4000
@ -82,6 +92,8 @@ class dsp(object):
self.demodulator = "nfm" self.demodulator = "nfm"
self.name = "csdr" self.name = "csdr"
self.base_bufsize = 512 self.base_bufsize = 512
self.decimation = None
self.last_decimation = None
self.nc_port = None self.nc_port = None
self.csdr_dynamic_bufsize = False self.csdr_dynamic_bufsize = False
self.csdr_print_bufsizes = False self.csdr_print_bufsizes = False
@ -94,31 +106,38 @@ class dsp(object):
self.secondary_fft_size = 1024 self.secondary_fft_size = 1024
self.secondary_process_fft = None self.secondary_process_fft = None
self.secondary_process_demod = None self.secondary_process_demod = None
self.pipe_names = [ self.pipe_names = {
"bpf_pipe", "bpf_pipe": Pipe.WRITE,
"shift_pipe", "shift_pipe": Pipe.WRITE,
"squelch_pipe", "squelch_pipe": Pipe.WRITE,
"smeter_pipe", "smeter_pipe": Pipe.READ,
"meta_pipe", "meta_pipe": Pipe.READ,
"iqtee_pipe", "iqtee_pipe": Pipe.NONE,
"iqtee2_pipe", "iqtee2_pipe": Pipe.NONE,
"dmr_control_pipe", "dmr_control_pipe": Pipe.WRITE,
] }
self.secondary_pipe_names = ["secondary_shift_pipe"] self.pipes = {}
self.secondary_pipe_names = {"secondary_shift_pipe": Pipe.WRITE}
self.secondary_offset_freq = 1000 self.secondary_offset_freq = 1000
self.unvoiced_quality = 1 self.unvoiced_quality = 1
self.modification_lock = threading.Lock() self.modification_lock = threading.Lock()
self.output = output self.output = output
self.temporary_directory = "/tmp"
self.temporary_directory = None
self.pipe_base_path = None
self.set_temporary_directory("/tmp")
self.is_service = False self.is_service = False
self.direwolf_config = None self.direwolf_config = None
self.direwolf_port = None self.direwolf_port = None
self.process = None
def set_service(self, flag=True): def set_service(self, flag=True):
self.is_service = flag self.is_service = flag
def set_temporary_directory(self, what): def set_temporary_directory(self, what):
self.temporary_directory = what self.temporary_directory = what
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_".format(tmp_dir=self.temporary_directory)
def chain(self, which): def chain(self, which):
chain = ["nc -v 127.0.0.1 {nc_port}"] chain = ["nc -v 127.0.0.1 {nc_port}"]
@ -137,7 +156,7 @@ class dsp(object):
if self.fft_compression == "adpcm": if self.fft_compression == "adpcm":
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
return chain return chain
chain += ["csdr shift_addition_cc --fifo {shift_pipe}"] chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
if self.decimation > 1: if self.decimation > 1:
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"] chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"] chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
@ -153,9 +172,12 @@ class dsp(object):
if not self.output.supports_type("audio"): if not self.output.supports_type("audio"):
return chain return chain
# safe some cpu cycles... no need to decimate if decimation factor is 1 # safe some cpu cycles... no need to decimate if decimation factor is 1
last_decimation_block = ( last_decimation_block = []
["csdr fractional_decimator_ff {last_decimation}"] if self.last_decimation != 1.0 else [] if self.last_decimation >= 2.0:
) # activate prefilter if signal has been oversampled, e.g. WFM
last_decimation_block = ["csdr fractional_decimator_ff {last_decimation} 12 --prefilter"]
elif self.last_decimation >= 1.0:
last_decimation_block = ["csdr fractional_decimator_ff {last_decimation}"]
if which == "nfm": if which == "nfm":
chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"] chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"]
chain += last_decimation_block chain += last_decimation_block
@ -166,6 +188,16 @@ class dsp(object):
] ]
else: else:
chain += ["csdr convert_f_s16"] chain += ["csdr convert_f_s16"]
elif which == "wfm":
chain += [
"csdr fmdemod_quadri_cf",
"csdr limit_ff",
]
chain += last_decimation_block
chain += [
"csdr deemphasis_wfm_ff {audio_rate} 50e-6",
"csdr convert_f_s16"
]
elif self.isDigitalVoice(which): elif self.isDigitalVoice(which):
chain += ["csdr fmdemod_quadri_cf", "dc_block "] chain += ["csdr fmdemod_quadri_cf", "dc_block "]
chain += last_decimation_block chain += last_decimation_block
@ -202,6 +234,15 @@ class dsp(object):
"csdr limit_ff", "csdr limit_ff",
"csdr convert_f_s16", "csdr convert_f_s16",
] ]
elif self.isFreeDV(which):
chain += ["csdr realpart_cf"]
chain += last_decimation_block
chain += [
"csdr agc_ff",
"csdr convert_f_s16",
"freedv_rx 1600 - -",
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
]
elif which == "ssb": elif which == "ssb":
chain += ["csdr realpart_cf"] chain += ["csdr realpart_cf"]
chain += last_decimation_block chain += last_decimation_block
@ -231,14 +272,14 @@ class dsp(object):
return chain return chain
elif which == "bpsk31" or which == "bpsk63": elif which == "bpsk31" or which == "bpsk63":
return chain + [ return chain + [
"csdr shift_addition_cc --fifo {secondary_shift_pipe}", "csdr shift_addfast_cc --fifo {secondary_shift_pipe}",
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}", "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
"csdr simple_agc_cc 0.001 0.5", "csdr simple_agc_cc 0.001 0.5",
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q", "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q",
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8", "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8",
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8", "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8",
] ]
elif self.isWsjtMode(which): elif self.isWsjtMode(which) or self.isJs8(which):
chain += ["csdr realpart_cf"] chain += ["csdr realpart_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"] chain += ["csdr fractional_decimator_ff {last_decimation}"]
@ -247,7 +288,7 @@ class dsp(object):
chain += ["csdr fmdemod_quadri_cf"] chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"] chain += ["csdr fractional_decimator_ff {last_decimation}"]
return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h - 1>&2"] return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h 1>&2"]
elif which == "pocsag": elif which == "pocsag":
chain += ["csdr fmdemod_quadri_cf"] chain += ["csdr fmdemod_quadri_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
@ -305,8 +346,8 @@ class dsp(object):
self.try_create_configs(secondary_command_demod) self.try_create_configs(secondary_command_demod)
secondary_command_demod = secondary_command_demod.format( secondary_command_demod = secondary_command_demod.format(
input_pipe=self.iqtee2_pipe, input_pipe=self.pipes["iqtee2_pipe"],
secondary_shift_pipe=self.secondary_shift_pipe, secondary_shift_pipe=self.pipes["secondary_shift_pipe"],
secondary_decimation=self.secondary_decimation(), secondary_decimation=self.secondary_decimation(),
secondary_samples_per_bits=self.secondary_samples_per_bits(), secondary_samples_per_bits=self.secondary_samples_per_bits(),
secondary_bpf_cutoff=self.secondary_bpf_cutoff(), secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
@ -325,7 +366,7 @@ class dsp(object):
if self.output.supports_type("secondary_fft"): if self.output.supports_type("secondary_fft"):
secondary_command_fft = " | ".join(self.secondary_chain("fft")) secondary_command_fft = " | ".join(self.secondary_chain("fft"))
secondary_command_fft = secondary_command_fft.format( secondary_command_fft = secondary_command_fft.format(
input_pipe=self.iqtee_pipe, input_pipe=self.pipes["iqtee_pipe"],
secondary_fft_input_size=self.secondary_fft_size, secondary_fft_input_size=self.secondary_fft_size,
secondary_fft_size=self.secondary_fft_size, secondary_fft_size=self.secondary_fft_size,
secondary_fft_block_size=self.secondary_fft_block_size(), secondary_fft_block_size=self.secondary_fft_block_size(),
@ -351,18 +392,25 @@ class dsp(object):
if self.isWsjtMode(): if self.isWsjtMode():
smd = self.get_secondary_demodulator() smd = self.get_secondary_demodulator()
chopper_profile = None
if smd == "ft8": if smd == "ft8":
chopper = Ft8Chopper(self.secondary_process_demod.stdout) chopper_profile = Ft8Profile()
elif smd == "wspr": elif smd == "wspr":
chopper = WsprChopper(self.secondary_process_demod.stdout) chopper_profile = WsprProfile()
elif smd == "jt65": elif smd == "jt65":
chopper = Jt65Chopper(self.secondary_process_demod.stdout) chopper_profile = Jt65Profile()
elif smd == "jt9": elif smd == "jt9":
chopper = Jt9Chopper(self.secondary_process_demod.stdout) chopper_profile = Jt9Profile()
elif smd == "ft4": elif smd == "ft4":
chopper = Ft4Chopper(self.secondary_process_demod.stdout) chopper_profile = Ft4Profile()
if chopper_profile is not None:
chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
chopper.start()
self.output.send_output("wsjt_demod", chopper.read)
elif self.isJs8():
chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles())
chopper.start() chopper.start()
self.output.send_output("wsjt_demod", chopper.read) self.output.send_output("js8_demod", chopper.read)
elif self.isPacket(): elif self.isPacket():
# we best get the ax25 packets from the kiss socket # we best get the ax25 packets from the kiss socket
kiss = KissClient(self.direwolf_port) kiss = KissClient(self.direwolf_port)
@ -373,30 +421,34 @@ class dsp(object):
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1)) self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
# open control pipes for csdr and send initialization data # open control pipes for csdr and send initialization data
if self.secondary_shift_pipe != None: # TODO digimodes if self.has_pipe("secondary_shift_pipe"): # TODO digimodes
self.secondary_shift_pipe_file = open(self.secondary_shift_pipe, "w") # TODO digimodes
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
def set_secondary_offset_freq(self, value): def set_secondary_offset_freq(self, value):
self.secondary_offset_freq = value self.secondary_offset_freq = value
if self.secondary_processes_running and hasattr(self, "secondary_shift_pipe_file"): if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
self.secondary_shift_pipe_file.write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())) self.pipes["secondary_shift_pipe"].write("%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate()))
self.secondary_shift_pipe_file.flush()
def stop_secondary_demodulator(self): def stop_secondary_demodulator(self):
if self.secondary_processes_running == False: if not self.secondary_processes_running:
return return
self.try_delete_pipes(self.secondary_pipe_names) self.try_delete_pipes(self.secondary_pipe_names)
self.try_delete_configs() self.try_delete_configs()
if self.secondary_process_fft: if self.secondary_process_fft:
try: try:
os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
# drain any leftover data to free file descriptors
self.secondary_process_fft.communicate()
self.secondary_process_fft = None
except ProcessLookupError: except ProcessLookupError:
# been killed by something else, ignore # been killed by something else, ignore
pass pass
if self.secondary_process_demod: if self.secondary_process_demod:
try: try:
os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
# drain any leftover data to free file descriptors
self.secondary_process_demod.communicate()
self.secondary_process_demod = None
except ProcessLookupError: except ProcessLookupError:
# been killed by something else, ignore # been killed by something else, ignore
pass pass
@ -447,11 +499,21 @@ class dsp(object):
def get_decimation(self, input_rate, output_rate): def get_decimation(self, input_rate, output_rate):
decimation = 1 decimation = 1
while input_rate / (decimation + 1) >= output_rate: correction = 1
# wideband fm has a much higher frequency deviation (75kHz).
# we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need
# to compensate here.
# the factor of 5 is by experimentation only, with a minimum audio rate of 36kHz (enforced by the client)
# this allows us to cover at least +/- 80kHz of frequency spectrum (may be higher, but that's the worst case).
# the correction factor is automatically compensated for by the secondary decimation stage, which comes
# after the demodulator.
if self.get_demodulator() == "wfm":
correction = 5
while input_rate / (decimation + 1) >= output_rate * correction:
decimation += 1 decimation += 1
fraction = float(input_rate / decimation) / output_rate fraction = float(input_rate / decimation) / output_rate
intermediate_rate = input_rate / decimation intermediate_rate = input_rate / decimation
return (decimation, fraction, intermediate_rate) return decimation, fraction, intermediate_rate
def if_samp_rate(self): def if_samp_rate(self):
return self.samp_rate / self.decimation return self.samp_rate / self.decimation
@ -462,11 +524,18 @@ class dsp(object):
def get_output_rate(self): def get_output_rate(self):
return self.output_rate return self.output_rate
def get_hd_output_rate(self):
return self.hd_output_rate
def get_audio_rate(self): def get_audio_rate(self):
if self.isDigitalVoice() or self.isPacket() or self.isPocsag(): if self.isDigitalVoice() or self.isPacket() or self.isPocsag():
return 48000 return 48000
elif self.isWsjtMode(): elif self.isWsjtMode() or self.isJs8():
return 12000 return 12000
elif self.isFreeDV():
return 8000
elif self.isHdAudio():
return self.get_hd_output_rate()
return self.get_output_rate() return self.get_output_rate()
def isDigitalVoice(self, demodulator=None): def isDigitalVoice(self, demodulator=None):
@ -479,6 +548,11 @@ class dsp(object):
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
def isJs8(self, demodulator = None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "js8"
def isPacket(self, demodulator=None): def isPacket(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
@ -489,6 +563,16 @@ class dsp(object):
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator == "pocsag" return demodulator == "pocsag"
def isFreeDV(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator == "freedv"
def isHdAudio(self, demodulator=None):
if demodulator is None:
demodulator = self.get_demodulator()
return demodulator == "wfm"
def set_output_rate(self, output_rate): def set_output_rate(self, output_rate):
if self.output_rate == output_rate: if self.output_rate == output_rate:
return return
@ -496,7 +580,16 @@ class dsp(object):
self.calculate_decimation() self.calculate_decimation()
self.restart() self.restart()
def set_hd_output_rate(self, hd_output_rate):
if self.hd_output_rate == hd_output_rate:
return
self.hd_output_rate = hd_output_rate
self.calculate_decimation()
self.restart()
def set_demodulator(self, demodulator): def set_demodulator(self, demodulator):
if demodulator in ["usb", "lsb", "cw"]:
demodulator = "ssb"
if self.demodulator == demodulator: if self.demodulator == demodulator:
return return
self.demodulator = demodulator self.demodulator = demodulator
@ -527,21 +620,22 @@ class dsp(object):
def set_offset_freq(self, offset_freq): def set_offset_freq(self, offset_freq):
self.offset_freq = offset_freq self.offset_freq = offset_freq
if self.running: if self.running:
self.modification_lock.acquire() self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
self.shift_pipe_file.write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
self.shift_pipe_file.flush() def set_center_freq(self, center_freq):
self.modification_lock.release() # dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq()
self.center_freq = center_freq
def get_operating_freq(self):
return self.center_freq + self.offset_freq
def set_bpf(self, low_cut, high_cut): def set_bpf(self, low_cut, high_cut):
self.low_cut = low_cut self.low_cut = low_cut
self.high_cut = high_cut self.high_cut = high_cut
if self.running: if self.running:
self.modification_lock.acquire() self.pipes["bpf_pipe"].write(
self.bpf_pipe_file.write(
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
) )
self.bpf_pipe_file.flush()
self.modification_lock.release()
def get_bpf(self): def get_bpf(self):
return [self.low_cut, self.high_cut] return [self.low_cut, self.high_cut]
@ -552,12 +646,9 @@ class dsp(object):
def set_squelch_level(self, squelch_level): def set_squelch_level(self, squelch_level):
self.squelch_level = squelch_level self.squelch_level = squelch_level
# no squelch required on digital voice modes # no squelch required on digital voice modes
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() else self.squelch_level actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV() else self.squelch_level
if self.running: if self.running:
self.modification_lock.acquire() self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
self.squelch_pipe_file.write("%g\n" % (self.convertToLinear(actual_squelch)))
self.squelch_pipe_file.flush()
self.modification_lock.release()
def set_unvoiced_quality(self, q): def set_unvoiced_quality(self, q):
self.unvoiced_quality = q self.unvoiced_quality = q
@ -567,39 +658,36 @@ class dsp(object):
return self.unvoiced_quality return self.unvoiced_quality
def set_dmr_filter(self, filter): def set_dmr_filter(self, filter):
if self.dmr_control_pipe_file: if self.has_pipe("dmr_control_pipe"):
self.dmr_control_pipe_file.write("{0}\n".format(filter)) self.pipes["dmr_control_pipe"].write("{0}\n".format(filter))
self.dmr_control_pipe_file.flush()
def mkfifo(self, path):
try:
os.unlink(path)
except:
pass
os.mkfifo(path)
def ddc_transition_bw(self): def ddc_transition_bw(self):
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate)) return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
def try_create_pipes(self, pipe_names, command_base): def try_create_pipes(self, pipe_names, command_base):
for pipe_name in pipe_names: for pipe_name, pipe_type in pipe_names.items():
if self.has_pipe(pipe_name):
logger.warning("{pipe_name} is still in use", pipe_name=pipe_name)
self.pipes[pipe_name].close()
if "{" + pipe_name + "}" in command_base: if "{" + pipe_name + "}" in command_base:
setattr(self, pipe_name, self.pipe_base_path + pipe_name) p = self.pipe_base_path + pipe_name
self.mkfifo(getattr(self, pipe_name)) encoding = None
# TODO make digiham output unicode and then change this here
# the whole pipe enoding feature onlye exists because of this
if pipe_name == "meta_pipe":
encoding = "cp437"
self.pipes[pipe_name] = Pipe.create(p, pipe_type, encoding=encoding)
else: else:
setattr(self, pipe_name, None) self.pipes[pipe_name] = None
def has_pipe(self, name):
return name in self.pipes and self.pipes[name] is not None
def try_delete_pipes(self, pipe_names): def try_delete_pipes(self, pipe_names):
for pipe_name in pipe_names: for pipe_name in pipe_names:
pipe_path = getattr(self, pipe_name, None) if self.has_pipe(pipe_name):
if pipe_path: self.pipes[pipe_name].close()
try: self.pipes[pipe_name] = None
os.unlink(pipe_path)
except FileNotFoundError:
# it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
pass
except Exception:
logger.exception("try_delete_pipes()")
def try_create_configs(self, command): def try_create_configs(self, command):
if "{direwolf_config}" in command: if "{direwolf_config}" in command:
@ -626,108 +714,95 @@ class dsp(object):
self.direwolf_config = None self.direwolf_config = None
def start(self): def start(self):
self.modification_lock.acquire() with self.modification_lock:
if self.running: if self.running:
self.modification_lock.release() return
return self.running = True
self.running = True
command_base = " | ".join(self.chain(self.demodulator)) command_base = " | ".join(self.chain(self.demodulator))
# create control pipes for csdr # create control pipes for csdr
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_{myid}_".format(tmp_dir=self.temporary_directory, myid=id(self)) self.try_create_pipes(self.pipe_names, command_base)
self.try_create_pipes(self.pipe_names, command_base) # send initial config through the pipes
if self.has_pipe("bpf_pipe"):
self.set_bpf(self.low_cut, self.high_cut)
if self.has_pipe("shift_pipe"):
self.set_offset_freq(self.offset_freq)
if self.has_pipe("squelch_pipe"):
self.set_squelch_level(self.squelch_level)
if self.has_pipe("dmr_control_pipe"):
self.set_dmr_filter(3)
# run the command # run the command
command = command_base.format( command = command_base.format(
bpf_pipe=self.bpf_pipe, bpf_pipe=self.pipes["bpf_pipe"],
shift_pipe=self.shift_pipe, shift_pipe=self.pipes["shift_pipe"],
decimation=self.decimation, squelch_pipe=self.pipes["squelch_pipe"],
last_decimation=self.last_decimation, smeter_pipe=self.pipes["smeter_pipe"],
fft_size=self.fft_size, meta_pipe=self.pipes["meta_pipe"],
fft_block_size=self.fft_block_size(), iqtee_pipe=self.pipes["iqtee_pipe"],
fft_averages=self.fft_averages, iqtee2_pipe=self.pipes["iqtee2_pipe"],
bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(), dmr_control_pipe=self.pipes["dmr_control_pipe"],
ddc_transition_bw=self.ddc_transition_bw(), decimation=self.decimation,
flowcontrol=int(self.samp_rate * 2), last_decimation=self.last_decimation,
start_bufsize=self.base_bufsize * self.decimation, fft_size=self.fft_size,
nc_port=self.nc_port, fft_block_size=self.fft_block_size(),
squelch_pipe=self.squelch_pipe, fft_averages=self.fft_averages,
smeter_pipe=self.smeter_pipe, bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
meta_pipe=self.meta_pipe, ddc_transition_bw=self.ddc_transition_bw(),
iqtee_pipe=self.iqtee_pipe, flowcontrol=int(self.samp_rate * 2),
iqtee2_pipe=self.iqtee2_pipe, start_bufsize=self.base_bufsize * self.decimation,
output_rate=self.get_output_rate(), nc_port=self.nc_port,
smeter_report_every=int(self.if_samp_rate() / 6000), output_rate=self.get_output_rate(),
unvoiced_quality=self.get_unvoiced_quality(), smeter_report_every=int(self.if_samp_rate() / 6000),
dmr_control_pipe=self.dmr_control_pipe, unvoiced_quality=self.get_unvoiced_quality(),
audio_rate=self.get_audio_rate(), 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 logger.debug("Command = %s", command)
if self.bpf_pipe: my_env = os.environ.copy()
self.bpf_pipe_file = open(self.bpf_pipe, "w") if self.csdr_dynamic_bufsize:
if self.shift_pipe: my_env["CSDR_DYNAMIC_BUFSIZE_ON"] = "1"
self.shift_pipe_file = open(self.shift_pipe, "w") if self.csdr_print_bufsizes:
if self.squelch_pipe: my_env["CSDR_PRINT_BUFSIZES"] = "1"
self.squelch_pipe_file = open(self.squelch_pipe, "w")
self.start_secondary_demodulator()
self.modification_lock.release() out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True, env=my_env)
# send initial config through the pipes def watch_thread():
if self.squelch_pipe: rc = self.process.wait()
self.set_squelch_level(self.squelch_level) logger.debug("dsp thread ended with rc=%d", rc)
if self.shift_pipe: if rc == 0 and self.running and not self.modification_lock.locked():
self.set_offset_freq(self.offset_freq) logger.debug("restarting since rc = 0, self.running = true, and no modification")
if self.bpf_pipe: self.restart()
self.set_bpf(self.low_cut, self.high_cut)
if self.smeter_pipe:
self.smeter_pipe_file = open(self.smeter_pipe, "r")
threading.Thread(target=watch_thread, name="csdr_watch_thread").start()
audio_type = "hd_audio" if self.isHdAudio() else "audio"
if self.output.supports_type(audio_type):
self.output.send_output(
audio_type,
partial(
self.process.stdout.read,
self.get_fft_bytes_to_read() if self.demodulator == "fft" else self.get_audio_bytes_to_read(),
),
)
self.start_secondary_demodulator()
if self.has_pipe("smeter_pipe"):
def read_smeter(): def read_smeter():
raw = self.smeter_pipe_file.readline() raw = self.pipes["smeter_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:
return None return None
else: else:
return float(raw.rstrip("\n")) return float(raw.rstrip("\n"))
self.output.send_output("smeter", read_smeter) self.output.send_output("smeter", read_smeter)
if self.meta_pipe != None: if self.has_pipe("meta_pipe"):
# TODO make digiham output unicode and then change this here
self.meta_pipe_file = open(self.meta_pipe, "r", encoding="cp437")
def read_meta(): def read_meta():
raw = self.meta_pipe_file.readline() raw = self.pipes["meta_pipe"].readline()
if len(raw) == 0: if len(raw) == 0:
return None return None
else: else:
@ -735,23 +810,25 @@ class dsp(object):
self.output.send_output("meta", read_meta) self.output.send_output("meta", read_meta)
if self.dmr_control_pipe: if self.csdr_dynamic_bufsize:
self.dmr_control_pipe_file = open(self.dmr_control_pipe, "w") self.process.stdout.read(8) # dummy read to skip bufsize & preamble
logger.debug("Note: CSDR_DYNAMIC_BUFSIZE_ON = 1")
def stop(self): def stop(self):
self.modification_lock.acquire() with self.modification_lock:
self.running = False self.running = False
if hasattr(self, "process"): if self.process is not None:
try: try:
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
except ProcessLookupError: # drain any leftover data to free file descriptors
# been killed by something else, ignore self.process.communicate()
pass self.process = None
self.stop_secondary_demodulator() except ProcessLookupError:
# been killed by something else, ignore
pass
self.stop_secondary_demodulator()
self.try_delete_pipes(self.pipe_names) self.try_delete_pipes(self.pipe_names)
self.modification_lock.release()
def restart(self): def restart(self):
if not self.running: if not self.running:
@ -761,4 +838,3 @@ class dsp(object):
def __del__(self): def __del__(self):
self.stop() self.stop()
del self.process

155
csdr/pipe.py Normal file
View File

@ -0,0 +1,155 @@
import os
import select
import time
import threading
import logging
logger = logging.getLogger(__name__)
class Pipe(object):
READ = "r"
WRITE = "w"
NONE = None
@staticmethod
def create(path, t, encoding=None):
if t == Pipe.READ:
return ReadingPipe(path, encoding=encoding)
elif t == Pipe.WRITE:
return WritingPipe(path, encoding=encoding)
elif t == Pipe.NONE:
return Pipe(path, None, encoding=encoding)
def __init__(self, path, direction, encoding=None):
self.doOpen = True
self.path = "{base}_{myid}".format(base=path, myid=id(self))
self.direction = direction
self.encoding = encoding
self.file = None
os.mkfifo(self.path)
def open(self):
"""
this method opens the file descriptor with an added O_NONBLOCK flag. This gives us a special behaviour for
FIFOS, when they are not opened by the opposing side:
- opening a pipe for writing will throw an OSError with errno = 6 (ENXIO). This is handled specially in the
WritingPipe class.
- opening a pipe for reading will pass through this method instantly, even if the opposing end has not been
opened yet, but the resulting file descriptor will behave as if O_NONBLOCK is set (even if we remove it
immediately here), resulting in empty reads until data is available. This is handled specially in the
ReadingPipe class.
"""
def opener(path, flags):
fd = os.open(path, flags | os.O_NONBLOCK)
os.set_blocking(fd, True)
return fd
self.file = open(self.path, self.direction, encoding=self.encoding, opener=opener)
def close(self):
self.doOpen = False
try:
if self.file is not None:
self.file.close()
os.unlink(self.path)
except FileNotFoundError:
# it seems like we keep calling this twice. no idea why, but we don't need the resulting error.
pass
except Exception:
logger.exception("Pipe.close()")
def __str__(self):
return self.path
class WritingPipe(Pipe):
def __init__(self, path, encoding=None):
self.queue = []
self.queueLock = threading.Lock()
super().__init__(path, "w", encoding=encoding)
self.open()
def open_and_dequeue(self):
"""
This method implements a retry loop that can be interrupted in case the Pipe gets shutdown before actually
being connected.
After the pipe is opened successfully, all data that has been queued is sent in the order it was passed into
write().
"""
retries = 0
while self.file is None and self.doOpen and retries < 10:
try:
super().open()
except OSError as error:
# ENXIO = FIFO has not been opened for reading
if error.errno == 6:
time.sleep(.1)
retries += 1
else:
raise
# if doOpen is false, opening has been canceled, so no warning in that case.
if self.file is None:
if self.doOpen:
logger.warning("could not open FIFO %s", self.path)
return
with self.queueLock:
for i in self.queue:
self.file.write(i)
self.file.flush()
self.queue = None
def open(self):
"""
This sends the opening operation off to a background thread. If we were to block the thread here, another pipe
may be waiting in the queue to be opened on the opposing side, resulting in a deadlock
"""
threading.Thread(target=self.open_and_dequeue, name="csdr_pipe_thread").start()
def write(self, data):
"""
This method queues all data to be written until the file is actually opened. As soon as a file is available,
it becomes a passthrough.
"""
if self.file is None:
with self.queueLock:
self.queue.append(data)
return
r = self.file.write(data)
self.file.flush()
return r
class ReadingPipe(Pipe):
def __init__(self, path, encoding=None):
super().__init__(path, "r", encoding=encoding)
def open(self):
"""
This method implements an interruptible loop that waits for the file descriptor to be opened and the first
batch of data coming in using repeated select() calls.
:return:
"""
if not self.doOpen:
return
super().open()
while self.doOpen:
(read, _, _) = select.select([self.file], [], [], 1)
if self.file in read:
break
def read(self):
if self.file is None:
self.open()
return self.file.read()
def readline(self):
if self.file is None:
self.open()
return self.file.readline()

149
debian/changelog vendored
View File

@ -1,5 +1,148 @@
openwebrx (0.18) UNRELEASED; urgency=low openwebrx (0.20.0) UNRELEASED; urgency=low
* Initial release. * Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver
on receiverbook.de
* Fixed file descriptor leaks to prevent "too many open files" errors
* Add new demodulator chain for FreeDV
* Added new HD audio streaming mode along with a new WFM demodulator
* New devices supported:
- FunCube Dongle Pro+ (`"type": "fcdpp"`)
- Support for connections to rtl_tcp
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sun, 08 Dec 2019 12:35:48 +0000 -- Jakob Ketterl <jakob.ketterl@gmx.de> Sat, 13 Jun 2020 17:22:00 +0000
openwebrx (0.19.1) buster focal; urgency=low
* Added ability to authenticate receivers with listing sites using
"receiver id" tokens
-- Jakob Ketterl <jakob.ketterl@gmx.de> Sat, 13 Jun 2020 16:46:00 +0000
openwebrx (0.19.0) buster focal; urgency=low
* Fix direwolf connection setup by implementing a retry loop
* Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector
* OSM maps instead of Google when google_maps_api_key is not set (thanks
@jquagga)
* Improved logic to pass parameters to soapy devices.
- `rtl_sdr_soapy`: added support for `bias_tee`
- `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch`
- `airspy`: added support for `bitpack`
* Added support for Perseus-SDR devices, (thanks @amontefusco)
* Property System has been rewritten so that defaults on sdr behave as
expected
* Waterfall range auto-adjustment now only takes the center 80% of the
spectrum into account, which should work better with SDRs that oversample
or have rather flat filter curves towards the spectrum edges
* Bugfix for negative network usage
* FiFi SDR: prevent arecord from shutting down after 2GB of data has been
sent
* Added support for bias tee control on rtl_sdr devices
* All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
* `rtl_sdr` type now also supports the `direct_sampling` option
* Added decoding implementation for for digimode "JS8Call" (requires an
installation of js8call and the js8py library)
* Reorganization of the frontend demodulator code
* Improve receiver load time by concatenating javascript assets
* HackRF support is now based on SoapyHackRF
* Removed sdr.hu server listing support since the site has been shut down
* Added support for Radioberry 2 Rasbperry Pi SDR Cape
-- Jakob Ketterl <jakob.ketterl@gmx.de> Mon, 01 Jun 2020 17:02:00 +0000
openwebrx (0.18.0) buster; urgency=low
* Compression, resampling and filtering in the frontend have been rewritten
in javascript, sdr.js has been removed
* Decoding of Pocsag modulation is now possible
* Removed the 3D waterfall since it had no real application and required ~1MB
of javascript code to be downloaded
* Improved the frontend handling of the "too many users" scenario
* PSK63 digimode is now available (same decoding pipeline as PSK31, but with
adopted parameters)
* The frequency can now be manipulated with the mousewheel, which should
allow the user to tune more precise. The tuning step size is determined by
the digit the mouse cursor is hovering over.
* Clicking on the frequency now opens an input for direct frequency selection
* URL hashes have been fixed and improved: They are now updated
automatically, so a shared URL will include frequency and demodulator,
which allows for improved sharing and linking.
* New daylight scheduler for background decoding, allows profiles to be
selected by local sunrise / sunset times
* The owrx_connector is now the default way of communicating with sdr
devices. The old sdr types have been replaced, all `_connector` suffixes on
the type must be removed!
* The sources have been refactored, making it a lot easier to add support for
other devices
* SDR device failure handling has been improved, including user feedback
* New devices supported:
* wsjt-x updated to 2.1.2
* The rtl_tcp compatibility mode of the owrx_connector is now configurable
using the `rtltcp_compat` flag
* explicit device filter for soapy devices for multi-device setups
* compatibility fixes for safari browsers (ios and mac)
* Offset tuning using the `lfo_offset` has been reworked in a way that
`center_freq` has to be set to the frequency you actually want to listen
to. If you're using an `lfo_offset` already, you will probably need to
change its sign.
* `initial_squelch_level` can now be set on each profile.
* Part of the frontend code has been reworked
- Audio buffer minimums have been completely stripped. As a result, you
should get better latency. Unfortunately, this also means there will be
some skipping when audio starts.
- Now also supports AudioWorklets (for those browser that have it).
- Mousewheel controls for the receiver sliders
* Error handling for failed SDR devices
* One of the most-requested features is finally coming to OpenWebRX:
Bookmarks (sometimes also referred to as labels).
There's two kinds of bookmarks available:
- Serverside bookmarks that are set up by the receiver administrator.
Check the file `bookmarks.json` for examples!
- Clientside bookmarks which every user can store for themselves. They are
stored in the browser's localStorage.
* Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is
now possible. Please have a look at the configuration on how to set it up.
* Websocket communication has been overhauled in large parts. It should now
be more reliable, and failing connections should now have no impact on
other users.
* Profile scheduling allows to set up band-hopping if you are running
background services.
* APRS now has the ability to show symbols on the map, if a corresponding
symbol set has been installed. Check the config!
* Debug logging has been disabled in a handful of modules, expect vastly
reduced output on the shell.
* New set of APRS-related features
- Decode Packet transmissions using direwolf (1k2 only for now)
- APRS packets are mostly decoded and shown both in a new panel and on the
map
- APRS is also available as a background service
- direwolfs I-gate functionality can be enabled, which allows your receiver
to work as a receive-only I-gate for the APRS network in the background
* Demodulation for background services has been optimized to use less total
bandwidth, saving CPU
* More metrics have been added; they can be used together with collectd and
its curl_json plugin for now, with some limitations.
* New bandplan feature, the first thing visible is the "dial" indicator that
brings you right to the dial frequency for digital modes
* fixed some bugs in the websocket communication which broke the map
* WSJT-X integration (FT8, FT4, WSPR, JT65, JT9 using wsjt-x demodulators)
* New Map Feature that shows both decoded grid squares from FT8 and Locations
decoded from YSF digital voice
* New Feature report that will show what functionality is available
* major rework on the openwebrx core
* Support of multiple SDR devices simultaneously
* Support for multiple profiles per SDR that allow the user to listen to
different frequencies
* Support for digital voice decoding
* Feature detection that will disable functionality when dependencies are not
available (if you're missing the digital
buttons, this is probably why)
* Support added for the following SDR sources:
- LimeSDR (`"type": "lime_sdr"`)
- PlutoSDR (`"type": "pluto_sdr"`)
- RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow
use of the direct sampling mode
- SoapyRemote (`"type": "soapy_remote"`)
- FiFiSDR (`"type": "fifi_sdr"`)
- airspyhf devices (Airspy HF+ / Discovery) (`"type": "airspyhf"`)
-- Jakob Ketterl <jakob.ketterl@gmx.de> Tue, 18 Feb 2020 20:09:00 +0000

9
debian/control vendored
View File

@ -3,11 +3,14 @@ Maintainer: Jakob Ketterl <jakob.ketterl@gmx.de>
Section: hamradio Section: hamradio
Priority: optional Priority: optional
Standards-Version: 4.2.0 Standards-Version: 4.2.0
Build-Depends: debhelper (>= 10), dh-python, python3 (>= 3.5), dh-systemd (>= 1.5) Build-Depends: debhelper (>= 11), dh-python, python3-all (>= 3.5), python3-setuptools
Homepage: https://www.openwebrx.de/
Vcs-Browser: https://github.com/jketterl/openwebrx
Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all Architecture: all
Depends: python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.1), ${python3:Depends} Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.2), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
Description: multi-user web sdr Description: multi-user web sdr
Open source, multi-user SDR receiver with a web interface Open source, multi-user SDR receiver with a web interface

View File

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

7
debian/postinst vendored Executable file
View File

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

3
debian/rules vendored
View File

@ -3,3 +3,6 @@ export PYBUILD_NAME=openwebrx
%: %:
dh $@ --with python3 --buildsystem=pybuild --with systemd dh $@ --with python3 --buildsystem=pybuild --with systemd
override_dh_strip_nondeterminism:
dh_strip_nondeterminism -X.png

View File

@ -2,9 +2,7 @@ ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-airspy.sh / ADD docker/scripts/install-dependencies-airspy.sh /
RUN /install-dependencies-airspy.sh RUN /install-dependencies-airspy.sh &&\
RUN rm /install-dependencies-airspy.sh rm /install-dependencies-airspy.sh
ADD docker/scripts/install-connectors.sh / ADD . /opt/openwebrx
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -1,19 +1,18 @@
FROM alpine:3.10 FROM debian:buster-slim
RUN apk add --no-cache bash ADD docker/files/js8call/js8call-hamlib.patch /
ADD docker/files/wsjtx/*.patch /
RUN ln -s /usr/local/lib /usr/local/lib64
ADD docker/scripts/direwolf-1.5.patch /
ADD docker/scripts/install-dependencies.sh / ADD docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh RUN /install-dependencies.sh && \
RUN rm /install-dependencies.sh rm /install-dependencies.sh && \
rm /*.patch
ADD . /opt/openwebrx ENTRYPOINT ["/init"]
WORKDIR /opt/openwebrx WORKDIR /opt/openwebrx
VOLUME /etc/openwebrx VOLUME /etc/openwebrx
ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ] CMD [ "/opt/openwebrx/docker/scripts/run.sh" ]
EXPOSE 8073 EXPOSE 8073

View File

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

View File

@ -2,16 +2,27 @@ ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-*.sh / ADD docker/scripts/install-dependencies-*.sh /
ADD docker/scripts/install-lib.*.patch / ADD docker/files/sdrplay/install-lib.*.patch /
RUN /install-dependencies-rtlsdr.sh RUN /install-dependencies-rtlsdr.sh &&\
RUN /install-dependencies-hackrf.sh /install-dependencies-soapysdr.sh &&\
RUN /install-dependencies-soapysdr.sh /install-dependencies-hackrf.sh &&\
RUN /install-dependencies-sdrplay.sh /install-dependencies-sdrplay.sh &&\
RUN /install-dependencies-airspy.sh /install-dependencies-airspy.sh &&\
RUN /install-dependencies-rtlsdr-soapy.sh /install-dependencies-rtlsdr-soapy.sh &&\
RUN rm /install-dependencies-*.sh /install-dependencies-plutosdr.sh &&\
/install-dependencies-limesdr.sh &&\
/install-dependencies-soapyremote.sh &&\
/install-dependencies-perseus.sh &&\
/install-dependencies-fcdpp.sh &&\
/install-dependencies-radioberry.sh &&\
rm /install-dependencies-*.sh &&\
rm /install-lib.*.patch
ADD docker/scripts/install-connectors.sh / ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh RUN /install-connectors.sh &&\
RUN rm /install-connectors.sh rm /install-connectors.sh
ADD docker/files/services/sdrplay /etc/services.d/sdrplay
ADD . /opt/openwebrx

View File

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

View File

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

View File

@ -0,0 +1,8 @@
ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-perseus.sh /
RUN /install-dependencies-perseus.sh &&\
rm /install-dependencies-perseus.sh
ADD . /opt/openwebrx

View File

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

View File

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

View File

@ -2,9 +2,11 @@ ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-rtlsdr.sh / ADD docker/scripts/install-dependencies-rtlsdr.sh /
RUN /install-dependencies-rtlsdr.sh
RUN rm /install-dependencies-rtlsdr.sh
ADD docker/scripts/install-connectors.sh / ADD docker/scripts/install-connectors.sh /
RUN /install-connectors.sh
RUN rm /install-connectors.sh RUN /install-dependencies-rtlsdr.sh &&\
rm /install-dependencies-rtlsdr.sh &&\
/install-connectors.sh &&\
rm /install-connectors.sh
ADD . /opt/openwebrx

View File

@ -2,9 +2,7 @@ ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh / ADD docker/scripts/install-dependencies-rtlsdr-soapy.sh /
RUN /install-dependencies-rtlsdr-soapy.sh RUN /install-dependencies-rtlsdr-soapy.sh &&\
RUN rm /install-dependencies-rtlsdr-soapy.sh rm /install-dependencies-rtlsdr-soapy.sh
ADD docker/scripts/install-connectors.sh / ADD . /opt/openwebrx
RUN /install-connectors.sh
RUN rm /install-connectors.sh

View File

@ -2,10 +2,11 @@ ARG ARCHTAG
FROM openwebrx-soapysdr-base:$ARCHTAG FROM openwebrx-soapysdr-base:$ARCHTAG
ADD docker/scripts/install-dependencies-sdrplay.sh / ADD docker/scripts/install-dependencies-sdrplay.sh /
ADD docker/scripts/install-lib.*.patch / ADD docker/files/sdrplay/install-lib.*.patch /
RUN /install-dependencies-sdrplay.sh RUN /install-dependencies-sdrplay.sh &&\
RUN rm /install-dependencies-sdrplay.sh rm /install-dependencies-sdrplay.sh &&\
rm /install-lib.*.patch
ADD docker/scripts/install-connectors.sh / ADD docker/files/services/sdrplay /etc/services.d/sdrplay
RUN /install-connectors.sh
RUN rm /install-connectors.sh ADD . /opt/openwebrx

View File

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

View File

@ -2,6 +2,8 @@ ARG ARCHTAG
FROM openwebrx-base:$ARCHTAG FROM openwebrx-base:$ARCHTAG
ADD docker/scripts/install-dependencies-soapysdr.sh / ADD docker/scripts/install-dependencies-soapysdr.sh /
RUN /install-dependencies-soapysdr.sh ADD docker/scripts/install-connectors.sh /
RUN rm /install-dependencies-soapysdr.sh RUN /install-dependencies-soapysdr.sh &&\
rm /install-dependencies-soapysdr.sh &&\
/install-connectors.sh &&\
rm /install-connectors.sh

5
docker/env Normal file
View File

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

View File

@ -0,0 +1,151 @@
diff -ur js8call-orig/CMake/Modules/Findhamlib.cmake js8call/CMake/Modules/Findhamlib.cmake
--- js8call-orig/CMake/Modules/Findhamlib.cmake 2020-07-22 18:14:18.014499840 +0200
+++ js8call/CMake/Modules/Findhamlib.cmake 2020-07-22 18:16:07.200375473 +0200
@@ -78,4 +78,4 @@
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
# TRUE if all listed variables are TRUE
include (FindPackageHandleStandardArgs)
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt
--- js8call-orig/CMakeLists.txt 2020-07-22 18:14:18.014499840 +0200
+++ js8call/CMakeLists.txt 2020-07-22 18:17:55.629633825 +0200
@@ -558,7 +558,7 @@
#
# libhamlib setup
#
-set (hamlib_STATIC 1)
+set (hamlib_STATIC 0)
find_package (hamlib 3 REQUIRED)
find_program (RIGCTL_EXE rigctl)
find_program (RIGCTLD_EXE rigctld)
@@ -911,56 +911,6 @@
target_link_libraries (js8 wsjt_fort wsjt_cxx Qt5::Core)
endif (${OPENMP_FOUND} OR APPLE)
-# build the main application
-add_executable (js8call MACOSX_BUNDLE
- ${sqlite3_CSRCS}
- ${wsjtx_CXXSRCS}
- ${wsjtx_GENUISRCS}
- wsjtx.rc
- ${WSJTX_ICON_FILE}
- ${wsjtx_RESOURCES_RCC}
- images.qrc
- )
-
-if (WSJT_CREATE_WINMAIN)
- set_target_properties (js8call PROPERTIES WIN32_EXECUTABLE ON)
-endif (WSJT_CREATE_WINMAIN)
-
-set_target_properties (js8call PROPERTIES
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.kn4crd.js8call"
- )
-
-target_include_directories (js8call PRIVATE ${FFTW3_INCLUDE_DIRS})
-if (APPLE)
- target_link_libraries (js8call wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
-else ()
- target_link_libraries (js8call wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
- if (OpenMP_C_FLAGS)
- set_target_properties (js8call PROPERTIES
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
- LINK_FLAGS "${OpenMP_C_FLAGS}"
- )
- endif ()
- set_target_properties (js8call PROPERTIES
- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
- )
- if (WIN32)
- set_target_properties (js8call PROPERTIES
- LINK_FLAGS -Wl,--stack,16777216
- )
- endif ()
-endif ()
-qt5_use_modules (js8call SerialPort) # not sure why the interface link library syntax above doesn't work
-
# if (UNIX)
# if (NOT WSJT_SKIP_MANPAGES)
# add_subdirectory (manpages)
@@ -976,38 +926,10 @@
#
# installation
#
-install (TARGETS js8call
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- BUNDLE DESTINATION . COMPONENT runtime
- )
-
install (TARGETS js8 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
-install (PROGRAMS
- ${RIGCTL_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctl-local${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (PROGRAMS
- ${RIGCTLD_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (FILES
- README
- COPYING
- INSTALL
- INSTALL-WSJTX
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
- #COMPONENT runtime
- )
-
install (FILES
contrib/Ephemeris/JPLEPH
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
@@ -1061,32 +983,6 @@
"${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h"
)
-
-if (NOT WIN32 AND NOT APPLE)
- # install a desktop file so js8call appears in the application start
- # menu with an icon
- install (
- FILES js8call.desktop
- DESTINATION /usr/share/applications
- #COMPONENT runtime
- )
- install (
- FILES icons/Unix/js8call_icon.png
- DESTINATION /usr/share/pixmaps
- #COMPONENT runtime
- )
-
- IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/opt/js8call")
- execute_process(COMMAND ln -s /opt/js8call/bin/js8call ljs8call)
-
- install(FILES
- ${CMAKE_BINARY_DIR}/ljs8call DESTINATION /usr/bin/ RENAME js8call
- #COMPONENT runtime
- )
- endif()
-endif (NOT WIN32 AND NOT APPLE)
-
-
#
# bundle fixup only done in Release or MinSizeRel configurations
#
Only in js8call/: .idea

View File

@ -0,0 +1,23 @@
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
--- sdrplay-orig/install_lib.sh 2020-05-24 14:30:06.022483867 +0000
+++ sdrplay/install_lib.sh 2020-05-24 14:30:49.093435726 +0000
@@ -4,19 +4,6 @@
export MAJVERS="3"
echo "Installing SDRplay RSP API library ${VERS}..."
-read -p "Press RETURN to view the license agreement" ret
-
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
export ARCH=`uname -m`

View File

@ -0,0 +1,40 @@
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
--- sdrplay-orig/install_lib.sh 2020-05-24 14:13:04.561271707 +0000
+++ sdrplay/install_lib.sh 2020-05-24 14:16:20.068329040 +0000
@@ -4,19 +4,6 @@
MAJVERS="3"
echo "Installing SDRplay RSP API library ${VERS}..."
-read -p "Press RETURN to view the license agreement" ret
-
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
ARCH=`uname -m`
@@ -141,16 +128,6 @@
echo "SDRplay API ${VERS} Installation Finished"
echo " "
-while true; do
- echo "Would you like to add SDRplay USB IDs to the local database for easier
-"
- read -p "identification in applications such as lsusb? [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.

View File

@ -0,0 +1,39 @@
diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh
--- sdrplay-orig/install_lib.sh 2020-05-24 13:56:56.622000041 +0000
+++ sdrplay/install_lib.sh 2020-05-24 13:58:51.837801559 +0000
@@ -4,19 +4,6 @@
MAJVERS="3"
echo "Installing SDRplay RSP API library ${VERS}..."
-read -p "Press RETURN to view the license agreement" ret
-
-more sdrplay_license.txt
-
-while true; do
- echo "Press y and RETURN to accept the license agreement and continue with"
- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
ARCH=`uname -m`
OSDIST="Unknown"
@@ -157,15 +144,6 @@
echo " "
echo "SDRplay API ${VERS} Installation Finished"
echo " "
-while true; do
- echo "Would you like to add SDRplay USB IDs to the local database for easier"
- read -p "identification in applications such as lsusb? [y/n] " yn
- case $yn in
- [Yy]* ) break;;
- [Nn]* ) exit;;
- * ) echo "Please answer y or n";;
- esac
-done
sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/.
sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh
sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/.

View File

@ -0,0 +1,2 @@
#!/usr/bin/execlineb -P
/usr/local/bin/sdrplay_apiService

View File

@ -0,0 +1,43 @@
--- CMakeLists.txt.orig 2020-07-21 20:59:55.982026645 +0200
+++ CMakeLists.txt 2020-07-21 21:01:25.444836112 +0200
@@ -80,24 +80,6 @@
include (ExternalProject)
-
-#
-# build and install hamlib locally so it can be referenced by the
-# WSJT-X build
-#
-ExternalProject_Add (hamlib
- GIT_REPOSITORY ${hamlib_repo}
- GIT_TAG ${hamlib_TAG}
- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}
- URL_HASH MD5=${hamlib_md5sum}
- #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap"
- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch
- CONFIGURE_COMMAND <SOURCE_DIR>/configure --prefix=<INSTALL_DIR> --disable-shared --enable-static --without-cxx-binding ${EXTRA_FLAGS} # LIBUSB_LIBS=${USB_LIBRARY}
- BUILD_COMMAND $(MAKE) all V=1 # $(MAKE) is ExternalProject_Add() magic to do recursive make
- INSTALL_COMMAND $(MAKE) install-strip V=1 DESTDIR=""
- STEP_TARGETS update install
- )
-
#
# custom target to make a hamlib source tarball
#
@@ -136,7 +118,6 @@
# build and optionally install WSJT-X using the hamlib package built
# above
#
-ExternalProject_Get_Property (hamlib INSTALL_DIR)
ExternalProject_Add (wsjtx
GIT_REPOSITORY ${wsjtx_repo}
GIT_TAG ${WSJTX_TAG}
@@ -160,7 +141,6 @@
DEPENDEES build
)
-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1)
set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1)
add_dependencies (wsjtx-configure hamlib-install)

View File

@ -0,0 +1,155 @@
diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamlib.cmake
--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2020-07-21 21:10:43.124810140 +0200
+++ wsjtx/CMake/Modules/Findhamlib.cmake 2020-07-21 21:11:03.368019114 +0200
@@ -85,4 +85,4 @@
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
# TRUE if all listed variables are TRUE
include (FindPackageHandleStandardArgs)
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt
--- wsjtx-orig/CMakeLists.txt 2020-07-21 21:10:43.124810140 +0200
+++ wsjtx/CMakeLists.txt 2020-07-21 22:14:04.454639589 +0200
@@ -871,7 +871,7 @@
#
# libhamlib setup
#
-set (hamlib_STATIC 1)
+set (hamlib_STATIC 0)
find_package (hamlib 3 REQUIRED)
find_program (RIGCTL_EXE rigctl)
find_program (RIGCTLD_EXE rigctld)
@@ -1348,53 +1348,10 @@
endif(WSJT_BUILD_UTILS)
-# build the main application
-add_executable (wsjtx MACOSX_BUNDLE
- ${wsjtx_CXXSRCS}
- ${wsjtx_GENUISRCS}
- wsjtx.rc
- ${WSJTX_ICON_FILE}
- ${wsjtx_RESOURCES_RCC}
- )
-
if (WSJT_CREATE_WINMAIN)
set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON)
endif (WSJT_CREATE_WINMAIN)
-set_target_properties (wsjtx PROPERTIES
- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in"
- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}"
- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}"
- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION}
- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}"
- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}"
- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}"
- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}"
- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx"
- )
-
-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS})
-if (APPLE)
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
-else ()
- target_link_libraries (wsjtx Qt5::SerialPort wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES})
- if (OpenMP_C_FLAGS)
- set_target_properties (wsjtx PROPERTIES
- COMPILE_FLAGS "${OpenMP_C_FLAGS}"
- LINK_FLAGS "${OpenMP_C_FLAGS}"
- )
- endif ()
- set_target_properties (wsjtx PROPERTIES
- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp
- )
- if (WIN32)
- set_target_properties (wsjtx PROPERTIES
- LINK_FLAGS -Wl,--stack,16777216
- )
- endif ()
-endif ()
-
# make a library for WSJT-X UDP servers
# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS})
add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS})
@@ -1437,24 +1394,9 @@
set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON)
endif (WSJT_CREATE_WINMAIN)
-if (UNIX)
- if (NOT WSJT_SKIP_MANPAGES)
- add_subdirectory (manpages)
- add_dependencies (wsjtx manpages)
- endif (NOT WSJT_SKIP_MANPAGES)
- if (NOT APPLE)
- add_subdirectory (debian)
- add_dependencies (wsjtx debian)
- endif (NOT APPLE)
-endif (UNIX)
-
#
# installation
#
-install (TARGETS wsjtx
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- BUNDLE DESTINATION . COMPONENT runtime
- )
# install (TARGETS wsjtx_udp EXPORT udp
# RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -1473,12 +1415,7 @@
# DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx
# )
-install (TARGETS udp_daemon message_aggregator
- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
- )
-
-install (TARGETS jt9 wsprd fmtave fcal fmeasure
+install (TARGETS jt9 wsprd
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
@@ -1491,39 +1428,6 @@
)
endif(WSJT_BUILD_UTILS)
-install (PROGRAMS
- ${RIGCTL_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctl-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (PROGRAMS
- ${RIGCTLD_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctld-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (PROGRAMS
- ${RIGCTLCOM_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctlcom-wsjtx${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (FILES
- README
- COPYING
- AUTHORS
- THANKS
- NEWS
- INSTALL
- BUGS
- DESTINATION ${CMAKE_INSTALL_DOCDIR}
- #COMPONENT runtime
- )
-
install (FILES
contrib/Ephemeris/JPLEPH
DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME}
Only in wsjtx: .idea

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,5 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,12 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
BUILD_PACKAGES="git cmake make gcc g++ musl-dev" BUILD_PACKAGES="git cmake make gcc g++"
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
apt-get update
apt-get -y install --no-install-recommends $BUILD_PACKAGES
git clone https://github.com/jketterl/owrx_connector.git git clone https://github.com/jketterl/owrx_connector.git
cmakebuild owrx_connector 84909c53cde78cbf4be408037e31209fbc702ad3 # this is the latest development version as of 2020-08-20 (includes rtl_tcp_connector)
cmakebuild owrx_connector a5a4d78ff7f029d2b86e9ddbc30187ced1c3ecf7
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,22 +18,24 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/airspy/airspyone_host.git git clone https://github.com/airspy/airspyone_host.git
cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3 cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3
git clone https://github.com/pothosware/SoapyAirspy.git git clone https://github.com/pothosware/SoapyAirspy.git
cmakebuild SoapyAirspy 99756be5c3413a2d447baf70cb5a880662452655 cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3
git clone https://github.com/airspy/airspyhf.git git clone https://github.com/airspy/airspyhf.git
cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475 cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
git clone https://github.com/pothosware/SoapyAirspyHF.git git clone https://github.com/pothosware/SoapyAirspyHF.git
cmakebuild SoapyAirspyHF 54f5487dd96207540b2dd562ff9e718e0588770b cmakebuild SoapyAirspyHF 81ca737bb044dd930a9de738bced1e4915491f1b
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,32 @@
#!/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="libhidapi-hidraw0 libhidapi-libusb0 libasound2"
BUILD_PACKAGES="git cmake make gcc g++ libhidapi-dev libasound2-dev"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapyFCDPP.git
cmakebuild SoapyFCDPP soapy-fcdpp-0.1.1
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,17 +18,22 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb fftw udev" STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/mossmann/hackrf.git git clone https://github.com/mossmann/hackrf.git
cd hackrf cd hackrf
git checkout 06eb9192cd348083f5f7de9c0da9ead276020011 git checkout 43e6f99fe8543094d18ff3a6550ed2066c398862
cmakebuild host cmakebuild host
cd .. cd ..
rm -rf hackrf rm -rf hackrf
apk del .build-deps git clone https://github.com/pothosware/SoapyHackRF.git
cmakebuild SoapyHackRF 3c514cecd3dd2fdf4794aebc96c482940c11d7ff
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
cd /tmp
STATIC_PACKAGES="libusb-1.0-0 libatomic1"
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
SIMD_FLAGS=""
if [[ 'x86_64' == `uname -m` ]] ; then
SIMD_FLAGS="-DDEFAULT_SIMD_FLAGS=SSE3"
fi
git clone https://github.com/myriadrf/LimeSuite.git
cd LimeSuite
git checkout 0854a51ec06b30b01f19a562149c39461e92f24d
mkdir builddir
cd builddir
cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" ${SIMD_FLAGS}
make
make install
cd ../..
rm -rf LimeSuite
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euxo pipefail
export MAKEFLAGS="-j4"
cd /tmp
STATIC_PACKAGES="libusb-1.0-0 libudev1"
BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/Microtelecom/libperseus-sdr.git
cd libperseus-sdr
git checkout 72ac67c5b7936a1991be0ec97c03a59c1a8ac8f3
./bootstrap.sh
./configure
make
make install
ldconfig /etc/ld.so.conf.d
cd ..
rm -rf libperseus-sdr
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake .. ${3:-}
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="libusb-1.0-0 libxml2"
BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/analogdevicesinc/libiio.git
cmakebuild libiio 5f5af2e417129ad8f4e05fc5c1b730f0694dca12 -DCMAKE_INSTALL_PREFIX=/usr/local
git clone https://github.com/analogdevicesinc/libad9361-iio.git
cmakebuild libad9361-iio 8ac95f3325c18c2e34cd9cfd49c7b63d69a0a9d2
git clone https://github.com/pothosware/SoapyPlutoSDR.git
cmakebuild SoapyPlutoSDR c88b7f5bac1e5785f212f9a7c6ce8fef64eb719e
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,37 @@
#!/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-1.0-0 libfftw3-3 udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/pa3gsb/Radioberry-2.x
cd Radioberry-2.x/SBC/rpi-4
# latest commit on master as of 2020-08-15
cmakebuild SoapyRadioberrySDR ed8cbfd17b6d1e657a54a677b87479cf28dd77e8
cd ../../..
rm -rf Radioberry-2.x
SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,16 +18,18 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr b5af355b1d833b3c898a61cf1e072b59b0ea3440 cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
git clone https://github.com/pothosware/SoapyRTLSDR.git git clone https://github.com/pothosware/SoapyRTLSDR.git
cmakebuild SoapyRTLSDR 5c5d9503337c6d1c34b496dec6f908aab9478b0f cmakebuild SoapyRTLSDR 8ba18f17d64005e43ff2a4e46611f8c710b05007
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,13 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0.0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr b5af355b1d833b3c898a61cf1e072b59b0ea3440 cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,23 +18,23 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb udev" STATIC_PACKAGES="libusb-1.0.0 udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
ARCH=$(uname -m) ARCH=$(uname -m)
case $ARCH in case $ARCH in
x86_64) x86_64)
BINARY=SDRplay_RSP_API-Linux-2.13.1.run BINARY=SDRplay_RSP_API-Linux-3.07.1.run
;; ;;
armv*) armv*)
BINARY=SDRplay_RSP_API-RPi-2.13.1.run BINARY=SDRplay_RSP_API-ARM32-3.07.2.run
;; ;;
aarch64) aarch64)
BINARY=SDRplay_RSP_API-ARM64-2.13.1.run BINARY=SDRplay_RSP_API-ARM64-3.07.1.run
;; ;;
esac esac
@ -47,7 +48,9 @@ cd ..
rm -rf sdrplay rm -rf sdrplay
rm $BINARY rm $BINARY
git clone https://github.com/pothosware/SoapySDRPlay.git git clone https://github.com/SDRplay/SoapySDRPlay.git
cmakebuild SoapySDRPlay 14ec39e4ff0dab7ae7fdf1afbbd2d28b49b0ffae cmakebuild SoapySDRPlay 1c2728a04db5edf8154d02f5cca87e655152d7c1
apk del .build-deps SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() {
cd $1
if [[ ! -z "${2:-}" ]]; then
git checkout $2
fi
mkdir build
cd build
cmake ..
make
make install
cd ../..
rm -rf $1
}
cd /tmp
STATIC_PACKAGES="avahi-daemon libavahi-client3"
BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev"
apt-get update
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapyRemote.git
cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f
apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -17,13 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="udev" STATIC_PACKAGES="libudev1"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapySDR git clone https://github.com/pothosware/SoapySDR
cmakebuild SoapySDR a489f3dca9d3ccd9b276b95a608ac3ef0299f635 cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695
apk del .build-deps SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
set -euxo pipefail set -euxo pipefail
export MAKEFLAGS="-j4"
function cmakebuild() { function cmakebuild() {
cd $1 cd $1
@ -8,7 +9,7 @@ function cmakebuild() {
fi fi
mkdir build mkdir build
cd build cd build
cmake .. cmake ${CMAKE_ARGS:-} ..
make make
make install make install
cd ../.. cd ../..
@ -17,18 +18,45 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib" STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates libqt5gui5 libqt5sql5 libqt5printsupport5"
BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers" BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev patch xsltproc"
apt-get update
apt-get -y install auto-apt-proxy
apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
apk add --no-cache $STATIC_PACKAGES case `uname -m` in
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES arm*)
PLATFORM=armhf
;;
aarch64*)
PLATFORM=aarch64
;;
x86_64*)
PLATFORM=amd64
;;
esac
pushd /tmp
wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${PLATFORM}.tar.gz
tar xzf s6-overlay-${PLATFORM}.tar.gz -C /
rm s6-overlay-${PLATFORM}.tar.gz
popd
git clone https://github.com/jketterl/js8py.git
pushd js8py
git checkout 888e62be375316882ad2b2ac8e396c3bf857b6fc
python3 setup.py install
popd
rm -rf js8py
git clone https://git.code.sf.net/p/itpp/git itpp git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
git clone https://github.com/jketterl/csdr.git git clone https://github.com/jketterl/csdr.git
cd csdr cd csdr
git checkout 43c36df5dcd92d3bdb322f9d53f99ca0c7c816a4 git checkout c4d8a8a5590898e8c9e94b88b96a2fdc7cd0493a
autoreconf -i
./configure
make make
make install make install
cd .. cd ..
@ -38,28 +66,58 @@ git clone https://github.com/szechyjs/mbelib.git
cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f cmakebuild mbelib 9a04ed5c78176a9965f3d43f7aa1b1f5330e771f
git clone https://github.com/jketterl/digiham.git git clone https://github.com/jketterl/digiham.git
cmakebuild digiham b229990927922e977cecaa9369740790cff5c31e cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f
git clone https://github.com/f4exb/dsd.git git clone https://github.com/f4exb/dsd.git
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7 cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
WSJT_DIR=wsjtx-2.1.2 JS8CALL_VERSION=2.2.0
JS8CALL_DIR=js8call
JS8CALL_TGZ=js8call-${JS8CALL_VERSION}.tgz
wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ}
tar xfz ${JS8CALL_TGZ}
# patch allows us to build against the packaged hamlib
patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
rm /js8call-hamlib.patch
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_HAMLIB_THREE" cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.2.2
WSJT_TGZ=${WSJT_DIR}.tgz WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
tar xvfz $WSJT_TGZ tar xfz ${WSJT_TGZ}
cmakebuild $WSJT_DIR patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch
mv /wsjtx.patch ${WSJT_DIR}
cmakebuild ${WSJT_DIR}
rm ${WSJT_TGZ}
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
cd direwolf cd direwolf
patch -Np1 < /direwolf-1.5.patch # hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need.
make # by setting enable_hamlib we prevent direwolf from linking to it, and it can be stripped at the end of the script.
make enable_hamlib=
make install make install
cd .. cd ..
rm -rf direwolf rm -rf direwolf
git clone https://github.com/drowe67/codec2.git
cd codec2
# latest commit from master as of 2020-07-30
git checkout 1edc22e43f77fbb971429f9f7d4d8d909a7e12cb
mkdir build
cd build
cmake ..
make
make install
install -m 0755 src/freedv_rx /usr/local/bin
cd ../..
rm -rf codec2
git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols git clone https://github.com/hessu/aprs-symbols /opt/aprs-symbols
pushd /opt/aprs-symbols pushd /opt/aprs-symbols
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
popd popd
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

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

@ -12,6 +12,9 @@ fi
if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then if [[ ! -f /etc/openwebrx/bookmarks.json ]] ; then
cp bookmarks.json /etc/openwebrx/ cp bookmarks.json /etc/openwebrx/
fi fi
if [[ ! -f /etc/openwebrx/users.json ]] ; then
cp users.json /etc/openwebrx/
fi
_term() { _term() {

14
htdocs/css/admin.css Normal file
View File

@ -0,0 +1,14 @@
@import url("openwebrx-header.css");
@import url("openwebrx-globals.css");
.buttons {
text-align: right;
}
.row .map-input {
margin: 15px 15px 0;
}
.device {
margin-top: 20px;
}

12
htdocs/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,7 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented on features page */
#webrx-top-photo-clip {
max-height: 67px;
}
h1 { h1 {
text-align: center; text-align: center;
margin: 50px 0; margin: 50px 0;
} }

24
htdocs/css/login.css Normal file
View File

@ -0,0 +1,24 @@
@import url("openwebrx-header.css");
@import url("openwebrx-globals.css");
.login {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 500px;
padding: 20px;
border-radius: 10px;
border: 1px solid #575757;
box-shadow: 0 0 20px #000;
}
.login .btn {
width: 100%;
}
.btn-login {
height: 50px;
}

View File

@ -1,11 +1,6 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented on map page */
#webrx-top-photo-clip {
max-height: 67px;
}
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -2,6 +2,7 @@
{ {
position: relative; position: relative;
z-index:1000; z-index:1000;
background-color: #575757;
} }
#webrx-top-photo #webrx-top-photo
@ -13,7 +14,8 @@
#webrx-top-photo-clip #webrx-top-photo-clip
{ {
min-height: 67px; min-height: 67px;
max-height: 350px; max-height: 67px;
height: 350px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
@ -41,22 +43,24 @@
right: 0; right: 0;
} }
#webrx-tob-container, #webrx-top-container * {
line-height: initial;
box-sizing: initial;
}
#webrx-top-container img {
vertical-align: initial;
}
#webrx-top-logo #webrx-top-logo
{ {
padding: 12px; padding: 12px;
float: left; float: left;
} }
#webrx-ha5kfu-top-logo
{
float: right;
padding: 15px;
}
#webrx-rx-avatar #webrx-rx-avatar
{ {
background-color: rgba(154, 154, 154, .5); background-color: rgba(154, 154, 154, .5);
border-radius: 7px;
float: left; float: left;
margin: 7px; margin: 7px;
@ -107,46 +111,38 @@
cursor:pointer; cursor:pointer;
position: absolute; position: absolute;
left: 470px; left: 470px;
top: 51px; top: 55px;
} }
#openwebrx-rx-details-arrow a #openwebrx-rx-details-arrow a
{ {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 0;
display: block;
} }
#openwebrx-rx-details-arrow-down #openwebrx-main-buttons .button {
{ display: block;
display:none; width: 55px;
}
#openwebrx-main-buttons ul
{
display: table;
margin:0;
}
#openwebrx-main-buttons ul li
{
display: table-cell;
padding-left: 5px;
padding-right: 5px;
cursor:pointer; cursor:pointer;
} }
#openwebrx-main-buttons .button img {
height: 38px;
}
#openwebrx-main-buttons a { #openwebrx-main-buttons a {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
} }
#openwebrx-main-buttons li:hover #openwebrx-main-buttons .button:hover
{ {
background-color: rgba(255, 255, 255, 0.3); background-color: rgba(255, 255, 255, 0.3);
} }
#openwebrx-main-buttons li:active #openwebrx-main-buttons .button:active
{ {
background-color: rgba(255, 255, 255, 0.55); background-color: rgba(255, 255, 255, 0.55);
} }
@ -154,6 +150,9 @@
#openwebrx-main-buttons #openwebrx-main-buttons
{ {
padding: 5px 15px;
display: flex;
list-style: none;
float: right; float: right;
margin:0; margin:0;
color: white; color: white;

View File

@ -150,6 +150,10 @@ input[type=range]:focus::-ms-fill-upper
background: #B6B6B6; background: #B6B6B6;
} }
input[type=range]:disabled {
opacity: 0.5;
}
#webrx-page-container #webrx-page-container
{ {
height: 100%; height: 100%;
@ -311,20 +315,36 @@ input[type=range]:focus::-ms-fill-upper
font-style: normal; font-style: normal;
} }
#webrx-actual-freq .webrx-actual-freq {
{
width: 100%; width: 100%;
text-align: left; text-align: left;
font-size: 16pt;
font-family: 'roboto-mono';
padding: 0; padding: 0;
margin: 0; margin: 0;
line-height:22px; display: flex;
flex-direction: row;
} }
#webrx-mouse-freq .webrx-actual-freq > * {
{ flex: 1;
}
.webrx-actual-freq input {
font-family: 'roboto-mono';
width: 0;
box-sizing: border-box;
border: 0;
padding: 0;
background-color: inherit;
color: inherit;
}
.webrx-actual-freq, .webrx-actual-freq input {
font-size: 16pt;
font-family: 'roboto-mono';
line-height: 22px;
}
.webrx-mouse-freq {
width: 100%; width: 100%;
text-align: left; text-align: left;
font-size: 10pt; font-size: 10pt;
@ -364,6 +384,7 @@ input[type=range]:focus::-ms-fill-upper
border-radius: 15px; border-radius: 15px;
-moz-border-radius: 15px; -moz-border-radius: 15px;
margin: 5.9px; margin: 5.9px;
box-sizing: content-box;
} }
.openwebrx-panel a .openwebrx-panel a
@ -418,9 +439,12 @@ input[type=range]:focus::-ms-fill-upper
margin-right: 0; margin-right: 0;
} }
.openwebrx-button.disabled {
opacity: 0.5;
}
.openwebrx-demodulator-button .openwebrx-demodulator-button
{ {
width: 38px;
height: 19px; height: 19px;
font-size: 12pt; font-size: 12pt;
text-align: center; text-align: center;
@ -428,6 +452,10 @@ input[type=range]:focus::-ms-fill-upper
margin-right: 5px; margin-right: 5px;
} }
.openwebrx-demodulator-button.same-mod {
color: #FFC;
}
.openwebrx-square-button img .openwebrx-square-button img
{ {
height: 27px; height: 27px;
@ -591,6 +619,31 @@ img.openwebrx-mirror-img
padding-top: 0; padding-top: 0;
} }
.openwebrx-modes-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: -5px -5px 0 0;
}
.openwebrx-modes-grid .openwebrx-demodulator-button {
margin: 0;
white-space: nowrap;
flex: 1 0 38px;
margin: 5px 5px 0 0;
}
@supports(gap: 5px) {
.openwebrx-modes-grid {
margin: 0;
gap: 5px;
}
.openwebrx-modes-grid .openwebrx-demodulator-button {
margin: 0;
}
}
#openwebrx-smeter-outer #openwebrx-smeter-outer
{ {
border-color: #888; border-color: #888;
@ -706,8 +759,7 @@ img.openwebrx-mirror-img
color: White; color: White;
} }
#openwebrx-secondary-demod-listbox .openwebrx-secondary-demod-listbox {
{
width: 173px; width: 173px;
height: 27px; height: 27px;
padding-left:3px; padding-left:3px;
@ -906,37 +958,23 @@ img.openwebrx-mirror-img
display: inline-block; display: inline-block;
} }
#openwebrx-panel-wsjt-message, .openwebrx-message-panel {
#openwebrx-panel-packet-message,
#openwebrx-panel-pocsag-message
{
height: 180px; height: 180px;
} }
#openwebrx-panel-wsjt-message tbody, .openwebrx-message-panel tbody {
#openwebrx-panel-packet-message tbody,
#openwebrx-panel-pocsag-message tbody
{
display: block; display: block;
overflow: auto; overflow: auto;
height: 150px; height: 150px;
width: 100%; width: 100%;
} }
#openwebrx-panel-wsjt-message thead tr, .openwebrx-message-panel thead tr {
#openwebrx-panel-packet-message thead tr,
#openwebrx-panel-pocsag-message thead tr
{
display: block; display: block;
} }
#openwebrx-panel-wsjt-message th, .openwebrx-message-panel th,
#openwebrx-panel-wsjt-message td, .openwebrx-message-panel td {
#openwebrx-panel-packet-message th,
#openwebrx-panel-packet-message td,
#openwebrx-panel-pocsag-message th,
#openwebrx-panel-pocsag-message td
{
width: 50px; width: 50px;
text-align: left; text-align: left;
padding: 1px 3px; padding: 1px 3px;
@ -955,6 +993,31 @@ img.openwebrx-mirror-img
width: 70px; width: 70px;
} }
#openwebrx-panel-js8-message .message {
width: 465px;
max-width: 465px;
}
#openwebrx-panel-js8-message td.message {
white-space: nowrap;
overflow: hidden;
display: flex;
flex-direction: row-reverse;
}
#openwebrx-panel-js8-message .message div {
flex: 1;
}
#openwebrx-panel-js8-message .decimal {
text-align: right;
width: 35px;
}
#openwebrx-panel-js8-message .decimal.freq {
width: 70px;
}
#openwebrx-panel-packet-message .message { #openwebrx-panel-packet-message .message {
width: 410px; width: 410px;
max-width: 410px; max-width: 410px;
@ -1061,13 +1124,15 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft8"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="wspr"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt65"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel
{ {
display: none; display: none;
} }
@ -1078,7 +1143,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;

View File

@ -1,10 +1,12 @@
<HTML><HEAD> <HTML><HEAD>
<TITLE>OpenWebRX Feature report</TITLE> <TITLE>OpenWebRX Feature report</TITLE>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<link rel="stylesheet" href="static/css/features.css"> <link rel="stylesheet" href="static/css/features.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<script src="static/features.js"></script> <script src="static/features.js"></script>
</HEAD><BODY> </HEAD><BODY>
${header} ${header}

View File

@ -0,0 +1,20 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="https://unpkg.com/location-picker/dist/location-picker.min.js"></script>
<script src="compiled/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="col-12">
<h1>General settings</h1>
</div>
${sections}
</div>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,25 +1,23 @@
<div id="webrx-top-container"> <div id="webrx-top-container">
<div id="webrx-top-photo-clip"> <div id="webrx-top-photo-clip">
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/> <img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo" alt="Receiver panorama"/>
<div id="webrx-top-bar" class="webrx-top-bar-parts"> <div id="webrx-top-bar" class="webrx-top-bar-parts">
<a href="https://sdr.hu/openwebrx" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a> <a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a> <img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
<div id="webrx-rx-texts"> <div id="webrx-rx-texts">
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div> <div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div> <div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
</div> </div>
<div id="openwebrx-rx-details-arrow"> <div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a> <a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a>
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a> <a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
</div> </div>
<section id="openwebrx-main-buttons"> <section id="openwebrx-main-buttons">
<ul> <div class="button" data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" alt="Status"/><br/>Status</div>
<li data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</li> <div class="button" data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" alt="Log"/><br/>Log</div>
<li data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" /><br/>Log</li> <div class="button" data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" alt="Receiver"/><br/>Receiver</div>
<li data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</li> <a class="button" href="map" target="openwebrx-map"><img src="static/gfx/openwebrx-panel-map.png" alt="Map"/><br/>Map</a>
<li><a href="map" target="_blank"><img src="static/gfx/openwebrx-panel-map.png" /><br/>Map</a></li> ${settingslink}
</ul>
</section> </section>
</div> </div>
<div id="webrx-rx-photo-title"></div> <div id="webrx-rx-photo-title"></div>

View File

@ -24,14 +24,7 @@
<head> <head>
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title> <title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/openwebrx.js"></script> <script src="compiled/receiver.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/jquery.nanoscroller.js"></script>
<script src="static/lib/BookmarkBar.js"></script>
<script src="static/lib/AudioEngine.js"></script>
<script src="static/lib/ProgressBar.js"></script>
<script src="static/lib/Measurement.js"></script>
<script src="static/lib/FrequencyDisplay.js"></script>
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" /> <link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" /> <link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
<meta charset="utf-8"> <meta charset="utf-8">
@ -67,7 +60,7 @@
</div> </div>
</div> </div>
</div> </div>
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
<thead><tr> <thead><tr>
<th>UTC</th> <th>UTC</th>
<th class="decimal">dB</th> <th class="decimal">dB</th>
@ -77,7 +70,15 @@
</tr></thead> </tr></thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<table class="openwebrx-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message">
<thead><tr>
<th>UTC</th>
<th class="decimal freq">Freq</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
<thead><tr> <thead><tr>
<th>UTC</th> <th>UTC</th>
<th class="callsign">Callsign</th> <th class="callsign">Callsign</th>
@ -86,7 +87,7 @@
</tr></thead> </tr></thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<table class="openwebrx-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
<thead><tr> <thead><tr>
<th class="address">Address</th> <th class="address">Address</th>
<th class="message">Message</th> <th class="message">Message</th>
@ -126,26 +127,30 @@
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll"> <div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content"> <div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</div> <div id="openwebrx-client-log-title">OpenWebRX client log</div>
<div>Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a></div> <div>
Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a> |
<a href="https://www.openwebrx.de" target="_blank">OpenWebRX homepage</a>
</div>
<div>Support and information: <a href="https://groups.io/g/openwebrx" target="_blank">Groups.io Mailinglist</a></div>
<div id="openwebrx-debugdiv"></div> <div id="openwebrx-debugdiv"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true"> <div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true">
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer" data-type="audiobuffer"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-output" data-type="audiooutput"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed" data-type="audiospeed"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-network-speed" data-type="networkspeed"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu" data-type="cpu"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-clients" data-type="clients"></div>
</div> </div>
</div> </div>
<div id="openwebrx-panels-container-right"> <div id="openwebrx-panels-container-right">
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;"> <div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;">
<div class="openwebrx-panel-line frequencies-container"> <div class="openwebrx-panel-line frequencies-container">
<div class="frequencies"> <div class="frequencies">
<div id="webrx-actual-freq">---.--- MHz</div> <div class="webrx-actual-freq"></div>
<div id="webrx-mouse-freq">---.--- MHz</div> <div class="webrx-mouse-freq"></div>
</div> </div>
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark..."> <div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
<img src="static/gfx/openwebrx-bookmark.png"> <img src="static/gfx/openwebrx-bookmark.png">
@ -155,47 +160,7 @@
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();"> <select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
</select> </select>
</div> </div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line"> <div class="openwebrx-modes openwebrx-panel-line"></div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
onclick="demodulator_analog_replace('nfm');">FM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
onclick="demodulator_analog_replace('am');">AM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
onclick="demodulator_analog_replace('lsb');">LSB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
onclick="demodulator_analog_replace('usb');">USB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
onclick="demodulator_analog_replace('cw');">CW</div>
</div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('dmr');">DMR</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('dstar');">DStar</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('nxdn');">NXDN</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('ysf');">YSF</div>
</div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option>
<option value="bpsk31">BPSK31</option>
<option value="bpsk63">BPSK63</option>
<option value="ft8" data-feature="wsjt-x">FT8</option>
<option value="wspr" data-feature="wsjt-x">WSPR</option>
<option value="jt65" data-feature="wsjt-x">JT65</option>
<option value="jt9" data-feature="wsjt-x">JT9</option>
<option value="ft4" data-feature="wsjt-x">FT4</option>
<option value="packet" data-feature="packet">Packet</option>
<option value="pocsag" data-feature="pocsag">Pocsag</option>
</select>
</div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div> <div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()"> <input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
@ -203,8 +168,8 @@
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()"> <input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div> <div title="Auto-set squelch level" class="openwebrx-squelch-default openwebrx-button"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()"> <input title="Squelch" class="openwebrx-squelch-slider openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1">
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div> <div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()"> <input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
</div> </div>
@ -249,17 +214,7 @@
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="modulation">Modulation:</label> <label for="modulation">Modulation:</label>
<select name="modulation" id="modulation"> <select name="modulation" id="modulation"></select>
<option value="nfm">FM</option>
<option value="am">AM</option>
<option value="usb">USB</option>
<option value="lsb">LSB</option>
<option value="cw">CW</option>
<option value="dmr">DMR</option>
<option value="dstar">D-Star</option>
<option value="nxdn">NXDN</option>
<option value="ysf">YSF</option>
</select>
</div> </div>
<div class="buttons"> <div class="buttons">
<div class="openwebrx-button" data-action="cancel">Cancel</div> <div class="openwebrx-button" data-action="cancel">Cancel</div>

View File

@ -10,125 +10,151 @@ function AudioEngine(maxBufferLength, audioReporter) {
if (!ctx) { if (!ctx) {
return; return;
} }
this.audioContext = new ctx();
this.allowed = this.audioContext.state === 'running'; this.onStartCallbacks = [];
this.started = false; this.started = false;
this.audioContext = new ctx();
var me = this;
this.audioContext.onstatechange = function() {
if (me.audioContext.state !== 'running') return;
me._start();
}
this.audioCodec = new ImaAdpcmCodec(); this.audioCodec = new ImaAdpcmCodec();
this.compression = 'none'; this.compression = 'none';
this.setupResampling(); this.setupResampling();
this.resampler = new Interpolator(this.resamplingFactor); this.resampler = new Interpolator(this.resamplingFactor);
this.hdResampler = new Interpolator(this.hdResamplingFactor);
this.maxBufferSize = maxBufferLength * this.getSampleRate(); this.maxBufferSize = maxBufferLength * this.getSampleRate();
} }
AudioEngine.prototype.start = function(callback) { AudioEngine.prototype.resume = function(){
this.audioContext.resume();
}
AudioEngine.prototype._start = function() {
var me = this; var me = this;
if (me.resamplingFactor === 0) return; //if failed to find a valid resampling factor...
// if failed to find a valid resampling factor...
if (me.resamplingFactor === 0) {
return;
}
// been started before?
if (me.started) { if (me.started) {
if (callback) callback(false);
return; return;
} }
me.audioContext.resume().then(function(){ // are we allowed to play audio?
me.allowed = me.audioContext.state === 'running'; if (!me.isAllowed()) {
if (!me.allowed) { return;
if (callback) callback(false); }
return; me.started = true;
}
me.started = true;
me.gainNode = me.audioContext.createGain(); me.gainNode = me.audioContext.createGain();
me.gainNode.connect(me.audioContext.destination); me.gainNode.connect(me.audioContext.destination);
if (useAudioWorklets && me.audioContext.audioWorklet) { if (useAudioWorklets && me.audioContext.audioWorklet) {
me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){
me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', { me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', {
numberOfInputs: 0, numberOfInputs: 0,
numberOfOutputs: 1, numberOfOutputs: 1,
outputChannelCount: [1], outputChannelCount: [1],
processorOptions: { processorOptions: {
maxBufferSize: me.maxBufferSize 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); me.audioNode.connect(me.gainNode);
if (callback) callback(true, 'ScriptProcessorNode'); 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();
me.workletType = '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];
}
} }
setInterval(me.reportStats.bind(me), 1000); 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);
me.workletType = 'ScriptProcessorNode';
}
setInterval(me.reportStats.bind(me), 1000);
var callbacks = this.onStartCallbacks;
this.onStartCallbacks = false;
callbacks.forEach(function(c) { c(me.workletType); });
};
AudioEngine.prototype.onStart = function(callback) {
if (this.onStartCallbacks) {
this.onStartCallbacks.push(callback);
} else {
callback();
}
}; };
AudioEngine.prototype.isAllowed = function() { AudioEngine.prototype.isAllowed = function() {
return this.allowed; return this.audioContext.state === 'running';
}; };
AudioEngine.prototype.reportStats = function() { AudioEngine.prototype.reportStats = function() {
@ -165,35 +191,57 @@ AudioEngine.prototype.resetStats = function() {
}; };
AudioEngine.prototype.setupResampling = function() { //both at the server and the client AudioEngine.prototype.setupResampling = function() { //both at the server and the client
var output_range_max = 12000; var audio_params = this.findRate(8000, 12000);
var output_range_min = 8000; if (!audio_params) {
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);
} else {
this.resamplingFactor = audio_params.resamplingFactor;
this.outputRate = audio_params.outputRate;
}
var hd_audio_params = this.findRate(36000, 48000);
if (!hd_audio_params) {
this.hdResamplingFactor = 0;
this.hdOutputRate = 0;
divlog('Your audio card sampling rate (' + targetRate + ') is not supported for HD audio<br />Please change your operating system default settings in order to fix this.', 1);
} else {
this.hdResamplingFactor = hd_audio_params.resamplingFactor;
this.hdOutputRate = hd_audio_params.outputRate;
}
};
AudioEngine.prototype.findRate = function(low, high) {
var targetRate = this.audioContext.sampleRate; var targetRate = this.audioContext.sampleRate;
var i = 1; var i = 1;
while (true) { while (true) {
var audio_server_output_rate = Math.floor(targetRate / i); var audio_server_output_rate = Math.floor(targetRate / i);
if (audio_server_output_rate < output_range_min) { if (audio_server_output_rate < low) {
this.resamplingFactor = 0; return;
this.outputRate = 0; } else if (audio_server_output_rate >= low && audio_server_output_rate <= high) {
divlog('Your audio card sampling rate (' + targetRate + ') is not supported.<br />Please change your operating system default settings in order to fix this.', 1); return {
break; resamplingFactor: i,
} else if (audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) { outputRate: audio_server_output_rate
this.resamplingFactor = i; }
this.outputRate = audio_server_output_rate;
break; //okay, we're done
} }
i++; i++;
} };
}; }
AudioEngine.prototype.getOutputRate = function() { AudioEngine.prototype.getOutputRate = function() {
return this.outputRate; return this.outputRate;
}; };
AudioEngine.prototype.getHdOutputRate = function() {
return this.hdOutputRate;
}
AudioEngine.prototype.getSampleRate = function() { AudioEngine.prototype.getSampleRate = function() {
return this.audioContext.sampleRate; return this.audioContext.sampleRate;
}; };
AudioEngine.prototype.pushAudio = function(data) { AudioEngine.prototype.processAudio = function(data, resampler) {
if (!this.audioNode) return; if (!this.audioNode) return;
this.audioBytes.add(data.byteLength); this.audioBytes.add(data.byteLength);
var buffer; var buffer;
@ -203,7 +251,7 @@ AudioEngine.prototype.pushAudio = function(data) {
} else { } else {
buffer = new Int16Array(data); buffer = new Int16Array(data);
} }
buffer = this.resampler.process(buffer); buffer = resampler.process(buffer);
if (this.audioNode.port) { if (this.audioNode.port) {
// AudioWorklets supported // AudioWorklets supported
this.audioNode.port.postMessage(buffer); this.audioNode.port.postMessage(buffer);
@ -213,8 +261,16 @@ AudioEngine.prototype.pushAudio = function(data) {
this.audioBuffers.push(buffer); this.audioBuffers.push(buffer);
} }
} }
}
AudioEngine.prototype.pushAudio = function(data) {
this.processAudio(data, this.resampler);
}; };
AudioEngine.prototype.pushHdAudio = function(data) {
this.processAudio(data, this.hdResampler);
}
AudioEngine.prototype.setCompression = function(compression) { AudioEngine.prototype.setCompression = function(compression) {
this.compression = compression; this.compression = compression;
}; };

View File

@ -8,12 +8,10 @@ function BookmarkBar() {
var $bookmark = $(e.target).closest('.bookmark'); var $bookmark = $(e.target).closest('.bookmark');
me.$container.find('.bookmark').removeClass('selected'); me.$container.find('.bookmark').removeClass('selected');
var b = $bookmark.data(); var b = $bookmark.data();
if (!b || !b.frequency || (!b.modulation && !b.digital_modulation)) return; if (!b || !b.frequency || !b.modulation) return;
demodulators[0].set_offset_frequency(b.frequency - center_freq); me.getDemodulator().set_offset_frequency(b.frequency - center_freq);
if (b.modulation) { if (b.modulation) {
demodulator_analog_replace(b.modulation); me.getDemodulatorPanel().setMode(b.modulation);
} else if (b.digital_modulation) {
demodulator_digital_replace(b.digital_modulation);
} }
$bookmark.addClass('selected'); $bookmark.addClass('selected');
}); });
@ -104,40 +102,26 @@ BookmarkBar.prototype.render = function(){
}; };
BookmarkBar.prototype.showEditDialog = function(bookmark) { BookmarkBar.prototype.showEditDialog = function(bookmark) {
var $form = this.$dialog.find("form");
if (!bookmark) { if (!bookmark) {
bookmark = { bookmark = {
name: "", name: "",
frequency: center_freq + demodulators[0].offset_frequency, frequency: center_freq + this.getDemodulator().get_offset_frequency(),
modulation: demodulators[0].subtype modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation()
} }
} }
['name', 'frequency', 'modulation'].forEach(function(key){ this.$dialog.bookmarkDialog().setValues(bookmark);
$form.find('#' + key).val(bookmark[key]);
});
this.$dialog.data('id', bookmark.id);
this.$dialog.show(); this.$dialog.show();
this.$dialog.find('#name').focus(); this.$dialog.find('#name').focus();
}; };
BookmarkBar.prototype.storeBookmark = function() { BookmarkBar.prototype.storeBookmark = function() {
var me = this; var me = this;
var bookmark = {}; var bookmark = this.$dialog.bookmarkDialog().getValues();
var valid = true; if (!bookmark) return;
['name', 'frequency', 'modulation'].forEach(function(key){
var $input = me.$dialog.find('#' + key);
valid = valid && $input[0].checkValidity();
bookmark[key] = $input.val();
});
if (!valid) {
me.$dialog.find("form :submit").click();
return;
}
bookmark.frequency = Number(bookmark.frequency); bookmark.frequency = Number(bookmark.frequency);
var bookmarks = me.localBookmarks.getBookmarks(); var bookmarks = me.localBookmarks.getBookmarks();
bookmark.id = me.$dialog.data('id');
if (!bookmark.id) { if (!bookmark.id) {
if (bookmarks.length) { if (bookmarks.length) {
bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; })); bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; }));
@ -154,6 +138,14 @@ BookmarkBar.prototype.storeBookmark = function() {
me.$dialog.hide(); me.$dialog.hide();
}; };
BookmarkBar.prototype.getDemodulatorPanel = function() {
return $('#openwebrx-panel-receiver').demodulatorPanel();
};
BookmarkBar.prototype.getDemodulator = function() {
return this.getDemodulatorPanel().getDemodulator();
};
BookmarkLocalStorage = function(){ BookmarkLocalStorage = function(){
}; };
@ -171,7 +163,3 @@ BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
bookmarks = bookmarks.filter(function(b) { return b.id !== data; }); bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
this.setBookmarks(bookmarks); this.setBookmarks(bookmarks);
}; };

View File

@ -0,0 +1,36 @@
$.fn.bookmarkDialog = function() {
var $el = this;
return {
setModes: function(modes) {
$el.find('#modulation').html(modes.filter(function(m){
return m.isAvailable();
}).map(function(m) {
return '<option value="' + m.modulation + '">' + m.name + '</option>';
}).join(''));
return this;
},
setValues: function(bookmark) {
var $form = $el.find('form');
['name', 'frequency', 'modulation'].forEach(function(key){
$form.find('#' + key).val(bookmark[key]);
});
$el.data('id', bookmark.id || false);
return this;
},
getValues: function() {
var bookmark = {};
var valid = true;
['name', 'frequency', 'modulation'].forEach(function(key){
var $input = $el.find('#' + key);
valid = valid && $input[0].checkValidity();
bookmark[key] = $input.val();
});
if (!valid) {
$el.find("form :submit").click();
return;
}
bookmark.id = $el.data('id');
return bookmark;
}
}
}

358
htdocs/lib/Demodulator.js Normal file
View File

@ -0,0 +1,358 @@
function Filter(demodulator) {
this.demodulator = demodulator;
this.min_passband = 100;
}
Filter.prototype.getLimits = function() {
var max_bw;
if (this.demodulator.get_secondary_demod() === 'pocsag') {
max_bw = 12500;
} else if (this.demodulator.get_modulation() === 'wfm') {
max_bw = 80000;
} else {
max_bw = (audioEngine.getOutputRate() / 2) - 1;
}
return {
high: max_bw,
low: -max_bw
};
};
function Envelope(demodulator) {
this.demodulator = demodulator;
this.dragged_range = Demodulator.draggable_ranges.none;
}
Envelope.prototype.draw = function(visible_range){
this.visible_range = visible_range;
var line = center_freq + this.demodulator.offset_frequency;
// ____
// Draws a standard filter envelope like this: _/ \_
// Parameters are given in offset frequency (Hz).
// Envelope is drawn on the scale canvas.
// A "drag range" object is returned, containing information about the draggable areas of the envelope
// (beginning, ending and the line showing the offset frequency).
var env_bounding_line_w = 5; //
var env_att_w = 5; // _______ ___env_h2 in px ___|_____
var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_
var env_h2 = 5; // |||env_att_line_w |_env_lineplus
var env_lineplus = 1; // ||env_bounding_line_w
var env_line_click_area = 6;
//range=get_visible_freq_range();
var from = center_freq + this.demodulator.offset_frequency + this.demodulator.low_cut;
var from_px = scale_px_from_freq(from, range);
var to = center_freq + this.demodulator.offset_frequency + this.demodulator.high_cut;
var to_px = scale_px_from_freq(to, range);
if (to_px < from_px) /* swap'em */ {
var temp_px = to_px;
to_px = from_px;
from_px = temp_px;
}
from_px -= (env_att_w + env_bounding_line_w);
to_px += (env_att_w + env_bounding_line_w);
// do drawing:
scale_ctx.lineWidth = 3;
var color = this.color || '#ffff00'; // yellow
scale_ctx.strokeStyle = color;
scale_ctx.fillStyle = color;
var drag_ranges = {envelope_on_screen: false, line_on_screen: false};
if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen?
{
drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w};
drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px};
drag_ranges.whole_envelope = {x1: from_px, x2: to_px};
drag_ranges.envelope_on_screen = true;
scale_ctx.beginPath();
scale_ctx.moveTo(from_px, env_h1);
scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1);
scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2);
scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
scale_ctx.lineTo(to_px, env_h1);
scale_ctx.globalAlpha = 0.3;
scale_ctx.fill();
scale_ctx.globalAlpha = 1;
scale_ctx.stroke();
}
if (typeof line !== "undefined") // out of screen?
{
var line_px = scale_px_from_freq(line, range);
if (!(line_px < 0 || line_px > window.innerWidth)) {
drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2};
drag_ranges.line_on_screen = true;
scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
scale_ctx.stroke();
}
}
this.drag_ranges = drag_ranges;
};
Envelope.prototype.drag_start = function(x, key_modifiers){
this.key_modifiers = key_modifiers;
this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers);
this.drag_origin = {
x: x,
low_cut: this.demodulator.low_cut,
high_cut: this.demodulator.high_cut,
offset_frequency: this.demodulator.offset_frequency
};
return this.dragged_range !== Demodulator.draggable_ranges.none;
};
Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw().
var in_range = function (x, range) {
return range.x1 <= x && range.x2 >= x;
};
var dr = Demodulator.draggable_ranges;
if (key_modifiers.shiftKey) {
//Check first: shift + center drag emulates BFO knob
if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo;
//Check second: shift + envelope drag emulates PBF knob
if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs;
}
if (drag_ranges.envelope_on_screen) {
// For low and high cut:
if (in_range(x, drag_ranges.beginning)) return dr.beginning;
if (in_range(x, drag_ranges.ending)) return dr.ending;
// Last priority: having clicked anything else on the envelope, without holding the shift key
if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else;
}
return dr.none; //User doesn't drag the envelope for this demodulator
};
Envelope.prototype.drag_move = function(x) {
var dr = Demodulator.draggable_ranges;
var new_value;
if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all
var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x));
//dragging the line in the middle of the filter envelope while holding Shift does emulate
//the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged
//Filter passband moves in the opposite direction than dragged, hence the minus below.
var minus = (this.dragged_range === dr.bfo) ? -1 : 1;
//dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob
//(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset
//frequency.
if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
//we don't let low_cut go beyond its limits
if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.demodulator.filter.getLimits().low) return true;
//nor the filter passband be too small
if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) return true;
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
if (new_value >= this.demodulator.high_cut) return true;
this.demodulator.setLowCut(new_value);
}
if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
//we don't let high_cut go beyond its limits
if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.demodulator.filter.getLimits().high) return true;
//nor the filter passband be too small
if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) return true;
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
if (new_value <= this.demodulator.low_cut) return true;
this.demodulator.setHighCut(new_value);
}
if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) {
//when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it)
new_value = this.drag_origin.offset_frequency + freq_change;
if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-)
this.demodulator.set_offset_frequency(new_value);
}
//now do the actual modifications:
//mkenvelopes(this.visible_range);
//this.demodulator.set();
return true;
};
Envelope.prototype.drag_end = function(){
var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset
this.dragged_range = Demodulator.draggable_ranges.none;
return to_return;
};
//******* class Demodulator_default_analog *******
// This can be used as a base for basic audio demodulators.
// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB
function Demodulator(offset_frequency, modulation) {
this.offset_frequency = offset_frequency;
this.envelope = new Envelope(this);
this.color = Demodulator.get_next_color();
this.modulation = modulation;
this.filter = new Filter(this);
this.squelch_level = -150;
this.dmr_filter = 3;
this.started = false;
this.state = {};
this.secondary_demod = false;
var mode = Modes.findByModulation(modulation);
if (mode) {
this.low_cut = mode.bandpass.low_cut;
this.high_cut = mode.bandpass.high_cut;
}
this.listeners = {
"frequencychange": [],
"squelchchange": []
};
}
//ranges on filter envelope that can be dragged:
Demodulator.draggable_ranges = {
none: 0,
beginning: 1 /*from*/,
ending: 2 /*to*/,
anything_else: 3,
bfo: 4 /*line (while holding shift)*/,
pbs: 5
}; //to which parameter these correspond in envelope_draw()
Demodulator.color_index = 0;
Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"];
Demodulator.get_next_color = function() {
if (this.color_index >= this.colors.length) this.color_index = 0;
return (this.colors[this.color_index++]);
}
Demodulator.prototype.on = function(event, handler) {
this.listeners[event].push(handler);
};
Demodulator.prototype.emit = function(event, params) {
this.listeners[event].forEach(function(fn) {
fn(params);
});
};
Demodulator.prototype.set_offset_frequency = function(to_what) {
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
to_what = Math.round(to_what);
if (this.offset_frequency === to_what) {
return;
}
this.offset_frequency = to_what;
this.set();
this.emit("frequencychange", to_what);
mkenvelopes(get_visible_freq_range());
};
Demodulator.prototype.get_offset_frequency = function() {
return this.offset_frequency;
};
Demodulator.prototype.get_modulation = function() {
return this.modulation;
};
Demodulator.prototype.start = function() {
this.started = true;
this.set();
ws.send(JSON.stringify({
"type": "dspcontrol",
"action": "start"
}));
};
// TODO check if this is actually used
Demodulator.prototype.stop = function() {
};
Demodulator.prototype.send = function(params) {
ws.send(JSON.stringify({"type": "dspcontrol", "params": params}));
}
Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server
if (!this.started) return;
var params = {
"low_cut": this.low_cut,
"high_cut": this.high_cut,
"offset_freq": this.offset_frequency,
"mod": this.modulation,
"dmr_filter": this.dmr_filter,
"squelch_level": this.squelch_level,
"secondary_mod": this.secondary_demod,
"secondary_offset_freq": this.secondary_offset_freq
};
var to_send = {};
for (var key in params) {
if (!(key in this.state) || params[key] !== this.state[key]) {
to_send[key] = params[key];
}
}
if (Object.keys(to_send).length > 0) {
this.send(to_send);
for (var key in to_send) {
this.state[key] = to_send[key];
}
}
mkenvelopes(get_visible_freq_range());
};
Demodulator.prototype.setSquelch = function(squelch) {
if (this.squelch_level == squelch) {
return;
}
this.squelch_level = squelch;
this.set();
this.emit("squelchchange", squelch);
};
Demodulator.prototype.getSquelch = function() {
return this.squelch_level;
};
Demodulator.prototype.setDmrFilter = function(dmr_filter) {
this.dmr_filter = dmr_filter;
this.set();
};
Demodulator.prototype.setBandpass = function(bandpass) {
this.bandpass = bandpass;
this.low_cut = bandpass.low_cut;
this.high_cut = bandpass.high_cut;
this.set();
};
Demodulator.prototype.setLowCut = function(low_cut) {
this.low_cut = low_cut;
this.set();
};
Demodulator.prototype.setHighCut = function(high_cut) {
this.high_cut = high_cut;
this.set();
};
Demodulator.prototype.getBandpass = function() {
return {
low_cut: this.low_cut,
high_cut: this.high_cut
};
};
Demodulator.prototype.set_secondary_demod = function(secondary_demod) {
if (this.secondary_demod === secondary_demod) {
return;
}
this.secondary_demod = secondary_demod;
this.set();
};
Demodulator.prototype.get_secondary_demod = function() {
return this.secondary_demod;
};
Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) {
if (this.secondary_offset_freq === secondary_offset) {
return;
}
this.secondary_offset_freq = secondary_offset;
this.set();
};

View File

@ -0,0 +1,323 @@
function DemodulatorPanel(el) {
var self = this;
self.el = el;
self.demodulator = null;
self.mode = null;
var displayEl = el.find('.webrx-actual-freq')
this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
displayEl.on('frequencychange', function(event, freq) {
self.getDemodulator().set_offset_frequency(freq - self.center_freq);
});
Modes.registerModePanel(this);
el.on('click', '.openwebrx-demodulator-button', function() {
var modulation = $(this).data('modulation');
if (modulation) {
self.setMode(modulation);
} else {
self.disableDigiMode();
}
});
el.on('change', '.openwebrx-secondary-demod-listbox', function() {
var value = $(this).val();
if (value === 'none') {
self.disableDigiMode();
} else {
self.setMode(value);
}
});
el.on('click', '.openwebrx-squelch-default', function() {
if (!self.squelchAvailable()) return;
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + 10);
self.updateSquelch();
});
el.on('change', '.openwebrx-squelch-slider', function() {
self.updateSquelch();
});
window.addEventListener('hashchange', function() {
self.onHashChange();
});
};
DemodulatorPanel.prototype.render = function() {
var available = Modes.getModes().filter(function(m){ return m.isAvailable(); });
var normalModes = available.filter(function(m){ return m.type === 'analog'; });
var digiModes = available.filter(function(m){ return m.type === 'digimode'; });
var html = []
var buttons = normalModes.map(function(m){
return $(
'<div ' +
'class="openwebrx-button openwebrx-demodulator-button" ' +
'data-modulation="' + m.modulation + '" ' +
'id="openwebrx-button-' + m.modulation + '" r' +
'>' + m.name + '</div>'
);
});
var $modegrid = $('<div class="openwebrx-modes-grid"></div>');
$modegrid.append.apply($modegrid, buttons);
html.push($modegrid);
html.push($(
'<div class="openwebrx-panel-line openwebrx-panel-flex-line">' +
'<div class="openwebrx-button openwebrx-demodulator-button openwebrx-button-dig">DIG</div>' +
'<select class="openwebrx-secondary-demod-listbox">' +
'<option value="none"></option>' +
digiModes.map(function(m){
return '<option value="' + m.modulation + '">' + m.name + '</option>';
}).join('') +
'</select>' +
'</div>'
));
this.el.find(".openwebrx-modes").html(html);
};
DemodulatorPanel.prototype.setMode = function(requestedModulation) {
var mode = Modes.findByModulation(requestedModulation);
if (!mode) {
return;
}
if (this.mode === mode) {
return;
}
if (!mode.isAvailable()) {
divlog('Modulation "' + mode.name + '" not supported. Please check requirements', true);
return;
}
if (mode.type === 'digimode') {
modulation = mode.underlying[0];
} else {
if (this.mode && this.mode.type === 'digimode' && this.mode.underlying.indexOf(requestedModulation) >= 0) {
// keep the mode, just switch underlying modulation
mode = this.mode;
modulation = requestedModulation;
} else {
modulation = mode.modulation;
}
}
var current = this.collectParams();
if (this.demodulator) {
current.offset_frequency = this.demodulator.get_offset_frequency();
current.squelch_level = this.demodulator.getSquelch();
}
this.stopDemodulator();
this.demodulator = new Demodulator(current.offset_frequency, modulation);
this.demodulator.setSquelch(current.squelch_level);
var self = this;
var updateFrequency = function(freq) {
self.tuneableFrequencyDisplay.setFrequency(self.center_freq + freq);
self.updateHash();
};
this.demodulator.on("frequencychange", updateFrequency);
updateFrequency(this.demodulator.get_offset_frequency());
var updateSquelch = function(squelch) {
self.el.find('.openwebrx-squelch-slider').val(squelch);
self.updateHash();
};
this.demodulator.on('squelchchange', updateSquelch);
updateSquelch(this.demodulator.getSquelch());
if (mode.type === 'digimode') {
this.demodulator.set_secondary_demod(mode.modulation);
if (mode.bandpass) {
this.demodulator.setBandpass(mode.bandpass);
}
} else {
this.demodulator.set_secondary_demod(false);
}
this.demodulator.start();
this.mode = mode;
this.updateButtons();
this.updatePanels();
this.updateHash();
};
DemodulatorPanel.prototype.disableDigiMode = function() {
// just a little trick to get out of the digimode
delete this.mode;
this.setMode(this.getDemodulator().get_modulation());
};
DemodulatorPanel.prototype.updatePanels = function() {
var modulation = this.getDemodulator().get_secondary_demod();
$('#openwebrx-panel-digimodes').attr('data-mode', modulation);
toggle_panel("openwebrx-panel-digimodes", !!modulation);
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(modulation) >= 0);
toggle_panel("openwebrx-panel-js8-message", modulation == "js8");
toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
modulation = this.getDemodulator().get_modulation();
var showing = 'openwebrx-panel-metadata-' + modulation;
$(".openwebrx-meta-panel").each(function (_, p) {
toggle_panel(p.id, p.id === showing);
});
clear_metadata();
};
DemodulatorPanel.prototype.getDemodulator = function() {
return this.demodulator;
};
DemodulatorPanel.prototype.collectParams = function() {
var defaults = {
offset_frequency: 0,
squelch_level: -150,
mod: 'nfm'
}
return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.startDemodulator = function() {
if (!Modes.initComplete()) return;
var params = this.collectParams();
this._apply(params);
};
DemodulatorPanel.prototype.stopDemodulator = function() {
if (!this.demodulator) {
return;
}
this.demodulator.stop();
this.demodulator = null;
this.mode = null;
}
DemodulatorPanel.prototype._apply = function(params) {
this.setMode(params.mod);
this.getDemodulator().set_offset_frequency(params.offset_frequency);
this.getDemodulator().setSquelch(params.squelch_level);
this.updateButtons();
};
DemodulatorPanel.prototype.setInitialParams = function(params) {
this.initialParams = params;
};
DemodulatorPanel.prototype.onHashChange = function() {
this._apply(this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.transformHashParams = function(params) {
var ret = {
mod: params.secondary_mod || params.mod
};
if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency;
if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql);
return ret;
};
DemodulatorPanel.prototype.squelchAvailable = function () {
return this.mode && this.mode.squelch;
}
DemodulatorPanel.prototype.updateButtons = function() {
var $buttons = this.el.find(".openwebrx-demodulator-button");
$buttons.removeClass("highlighted").removeClass('same-mod');
var demod = this.getDemodulator()
if (!demod) return;
this.el.find('[data-modulation=' + demod.get_modulation() + ']').addClass("highlighted");
var secondary_demod = demod.get_secondary_demod()
if (secondary_demod) {
this.el.find(".openwebrx-button-dig").addClass("highlighted");
this.el.find('.openwebrx-secondary-demod-listbox').val(secondary_demod);
var mode = Modes.findByModulation(secondary_demod);
if (mode) {
var self = this;
mode.underlying.filter(function(m) {
return m !== demod.get_modulation();
}).forEach(function(m) {
self.el.find('[data-modulation=' + m + ']').addClass('same-mod')
});
}
} else {
this.el.find('.openwebrx-secondary-demod-listbox').val('none');
}
var squelch_disabled = !this.squelchAvailable();
this.el.find('.openwebrx-squelch-slider').prop('disabled', squelch_disabled);
this.el.find('.openwebrx-squelch-default')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
}
DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
if (this.center_freq === center_freq) {
return;
}
this.stopDemodulator();
this.center_freq = center_freq;
this.startDemodulator();
};
DemodulatorPanel.prototype.parseHash = function() {
if (!window.location.hash) {
return {};
}
var params = window.location.hash.substring(1).split(",").map(function(x) {
var harr = x.split('=');
return [harr[0], harr.slice(1).join('=')];
}).reduce(function(params, p){
params[p[0]] = p[1];
return params;
}, {});
return this.validateHash(params);
};
DemodulatorPanel.prototype.validateHash = function(params) {
var self = this;
params = Object.keys(params).filter(function(key) {
if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth / 2;
}
return true;
}).reduce(function(p, key) {
p[key] = params[key];
return p;
}, {});
if (params['freq']) {
params['offset_frequency'] = params['freq'] - self.center_freq;
delete params['freq'];
}
return params;
};
DemodulatorPanel.prototype.updateHash = function() {
var demod = this.getDemodulator();
if (!demod) return;
var self = this;
window.location.hash = $.map({
freq: demod.get_offset_frequency() + self.center_freq,
mod: demod.get_modulation(),
secondary_mod: demod.get_secondary_demod(),
sql: demod.getSquelch(),
}, function(value, key){
if (typeof(value) === 'undefined' || value === false) return undefined;
return key + '=' + value;
}).filter(function(v) {
return !!v;
}).join(',');
};
DemodulatorPanel.prototype.updateSquelch = function() {
var sliderValue = parseInt(this.el.find(".openwebrx-squelch-slider").val());
var demod = this.getDemodulator();
if (demod) demod.setSquelch(sliderValue);
};
$.fn.demodulatorPanel = function(){
if (!this.data('panel')) {
this.data('panel', new DemodulatorPanel(this));
};
return this.data('panel');
};

View File

@ -1,12 +1,17 @@
function FrequencyDisplay(element) { function FrequencyDisplay(element) {
this.element = $(element); this.element = $(element);
this.digits = []; this.digits = [];
this.digitContainer = $('<span>'); this.setupElements();
this.element.html([this.digitContainer, $('<span> MHz</span>')]);
this.decimalSeparator = (0.1).toLocaleString().substring(1, 2);
this.setFrequency(0); 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) { FrequencyDisplay.prototype.setFrequency = function(freq) {
this.frequency = freq; this.frequency = freq;
var formatted = (freq / 1e6).toLocaleString(undefined, {maximumFractionDigits: 4, minimumFractionDigits: 4}); var formatted = (freq / 1e6).toLocaleString(undefined, {maximumFractionDigits: 4, minimumFractionDigits: 4});
@ -36,9 +41,17 @@ function TuneableFrequencyDisplay(element) {
TuneableFrequencyDisplay.prototype = new FrequencyDisplay(); 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() { TuneableFrequencyDisplay.prototype.setupEvents = function() {
var me = this; var me = this;
this.element.on('wheel', function(e){
me.element.on('wheel', function(e){
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -49,13 +62,45 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
if (e.originalEvent.deltaY > 0) delta *= -1; if (e.originalEvent.deltaY > 0) delta *= -1;
var newFrequency = me.frequency + delta; var newFrequency = me.frequency + delta;
me.listeners.forEach(function(l) { me.element.trigger('frequencychange', newFrequency);
l(newFrequency); });
});
var submit = function(){
var freq = parseInt(me.input.val());
if (!isNaN(freq)) {
me.element.trigger('frequencychange', 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();
}); });
this.listeners = [];
}; };
TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){ $.fn.frequencyDisplay = function() {
this.listeners.push(listener); if (!this.data('frequencyDisplay')) {
}; this.data('frequencyDisplay', new FrequencyDisplay(this));
}
return this.data('frequencyDisplay');
}
$.fn.tuneableFrequencyDisplay = function() {
if (!this.data('frequencyDisplay')) {
this.data('frequencyDisplay', new TuneableFrequencyDisplay(this));
}
return this.data('frequencyDisplay');
}

77
htdocs/lib/Header.js Normal file
View File

@ -0,0 +1,77 @@
function Header(el) {
this.el = el;
this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
toggle_panel($(this).data('toggle-panel'));
});
this.init_rx_photo();
this.download_details();
};
Header.prototype.setDetails = function(details) {
this.el.find('#webrx-rx-title').html(details['receiver_name']);
var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']);
this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, <a href="https://www.google.com/maps/search/?api=1&query=' + query + '" target="_blank">[maps]</a>');
this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
};
Header.prototype.init_rx_photo = function() {
this.rx_photo_state = 0;
$.extend($.easing, {
easeOutCubic:function(x) {
return 1 - Math.pow( 1 - x, 3 );
}
});
$('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
};
Header.prototype.close_rx_photo = function() {
this.rx_photo_state = 0;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 0});
this.el.find("#webrx-rx-photo-title").animate({opacity: 0});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").show();
this.el.find("#openwebrx-rx-details-arrow-up").hide();
}
Header.prototype.open_rx_photo = function() {
this.rx_photo_state = 1;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 1});
this.el.find("#webrx-rx-photo-title").animate({opacity: 1});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").hide();
this.el.find("#openwebrx-rx-details-arrow-up").show();
}
Header.prototype.toggle_rx_photo = function(ev) {
if (ev && ev.target && ev.target.tagName == 'A') {
return;
}
if (this.rx_photo_state) {
this.close_rx_photo();
} else {
this.open_rx_photo();
}
};
Header.prototype.download_details = function() {
var self = this;
$.ajax('api/receiverdetails').done(function(data){
self.setDetails(data);
});
};
$.fn.header = function() {
if (!this.data('header')) {
this.data('header', new Header(this));
}
return this.data('header');
};
$(function(){
$('#webrx-top-container').header();
});

150
htdocs/lib/Js8Threads.js Normal file
View File

@ -0,0 +1,150 @@
Js8Thread = function(el){
this.messages = [];
this.el = el;
};
Js8Thread.prototype.getAverageFrequency = function(){
var total = this.messages.map(function(message){
return message.freq;
}).reduce(function(t, f){
return t + f;
}, 0);
return total / this.messages.length;
};
Js8Thread.prototype.pushMessage = function(message) {
this.messages.push(message);
this.render();
};
Js8Thread.prototype.render = function() {
this.el.html(
'<td>' + this.renderTimestamp(this.getLatestTimestamp()) + '</td>' +
'<td class="decimal freq">' + Math.round(this.getAverageFrequency()) + '</td>' +
'<td class="message"><div>' + this.renderMessages() + '</div></td>'
);
};
Js8Thread.prototype.getLatestTimestamp = function() {
return this.messages[0].timestamp;
};
Js8Thread.prototype.isOpen = function() {
if (!this.messages.length) return true;
var last_message = this.messages[this.messages.length - 1];
return (last_message.thread_type & 2) === 0;
};
Js8Thread.prototype.renderMessages = function() {
var res = [];
for (var i = 0; i < this.messages.length; i++) {
var msg = this.messages[i];
if (msg.thread_type & 1) {
res.push('[ ');
} else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > this.getMessageDuration()) {
res.push(' ... ');
}
res.push(msg.msg);
if (msg.thread_type & 2) {
res.push(' ]');
} else if (i === this.messages.length -1) {
res.push(' ... ');
}
}
return res.join('');
};
Js8Thread.prototype.getMessageDuration = function() {
switch (this.getMode()) {
case 'A':
return 15000;
case 'E':
return 30000;
case 'B':
return 10000;
case 'C':
return 6000;
}
};
Js8Thread.prototype.getMode = function() {
// we filter messages by mode, so the first one is as good as any
if (!this.messages.length) return;
return this.messages[0].mode;
};
Js8Thread.prototype.acceptsMode = function(mode) {
var currentMode = this.getMode();
return typeof(currentMode) === 'undefined' || currentMode === mode;
};
Js8Thread.prototype.renderTimestamp = function(timestamp) {
var t = new Date(timestamp);
var pad = function (i) {
return ('' + i).padStart(2, "0");
};
return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds());
};
Js8Thread.prototype.purgeOldMessages = function() {
var now = new Date().getTime();
this.messages = this.messages.filter(function(m) {
// keep messages around for 20 minutes
return now - m.timestamp < 20 * 60 * 1000;
});
if (!this.messages.length) {
this.el.remove();
} else {
this.render();
}
return this.messages.length;
};
Js8Threader = function(el){
this.threads = [];
this.tbody = $(el).find('tbody');
var me = this;
this.interval = setInterval(function(){
me.purgeOldMessages();
}, 15000);
};
Js8Threader.prototype.purgeOldMessages = function() {
this.threads = this.threads.filter(function(t) {
return t.purgeOldMessages();
});
};
Js8Threader.prototype.findThread = function(freq, mode) {
var matching = this.threads.filter(function(thread) {
// max frequency deviation: 5 Hz. this may be a little tight.
return thread.isOpen() && thread.acceptsMode(mode) && Math.abs(thread.getAverageFrequency() - freq) <= 5;
});
matching.sort(function(a, b){
return b.getLatestTimestamp() - a.getLatestTimestamp();
});
return matching[0] || false;
};
Js8Threader.prototype.pushMessage = function(message) {
var thread;
// only look for exising threads if the message is not a starting message
if ((message.thread_type & 1) === 0) {
thread = this.findThread(message.freq, message.mode);
}
if (!thread) {
var line = $("<tr></tr>");
this.tbody.append(line);
thread = new Js8Thread(line);
this.threads.push(thread);
}
thread.pushMessage(message);
this.tbody.scrollTop(this.tbody[0].scrollHeight);
};
$.fn.js8 = function() {
if (!this.data('threader')) {
this.data('threader', new Js8Threader(this));
}
return this.data('threader');
};

View File

@ -1,4 +1,5 @@
function Measurement() { function Measurement() {
this.reporters = [];
this.reset(); this.reset();
} }
@ -21,10 +22,13 @@ Measurement.prototype.getRate = function() {
Measurement.prototype.reset = function() { Measurement.prototype.reset = function() {
this.value = 0; this.value = 0;
this.start = new Date(); this.start = new Date();
this.reporters.forEach(function(r){ r.reset(); });
}; };
Measurement.prototype.report = function(range, interval, callback) { Measurement.prototype.report = function(range, interval, callback) {
return new Reporter(this, range, interval, callback); var reporter = new Reporter(this, range, interval, callback);
this.reporters.push(reporter);
return reporter;
}; };
function Reporter(measurement, range, interval, callback) { function Reporter(measurement, range, interval, callback) {
@ -59,4 +63,8 @@ Reporter.prototype.report = function(){
var accumulated = newest.value - oldest.value; var accumulated = newest.value - oldest.value;
// we want rate per second, but our time is in milliseconds... compensate by 1000 // we want rate per second, but our time is in milliseconds... compensate by 1000
this.callback(accumulated * 1000 / elapsed); this.callback(accumulated * 1000 / elapsed);
};
Reporter.prototype.reset = function(){
this.samples = [];
}; };

55
htdocs/lib/Modes.js Normal file
View File

@ -0,0 +1,55 @@
var Modes = {
modes: [],
features: {},
panels: [],
setModes:function(json){
this.modes = json.map(function(m){ return new Mode(m); });
this.updatePanels();
$('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes);
},
getModes:function(){
return this.modes;
},
setFeatures:function(features){
this.features = features;
this.updatePanels();
},
findByModulation:function(modulation){
matches = this.modes.filter(function(m) { return m.modulation === modulation; });
if (matches.length) return matches[0]
},
registerModePanel: function(el) {
this.panels.push(el);
},
initComplete: function() {
return this.modes.length && Object.keys(this.features).length;
},
updatePanels: function() {
this.panels.forEach(function(p) {
p.render();
p.startDemodulator();
});
}
};
var Mode = function(json){
this.modulation = json.modulation;
this.name = json.name;
this.type = json.type;
this.requirements = json.requirements;
this.squelch = json.squelch;
if (json.bandpass) {
this.bandpass = json.bandpass;
}
if (this.type === 'digimode') {
this.underlying = json.underlying;
}
};
Mode.prototype.isAvailable = function(){
return this.requirements.map(function(r){
return Modes.features[r];
}).reduce(function(a, b){
return a && b;
}, true);
};

View File

@ -1,10 +1,15 @@
ProgressBar = function(el) { ProgressBar = function(el) {
this.$el = $(el); this.$el = $(el);
this.$innerText = this.$el.find('.openwebrx-progressbar-text'); this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
this.$innerBar = this.$el.find('.openwebrx-progressbar-bar'); this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
this.$el.empty().append(this.$innerText, this.$innerBar);
this.$innerBar.css('width', '0%'); this.$innerBar.css('width', '0%');
}; };
ProgressBar.prototype.getDefaultText = function() {
return '';
}
ProgressBar.prototype.set = function(val, text, over) { ProgressBar.prototype.set = function(val, text, over) {
this.setValue(val); this.setValue(val);
this.setText(text); this.setText(text);
@ -25,13 +30,20 @@ ProgressBar.prototype.setOver = function(over) {
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6"); this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
}; };
AudioBufferProgressBar = function(el, sampleRate) { AudioBufferProgressBar = function(el) {
ProgressBar.call(this, el); ProgressBar.call(this, el);
this.sampleRate = sampleRate;
}; };
AudioBufferProgressBar.prototype = new ProgressBar(); AudioBufferProgressBar.prototype = new ProgressBar();
AudioBufferProgressBar.prototype.getDefaultText = function() {
return 'Audio buffer [0 ms]';
};
AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) {
this.sampleRate = sampleRate;
};
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) { AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
var audio_buffer_value = buffersize / this.sampleRate; var audio_buffer_value = buffersize / this.sampleRate;
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
@ -53,6 +65,10 @@ NetworkSpeedProgressBar = function(el) {
NetworkSpeedProgressBar.prototype = new ProgressBar(); NetworkSpeedProgressBar.prototype = new ProgressBar();
NetworkSpeedProgressBar.prototype.getDefaultText = function() {
return 'Network usage [0 kbps]';
};
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) { NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
var speedInKilobits = speed * 8 / 1000; var speedInKilobits = speed * 8 / 1000;
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false); this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
@ -64,18 +80,29 @@ AudioSpeedProgressBar = function(el) {
AudioSpeedProgressBar.prototype = new ProgressBar(); AudioSpeedProgressBar.prototype = new ProgressBar();
AudioSpeedProgressBar.prototype.getDefaultText = function() {
return 'Audio stream [0 kbps]';
};
AudioSpeedProgressBar.prototype.setSpeed = function(speed) { AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false); this.set(speed / 1000000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
}; };
AudioOutputProgressBar = function(el, sampleRate) { AudioOutputProgressBar = function(el, sampleRate) {
ProgressBar.call(this, el); ProgressBar.call(this, el);
this.maxRate = sampleRate * 1.25;
this.minRate = sampleRate * .25;
}; };
AudioOutputProgressBar.prototype = new ProgressBar(); AudioOutputProgressBar.prototype = new ProgressBar();
AudioOutputProgressBar.prototype.getDefaultText = function() {
return 'Audio output [0 sps]';
};
AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) {
this.maxRate = sampleRate * 1.25;
this.minRate = sampleRate * .25;
};
AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) { AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) {
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate); this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
}; };
@ -88,6 +115,10 @@ ClientsProgressBar = function(el) {
ClientsProgressBar.prototype = new ProgressBar(); ClientsProgressBar.prototype = new ProgressBar();
ClientsProgressBar.prototype.getDefaultText = function() {
return 'Clients [1]';
};
ClientsProgressBar.prototype.setClients = function(clients) { ClientsProgressBar.prototype.setClients = function(clients) {
this.clients = clients; this.clients = clients;
this.render(); this.render();
@ -108,6 +139,27 @@ CpuProgressBar = function(el) {
CpuProgressBar.prototype = new ProgressBar(); CpuProgressBar.prototype = new ProgressBar();
CpuProgressBar.prototype.getDefaultText = function() {
return 'Server CPU [0%]';
};
CpuProgressBar.prototype.setUsage = function(usage) { CpuProgressBar.prototype.setUsage = function(usage) {
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85); this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
}; };
ProgressBar.types = {
cpu: CpuProgressBar,
audiobuffer: AudioBufferProgressBar,
audiospeed: AudioSpeedProgressBar,
audiooutput: AudioOutputProgressBar,
clients: ClientsProgressBar,
networkspeed: NetworkSpeedProgressBar
}
$.fn.progressbar = function() {
if (!this.data('progressbar')) {
var constructor = ProgressBar.types[this.data('type')] || ProgressBar;
this.data('progressbar', new constructor(this));
}
return this.data('progressbar');
};

View File

@ -0,0 +1,138 @@
function Input(name, value, options) {
this.name = name;
this.value = value;
this.options = options;
this.label = options && options.label || name;
};
Input.prototype.getClasses = function() {
return ['form-control', 'form-control-sm'];
}
Input.prototype.bootstrapify = function(input) {
this.getClasses().forEach(input.addClass.bind(input));
return [
'<div class="form-group row">',
'<label class="col-form-label col-form-label-sm col-3" for="' + this.name + '">' + this.label + '</label>',
'<div class="col-9">',
$.map(input, function(el) {
return el.outerHTML;
}).join(''),
'</div>',
'</div>'
].join('');
};
function TextInput() {
Input.apply(this, arguments);
};
TextInput.prototype = new Input();
TextInput.prototype.render = function() {
return this.bootstrapify($('<input type="text" name="' + this.name + '" value="' + this.value + '">'));
}
function NumberInput() {
Input.apply(this, arguments);
};
NumberInput.prototype = new Input();
NumberInput.prototype.render = function() {
return this.bootstrapify($('<input type="number" name="' + this.name + '" value="' + this.value + '">'));
};
function SoapyGainInput() {
Input.apply(this, arguments);
}
SoapyGainInput.prototype = new Input();
SoapyGainInput.prototype.getClasses = function() {
return [];
};
SoapyGainInput.prototype.render = function(){
var markup = $(
'<div class="row form-group">' +
'<div class="col-4">Gain mode</div>' +
'<div class="col-8">' +
'<select class="form-control form-control-sm">' +
'<option value="auto">automatic gain</option>' +
'<option value="single">single gain value</option>' +
'<option value="separate">separate gain values</option>' +
'</select>' +
'</div>' +
'</div>' +
'<div class="row option form-group gain-mode-single">' +
'<div class="col-4">Gain</div>' +
'<div class="col-8">' +
'<input class="form-control form-control-sm" type="number">' +
'</div>' +
'</div>' +
this.options.gains.map(function(g){
return '<div class="row option form-group gain-mode-separate">' +
'<div class="col-4">' + g + '</div>' +
'<div class="col-8">' +
'<input class="form-control form-control-sm" data-gain="' + g + '" type="number">' +
'</div>' +
'</div>';
}).join('')
);
var el = $(this.bootstrapify(markup))
var setMode = function(mode){
el.find('select').val(mode);
el.find('.option').hide();
el.find('.gain-mode-' + mode).show();
};
el.on('change', 'select', function(){
var mode = $(this).val();
setMode(mode);
});
if (typeof(this.value) === 'number') {
setMode('single');
el.find('.gain-mode-single input').val(this.value);
} else if (typeof(this.value) === 'string') {
if (this.value === 'auto') {
setMode('auto');
} else {
setMode('separate');
values = $.extend.apply($, this.value.split(',').map(function(seg){
var split = seg.split('=');
if (split.length < 2) return;
var res = {};
res[split[0]] = parseInt(split[1]);
return res;
}));
el.find('.gain-mode-separate input').each(function(){
var $input = $(this);
var g = $input.data('gain');
$input.val(g in values ? values[g] : 0);
});
}
} else {
setMode('auto');
}
return el;
};
function ProfileInput() {
Input.apply(this, arguments);
};
ProfileInput.prototype = new Input();
ProfileInput.prototype.render = function() {
return $('<div><h3>Profiles</h3></div>');
};
function SchedulerInput() {
Input.apply(this, arguments);
};
SchedulerInput.prototype = new Input();
SchedulerInput.prototype.render = function() {
return $('<div><h3>Scheduler</h3></div>');
};

View File

@ -0,0 +1,252 @@
function SdrDevice(el, data) {
this.el = el;
this.data = data;
this.inputs = {};
this.render();
var self = this;
el.on('click', '.fieldselector .btn', function() {
var key = el.find('.fieldselector select').val();
self.data[key] = self.getInitialValue(key);
self.render();
});
};
SdrDevice.create = function(el) {
var data = JSON.parse(decodeURIComponent(el.data('config')));
var type = data.type;
var constructor = SdrDevice.types[type] || SdrDevice;
return new constructor(el, data);
};
SdrDevice.prototype.getData = function() {
return $.extend(new Object(), this.getDefaults(), this.data);
};
SdrDevice.prototype.getDefaults = function() {
var defaults = {}
$.each(this.getMappings(), function(k, v) {
if (!v.includeInDefault) return;
defaults[k] = 'initialValue' in v ? v['initialValue'] : false;
});
return defaults;
};
SdrDevice.prototype.getMappings = function() {
return {
"name": {
constructor: TextInput,
inputOptions: {
label: "Name"
},
initialValue: "",
includeInDefault: true
},
"type": {
constructor: TextInput,
inputOptions: {
label: "Type"
},
initialValue: '',
includeInDefault: true
},
"ppm": {
constructor: NumberInput,
inputOptions: {
label: "PPM"
},
initialValue: 0
},
"profiles": {
constructor: ProfileInput,
inputOptions: {
label: "Profiles"
},
initialValue: [],
includeInDefault: true,
position: 100
},
"scheduler": {
constructor: SchedulerInput,
inputOptions: {
label: "Scheduler",
},
initialValue: {},
position: 101
},
"rf_gain": {
constructor: TextInput,
inputOptions: {
label: "Gain",
},
initialValue: 0
}
};
};
SdrDevice.prototype.getMapping = function(key) {
var mappings = this.getMappings();
return mappings[key];
};
SdrDevice.prototype.getInputClass = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.constructor || TextInput;
};
SdrDevice.prototype.getInitialValue = function(key) {
var mapping = this.getMapping(key);
return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false;
};
SdrDevice.prototype.getPosition = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.position || 10;
};
SdrDevice.prototype.getInputOptions = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.inputOptions || {};
};
SdrDevice.prototype.getLabel = function(key) {
var options = this.getInputOptions(key);
return options && options.label || key;
};
SdrDevice.prototype.render = function() {
var self = this;
self.el.empty();
var data = this.getData();
Object.keys(data).sort(function(a, b){
return self.getPosition(a) - self.getPosition(b);
}).forEach(function(key){
var value = data[key];
var inputClass = self.getInputClass(key);
var input = new inputClass(key, value, self.getInputOptions(key));
self.inputs[key] = input;
self.el.append(input.render());
});
self.el.append(this.renderFieldSelector());
};
SdrDevice.prototype.renderFieldSelector = function() {
var self = this;
return '<div class="fieldselector">' +
'<h3>Add new configuration options<h3>' +
'<div class="form-group row">' +
'<div class="col-3"><select class="form-control form-control-sm">' +
Object.keys(self.getMappings()).filter(function(m){
return !(m in self.data);
}).map(function(m) {
return '<option value="' + m + '">' + self.getLabel(m) + '</option>';
}).join('') +
'</select></div>' +
'<div class="col-2">' +
'<div class="btn btn-primary">Add to config</div>' +
'</div>' +
'</div>' +
'</div>';
};
RtlSdrDevice = function() {
SdrDevice.apply(this, arguments);
};
RtlSdrDevice.prototype = Object.create(SdrDevice.prototype);
RtlSdrDevice.prototype.constructor = RtlSdrDevice;
RtlSdrDevice.prototype.getMappings = function() {
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
return $.extend(new Object(), mappings, {
"device": {
constructor: TextInput,
inputOptions:{
label: "Serial number"
},
initialValue: ""
}
});
};
SoapySdrDevice = function() {
SdrDevice.apply(this, arguments);
};
SoapySdrDevice.prototype = Object.create(SdrDevice.prototype);
SoapySdrDevice.prototype.constructor = SoapySdrDevice;
SoapySdrDevice.prototype.getMappings = function() {
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
return $.extend(new Object(), mappings, {
"device": {
constructor: TextInput,
inputOptions:{
label: "Soapy device selector"
},
initialValue: ""
},
"rf_gain": {
constructor: SoapyGainInput,
initialValue: 0,
inputOptions: {
label: "Gain",
gains: this.getGains()
}
}
});
};
SoapySdrDevice.prototype.getGains = function() {
return [];
};
SdrplaySdrDevice = function() {
SoapySdrDevice.apply(this, arguments);
};
SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice;
SdrplaySdrDevice.prototype.getGains = function() {
return ['RFGR', 'IFGR'];
};
AirspyHfSdrDevice = function() {
SoapySdrDevice.apply(this, arguments);
};
AirspyHfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
AirspyHfSdrDevice.prototype.constructor = AirspyHfSdrDevice;
AirspyHfSdrDevice.prototype.getGains = function() {
return ['RF', 'VGA'];
};
HackRfSdrDevice = function() {
SoapySdrDevice.apply(this, arguments);
};
HackRfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
HackRfSdrDevice.prototype.constructor = HackRfSdrDevice;
HackRfSdrDevice.prototype.getGains = function() {
return ['LNA', 'VGA', 'AMP'];
};
SdrDevice.types = {
'rtl_sdr': RtlSdrDevice,
'sdrplay': SdrplaySdrDevice,
'airspyhf': AirspyHfSdrDevice,
'hackrf': HackRfSdrDevice
};
$.fn.sdrdevice = function() {
return this.map(function(){
var el = $(this);
if (!el.data('sdrdevice')) {
el.data('sdrdevice', SdrDevice.create(el));
}
return el.data('sdrdevice');
});
};

27
htdocs/login.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Login</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/login.css" />
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="login">
<form method="POST">
<div class="form-group">
<label for="user">Username</label>
<input type="text" class="form-control" id="user" name="user" autofocus="autofocus" placeholder="Username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password">
</div>
<button type="submit" class="btn btn-secondary btn-login">Login</button>
</form>
</div>
</body>

View File

@ -3,9 +3,7 @@
<head> <head>
<title>OpenWebRX Map</title> <title>OpenWebRX Map</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="compiled/map.js"></script>
<script src="static/lib/chroma.min.js"></script>
<script src="static/map.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="static/css/map.css" /> <link rel="stylesheet" type="text/css" href="static/css/map.css" />
<meta charset="utf-8"> <meta charset="utf-8">

View File

@ -135,7 +135,11 @@
if (expectedCallsign && expectedCallsign == update.callsign.trim()) { if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
map.panTo(pos); map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos); showMarkerInfoWindow(update.callsign, pos);
delete(expectedCallsign); expectedCallsign = false;
}
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
showMarkerInfoWindow(infowindow.callsign, pos);
} }
break; break;
case 'locator': case 'locator':
@ -176,7 +180,11 @@
if (expectedLocator && expectedLocator == update.location.locator) { if (expectedLocator && expectedLocator == update.location.locator) {
map.panTo(center); map.panTo(center);
showLocatorInfoWindow(expectedLocator, center); showLocatorInfoWindow(expectedLocator, center);
delete(expectedLocator); expectedLocator = false;
}
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) {
showLocatorInfoWindow(infowindow.locator, center);
} }
break; break;
} }
@ -215,13 +223,26 @@
case "config": case "config":
var config = json.value; var config = json.value;
if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){
var mapTypeId = config.google_maps_api_key ? 'roadmap' : 'OSM';
map = new google.maps.Map($('.openwebrx-map')[0], { map = new google.maps.Map($('.openwebrx-map')[0], {
center: { center: {
lat: config.receiver_gps[0], lat: config.receiver_gps.lat,
lng: config.receiver_gps[1] lng: config.receiver_gps.lon
}, },
zoom: 5 zoom: 5,
mapTypeId: mapTypeId
}); });
map.mapTypes.set("OSM", new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
return "https://maps.wikimedia.org/osm-intl/" + zoom + "/" + coord.x + "/" + coord.y + ".png";
},
tileSize: new google.maps.Size(256, 256),
name: "OpenStreetMap",
maxZoom: 18
}));
$.getScript("static/lib/nite-overlay.js").done(function(){ $.getScript("static/lib/nite-overlay.js").done(function(){
nite.init(map); nite.init(map);
setInterval(function() { nite.refresh() }, 10000); // every 10s setInterval(function() { nite.refresh() }, 10000); // every 10s
@ -237,6 +258,11 @@
case "update": case "update":
processUpdates(json.value); processUpdates(json.value);
break; break;
case 'receiver_details':
$('#webrx-top-container').header().setDetails(json['value']);
break;
default:
console.warn('received message of unknown type: ' + json['type']);
} }
} catch (e) { } catch (e) {
// don't lose exception // don't lose exception
@ -269,9 +295,21 @@
connect(); connect();
var getInfoWindow = function() {
if (!infowindow) {
infowindow = new google.maps.InfoWindow();
google.maps.event.addListener(infowindow, 'closeclick', function() {
delete infowindow.locator;
delete infowindow.callsign;
});
}
return infowindow;
}
var infowindow; var infowindow;
var showLocatorInfoWindow = function(locator, pos) { var showLocatorInfoWindow = function(locator, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow(); var infowindow = getInfoWindow();
infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) { var inLocator = $.map(rectangles, function(r, callsign) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
}).filter(function(d) { }).filter(function(d) {
@ -297,7 +335,8 @@
}; };
var showMarkerInfoWindow = function(callsign, pos) { var showMarkerInfoWindow = function(callsign, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow(); var infowindow = getInfoWindow();
infowindow.callsign = callsign;
var marker = markers[callsign]; var marker = markers[callsign];
var timestring = moment(marker.lastseen).fromNow(); var timestring = moment(marker.lastseen).fromNow();
var commentString = ""; var commentString = "";

File diff suppressed because it is too large Load Diff

21
htdocs/sdrsettings.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="compiled/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="col-12">
<h1>SDR device settings</h1>
</div>
<div class="col-12">
${devices}
</div>
</div>
</body>

27
htdocs/settings.html Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="compiled/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="col-12">
<h1>Settings</h1>
</div>
<div class="col-12">
<a href="generalsettings">General settings</a>
</div>
<div class="col-12">
<a href="sdrsettings">SDR device settings</a>
</div>
<div class="col-12">
<a href="features">Feature report</a>
</div>
</div>
</body>

25
htdocs/settings.js Normal file
View File

@ -0,0 +1,25 @@
$(function(){
$(".map-input").each(function(el) {
var $el = $(this);
var field_id = $el.attr("for");
var $lat = $('#' + field_id + '-lat');
var $lon = $('#' + field_id + '-lon');
$.getScript("https://maps.googleapis.com/maps/api/js?key=" + $el.data("key")).done(function(){
$el.css("height", "200px");
var lp = new locationPicker($el.get(0), {
lat: parseFloat($lat.val()),
lng: parseFloat($lon.val())
}, {
zoom: 7
});
google.maps.event.addListener(lp.map, 'idle', function(event){
var pos = lp.getMarkerPosition();
$lat.val(pos.lat);
$lon.val(pos.lng);
});
});
});
$(".sdrdevice").sdrdevice();
});

15
manifest.sh Executable file
View File

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

View File

@ -1,17 +1,18 @@
from http.server import HTTPServer
from owrx.http import RequestHandler
from owrx.config import PropertyManager
from owrx.feature import FeatureDetector
from owrx.sdr import SdrService
from socketserver import ThreadingMixIn
from owrx.sdrhu import SdrHuUpdater
from owrx.service import Services
from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter
import logging import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
from http.server import HTTPServer
from owrx.http import RequestHandler
from owrx.config import Config
from owrx.feature import FeatureDetector
from owrx.sdr import SdrService
from socketserver import ThreadingMixIn
from owrx.service import Services
from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter
from owrx.version import openwebrx_version
class ThreadedHttpServer(ThreadingMixIn, HTTPServer): class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
@ -26,32 +27,41 @@ OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE fil
_________________________________________________________________________________________________ _________________________________________________________________________________________________
Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de> Author contact info: Jakob Ketterl, DD5JFK <dd5jfk@darc.de>
Documentation: https://github.com/jketterl/openwebrx/wiki
Support and info: https://groups.io/g/openwebrx
""" """
) )
pm = PropertyManager.getSharedInstance().loadConfig() logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version))
pm = Config.get()
configErrors = Config.validateConfig()
if configErrors:
logger.error(
"your configuration contains errors. please address the following errors:"
)
for e in configErrors:
logger.error(e)
return
featureDetector = FeatureDetector() featureDetector = FeatureDetector()
if not featureDetector.is_available("core"): if not featureDetector.is_available("core"):
print( logger.error(
"you are missing required dependencies to run openwebrx. " "you are missing required dependencies to run openwebrx. "
"please check that the following core requirements are installed:" "please check that the following core requirements are installed:"
) )
print(", ".join(featureDetector.get_requirements("core"))) logger.error(", ".join(featureDetector.get_requirements("core")))
return return
# Get error messages about unknown / unavailable features as soon as possible # Get error messages about unknown / unavailable features as soon as possible
SdrService.loadProps() SdrService.loadProps()
if "sdrhu_key" in pm and pm["sdrhu_public_listing"]:
updater = SdrHuUpdater()
updater.start()
Services.start() Services.start()
try: try:
server = ThreadedHttpServer(("0.0.0.0", pm.getPropertyValue("web_port")), RequestHandler) server = ThreadedHttpServer(("0.0.0.0", pm["web_port"]), RequestHandler)
server.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
WebSocketConnection.closeAll() WebSocketConnection.closeAll()

270
owrx/audio.py Normal file
View File

@ -0,0 +1,270 @@
from abc import ABC, ABCMeta, abstractmethod
from owrx.config import Config
from owrx.metrics import Metrics, CounterMetric, DirectMetric
import threading
import wave
import subprocess
import os
from multiprocessing.connection import Pipe, wait
from datetime import datetime, timedelta
from queue import Queue, Full
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class QueueJob(object):
def __init__(self, decoder, file, freq):
self.decoder = decoder
self.file = file
self.freq = freq
def run(self):
self.decoder.decode(self)
def unlink(self):
try:
os.unlink(self.file)
except FileNotFoundError:
pass
class QueueWorker(threading.Thread):
def __init__(self, queue):
self.queue = queue
self.doRun = True
super().__init__(daemon=True)
def run(self) -> None:
while self.doRun:
job = self.queue.get()
try:
job.run()
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
finally:
job.unlink()
self.queue.task_done()
class DecoderQueue(Queue):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is None:
pm = Config.get()
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
return DecoderQueue.sharedInstance
def __init__(self, maxsize, workers):
super().__init__(maxsize)
metrics = Metrics.getSharedInstance()
metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize))
self.inCounter = CounterMetric()
metrics.addMetric("decoding.queue.in", self.inCounter)
self.outCounter = CounterMetric()
metrics.addMetric("decoding.queue.out", self.outCounter)
self.overflowCounter = CounterMetric()
metrics.addMetric("decoding.queue.overflow", self.overflowCounter)
self.errorCounter = CounterMetric()
metrics.addMetric("decoding.queue.error", self.errorCounter)
self.workers = [self.newWorker() for _ in range(0, workers)]
def put(self, item, **kwars):
self.inCounter.inc()
try:
super(DecoderQueue, self).put(item, block=False)
except Full:
self.overflowCounter.inc()
raise
def get(self, **kwargs):
# super.get() is blocking, so it would mess up the stats to inc() first
out = super(DecoderQueue, self).get(**kwargs)
self.outCounter.inc()
return out
def newWorker(self):
worker = QueueWorker(self)
worker.start()
return worker
def onError(self):
self.errorCounter.inc()
class AudioChopperProfile(ABC):
@abstractmethod
def getInterval(self):
pass
@abstractmethod
def getFileTimestampFormat(self):
pass
@abstractmethod
def decoder_commandline(self, file):
pass
class AudioWriter(object):
def __init__(self, dsp, source, profile: AudioChopperProfile):
self.dsp = dsp
self.source = source
self.profile = profile
self.tmp_dir = Config.get()["temporary_directory"]
self.wavefile = None
self.wavefilename = None
self.switchingLock = threading.Lock()
self.timer = None
(self.outputReader, self.outputWriter) = Pipe()
def getWaveFile(self):
filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format(
tmp_dir=self.tmp_dir,
id=id(self),
timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()),
)
wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1)
wavefile.setsampwidth(2)
wavefile.setframerate(12000)
return filename, wavefile
def getNextDecodingTime(self):
t = datetime.utcnow()
zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed
interval = self.profile.getInterval()
seconds = (int(delta.total_seconds() / interval) + 1) * interval
t = zeroed + timedelta(seconds=seconds)
logger.debug("scheduling: {0}".format(t))
return t
def cancelTimer(self):
if self.timer:
self.timer.cancel()
self.timer = None
def _scheduleNextSwitch(self):
self.cancelTimer()
delta = self.getNextDecodingTime() - datetime.utcnow()
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
self.timer.start()
def switchFiles(self):
self.switchingLock.acquire()
file = self.wavefile
filename = self.wavefilename
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock.release()
file.close()
job = QueueJob(self, filename, self.dsp.get_operating_freq())
try:
DecoderQueue.getSharedInstance().put(job)
except Full:
logger.warning("decoding queue overflow; dropping one file")
job.unlink()
self._scheduleNextSwitch()
def decode(self, job: QueueJob):
logger.debug("processing file %s", job.file)
decoder = subprocess.Popen(
["nice", "-n", "10"] + self.profile.decoder_commandline(job.file),
stdout=subprocess.PIPE,
cwd=self.tmp_dir,
close_fds=True,
)
try:
for line in decoder.stdout:
self.outputWriter.send((job.freq, line))
except OSError:
decoder.stdout.flush()
# TODO uncouple parsing from the output so that decodes can still go to the map and the spotters
logger.debug("output has gone away while decoding job.")
try:
rc = decoder.wait(timeout=10)
if rc != 0:
logger.warning("decoder return code: %i", rc)
except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill()
def start(self):
(self.wavefilename, self.wavefile) = self.getWaveFile()
self._scheduleNextSwitch()
def write(self, data):
self.switchingLock.acquire()
self.wavefile.writeframes(data)
self.switchingLock.release()
def stop(self):
self.outputWriter.close()
self.outputWriter = None
# drain messages left in the queue so that the queue can be successfully closed
# this is necessary since python keeps the file descriptors open otherwise
try:
while True:
self.outputReader.recv()
except EOFError:
pass
self.outputReader.close()
self.outputReader = None
self.cancelTimer()
try:
self.wavefile.close()
except Exception:
logger.exception("error closing wave file")
try:
os.unlink(self.wavefilename)
except Exception:
logger.exception("error removing undecoded file")
self.wavefile = None
self.wavefilename = None
class AudioChopper(threading.Thread, metaclass=ABCMeta):
def __init__(self, dsp, source, *profiles: AudioChopperProfile):
self.source = source
self.writers = [AudioWriter(dsp, source, p) for p in profiles]
self.doRun = True
super().__init__()
def run(self) -> None:
logger.debug("Audio chopper starting up")
for w in self.writers:
w.start()
while self.doRun:
data = None
try:
data = self.source.read(256)
except ValueError:
pass
if data is None or (isinstance(data, bytes) and len(data) == 0):
self.doRun = False
else:
for w in self.writers:
w.write(data)
logger.debug("Audio chopper shutting down")
for w in self.writers:
w.stop()
def read(self):
try:
readers = wait([w.outputReader for w in self.writers])
return [r.recv() for r in readers]
except (EOFError, OSError):
return None

View File

@ -58,7 +58,10 @@ class Bandplan(object):
except FileNotFoundError: except FileNotFoundError:
pass pass
except json.JSONDecodeError: except json.JSONDecodeError:
logger.exception("error while parsing bandplan from %s", file) logger.exception("error while parsing bandplan file %s", file)
return []
except Exception:
logger.exception("error while processing bandplan from %s", file)
return [] return []
return [] return []

View File

@ -50,7 +50,10 @@ class Bookmarks(object):
except FileNotFoundError: except FileNotFoundError:
pass pass
except json.JSONDecodeError: except json.JSONDecodeError:
logger.exception("error while parsing bookmarks from %s", file) logger.exception("error while parsing bookmarks file %s", file)
return []
except Exception:
logger.exception("error while processing bookmarks from %s", file)
return [] return []
return [] return []

View File

@ -1,5 +1,4 @@
from owrx.config import PropertyManager from owrx.config import Config
from owrx.metrics import Metrics, DirectMetric
import threading import threading
import logging import logging
@ -24,7 +23,6 @@ class ClientRegistry(object):
def __init__(self): def __init__(self):
self.clients = [] self.clients = []
Metrics.getSharedInstance().addMetric("openwebrx.users", DirectMetric(self.clientCount))
super().__init__() super().__init__()
def broadcast(self): def broadcast(self):
@ -33,7 +31,7 @@ class ClientRegistry(object):
c.write_clients(n) c.write_clients(n)
def addClient(self, client): def addClient(self, client):
pm = PropertyManager.getSharedInstance() pm = Config.get()
if len(self.clients) >= pm["max_clients"]: if len(self.clients) >= pm["max_clients"]:
raise TooManyClientsException() raise TooManyClientsException()
self.clients.append(client) self.clients.append(client)

View File

@ -33,6 +33,9 @@ class CommandMapper(object):
self.static = static self.static = static
return self return self
def keys(self):
return self.mappings.keys()
class CommandMapping(ABC): class CommandMapping(ABC):
@abstractmethod @abstractmethod
@ -69,3 +72,8 @@ class Option(CommandMapping):
def setSpacer(self, spacer): def setSpacer(self, spacer):
self.spacer = spacer self.spacer = spacer
return self return self
class Argument(CommandMapping):
def map(self, value):
return value

View File

@ -1,149 +1,134 @@
from owrx.property import PropertyManager, PropertyLayer
import importlib.util import importlib.util
import os
import logging import logging
import json
from abc import ABC, abstractmethod
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Subscription(object):
def __init__(self, subscriptee, subscriber):
self.subscriptee = subscriptee
self.subscriber = subscriber
def call(self, *args, **kwargs):
self.subscriber(*args, **kwargs)
def cancel(self):
self.subscriptee.unwire(self)
class Property(object):
def __init__(self, value=None):
self.value = value
self.subscribers = []
def getValue(self):
return self.value
def setValue(self, value):
if self.value == value:
return self
self.value = value
for c in self.subscribers:
try:
c.call(self.value)
except Exception as e:
logger.exception(e)
return self
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
if not self.value is None:
sub.call(self.value)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
class ConfigNotFoundException(Exception): class ConfigNotFoundException(Exception):
pass pass
class PropertyManager(object): class ConfigError(object):
sharedInstance = None def __init__(self, key, message):
self.key = key
self.message = message
def __str__(self):
return "Configuration Error (key: {0}): {1}".format(self.key, self.message)
class ConfigMigrator(ABC):
@abstractmethod
def migrate(self, config):
pass
def renameKey(self, config, old, new):
if old in config and not new in config:
config[new] = config[old]
del config[old]
class ConfigMigratorVersion1(ConfigMigrator):
def migrate(self, config):
if "receiver_gps" in config:
gps = config["receiver_gps"]
config["receiver_gps"] = {"lat": gps[0], "lon": gps[1]}
if "waterfall_auto_level_margin" in config:
levels = config["waterfall_auto_level_margin"]
config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]}
self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers")
self.renameKey(config, "wsjt_queue_length", "decoding_queue_length")
config["version"] = 2
return config
class Config:
sharedConfig = None
currentVersion = 2
migrators = {
1: ConfigMigratorVersion1()
}
@staticmethod @staticmethod
def getSharedInstance(): def _loadPythonFile(file):
if PropertyManager.sharedInstance is None: spec = importlib.util.spec_from_file_location("config_webrx", file)
PropertyManager.sharedInstance = PropertyManager() cfg = importlib.util.module_from_spec(spec)
return PropertyManager.sharedInstance spec.loader.exec_module(cfg)
pm = PropertyLayer()
for name, value in cfg.__dict__.items():
if name.startswith("__"):
continue
pm[name] = value
return pm
def collect(self, *props): @staticmethod
return PropertyManager( def _loadJsonFile(file):
{name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props} with open(file, "r") as f:
) pm = PropertyLayer()
for k, v in json.load(f).items():
pm[k] = v
return pm
def __init__(self, properties=None): @staticmethod
self.properties = {} def _loadConfig():
self.subscribers = [] for file in ["./settings.json", "/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
if properties is not None:
for (name, prop) in properties.items():
self.add(name, prop)
def add(self, name, prop):
self.properties[name] = prop
def fireCallbacks(value):
for c in self.subscribers:
try:
c.call(name, value)
except Exception as e:
logger.exception(e)
prop.wire(fireCallbacks)
return self
def __contains__(self, name):
return self.hasProperty(name)
def __getitem__(self, name):
return self.getPropertyValue(name)
def __setitem__(self, name, value):
if not self.hasProperty(name):
self.add(name, Property())
self.getProperty(name).setValue(value)
def __dict__(self):
return {k: v.getValue() for k, v in self.properties.items()}
def hasProperty(self, name):
return name in self.properties
def getProperty(self, name):
if not self.hasProperty(name):
self.add(name, Property())
return self.properties[name]
def getPropertyValue(self, name):
return self.getProperty(name).getValue()
def wire(self, callback):
sub = Subscription(self, callback)
self.subscribers.append(sub)
return sub
def unwire(self, sub):
try:
self.subscribers.remove(sub)
except ValueError:
# happens when already removed before
pass
return self
def defaults(self, other_pm):
for (key, p) in self.properties.items():
if p.getValue() is None:
p.setValue(other_pm[key])
return self
def loadConfig(self):
for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]:
try: try:
spec = importlib.util.spec_from_file_location("config_webrx", file) if file.endswith(".py"):
cfg = importlib.util.module_from_spec(spec) return Config._loadPythonFile(file)
spec.loader.exec_module(cfg) elif file.endswith(".json"):
for name, value in cfg.__dict__.items(): return Config._loadJsonFile(file)
if name.startswith("__"): else:
continue logger.warning("unsupported file type: %s", file)
self[name] = value
return self
except FileNotFoundError: except FileNotFoundError:
logger.debug("not found: %s", file) pass
raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!") raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!")
@staticmethod
def get():
if Config.sharedConfig is None:
Config.sharedConfig = Config._migrate(Config._loadConfig())
return Config.sharedConfig
@staticmethod
def store():
with open("settings.json", "w") as file:
json.dump(Config.get().__dict__(), file, indent=4)
@staticmethod
def validateConfig():
pm = Config.get()
errors = [
Config.checkTempDirectory(pm)
]
return [e for e in errors if e is not None]
@staticmethod
def checkTempDirectory(pm: PropertyManager):
key = "temporary_directory"
if key not in pm or pm[key] is None:
return ConfigError(key, "temporary directory is not set")
if not os.path.exists(pm[key]):
return ConfigError(key, "temporary directory doesn't exist")
if not os.path.isdir(pm[key]):
return ConfigError(key, "temporary directory path is not a directory")
if not os.access(pm[key], os.W_OK):
return ConfigError(key, "temporary directory is not writable")
return None
@staticmethod
def _migrate(config):
version = config["version"] if "version" in config else 1
if version == Config.currentVersion:
return config
logger.debug("migrating config from version %i", version)
migrator = Config.migrators[version]
return migrator.migrate(config)

View File

@ -1,4 +1,5 @@
from owrx.config import PropertyManager from owrx.config import Config
from owrx.details import ReceiverDetails
from owrx.dsp import DspManager from owrx.dsp import DspManager
from owrx.cpu import CpuUsageThread from owrx.cpu import CpuUsageThread
from owrx.sdr import SdrService from owrx.sdr import SdrService
@ -9,9 +10,11 @@ from owrx.version import openwebrx_version
from owrx.bands import Bandplan from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks from owrx.bookmarks import Bookmarks
from owrx.map import Map from owrx.map import Map
from owrx.locator import Locator from owrx.property import PropertyStack
from multiprocessing import Queue from owrx.modes import Modes, DigitalMode
from queue import Full from queue import Queue, Full
from js8py import Js8Frame
from abc import ABC, ABCMeta, abstractmethod
import json import json
import threading import threading
@ -19,36 +22,54 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PoisonPill = object()
class Client(object):
class Client(ABC):
def __init__(self, conn): def __init__(self, conn):
self.conn = conn self.conn = conn
self.multiprocessingPipe = Queue(100) self.multithreadingQueue = Queue(100)
def mp_passthru(): def mp_passthru():
run = True run = True
while run: while run:
try: try:
data = self.multiprocessingPipe.get() data = self.multithreadingQueue.get()
self.send(data) if data is PoisonPill:
except (EOFError, OSError): run = False
else:
self.send(data)
self.multithreadingQueue.task_done()
except (EOFError, OSError, ValueError):
run = False run = False
except Exception:
logger.exception("Exception on client multithreading queue")
threading.Thread(target=mp_passthru).start() # unset the queue object to free shared memory file descriptors
self.multithreadingQueue = None
threading.Thread(target=mp_passthru, name="connection_mp_passthru").start()
def send(self, data): def send(self, data):
self.conn.send(data) try:
self.conn.send(data)
except IOError:
self.close()
def close(self): def close(self):
if self.multithreadingQueue is not None:
self.multithreadingQueue.put(PoisonPill)
self.conn.close() self.conn.close()
self.multiprocessingPipe.close()
def mp_send(self, data): def mp_send(self, data):
if self.multithreadingQueue is None:
return
try: try:
self.multiprocessingPipe.put(data, block=False) self.multithreadingQueue.put(data, block=False)
except Full: except Full:
self.close() self.close()
@abstractmethod
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
pass pass
@ -59,7 +80,25 @@ class Client(object):
self.close() self.close()
class OpenWebRxReceiverClient(Client): class OpenWebRxClient(Client, metaclass=ABCMeta):
def __init__(self, conn):
super().__init__(conn)
receiver_details = ReceiverDetails()
def send_receiver_info(*args):
receiver_info = receiver_details.__dict__()
self.write_receiver_details(receiver_info)
# TODO unsubscribe
receiver_details.wire(send_receiver_info)
send_receiver_info()
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
class OpenWebRxReceiverClient(OpenWebRxClient):
config_keys = [ config_keys = [
"waterfall_colors", "waterfall_colors",
"waterfall_min_level", "waterfall_min_level",
@ -67,7 +106,6 @@ class OpenWebRxReceiverClient(Client):
"waterfall_auto_level_margin", "waterfall_auto_level_margin",
"samp_rate", "samp_rate",
"fft_size", "fft_size",
"fft_fps",
"audio_compression", "audio_compression",
"fft_compression", "fft_compression",
"max_clients", "max_clients",
@ -93,28 +131,16 @@ class OpenWebRxReceiverClient(Client):
self.close() self.close()
raise raise
pm = PropertyManager.getSharedInstance()
self.setSdr() self.setSdr()
# send receiver info
receiver_keys = [
"receiver_name",
"receiver_location",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
]
receiver_details = dict((key, pm.getPropertyValue(key)) for key in receiver_keys)
receiver_details["locator"] = Locator.fromCoordinates(receiver_details["receiver_gps"])
self.write_receiver_details(receiver_details)
self.__sendProfiles()
features = FeatureDetector().feature_availability() features = FeatureDetector().feature_availability()
self.write_features(features) self.write_features(features)
modes = Modes.getModes()
self.write_modes(modes)
self.__sendProfiles()
CpuUsageThread.getSharedInstance().add_client(self) CpuUsageThread.getSharedInstance().add_client(self)
def __sendProfiles(self): def __sendProfiles(self):
@ -134,8 +160,12 @@ class OpenWebRxReceiverClient(Client):
self.startDsp() self.startDsp()
if "params" in message: if "params" in message:
params = message["params"] dsp = self.getDsp()
self.setDspProperties(params) if dsp is None:
logger.warning("DSP not available; discarding client data")
else:
params = message["params"]
dsp.setProperties(params)
elif message["type"] == "config": elif message["type"] == "config":
if "params" in message: if "params" in message:
@ -152,7 +182,7 @@ class OpenWebRxReceiverClient(Client):
if "params" in message: if "params" in message:
self.connectionProperties = message["params"] self.connectionProperties = message["params"]
if self.dsp: if self.dsp:
self.setDspProperties(self.connectionProperties) self.getDsp().setProperties(self.connectionProperties)
else: else:
logger.warning("received message without type: {0}".format(message)) logger.warning("received message without type: {0}".format(message))
@ -169,6 +199,7 @@ class OpenWebRxReceiverClient(Client):
next = SdrService.getFirstSource() next = SdrService.getFirstSource()
if next is None: if next is None:
# exit condition: no sdrs available # exit condition: no sdrs available
logger.warning("no more SDR devices available")
self.handleNoSdrsAvailable() self.handleNoSdrsAvailable()
return return
@ -184,25 +215,25 @@ class OpenWebRxReceiverClient(Client):
self.sdr = next self.sdr = next
self.startDsp() self.getDsp()
# keep trying until we find a suitable SDR # found a working sdr, exit the loop
if self.sdr.getState() == SdrSource.STATE_FAILED: if self.sdr.getState() != SdrSource.STATE_FAILED:
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
else:
break break
# send initial config logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName())
self.setDspProperties(self.connectionProperties) self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
configProps = ( # send initial config
self.sdr.getProps() self.getDsp().setProperties(self.connectionProperties)
.collect(*OpenWebRxReceiverClient.config_keys)
.defaults(PropertyManager.getSharedInstance()) stack = PropertyStack()
) stack.addLayer(0, self.sdr.getProps())
stack.addLayer(1, Config.get())
configProps = stack.filter(*OpenWebRxReceiverClient.config_keys)
def sendConfig(key, value): def sendConfig(key, value):
config = dict((key, configProps[key]) for key in OpenWebRxReceiverClient.config_keys) config = configProps.__dict__()
# TODO mathematical properties? hmmmm # TODO mathematical properties? hmmmm
config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"]
# TODO this is a hack to support multiple sdrs # TODO this is a hack to support multiple sdrs
@ -226,9 +257,7 @@ class OpenWebRxReceiverClient(Client):
self.write_sdr_error("No SDR Devices available") self.write_sdr_error("No SDR Devices available")
def startDsp(self): def startDsp(self):
if self.dsp is None and self.sdr is not None: self.getDsp().start()
self.dsp = DspManager(self, self.sdr)
self.dsp.start()
def close(self): def close(self):
self.stopDsp() self.stopDsp()
@ -247,18 +276,28 @@ class OpenWebRxReceiverClient(Client):
self.sdr.removeSpectrumClient(self) self.sdr.removeSpectrumClient(self)
def setParams(self, params): def setParams(self, params):
config = Config.get()
# allow direct configuration only if enabled in the config
if "configurable_keys" not in config:
return
keys = config["configurable_keys"]
if not keys:
return
# only the keys in the protected property manager can be overridden from the web # only the keys in the protected property manager can be overridden from the web
protected = ( stack = PropertyStack()
self.sdr.getProps() stack.addLayer(0, self.sdr.getProps())
.collect("samp_rate", "center_freq", "rf_gain", "type") stack.addLayer(1, config)
.defaults(PropertyManager.getSharedInstance()) protected = stack.filter(*keys)
)
for key, value in params.items(): for key, value in params.items():
protected[key] = value try:
protected[key] = value
except KeyError:
pass
def setDspProperties(self, params): def getDsp(self):
for key, value in params.items(): if self.dsp is None and self.sdr is not None:
self.dsp.setProperty(key, value) self.dsp = DspManager(self, self.sdr)
return self.dsp
def write_spectrum_data(self, data): def write_spectrum_data(self, data):
self.mp_send(bytes([0x01]) + data) self.mp_send(bytes([0x01]) + data)
@ -266,6 +305,9 @@ class OpenWebRxReceiverClient(Client):
def write_dsp_data(self, data): def write_dsp_data(self, data):
self.send(bytes([0x02]) + data) self.send(bytes([0x02]) + data)
def write_hd_audio(self, data):
self.send(bytes([0x04]) + data)
def write_s_meter_level(self, level): def write_s_meter_level(self, level):
self.send({"type": "smeter", "value": level}) self.send({"type": "smeter", "value": level})
@ -288,9 +330,6 @@ class OpenWebRxReceiverClient(Client):
def write_config(self, cfg): def write_config(self, cfg):
self.send({"type": "config", "value": cfg}) self.send({"type": "config", "value": cfg})
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
def write_profiles(self, profiles): def write_profiles(self, profiles):
self.send({"type": "profiles", "value": profiles}) self.send({"type": "profiles", "value": profiles})
@ -324,13 +363,44 @@ class OpenWebRxReceiverClient(Client):
def write_backoff_message(self, reason): def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason}) self.send({"type": "backoff", "reason": reason})
def write_js8_message(self, frame: Js8Frame, freq: int):
self.send({"type": "js8_message", "value": {
"msg": str(frame),
"timestamp": frame.timestamp,
"db": frame.db,
"dt": frame.dt,
"freq": freq + frame.freq,
"thread_type": frame.thread_type,
"mode": frame.mode
}})
class MapConnection(Client): def write_modes(self, modes):
def to_json(m):
res = {
"modulation": m.modulation,
"name": m.name,
"type": "digimode" if isinstance(m, DigitalMode) else "analog",
"requirements": m.requirements,
"squelch": m.squelch,
}
if m.bandpass is not None:
res["bandpass"] = {
"low_cut": m.bandpass.low_cut,
"high_cut": m.bandpass.high_cut
}
if isinstance(m, DigitalMode):
res["underlying"] = m.underlying
return res
self.send({"type": "modes", "value": [to_json(m) for m in modes]})
class MapConnection(OpenWebRxClient):
def __init__(self, conn): def __init__(self, conn):
super().__init__(conn) super().__init__(conn)
pm = PropertyManager.getSharedInstance() pm = Config.get()
self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__()) self.write_config(pm.filter("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__())
Map.getSharedInstance().addClient(self) Map.getSharedInstance().addClient(self)

View File

@ -1,168 +0,0 @@
import os
import mimetypes
import json
import pkg_resources
from datetime import datetime
from string import Template
from owrx.websocket import WebSocketConnection
from owrx.config import PropertyManager
from owrx.client import ClientRegistry
from owrx.connection import WebSocketMessageHandler
from owrx.version import openwebrx_version
from owrx.feature import FeatureDetector
from owrx.metrics import Metrics
import logging
logger = logging.getLogger(__name__)
class Controller(object):
def __init__(self, handler, request):
self.handler = handler
self.request = request
def send_response(self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None):
self.handler.send_response(code)
if content_type is not None:
self.handler.send_header("Content-Type", content_type)
if last_modified is not None:
self.handler.send_header("Last-Modified", last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT"))
if max_age is not None:
self.handler.send_header("Cache-Control", "max-age: {0}".format(max_age))
self.handler.end_headers()
if type(content) == str:
content = content.encode()
self.handler.wfile.write(content)
class StatusController(Controller):
def handle_request(self):
pm = PropertyManager.getSharedInstance()
# TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
vars = {
"status": "active",
"name": pm["receiver_name"],
"op_email": pm["receiver_admin"],
"users": ClientRegistry.getSharedInstance().clientCount(),
"users_max": pm["max_clients"],
"gps": pm["receiver_gps"],
"asl": pm["receiver_asl"],
"loc": pm["receiver_location"],
"sw_version": openwebrx_version,
"avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"),
}
self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))
class AssetsController(Controller):
def getModified(self, file):
return None
def openFile(self, file):
pass
def serve_file(self, file, content_type=None):
try:
modified = self.getModified(file)
if modified is not None and "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
)
if modified <= client_modified:
self.send_response("", code=304)
return
f = self.openFile(file)
data = f.read()
f.close()
if content_type is None:
(content_type, encoding) = mimetypes.MimeTypes().guess_type(file)
self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600)
except FileNotFoundError:
self.send_response("file not found", code=404)
def handle_request(self):
filename = self.request.matches.group(1)
self.serve_file(filename)
class OwrxAssetsController(AssetsController):
def openFile(self, file):
return pkg_resources.resource_stream("htdocs", file)
class AprsSymbolsController(AssetsController):
def __init__(self, handler, request):
pm = PropertyManager.getSharedInstance()
path = pm["aprs_symbols_path"]
if not path.endswith("/"):
path += "/"
self.path = path
super().__init__(handler, request)
def getFilePath(self, file):
return self.path + file
def getModified(self, file):
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
def openFile(self, file):
return open(self.getFilePath(file), "rb")
class TemplateController(Controller):
def render_template(self, file, **vars):
file_content = pkg_resources.resource_string("htdocs", file).decode("utf-8")
template = Template(file_content)
return template.safe_substitute(**vars)
def serve_template(self, file, **vars):
self.send_response(self.render_template(file, **vars), content_type="text/html")
def default_variables(self):
return {}
class WebpageController(TemplateController):
def template_variables(self):
header = self.render_template("include/header.include.html")
return {"header": header}
class IndexController(WebpageController):
def handle_request(self):
self.serve_template("index.html", **self.template_variables())
class MapController(WebpageController):
def handle_request(self):
# TODO check if we have a google maps api key first?
self.serve_template("map.html", **self.template_variables())
class FeatureController(WebpageController):
def handle_request(self):
self.serve_template("features.html", **self.template_variables())
class ApiController(Controller):
def handle_request(self):
data = json.dumps(FeatureDetector().feature_report())
self.send_response(data, content_type="application/json")
class MetricsController(Controller):
def handle_request(self):
data = json.dumps(Metrics.getSharedInstance().getMetrics())
self.send_response(data, content_type="application/json")
class WebSocketController(Controller):
def handle_request(self):
conn = WebSocketConnection(self.handler, WebSocketMessageHandler())
# enter read loop
conn.handle()

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