added docker compose and plugins for openwebrx installation

This commit is contained in:
Dustin Brunner
2025-11-02 14:51:28 +01:00
parent 5749b59c0d
commit 688f529b7a
51 changed files with 6047 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
services:
openwebrx:
image: 'slechev/openwebrxplus-softmbe:latest'
container_name: openwebrx_docker
restart: unless-stopped
ports:
- '8073:8073'
- '5678:5678'
environment:
TZ: Europe/Berlin
FORWARD_LOCALPORT_1234: 5678
# OPENWEBRX_ADMIN_USER: admin
# OPENWEBRX_ADMIN_PASSWORD: 123465
HEALTHCHECK_USB_1df7_3000: 1 # SDRPlay Healthcheck
HEALTHCHECK_USB_0bda_2838: 1 # NeSDR Smart Healthcheck
devices:
- /dev/bus/usb:/dev/bus/usb
volumes:
- ./etc:/etc/openwebrx
- ./var:/var/lib/openwebrx
- ./plugins:/usr/lib/python3/dist-packages/htdocs/plugins
# if you want your container to restart automatically if the HEALTHCHECK fails
# (see here: https://stackoverflow.com/a/48538213/420585)
openwebrx_autoheal:
restart: unless-stopped
container_name: openwebrx_docker_autoheal
image: willfarrell/autoheal
environment:
- AUTOHEAL_CONTAINER_LABEL=all
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restarter:
image: docker:cli
container_name: openwebrx_docker_restarter
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./restarter:/restarter
entrypoint: ["/bin/sh", "-c"]
command: ["chmod +x /restarter/script.sh && /restarter/script.sh"]
restart: unless-stopped

View File

@@ -0,0 +1,57 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Antenna Switcher"
permalink: /receiver/antenna_switcher
---
This is a `receiver` plugin to add antenna switching functionality for Raspberry Pi devices, providing logical levels on their GPIO ports that correspond to the user's antenna selection via buttons on the OWRX's front-end.
It consists of a **front-end** and **back-end** part.
The front-end is a standard OpenWebRX+ plugin and its installation does not differ from the standard way you [install plugins](/openwebrxplus-plugins/#load-plugins):
## I. Front-end installation
### Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/antenna_switcher/antenna_switcher.js');
```
#### Back-end URL
You probably don't want to change the default back-end instance URL, but if you do want to, you can do it from `init.js`, before or after the plugin loading, with:
`Plugins.antenna_switcher.API_URL = 'HOST:PORT/antenna_switch'`
### init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).
## II. Back-end installation
The back-end installation script **is designed to work with OpenWebRX+ installed from the repository as a package**. You need to adjust it in case you want to use it inside a Docker container.
For the back-end, download and run the [install_backend.sh](install_backend.sh) Bash script **as root**:
```sh
wget 'https://0xaf.github.io/openwebrxplus-plugins/receiver/antenna_switcher/install_backend.sh'
chmod +x ./install_backend.sh
sudo ./install_backend.sh
```
The script will download and install the Flask back-end in a Python virtual environment located in `/opt/antenna_switcher`, so you don't have to worry about libraries messing up your system's default Python installation.
It will also create a systemd service file called `antenna_switcher`, then start and enable it.
The nginx configuration will be extended to provide reverse proxying from the OpenWebRX front-end *(default port 8073)* to the Flask back-end *(port 8075, binding only on 127.0.0.1)*.
## Configuration
Things like which GPIO pins to raise high when the corresponding button from the front-end is selected can be configured in `/opt/antenna_switcher/antenna_switcher.cfg`.
```sh
sudo ${EDITOR-nano} /opt/antenna_switcher/antenna_switcher.cfg
```

View File

@@ -0,0 +1 @@
antenna_pins=[23,24]

View File

@@ -0,0 +1,24 @@
.openwebrx-ant-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: -5px -5px 0 0;
}
.openwebrx-ant-grid {
margin: 0;
white-space: nowrap;
flex: 1 0 38px;
margin: 5px 5px 0 0;
}
@supports(gap: 5px) {
.openwebrx-ant-grid {
margin: 0;
gap: 5px;
}
.openwebrx-ants-grid {
margin: 0;
}
}

View File

@@ -0,0 +1,98 @@
// Antenna switch UI plugin for OpenWebRX+
// License: MIT
// Original Example File Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
// Modified by DL9UL to provide UI buttons used to call a WebAPI
// Re-written by Dimitar Milkov, LZ2DMV to a more optimized and clean state
Plugins.antenna_switcher.API_URL ??= `${window.location.origin}/antenna_switch`;
// Init function of the plugin
Plugins.antenna_switcher.init = function () {
let antennaNum = 0;
const buttons = [];
// Function to send a command via POST
function sendCommand(command) {
fetch(Plugins.antenna_switcher.API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command }),
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
const response = data.payload.response;
const match = response.match(/^n:(\d)$/);
if (match) {
antennaNum = parseInt(match[1], 10);
if (!buttonsCreated) createButtons();
} else {
updateButtonState(response);
}
})
.catch(error => console.error('Error:', error));
}
// Function to update the button state based on the active antenna
function updateButtonState(activeAntenna) {
buttons.forEach((button, index) => {
button.classList.toggle('highlighted', (index + 1).toString() === activeAntenna);
});
}
// Create buttons and add them to the container
function createButtons() {
// Create antenna section
const antSection = document.createElement('div');
antSection.classList.add('openwebrx-section');
const antPanelLine = document.createElement('div');
antPanelLine.classList.add('openwebrx-ant', 'openwebrx-panel-line');
antSection.appendChild(antPanelLine);
const antGrid = document.createElement('div');
antGrid.classList.add('openwebrx-ant-grid');
antPanelLine.appendChild(antGrid);
for (let i = 1; i <= antennaNum; i++) {
const button = createButton(i);
buttons.push(button);
antGrid.appendChild(button);
}
// Section Divider to hide ANT panel
const antSectionDivider = document.createElement('div');
antSectionDivider.id = 'openwebrx-section-ant';
antSectionDivider.classList.add('openwebrx-section-divider');
antSectionDivider.onclick = () => UI.toggleSection(antSectionDivider);
antSectionDivider.innerHTML = "&blacktriangledown;&nbsp;Antenna";
// Append the container above the "openwebrx-section-modes"
const targetElement = document.getElementById('openwebrx-section-modes');
targetElement.parentNode.insertBefore(antSectionDivider, targetElement);
targetElement.parentNode.insertBefore(antSection, targetElement);
buttonsCreated = true;
}
function createButton(i) {
const button = document.createElement('div');
button.id = `owrx-ant-button-${i}`;
button.classList.add('openwebrx-button');
button.textContent = `ANT ${i}`;
button.onclick = () => sendCommand(String(i));
return button;
}
let buttonsCreated = false;
sendCommand('n');
sendCommand('s');
setInterval(() => sendCommand('s'), 2000);
return true;
};

View File

@@ -0,0 +1,103 @@
# Simple Flask backend to work with the JavaScript frontend for the antenna switcher by DL9UL
# License: Apache 2
# Copyright (c) 2024 Dimitar Milkov, LZ2DMV
# pip install flask flask-cors RPi.GPIO
from flask import Flask, request, jsonify
from flask_cors import CORS
import RPi.GPIO as GPIO
import os
config_file = 'antenna_switcher.cfg'
antenna_file = 'ant'
def load_config():
config = {}
if os.path.exists(config_file):
with open(config_file, 'r') as file:
for line in file:
name, value = line.strip().split('=')
if name == 'antenna_pins':
config[name] = eval(value)
return config
config = load_config()
antenna_pins = config.get('antenna_pins')
if antenna_pins is None:
raise ValueError("Configuration file is missing required values.")
num_antennas = len(antenna_pins)
app = Flask(__name__)
CORS(app)
GPIO.setmode(GPIO.BCM)
for pin in antenna_pins:
GPIO.setup(pin, GPIO.OUT)
def read_active_antenna():
if os.path.exists(antenna_file):
with open(antenna_file, 'r') as file:
return file.read().strip()
return None
def write_active_antenna(value):
with open(antenna_file, 'w') as file:
file.write(value)
def set_gpio_for_antenna(antenna_id):
for i, pin in enumerate(antenna_pins):
GPIO.output(pin, GPIO.HIGH if i == antenna_id else GPIO.LOW)
def initialize_antenna():
active_antenna = read_active_antenna()
if active_antenna and active_antenna.isdigit() and 1 <= int(active_antenna) <= num_antennas:
set_gpio_for_antenna(int(active_antenna) - 1)
else:
set_gpio_for_antenna(0)
write_active_antenna('1')
@app.route('/antenna_switch', methods=['POST'])
def antennaswitch():
data = request.get_json()
command = data.get('command')
if command.isdigit() and 1 <= int(command) <= num_antennas:
return set_antenna(command)
elif command == 's':
return get_active_antenna()
elif command == 'n':
return get_antenna_count()
else:
return jsonify({'error': 'Invalid command'}), 400
def get_active_antenna():
active_antenna = read_active_antenna()
if active_antenna is None:
return jsonify(payload={'response': '0'})
return jsonify(payload={'response': active_antenna})
def get_antenna_count():
return jsonify(payload={'response': f'n:{num_antennas}'})
def set_antenna(antenna_id):
active_antenna = read_active_antenna()
if active_antenna == antenna_id:
return jsonify(payload={'response': antenna_id})
try:
set_gpio_for_antenna(int(antenna_id) - 1)
write_active_antenna(antenna_id)
return jsonify(payload={'response': antenna_id})
except Exception as e:
print(f"Error: {e}")
return jsonify(payload={'response': '0'}), 500
if __name__ == '__main__':
try:
initialize_antenna()
app.run(host='127.0.0.1', port=8075)
finally:
GPIO.cleanup()

View File

@@ -0,0 +1,101 @@
#!/bin/bash
# Install script for the antenna_switcher backend.
# Assumes a Debian / Raspberry Pi OS / Ubuntu system with an APT
# package manager and a OpenWebRX+ installation from the repository.
#
# You would need to adjust the script if you want to use it inside
# a Docker container, but it is probably a bad idea anyway.
#
# License: Apache 2
# Copyright (c) 2024 Dimitar Milkov, LZ2DMV
apt-get update
apt-get install -y python3 python3-pip python3-venv nginx
# download backend
mkdir -p /opt/antenna_switcher
pushd /opt/antenna_switcher
repo=https://raw.githubusercontent.com/0xAF/openwebrxplus-plugins/main/receiver/antenna_switcher
wget --no-clobber -O antenna_switcher.py "$repo"/antenna_switcher.py
wget --no-clobber -O antenna_switcher.cfg "$repo"/antenna_switcher.cfg
echo "1" > ant
# prepare venv
python3 -m venv venv
source venv/bin/activate
pip install flask flask-cors RPi.GPIO
deactivate
popd
# systemd service
cat << _EOF_ > /etc/systemd/system/antenna_switcher.service
[Unit]
Description=Antenna Switcher Backend
After=network.target
[Service]
User=root
Group=root
WorkingDirectory=/opt/antenna_switcher
ExecStart=/opt/antenna_switcher/venv/bin/python3 -m antenna_switcher
[Install]
WantedBy=multi-user.target
_EOF_
systemctl daemon-reload
systemctl enable --now antenna_switcher
# nginx configuration
mkdir -p /etc/nginx/snippets
cat << _EOF_ > /etc/nginx/snippets/antenna_switcher.conf
set \$antennaBackend 127.0.0.1:8075;
location /antenna_switch {
proxy_pass http://\$antennaBackend;
proxy_http_version 1.1;
proxy_buffering off;
# required for websockets
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$http_connection;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
_EOF_
nginx_owrx_config="/etc/nginx/sites-available/openwebrx"
if [ -f "$nginx_owrx_config" ]; then
# some black magic with sed to add include line in the end of the server block
grep -qF 'include snippets/antenna_switcher.conf' $nginx_owrx_config || sed -ri.bak \
':a;N;$!ba;s/}[\s\r\n]*$/\n\tinclude snippets\/antenna_switcher.conf;\n}/' \
$nginx_owrx_config
systemctl restart varnish nginx
else
echo "You need to create your nginx site in /etc/nginx/sites-enabled and include snippets/antenna_switcher.conf."
echo "Sample config [/etc/nginx/sites-enabled/openwebrx]:"
cat << _EOF_
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
gzip off;
include snippets/snakeoil.conf;
set \$upstream 127.0.0.1:8073; # OWRX
location / {
proxy_pass http://\$upstream;
proxy_http_version 1.1;
proxy_buffering off;
# required for websockets
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection \$http_connection;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
include snippets/antenna_switcher.conf;
}
_EOF_
fi

View File

@@ -0,0 +1,23 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Colorful Spectrum"
permalink: /receiver/colorful_spectrum
---
This `receiver` plugin will colorify your spectrum analyzer.
## Preview
![spectrum](colorful_spectrum/colorful_spectrum.png "Preview")
## Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/colorful_spectrum/colorful_spectrum.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,65 @@
/*
* Plugin: colorify the spectrum analyzer.
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*/
// do not load CSS for this plugin
Plugins.colorful_spectrum.no_css = true;
Plugins.colorful_spectrum.init = async function () {
// Check if utils plugin is loaded
if (!Plugins.isLoaded('utils', 0.1)) {
// try to load the utils plugin
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js');
// check again if it was loaded successfully
if (!Plugins.isLoaded('utils', 0.1)) {
console.error('colorful_spectrum plugin depends on "utils >= 0.1".');
return false;
} else {
Plugins._debug('Plugin "utils" has been loaded as dependency.');
}
}
// wait for OWRX to initialize
$(document).on('event:owrx_initialized', function () {
Plugins.utils.wrap_func(
'draw',
function (orig, thisArg, args) {
return true;
},
function (res, thisArg, args) {
var vis_freq = get_visible_freq_range();
var vis_start = 0.5 - (center_freq - vis_freq.start) / bandwidth;
var vis_end = 0.5 - (center_freq - vis_freq.end) / bandwidth;
var data_start = Math.round(fft_size * vis_start);
var data_end = Math.round(fft_size * vis_end);
var data_width = data_end - data_start;
var data_height = Math.abs(thisArg.max - thisArg.min);
var spec_width = thisArg.el.offsetWidth;
var spec_height = thisArg.el.offsetHeight;
if (spec_width <= data_width) {
var x_ratio = data_width / spec_width;
var y_ratio = spec_height / data_height;
for (var x = 0; x < spec_width; x++) {
var data = (thisArg.data[data_start + ((x * x_ratio) | 0)]);
var y = (data - thisArg.min) * y_ratio;
thisArg.ctx.fillRect(x, spec_height, 1, -y);
if (data) {
var c = Waterfall.makeColor(data);
thisArg.ctx.fillStyle = "rgba(" +
c[0] + ", " + c[1] + ", " + c[2] + ", " +
(25 + y * 2) + "%)";
}
}
}
},
spectrum
);
});
return true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

View File

@@ -0,0 +1,29 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Connect/Disconnect notifications"
permalink: /receiver/connect_notify
---
This `receiver` plugin will:
* Send a chat message to all users when you connect/disconnect to SDR
* Show notification when another user is connected/disconnected to SDR
The plugin depends on [notify](https://0xaf.github.io/openwebrxplus-plugins/receiver/notify) plugin.
## Preview
![connect](connect_notify/connect_notify.png "Preview")
## Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/notify/notify.js');
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/connect_notify/connect_notify.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,56 @@
/*
* Plugin: User connect notification
*
* - Send a chat message to all users when you connect to SDR
* - Show notification when another user is connected to SDR
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*/
// no css for this plugin
Plugins.connect_notify.no_css = true;
// Initialize the plugin
Plugins.connect_notify.init = async function () {
if (!Plugins.isLoaded('notify', 0.1)) {
// try to load the notify plugin
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/notify/notify.js');
// check again if it was loaded successfully
if (!Plugins.isLoaded('notify', 0.1)) {
console.error('connect_notify plugin depends on "notify >= 0.1".');
return false;
} else {
Plugins._debug('Plugin "notify" has been loaded as dependency.');
}
}
Plugins.connect_notify.last = -1;
$(document).on('server:clients:after', function (e, data) {
var users = data - 1;
if (Plugins.connect_notify.last < 0) {
// this is our connection, so initialize.
Plugins.connect_notify.last = users;
// delay 100ms so the page initialize
setTimeout(function () {
var nick = LS.has('chatname') ? LS.loadStr('chatname') : 'Unknown';
Chat.sendMessage('Connected.', nick);
}, 100);
return;
}
if (users != Plugins.connect_notify.last) {
Plugins.notify.show('User ' + (
(users > Plugins.connect_notify.last) ? 'Connected' : 'Disconnected'
));
Plugins.connect_notify.last = users;
}
});
$(window).bind('beforeunload', function () {
var nick = LS.has('chatname') ? LS.loadStr('chatname') : 'Unknown';
Chat.sendMessage('Disconnected.', nick);
});
return true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

View File

@@ -0,0 +1,34 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Doppler"
permalink: /receiver/doppler
---
This `receiver` plugin will track the Doppler shift frequency of a chosen satellite. Useful for SSTV/Packet.
This plugin started as a port of [work](https://github.com/studentkra/OpenWebRX-Doppler) by [Sergey Osipov](https://github.com/studentkra).
Then I switched to [CelesTrak JSON API](https://celestrak.org/) and created Satellite Finder modal window.
## Preview
![doppler](doppler/doppler.png "Preview")
## Usage
1. Open the Satellite finder window to choose a satellite or enter the SatID if you know it.
2. Click TRACK.
The Satellite Finder window will help you find a satellite and will give useful information on each satellite.
![doppler1](doppler/doppler1.png "FindSat")
## Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/doppler/doppler.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,182 @@
:root {
--satellite-bg: #333333;
--satellite-fg: #ffffff;
--satellite-bg-bars: #575757;
--satellite-grad-1: #373737;
--satellite-grad-2: #4F4F4F;
}
body.has-theme {
--satellite-bg: var(--theme-color1);
--satellite-bg-bars: var(--theme-color2);
--satellite-grad-1: var(--theme-gradient-color1);
--satellite-grad-2: var(--theme-gradient-color2);
}
.blocker {
z-index: 2001 !important;
}
.modal {
z-index: 2002 !important;
}
.modal.satellite-modal {
height: 500px;
background-color: var(--satellite-bg);
color: var(--satellite-fg);
padding: 2rem 0.5rem;
}
.satellite-modal-header {
background-color: var(--satellite-bg-bars);
position: absolute;
top: 0;
left: 0;
right: 0;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
padding: 4px 10px;
font-variant: small-caps;
text-align: center;
}
.satellite-modal-body {
position: absolute;
top: 1.7rem;
bottom: 2.3rem;
left: 0;
right: 0;
padding: 0;
}
.satellite-modal-footer {
background-color: var(--satellite-bg-bars);
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
padding: 4px 10px;
display: flex;
justify-content: flex-end;
}
.satellite-modal-tabs-wrapper {
max-width: 50rem;
width: 100%;
margin: 0 auto;
}
.satellite-modal-tabs {
position: relative;
background-color: var(--satellite-bg-bars);
/* background: linear-gradient(to bottom, var(--satellite-grad-1) 0%, var(--satellite-grad-2) 100%); */
/* height: 14.75rem; */
}
.satellite-modal-tabs::before,
.satellite-modal-tabs::after {
content: "";
display: table;
}
.satellite-modal-tabs::after {
clear: both;
}
.satellite-modal-tab {
float: left;
}
.satellite-modal-tab-switch {
display: none;
}
.satellite-modal-tab-label {
position: relative;
display: block;
line-height: 1.75em;
height: 2em;
padding: 0 .6em;
/* background: linear-gradient(to bottom, var(--satellite-grad-1) 0%, var(--satellite-grad-2) 100%); */
background: var(--satellite-bg-bars);
border-right: 0.125rem solid var(--satellite-bg);
color: var(--satellite-fg);
cursor: pointer;
top: 0;
transition: all 0.25s;
}
.satellite-modal-tab-label:hover {
border-top: var(--satellite-bg) solid 1px;
top: -0.25rem;
transition: top 0.25s;
}
.satellite-modal-tab-content {
height: 12rem;
position: absolute;
z-index: 1;
top: 2em;
left: 0;
padding: .25rem .25rem;
background: linear-gradient(to bottom, var(--satellite-grad-1) 0%, var(--satellite-grad-2) 100%);
color: var(--satellite-fg);
/* border-bottom: 0.25rem solid var(--satellite-bg); */
opacity: 0;
transition: all 0.35s;
height: 404px;
width: calc(100% - .5rem);
}
.satellite-modal-tab-switch:checked+.satellite-modal-tab-label {
background: linear-gradient(to bottom, var(--satellite-grad-1) 0%, var(--satellite-grad-2) 100%);
color: var(--satellite-fg);
border-bottom: 0;
border-right: 0.125rem solid var(--satellite-bg);
transition: all 0.35s;
z-index: 1;
top: -0.0625rem;
}
.satellite-modal-tab-switch:checked+label+.satellite-modal-tab-content {
z-index: 2;
opacity: 1;
transition: all 0.35s;
}
.satellite-table {
width: 100%;
cursor: pointer;
}
.satellite-table>tbody>tr:hover {
background-color: var(--satellite-bg-bars);
filter: drop-shadow(3px 3px 10px black);
}
#satellite-name {
border: 0px solid;
width: 126px;
align-content: center;
text-align: center;
height: 15px;
overflow: hidden;
}
#satellite-row {
padding: 4px 0px;
justify-content: space-between;
}
#satellite-input {
width: 54px;
}
#satellite-track {
width: 48px;
text-align: center;
}

View File

@@ -0,0 +1,450 @@
/*
* Plugin: Doppler - Track satellite frequency (Doppler shift/effect)
*
* This plugin started as a port of Sergey Osipov work:
* https://github.com/studentkra/OpenWebRX-Doppler
* Then evolved.
*
* License: MIT
* Copyright (c) 2024 Stanislav Lechev [0xAF], LZ2SLL
*
* TODO:
* - Option to integrate sat bookmarks
* - Associate the bookmarks with modulation and SatID so it can be easily tracked, once bookmark is clicked.
* - Scan the LocalStorage and remove old groups.
*/
// no css for this plugin
// Plugins.doppler.no_css = true;
// Initialize the plugin
Plugins.doppler.init = async function () {
// Check if utils plugin is loaded
// if (!Plugins.isLoaded('utils', 0.3)) {
// console.error('Example plugin depends on "utils >= 0.3".');
// return false;
// }
// await Plugins._load_script('http://192.168.175.99:8080/doppler/sat.js').catch(function () {
await Plugins._load_script('https://0xaf.github.io/openwebrxplus-plugins/receiver/doppler/sat.js').catch(function() {
throw ("Cannot load satellite-js script.");
});
await Plugins._load_script('https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js').catch(function () {
throw ("Cannot load lz-string script.");
});
await Plugins._load_style('https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.css').catch(function () {
throw ("Cannot load jquery-modal style.");
});
await Plugins._load_script('https://cdnjs.cloudflare.com/ajax/libs/jquery-modal/0.9.1/jquery.modal.min.js').catch(function () {
throw ("Cannot load jquery-modal script.");
}).then(() => {
// $.modal.defaults.escapeClose = true;
// $.modal.defaults.clickClose = false;
// $.modal.defaults.showClose = false;
});
// initialize on load
if ($("#satellite-row").length < 1) {
$(".openwebrx-modes").after(`
<div id="satellite-row" class="openwebrx-panel-line openwebrx-panel-flex-line">
<input id="satellite-input" type="text" placeholder="Sat ID">
<div id="satellite-name" class="openwebrx-button">Open SAT Finder</div>
<div id="satellite-track" class="openwebrx-button">TRACK</div>
</div>
`);
var modalTabs = `
<div class="satellite-modal-tabs-wrapper">
<div class="satellite-modal-tabs">
`;
var groups = Object.keys(Plugins.doppler.satelliteGroups);
for (let i = 0; i < groups.length; i++) {
modalTabs += `
<div class="satellite-modal-tab">
<input type="radio" name="css-tabs" id="satellite-tab-${i}" ${i == 0 ? 'xxx-checked' : ''} onclick="Plugins.doppler.tabChange(${i}, '${groups[i]}')" class="satellite-modal-tab-switch" data-group="${groups[i]}">
<label for="satellite-tab-${i}" class="satellite-modal-tab-label">${groups[i]}</label>
<div class="satellite-modal-tab-content" id="satellite-tab-content-${i}">
<div class="openwebrx-panel" style="transform: none; padding:0; background: none;">
<select id="satellite-tab-content-${i}-select" class="openwebrx-panel-listbox" style="width: 70%; text-align: center" onchange="Plugins.doppler.selectChange(${i})">
</select>
<label style="">
Above <input id="satellite-tab-content-${i}-elevation" style="width: 8%" type="number" value="20" onchange="Plugins.doppler.selectChange(${i})" title="Find satellites above X degrees elevation"> °
</label>
<div id="satellite-tab-content-${i}-refresh" class="openwebrx-button" style="float: right;" onclick="Plugins.doppler.toggleRefresh(${i})" title="Refresh every 5sec">
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round" d="M18.6091 5.89092L15.5 9H21.5V3L18.6091 5.89092ZM18.6091 5.89092C16.965 4.1131 14.6125 3 12 3C7.36745 3 3.55237 6.50005 3.05493 11M5.39092 18.1091L2.5 21V15H8.5L5.39092 18.1091ZM5.39092 18.1091C7.03504 19.8869 9.38753 21 12 21C16.6326 21 20.4476 17.5 20.9451 13"/>
</svg>
</div>
</div>
<div id="satellite-tab-content-${i}-list" style="height: 370px; overflow-y: scroll">
<table class="satellite-table">
<thead>
<tr>
<th align="left">SatID</th>
<th align="center">Name</th>
<th align="center">Elev</th>
<th align="center">Az</th>
<th align="center">Dist</th>
<th align="right">Visible</th>
</tr>
</thead>
<tbody id="satellite-tab-content-${i}-tbody">
</tbody>
</table>
</div>
</div>
</div>
`;
}
modalTabs += `
</div>
</div>
`;
$('#satellite-row').append(`
<div id="satellite-modal" class="modal satellite-modal">
<div class="satellite-modal-header">
Satellite Finder (up to 2 hours)
</div>
<div class="satellite-modal-body">
${modalTabs}
<br><br><center style="vertical-align: middle">Select Category</center>
</div>
<div class="satellite-modal-footer">
<div class="openwebrx-button" rel="modal:close" onclick="$.modal.close()">Close</div>
</div>
</div>
`);
$('#satellite-modal').on($.modal.BEFORE_CLOSE, function(event, modal) {
if (Plugins.doppler.scanRunning !== undefined) {
Plugins.doppler.toggleRefresh(Plugins.doppler.scanRunning);
}
});
$("#satellite-name").click(() => {
// window.open("https://tle.ivanstanojevic.me/", "_blank");
$('#satellite-modal').modal({
escapeClose: true,
clickClose: false,
showClose: false,
});
});
$("#satellite-track").click(() => {
if (Plugins.doppler.intervalId) {
Plugins.doppler.stop_tracker();
return;
}
if (($('#satellite-input').val()).length < 1) {
Plugins.doppler.stop_tracker("NOT FOUND!");
return;
}
var satObj = null;
if (Plugins.doppler.lastGroupName) {
try {
var store = JSON.parse(LZString.decompress(LS.loadStr('satellites.' + Plugins.doppler.lastGroupName)));
satObj = store.data.find(obj => obj.NORAD_CAT_ID === parseInt($('#satellite-input').val() ,10));
Plugins.doppler.start_tracker(satObj);
} catch (e) { satObj = null; }
}
if (!satObj) {
fetch('https://celestrak.org/NORAD/elements/gp.php?CATNR=' + $('#satellite-input').val() + '&FORMAT=JSON')
.then(response => response.json())
.then(data2 => {
Plugins.doppler.start_tracker(data2[0])
})
.catch((error) => {
console.error(error);
Plugins.doppler.stop_tracker(error);
});
}
});
} // initialize
return true; // plugin init return
}
Plugins.doppler.stop_tracker = function (info) {
clearInterval(Plugins.doppler.intervalId);
Plugins.doppler.intervalId = undefined;
$('#satellite-track').removeClass('highlighted').text('TRACK');
$('#satellite-name').text((info && info.length) ? info : "Open SAT Finder");
}
Plugins.doppler.start_tracker = function (obj) {
// var satrec = satellite.twoline2satrec(line1, line2);
// var satrec = satellite.object2satrec(obj);
var satrec = satellite.json2satrec(obj);
if (satrec.error>0) {
Plugins.doppler.stop_tracker("Bad SAT Data");
return;
};
var asl = $('.webrx-rx-desc').text().match(/ASL:\s*(\d+)\s*m/);
asl = (asl && asl[1] && parseInt(asl[1]) > 0) ? parseInt(asl[1]) / 1000 : 0;
var receiverPos = Utils.getReceiverPos();
if (!receiverPos || !receiverPos.lat || !receiverPos.lon) {
Plugins.doppler.stop_tracker("Set Receiver Position");
return;
}
$("#satellite-name").text(obj.OBJECT_NAME);
$("#satellite-track").addClass('highlighted').text("STOP");
var demodulator = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator();
var startFreq = demodulator.get_offset_frequency() + center_freq;
Plugins.doppler.intervalId = setInterval(() => {
var demodulator = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator();
newFreq = Plugins.doppler.getDoppler(satrec, asl, receiverPos.lat, receiverPos.lon, startFreq);
demodulator.set_offset_frequency(newFreq - center_freq);
console.debug(`Doppler Freq: ${newFreq}`);
}, 1000);
}
Plugins.doppler.getDoppler = function (satrec, asl, lat, lon, center) {
var positionAndVelocity = satellite.propagate(satrec, new Date());
var positionEci = positionAndVelocity.position;
var velocityEci = positionAndVelocity.velocity;
var observerGd = {
longitude: satellite.degreesToRadians(lon),
latitude: satellite.degreesToRadians(lat),
height: asl
};
var gmst = satellite.gstime(new Date());
var positionEcf = satellite.eciToEcf(positionEci, gmst);
var observerEcf = satellite.geodeticToEcf(observerGd);
var velocityEcf = satellite.eciToEcf(velocityEci, gmst);
var dopplerFactor = satellite.dopplerFactor(observerEcf, positionEcf, velocityEcf);
return (Math.round(dopplerFactor * center));
}
Plugins.doppler.tabChange = function (id, tab) {
var options = `<option value="--empty--">-- Select group --`;
for (const [key, val] of Object.entries(Plugins.doppler.satelliteGroups[tab])) {
options += `<option value="${val}">${key}`;
}
var opts = $('#satellite-tab-content-' + id + '-select');
opts.empty().append(`${options}`);
}
Plugins.doppler.selectChange = async function (id) {
var groupName = $('#satellite-tab-content-' + id + '-select').val();
if (!groupName.length || groupName === '--empty--') { console.error('no sat group selected'); return; }
var elev = parseInt($('#satellite-tab-content-' + id +'-elevation').val(), 10);
if (isNaN(elev) || elev < 1) { console.error('bad elevation: '+elev); return; }
var store;
try { store = JSON.parse(LZString.decompress(LS.loadStr('satellites.' + groupName))); }
catch (e) { store = null; }
// if we don't have cached data, or it is more than 2h old, get new data
if (store === null || ((Math.floor(Date.now() / 1000) - store.last_sync) > (2 * 60 * 60))) {
await fetch('https://celestrak.org/NORAD/elements/gp.php?GROUP=' + groupName + '&FORMAT=JSON')
.then(response => response.json())
.then(data => {
store = {};
store.last_sync = Math.floor(Date.now() / 1000);
store.data = data;
LS.save('satellites.' + groupName, LZString.compress(JSON.stringify(store)));
})
.catch((error) => {
console.error(error);
});
}
var asl = $('.webrx-rx-desc').text().match(/ASL:\s*(\d+)\s*m/);
asl = (asl && asl[1] && parseInt(asl[1]) > 0) ? parseInt(asl[1]) / 1000 : 0;
var receiverPos = Utils.getReceiverPos();
var observerGd = (receiverPos && receiverPos.lat && receiverPos.lon) ? {
longitude: satellite.degreesToRadians(receiverPos.lon),
latitude: satellite.degreesToRadians(receiverPos.lat),
height: asl
} : null;
for (let i = 0; i < store.data.length; i++) {
if (observerGd === null) {
store.data[i].next_pass = 'Receiver position unknown.';
break;
}
// lets find if the sat will be visible in the next hour
// https://github.com/shashwatak/satellite-js/issues/56
let curDate = new Date();
// let satrec = satellite.object2satrec(store.data[i]);
let satrec = satellite.json2satrec(store.data[i]);
for (let m = 0; m < 120; m++) {
// check every minute for the next hour
var positionAndVelocity = satellite.propagate(satrec, curDate);
if (satrec.error > 0) {
console.error("Cant propagate. Bad satrec for " + store.data[i].OBJECT_NAME);
console.log(store.data[i]);
console.log(satrec);
break;
}
var gmst = satellite.gstime(curDate);
// var gmst = satellite.gstime(new Date());
var positionEci = positionAndVelocity.position;
var positionEcf = satellite.eciToEcf(positionEci, gmst);
var lookAngles = satellite.ecfToLookAngles(observerGd, positionEcf);
store.data[i].elevation = satellite.radiansToDegrees(lookAngles.elevation);
store.data[i].azimuth = satellite.radiansToDegrees(lookAngles.azimuth);
store.data[i].distance = lookAngles.rangeSat;
if (store.data[i].elevation > elev) {
store.data[i].next_pass = curDate;
break;
}
curDate.setMinutes(curDate.getMinutes() + 1); // add one minute
}
}
store.data.sort(function(a, b) {
if ((!a.next_pass || !(a.next_pass instanceof Date)) && (!b.next_pass || !(b.next_pass instanceof Date))) return 0;
if (!a.next_pass || !(a.next_pass instanceof Date)) return 1;
if (!b.next_pass || !(b.next_pass instanceof Date)) return -1;
return a.next_pass - b.next_pass;
});
var tableRows;
for (let i = 0; i < store.data.length; i++) {
const s = store.data[i];
let vis = "&gt;2h";
if (s.next_pass) {
if (s.next_pass <= new Date()) {
vis = "<b>*NOW*</b>";
} else {
vis = String(s.next_pass.getHours()).padStart(2, '0') + ":" + String(s.next_pass.getMinutes()).padStart(2, '0');
}
}
tableRows += `
<tr onclick="Plugins.doppler.selectSatellite(${s.NORAD_CAT_ID}, '${groupName}')">
<td align="left">${s.NORAD_CAT_ID}</td>
<td align="center" style="max-width:160px; width: 160px;">${s.OBJECT_NAME}</td>
<td align="center">${Math.round(s.elevation)}°</td>
<td align="center">${Math.round(s.azimuth)}°</td>
<td align="center">${Math.round(s.distance)}km</td>
<td align="right">${vis}</td>
</tr>
`;
}
$('#satellite-tab-content-' + id + '-tbody').empty().append(`${tableRows}`);
}
Plugins.doppler.selectSatellite = function (id, grp) {
Plugins.doppler.lastGroupName = grp;
Plugins.doppler.lastSatId = id;
$('#satellite-input').val(id);
if (Plugins.doppler.intervalId) Plugins.doppler.stop_tracker();
$.modal.close();
}
Plugins.doppler.toggleRefresh = function (id) {
const refresh = $('#satellite-tab-content-' + id + '-refresh');
if (Plugins.doppler.scanRunning === undefined) {
refresh.css({ animationName: 'openwebrx-scan-animation', animationDuration: '1s', animationIterationCount: 'infinite', filter: 'none'});
Plugins.doppler.scanRunning = id;
Plugins.doppler.selectChange(id);
Plugins.doppler.scanIntervalId = setInterval(() => {
Plugins.doppler.selectChange(Plugins.doppler.scanRunning);
}, 5000);
} else {
if (Plugins.doppler.scanRunning !== id) { // another scan is running
Plugins.doppler.toggleRefresh(Plugins.doppler.scanRunning);
} else {
clearInterval(Plugins.doppler.scanIntervalId);
Plugins.doppler.scanIntervalId = undefined;
Plugins.doppler.scanRunning = undefined;
refresh.css({ animationName: ''});
}
}
}
// cSpell:disable
// Groups: https://celestrak.org/NORAD/elements/index.php?FORMAT=json
Plugins.doppler.satelliteGroups = {
'HAM': { // our custom set
'Space Stations': 'stations',
'Amateur Radio': 'amateur',
'Weather': 'weather',
'NOAA': 'noaa',
},
'Special': {
'Last 30 Days\' Launches': 'last-30-days',
'Space Stations': 'stations',
'100 (or so) Brightest': 'visual',
'Active Satellites': 'active',
'Analyst Satellites': 'analyst',
'Russian ASAT Test Debris (COSMOS 1408)': 'cosmos-1408-debris',
'Chinese ASAT Test Debris (FENGYUN 1C)': 'fengyun-1c-debris',
'IRIDIUM 33 Debris': 'iridium-33-debris',
'COSMOS 2251 Debris': 'cosmos-2251-debris',
},
'Weather': {
'Weather': 'weather',
'NOAA': 'noaa',
'GOES': 'goes',
'Earth Resources': 'resource',
'Search & Rescue (SARSAT)': 'sarsat',
'Disaster Monitoring': 'dmc',
'Tracking and Data Relay Satellite System (TDRSS)': 'tdrss',
'ARGOS Data Collection System': 'argos',
'Planet': 'planet',
'Spire': 'spire',
},
'Comms': {
'Active Geosynchronous': 'geo',
'GEO Protected Zone': 'gpz',
'GEO Protected Zone Plus': 'gpz-plus',
'Intelsat': 'intelsat',
'SES': 'ses',
'Eutelsat': 'eutelsat',
'Iridium': 'iridium',
'Iridium NEXT': 'iridium-NEXT',
'Starlink': 'starlink',
'OneWeb': 'oneweb',
'Orbcomm': 'orbcomm',
'Globalstar': 'globalstar',
'Swarm': 'swarm',
'Amateur Radio': 'amateur',
'SatNOGS': 'satnogs',
'Experimental Comm': 'x-comm',
'Other Comm': 'other-comm',
'Gorizont': 'gorizont',
'Raduga': 'raduga',
'Molniya': 'molniya',
},
'Nav': {
'GNSS': 'gnss',
'GPS Operational': 'gps-ops',
'GLONASS Operational': 'glo-ops',
'Galileo': 'galileo',
'Beidou': 'beidou',
'Satellite-Based Augmentation System (WAAS/EGNOS/MSAS)': 'sbas',
'Navy Navigation Satellite System (NNSS)': 'nnss',
'Russian LEO Navigation': 'musson',
},
'Science': {
'Space & Earth Science': 'science',
'Geodetic': 'geodetic',
'Engineering': 'engineering',
'Education': 'education',
},
'Misc': {
'Miscellaneous Military': 'military',
'Radar Calibration': 'radar',
'CubeSats': 'cubesat',
'Other Satellites': 'other',
},
};
// cSpell:enable

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Example plugin (for devs)"
permalink: /receiver/example
---
This `receiver` plugin is an example for other plugin developers.
**There is no point in enabling this plugin.**
### Code
Code is in my [Github repo](https://github.com/0xAF/openwebrxplus-plugins/tree/main/receiver/example)

View File

@@ -0,0 +1,153 @@
/*
* example Receiver Plugin for OpenWebRx+
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*/
// Disable CSS loading for this plugin
Plugins.example.no_css = true;
// remove the next line if you really want to use this plugin
throw ("This is the example plugin. It is not made for real world use.");
// Init function of the plugin
Plugins.example.init = function () {
// Check if utils plugin is loaded
if (!Plugins.isLoaded('utils', 0.1)) {
console.error('Example plugin depends on "utils >= 0.1".');
return false;
}
// Listen to profile change and print the new profile name to console.
// NOTE: you cannot manipulate the data in events, you will need to wrap the original
// function if you want to manipulate data.
$(document).on('event:profile_changed', function (e, data) {
console.log('profile change event: ' + data);
});
// Another events:
// event:owrx_initialized - called when OWRX is initialized
// Server events are triggered when server sends data over the WS
// All server events have suffix ':before' or ':after', based on the original function call.
// :before events are before the original function call,
// :after events are after the original function call.
// Some interesting server events:
// server:config - server configuration
// server:bookmarks - the bookmarks from server
// server:clients - clients number change
// server:profiles - sdr profiles
// server:features - supported features
// Modify an existing OWRX function with utils plugin.
// See utils.js for documentation on wrap method.
// This will wrap profile changing function
Plugins.utils.wrap_func(
// function to wrap around
'sdr_profile_changed',
// before callback, to be run before the original function
// orig = original function
// thisArg = thisArg for the original function
// args = the arguments for the original function
// If you call the original function here (in the before_cb), always return false,
// so the wrap_func() will not call it later again.
// example of calling the original function: orig.apply(thisArg, args);
function (orig, thisArg, args) {
console.log("Before callback for: " + orig.name);
// check if newly selected profile is the PMR profile
if ($('#openwebrx-sdr-profiles-listbox').find(':selected').text() === "[RTL] 446 PMR") {
// prevent changing to this profile
console.log('This profile is disabled by proxy function');
// restore the previous selected profile
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
// return false to prevent execution of original function
return false;
}
// return true to allow execution of original function
return true;
},
// after callback, to be run after the original function,
// but only if the before callback returns true
// res = result of the original function, if any
function (res) {
console.log('profile changed.');
}
);
// this example will do the same (stop profile changing), but using another method
// replace the "onchange" handler of the profiles selectbox
// and call the original function "sdr_profile_changed"
$('#openwebrx-sdr-profiles-listbox')[0].onchange = function (e) {
// check the index of the selected profile (0 is the first profile in the list)
if (e.target.options.selectedIndex === 0) {
// prevent changing to this profile
console.log('This profile is disabled by onchange.');
// restore the previous profile
$('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString());
e.preventDefault();
e.stopPropagation();
return false;
}
// otherwise, call the original function
sdr_profile_changed();
};
// this example will manipulate bookmarks data when the server sends the bookmarks
// We will wrap the bookmarks.replace_bookmarks() function, once OWRX is initialized.
// We cannot wrap the replace_bookmarks() function before the bookmarks object is created.
// So we wait for OWRX to initialize and then wrap the function.
$(document).on('event:owrx_initialized', function () {
// Call the wrap method of utils plugin
Plugins.utils.wrap_func(
// function to wrap
'replace_bookmarks',
// before callback
function (orig, thisArg, args) {
// check if the bookmarks are "server bookmarks"
if (args[1] === 'server') {
// check if we have array of bookmarks (will be empty if the profile has no bookmarks to show)
if (typeof (args[0]) === 'object' && args[0].length)
// replace the name of the first bookmark
args[0][0].name = 'replaced';
}
// now call the original function
orig.apply(thisArg, args);
// and return false, so the wrap_func() will not call the original for second time
return false;
},
// after callback
function (res) {
/* not executed because the before function returns false always */
},
// this is the object, where the replace_bookmarks() function should be found
bookmarks
);
});
// return true for plugin init()
return true;
} // end of init function

View File

@@ -0,0 +1,11 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Example Theme plugin (for devs)"
permalink: /receiver/example_theme
---
This `receiver` plugin is an example for other plugin/theme developers.
**There is no point in enabling this plugin.**
### Code
Code is in my [Github repo](https://github.com/0xAF/openwebrxplus-plugins/tree/main/receiver/example_theme)

View File

@@ -0,0 +1,12 @@
/*
* colors for the new theme
*/
body.theme-eye-piercer {
--theme-color1: #ff6262;
--theme-color2: #ff626252;
--theme-gradient-color1: #ff6262;
--theme-gradient-color2: #ff0000;
}

View File

@@ -0,0 +1,17 @@
/*
* example plugin, creating a new theme for OpenWebRx+
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*/
// Add new entry in the Theme selectbox
$('#openwebrx-themes-listbox').append(
$('<option>').val(
// give it a value. you will need this for the css styles
"eye-piercer"
).text(
// lets name it
'Eye-Piercer'
)
);

View File

@@ -0,0 +1,27 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Frequency Far Jump"
permalink: /receiver/frequency_far_jump
---
This a simple `receiver` plugin to allow jumping to a frequency outside the boundary of the currently selected profile, by typing it in the receiver's frequency dial.
**Beware of the limitations of this approach:** the modulation and the other settings of the receiver **will stay the same** when jumping to the new, far frequency.
Please note that you **must** enable *"Allow users to change center frequency"* and in case you have set a **magic key**, you will have to provide it with a '*#key=[KEY]*' at the end of the URL.
The plugin depends on [utils](https://0xaf.github.io/openwebrxplus-plugins/receiver/utils) plugin.
## Load
Add this lines in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js').then(async function () {
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/frequency_far_jump/frequency_far_jump.js');
});
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,58 @@
/*
* Plugin: Jump to a frequency outside the boundaries of the selected profile by typing it in the receiver's frequency dial.
* Requires the option 'Allow users to change center frequency' to be enabled in the admin panel.
* Beware of the limitations of this approach: the modulation and the other settings of the receiver will stay the same when jumping to the new, far frequency.
*
* Please note that you must supply the magic key for your OpenWebRX+ instance if you have one configured with a '#key=[KEY]' at the end of the URL.
*
* License: Apache License 2.0
* Copyright (c) 2024 Dimitar Milkov, LZ2DMV
*/
Plugins.frequency_far_jump.no_css = true;
Plugins.frequency_far_jump.init = async function () {
if (!Plugins.isLoaded('utils', 0.1)) {
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js');
if (!Plugins.isLoaded('utils', 0.1)) {
console.error('Plugin "frequency_far_jump" depends on "utils >= 0.1".');
return false;
} else {
Plugins._debug('Plugin "utils" has been loaded as dependency.');
}
}
Plugins.utils.wrap_func(
'set_offset_frequency',
function (orig, thisArg, args) {
var to_what = Math.round(args[0]);
if (typeof(to_what) == 'undefined') return;
// The frequency is outside the boundaries of the current profile
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) {
// to_what is an offset, so we need to add the current full frequency (center_freq) to it
var f = center_freq + to_what;
var k = $('#openwebrx-panel-receiver').demodulatorPanel().getMagicKey();
// Ask the backend over the WS to switch the frequency for us
ws.send(JSON.stringify({
"type": "setfrequency", "params": { "frequency": f, "key": k }
}));
} else {
// The frequency is within the boundaries of the current profile,
// just use the original set_offset_frequency
orig.apply(thisArg, args);
}
return false;
},
null,
Demodulator.prototype
);
return true;
}

View File

@@ -0,0 +1,23 @@
// Receiver plugins initialization.
// everything after '//' is a comment.
// uncomment the next line to enable plugin debugging in browser console.
// Plugins._enable_debug = true;
// base URL for receiver plugins
const rp_url = 'https://0xaf.github.io/openwebrxplus-plugins/receiver';
// First load the utils, needed for some plugins
Plugins.load('utils').then(async function () {
// Load the notification plugin, used by some plugins. await to ensure it is loaded before the rest.
await Plugins.load('notify');
// load remote plugins
Plugins.load('colorful_spectrum');
Plugins.load('doppler');
Plugins.load('frequency_far_jump');
Plugins.load('keyboard_shortcuts');
Plugins.load('magic_key');
Plugins.load('frequency_far_jump');
});

View File

@@ -0,0 +1,31 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Keyboard Shortcuts"
permalink: /receiver/keyboard_shortcuts
---
This `receiver` plugin will add keyboard shortcuts to your OWRX+.
The plugin depends on [notify](https://0xaf.github.io/openwebrxplus-plugins/receiver/notify) plugin.
**OWRX+ v1.2.67 has this plugin integrated, hence the plugin will not install, even if loaded.**
## preview
![shortcuts](shortcuts.png "Preview")
## Usage
To show help screen, press `?`.
## Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/notify/notify.js');
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/keyboard_shortcuts/keyboard_shortcuts.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,99 @@
/*
* Plugin: Keyboard Shortcuts
*
* Add keyboard shortcuts to OWRX interface.
* press '?' to see help.
*
*/
/* import keyboard-css for keys in help */
@import url("https://unpkg.com/keyboard-css@1.2.4/dist/css/main.min.css");
#ks-overlay {
position: fixed;
top: 30vh;
left: calc((100vw - 800px) / 2);
max-height: 60vh;
width: 800px;
color: white;
background-color: #000;
opacity: 0.8;
z-index: 10000;
border: 3px solid white;
border-radius: 20px;
display: flex;
align-items: center;
flex-direction: column;
padding: 0.5rem;
}
.ks-title {
font-weight: bold;
font-size: 1.5rem;
margin-top: 1rem;
}
.ks-subtitle {
font-size: 0.8rem;
margin-top: 0.25rem;
}
.ks-separator {
border-top: 1px dotted white;
align-self: stretch;
margin: 1rem;
}
.ks-content {
padding: 0 0.75rem;
font-size: 1rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
overflow-y: auto;
}
.ks-item {
width: 240px;
padding: 0.25rem;
border: 1px dashed #444;
margin-bottom: 0.25rem;
display: flex;
justify-content: space-between;
flex-wrap: nowrap;
align-items: center;
flex-direction: column;
/* flex-direction: row; */
}
.ks-item-kbd {
text-wrap: nowrap;
}
a.kbc-button-xs,
button.kbc-button-xs {
padding: 0.02rem 0.25rem;
}
a.kbc-button-sm,
button.kbc-button-sm {
padding: 0 0.5rem;
font-size: .85rem;
}
/* we are using keyboard-css now, so no need of our key styling
.ks-item kbd {
display: inline-block;
padding: 3px 5px;
font: 12px monospace;
font-weight: bold;
line-height: normal;
vertical-align: middle;
color: white;
background-color: #555;
border: 1px solid #aaa;
border-radius: 6px;
box-shadow: inset -0px -2px 0px 0px #aaa;
} */

View File

@@ -0,0 +1,360 @@
/*
* Plugin: Keyboard Shortcuts
*
* Add keyboard shortcuts to OWRX receiver interface.
* press '?' for help.
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*/
// Plugin version
Plugins.keyboard_shortcuts._version = 0.3;
// Initialize the plugin
Plugins.keyboard_shortcuts.init = async function () {
if (window['Shortcuts'] && typeof (window.Shortcuts) === 'function') {
console.error('This OWRX+ installation already have Keyboard Shortcuts. This plugin will not load.');
return false;
}
if (!Plugins.isLoaded('notify', 0.1)) {
// try to load the notify plugin
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/notify/notify.js');
// check again if it was loaded successfully
if (!Plugins.isLoaded('notify', 0.1)) {
console.error('Keyboard shortcuts plugin depends on "notify >= 0.1".');
return false;
} else {
Plugins._debug('Plugin "notify" has been loaded as dependency.');
}
}
// create the help overlay
Plugins.keyboard_shortcuts.overlay = jQuery('<div id="ks-overlay"></div>');
Plugins.keyboard_shortcuts.overlay.hide();
Plugins.keyboard_shortcuts.overlay.appendTo(document.body);
// catch all key presses
$(document).on('keydown', function (e) {
// check if we are focusing an input
var on_input = !!($('input:focus').length && ($('input:focus')[0].type === 'text' || $('input:focus')[0].type === 'number'));
// var on_input = !!($(':focus').is('input:text'));
// handle the global shortcuts, which will work even if an input is focused
// please use modifier keys (like ctrl and alt) always
var handled = false;
switch (String(e.key).toLowerCase()) {
// open chat
case 'c':
if (e.metaKey) {
$('.button[data-toggle-panel="openwebrx-panel-log"').click();
setTimeout(function () {
$('#openwebrx-chat-message').focus();
}, 100);
handled = true;
Plugins.notify.show('CHAT: toggle');
e.preventDefault();
e.stopPropagation();
}
break;
// open map
case 'm':
if (e.metaKey) {
var z = $('a.button[href="map"]');
$('a.button[target="openwebrx-map"]')[0].click();
handled = true;
Plugins.notify.show('MAP: open');
e.preventDefault();
e.stopPropagation();
}
break;
// toggle panes
case ' ':
if (e.metaKey) {
handled = true;
if ($('#openwebrx-panel-receiver').is(':hidden')) {
toggle_panel('openwebrx-panel-receiver', true);
toggle_panel('openwebrx-panel-status', true);
// toggle_panel('openwebrx-panel-log', true);
} else {
toggle_panel('openwebrx-panel-receiver', false);
toggle_panel('openwebrx-panel-status', false);
toggle_panel('openwebrx-panel-log', false);
}
Plugins.notify.show('Toggle panels');
e.preventDefault();
e.stopPropagation();
}
break;
}
// these shortcuts will be handled only when no input is focused
if (!on_input && !handled)
switch (String(e.key).toLowerCase()) {
// hide help
case 'escape':
Plugins.keyboard_shortcuts.overlay.slideUp(100);
break;
// show/hide help
case '?':
Plugins.keyboard_shortcuts.overlay.slideToggle(100);
Plugins.notify.show('HELP: toggle');
break;
// change to previous profile
case ',':
var sel = $('#openwebrx-sdr-profiles-listbox');
var prev_val = sel.find(':selected').prev().val();
if (prev_val) sel.val(prev_val).change();
Plugins.notify.show('PROFILE: -');
break;
// change to next profile
case '.':
var sel = $('#openwebrx-sdr-profiles-listbox');
var next_val = sel.find(':selected').next().val();
if (next_val) sel.val(next_val).change();
Plugins.notify.show('PROFILE: +');
break;
// change 10 profiles behind
case '<':
var sel = $('#openwebrx-sdr-profiles-listbox');
var prev_el = sel.find(':selected'); // get current (option) element
var last_val;
for (var i = 0;
(i < 10 && prev_el.val()); i++) {
prev_el = prev_el.prev();
if (prev_el && prev_el.val()) last_val = prev_el.val();
}
if (last_val) sel.val(last_val).change();
Plugins.notify.show('PROFILE: -10');
break;
// change 10 profile ahead
case '>':
var sel = $('#openwebrx-sdr-profiles-listbox');
var next_el = sel.find(':selected'); // get current (option) element
var last_val;
for (var i = 0;
(i < 10 && next_el.val()); i++) {
next_el = next_el.next();
if (next_el && next_el.val()) last_val = next_el.val();
}
if (last_val) sel.val(last_val).change();
Plugins.notify.show('PROFILE: +10');
break;
// open frequency input
case 'f':
$('.webrx-actual-freq').frequencyDisplay().inputGroup.click().focus();
break;
// toggle mute
case 'm':
UI.toggleMute();
Plugins.notify.show('MUTE: toggle');
break;
// change volume
case '-':
case '+':
case '=':
var vol = $('#openwebrx-panel-volume');
vol.val(parseInt(vol.val()) + (e.key === '-' ? -1 : +1)).change();
Plugins.notify.show('VOL: ' + (e.key === '-' ? '-' : '+'));
break;
// change squelch
case ';':
case '\'':
var sql = $('.openwebrx-squelch-slider');
sql.val(parseInt(sql.val()) + (e.key === ';' ? -1 : +1)).change();
Plugins.notify.show('SQL: ' + (e.key === ';' ? '-' : '+'));
break;
// auto set squelch / start scanner
case 's':
$('.openwebrx-squelch-auto').trigger(e.shiftKey ? 'contextmenu' : 'click');
Plugins.notify.show((e.shiftKey ? 'SCANNER: toggle' : 'SQL: auto'));
break;
// update waterfall colors
case 'w':
$('#openwebrx-waterfall-colors-auto').trigger(e.shiftKey ? 'contextmenu' : 'click');
Plugins.notify.show((e.shiftKey ? 'WATERFALL: continuous' : 'WATERFALL: auto'));
break;
// zoom controls
case 'z':
e.shiftKey ? zoomInTotal() : zoomInOneStep();
Plugins.notify.show((e.shiftKey ? 'ZOOM: ++' : 'ZOOM: +'));
break;
case 'x':
e.shiftKey ? zoomOutTotal() : zoomOutOneStep();
Plugins.notify.show((e.shiftKey ? 'ZOOM: --' : 'ZOOM: -'));
break;
// bookmarks
case '{':
case '}':
var bms = $('#openwebrx-bookmarks-container .bookmark').find('.bookmark-content');
var idx = Plugins.keyboard_shortcuts.bookmarkIdx;
idx = typeof idx !== 'undefined' || idx == -1 ? idx : (e.key === '{' ? bms.length : -1);
if (bms.length) {
idx += (e.key === '{') ? -1 : 1; // change index
idx = Math.min(Math.max(idx, 0), bms.length - 1); // limit to min/max
bms.eq(idx).click();
Plugins.keyboard_shortcuts.bookmarkIdx = parseInt(idx);
Plugins.notify.show('BOOKMARK[' + (parseInt(idx) + 1) + ']: ' + bms.eq(idx).text());
}
break;
// enter change freq by step
case '[':
case ']':
tuneBySteps(e.key === '[' ? -1 : 1);
Plugins.notify.show('TUNE: ' + (e.key === '[' ? '-' : '+'));
break;
// add bookmark
case 'b':
$('.openwebrx-bookmark-button').trigger('click');
Plugins.notify.show('Add bookmark');
e.preventDefault();
e.stopPropagation();
break;
}
});
function gen_key(key) {
var keymap = {
',': ', <b style="font-size: 0.7rem">comma</b>',
'.': '. <b style="font-size: 0.7rem">dot</b>',
';': '; <b style="font-size: 0.7rem">semicolon</b>',
'\'': '\' <b style="font-size: 0.7rem">apostrophe</b>',
'SHIFT': '&#8679; Shift',
'CONTROL': '&#8963; Ctrl',
'COMMAND': '&#8984; Cmd',
'META': '&#8984; Meta',
'ALT': '&#8997; Alt',
'OPTION': '&#8997; Opt',
'ENTER': '&crarr; Enter',
'RETURN': '&crarr; Enter',
'DELETE': '&#8998; Del',
'BACKSPACE': '&#9003; BS',
'ESCAPE': '&#9099; ESC',
'ARROWRIGHT': '&rarr;',
'ARROWLEFT': '&larr;',
'ARROWUP': '&uarr;',
'ARROWDOWN': '&darr;',
'PAGEUP': '&#8670; PgUp',
'PAGEDOWN': '&#8671; PgDn',
'HOME': '&#8598; Home',
'END': '&#8600; End',
'TAB': '&#8677; Tab',
'SPACE': '&#9251; Space',
'INTERVAL': '&#9251; Space',
};
var k = keymap[key.toUpperCase()] || key.toUpperCase();
return `<button class="kbc-button kbc-button-sm" title="${key}"><b>${k}</b></button>`;
}
// fill the help overlay
// i'm not using overlay.html('') on purpose
// vscode syntax highlighting with 'nicolasparada.innerhtml' extension is working this way
Plugins.keyboard_shortcuts.overlay[0].innerHTML = `
<div class="ks-title">Keyboard shortcuts</div>
<div class="ks-subtitle">Hide this help with '?' or Escape.</div>
<div class="ks-separator"></div>
<div class="ks-content">
<div class="ks-item">
<div class="ks-item-kbd">${gen_key(',')}|${gen_key('.')}</div>
<div class="ks-item-txt">change profile</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('Z')}|${gen_key('X')}</div>
<div class="ks-item-txt">zoom IN/OUT 1 step </div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('W')}</div>
<div class="ks-item-txt">auto set waterfall colors</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('&lt;')}|${gen_key('&gt;')}</div>
<div class="ks-item-txt">change profile by 10</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('Shift')}+${gen_key('Z')}|${gen_key('X')}</div>
<div class="ks-item-txt">zoom IN/OUT full</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('Shift')}+${gen_key('W')}</div>
<div class="ks-item-txt">continuous set waterfall colors</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('M')}|${gen_key('-')}|${gen_key('+/=')}</div>
<div class="ks-item-txt">toggle mute or change volume</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('S')}|${gen_key(';')}|${gen_key('\'')}</div>
<div class="ks-item-txt">auto set or change squelch</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('Shift')}+${gen_key('S')}</div>
<div class="ks-item-txt">toggle scanner</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('Meta')}+${gen_key('C')}</div>
<div class="ks-item-txt">toggle chat panel</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('Meta')}+${gen_key('M')}</div>
<div class="ks-item-txt">open MAP</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('Meta')}+${gen_key('Space')}</div>
<div class="ks-item-txt">toggle all panel</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('{')}|${gen_key('}')}</div>
<div class="ks-item-txt">change bookmark</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('[')}|${gen_key(']')}</div>
<div class="ks-item-txt">tune freq by step</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('B')}</div>
<div class="ks-item-txt">add local bookmark</div>
</div>
<div class="ks-item">
<div class="ks-item-kbd">${gen_key('F')}</div>
<div class="ks-item-txt">freq input</div>
</div>
</div>
`;
// reset bookmark index on profile change.
// this will work only if 'utils' plugin is loaded, but it's not a requirement
$(document).on('event:profile_changed', function (e, data) {
Plugins.keyboard_shortcuts.bookmarkIdx = -1;
});
return true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

View File

@@ -0,0 +1,23 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: MagicKey"
permalink: /receiver/magic_key
---
This `receiver` plugin will allow you to set the MagicKey without typing it in the browser's address bar.
## Preview
![magic_key](magic_key/magic_key.png "Preview")
## Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/magic_key/magic_key.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,63 @@
/*
* Plugin: MagicKey - Set the MagicKey without typing it in browser's address bar
*
* License: MIT
* Copyright (c) 2024 Stanislav Lechev [0xAF], LZ2SLL
*/
// no css for this plugin
Plugins.magic_key.no_css = true;
// Initialize the plugin
Plugins.magic_key.init = async function () {
// Check if utils plugin is loaded
if (!Plugins.isLoaded('utils', 0.3)) {
console.error('Example plugin depends on "utils >= 0.3".');
return false;
}
if ($("#setMagicKeyBtn").length < 1) {
$(".frequencies-container").after(`
<div id="magic-key-line" class="openwebrx-panel-line openwebrx-panel-flex-line" style="display: none">
<input id="magic-key-input" type="text" placeholder="Magic Key">
<div id="magic-key-set" class="openwebrx-button">SET</div>
</div>
`);
$(".openwebrx-bookmark-button").before(`
<div id="setMagicKeyBtn" class="openwebrx-button openwebrx-square-button" style="width: 27px; height: 27px; text-align: center;">
<svg fill="#FFFFFF" width="27px" height="27px" viewBox="-18.91 0 122.88 122.88" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"><g>
<path d="M60.78,43.44c-1.49,0.81-3.35,0.26-4.15-1.22c-0.81-1.49-0.26-3.35,1.23-4.15c7.04-3.82,10.32-8.76,10.98-13.59 c0.35-2.58-0.05-5.17-1.02-7.57c-0.99-2.43-2.56-4.64-4.55-6.42c-3.87-3.46-9.3-5.28-14.97-3.87c-2.3,0.57-4.29,1.72-6.03,3.34 c-1.85,1.72-3.45,3.97-4.85,6.63c-0.79,1.5-2.64,2.07-4.13,1.29c-1.5-0.79-2.07-2.64-1.29-4.13c1.72-3.26,3.73-6.06,6.11-8.28 c2.49-2.31,5.38-3.97,8.74-4.8c7.8-1.93,15.23,0.53,20.51,5.25c2.68,2.4,4.81,5.39,6.15,8.69c1.35,3.33,1.9,6.99,1.39,10.7 C73.99,31.93,69.75,38.57,60.78,43.44L60.78,43.44z M37.32,67.61c-11.6-15.58-11.88-30.34,2.2-44.06l-10.14-5.6 C21.26,14.79,6.36,38.08,12.12,44.3l7.9,11.72l-1.63,3.4c-0.45,1.01-0.01,1.72,1.09,2.21l1.07,0.29L0,102.59l4.16,8.87l8.32-2.45 l2.14-4.16l-2.05-3.84l4.52-0.97L18.14,98l-2.36-3.6l1.55-3.01l4.51-0.57l1.47-2.85l-2.52-3.29l1.61-3.12l4.6-0.75l6.26-11.95 l1.06,0.58C36.16,70.56,37.11,69.84,37.32,67.61L37.32,67.61z M59.15,77.38l-3.06,11.42l-4.25,1.68l-0.89,3.33l3.1,2.63l-0.81,3.03 l-4.2,1.48l-0.86,3.2l3.01,2.95l-0.58,2.17l-4.13,1.87l2.76,3.25l-1.19,4.43l-7.45,4.07l-5.82-7.63l11.1-41.43l-2.69-0.72 c-0.55-0.15-0.89-0.72-0.74-1.28l1.13-4.21c-8.14-6.17-12.17-16.85-9.37-27.32c3.6-13.45,17.18-21.57,30.64-18.55 c0.06,0.72,0.05,1.45-0.05,2.18c-0.25,1.82-1.04,3.69-2.5,5.5c-0.2,0.24-0.41,0.49-0.63,0.73c-4.3-0.28-8.33,2.5-9.49,6.82 c-0.5,1.86-0.39,3.74,0.2,5.43c0.14,0.6,0.37,1.18,0.67,1.75c0.71,1.3,1.75,2.29,2.97,2.92c0.8,0.53,1.7,0.93,2.67,1.2 c4.83,1.29,9.78-1.49,11.22-6.24c1.46-1.29,2.73-2.65,3.82-4.05c2.12-2.73,3.57-5.63,4.43-8.58c5.84,6.3,8.41,15.37,6.02,24.29c-2.8,10.47-11.65,17.71-21.77,18.98l-1.13,4.21c-0.15,0.55-0.72,0.89-1.28,0.74L59.15,77.38L59.15,77.38z"/>
</g></svg>
</div>
`);
$('#setMagicKeyBtn').click(function () {
$('#magic-key-line').toggle(250, function() {
if ($(this).is(":visible")) $('#magic-key-input').focus();
});
});
function setKey() {
window.localKey = $('#magic-key-input').val();
if (!window.localKey.length) {
window.localKey = undefined;
} else {
$('#openwebrx-panel-receiver').demodulatorPanel().setMagicKey(window.localKey);
}
$('#magic-key-input').val('');
$('#magic-key-line').toggle(250);
}
$('#magic-key-set').click(setKey);
$('#magic-key-input').on('keypress',function(e) { if(e.which == 13) setKey(); });
Plugins.utils.wrap_func(
'getMagicKey',
// beforeCB: return true to call the afterCB
function (orig, thisArg, args) { return true; },
// afterCB: return localKey or the original one
function (res) { return window.localKey !== undefined ? window.localKey : res; },
$('#openwebrx-panel-receiver').demodulatorPanel() // wrap getMagicKey in the demodulatorPanel
);
}
return true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -0,0 +1,25 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Notify"
permalink: /receiver/notify
---
This is `utility` plugin. It provides notifications for other plugins.
## Usage
```js
Plugins.notify.show('some notification');
```
## Load
Add this line in your `init.js` file:
```js
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/notify/notify.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,21 @@
/*
* Plugin: Notifications for other plugins
*
*/
#plugins-notification {
text-align: center;
vertical-align: middle;
width: 200px;
position: absolute;
bottom: 50px;
left: calc((100vw / 2) - 100px);
border: 2px solid white;
background: #333;
margin: 20px;
display: none;
opacity: 0.9;
color: white;
border-radius: 10px;
font-family: monospace;
}

View File

@@ -0,0 +1,37 @@
/*
* Plugin: Provides notifications for other plugins
*
* Usage:
* Plugins.notify.send('some notification');
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*
*/
// Notify plugin version
Plugins.notify._version = 0.1;
// Initialize the plugin
Plugins.notify.init = function () {
Plugins.notify.show = function (text) {
// create the message div if it's not there
if ($("#plugins-notification").length < 1)
$("body").append("<div id='plugins-notification'></div>");
// set the message text
$("#plugins-notification").html(text);
// show the message
$("#plugins-notification").fadeIn('fast');
// clear the timeout of previous message (if exists)
clearTimeout(Plugins.notify.notify_timeout);
// timeout the current message
Plugins.notify.notify_timeout = setTimeout('$("#plugins-notification").fadeOut("fast")', 1000);
};
return true;
}

View File

@@ -0,0 +1,30 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Screen Reader"
permalink: /receiver/screen_reader
---
This `receiver` plugin will:
* Provide spoken notifications to users with Assistive Technology
* Add invisible div for screen readers
* Catch events and write to the div, so the screen reader will speak the content.
The plugin depends on [notify](https://0xaf.github.io/openwebrxplus-plugins/receiver/utils) v0.2 plugin.
## Preview
![screen_reader](screen_reader/screen_reader.png "Preview")
## Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/screen_reader/screen_reader.js');
Plugins.screen_reader.log_messages = true; // if you want to log the messages to the chat window.
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,85 @@
/*
* Plugin: Screen Reader - Provide spoken notifications to users with Assistive Technology
*
* - Add invisible div for screen readers
* - Catch events and write to the div, so the screen reader will speak the content.
*
* License: MIT
* Copyright (c) 2024 Stanislav Lechev [0xAF], LZ2SLL
*/
// no css for this plugin
Plugins.screen_reader.no_css = true;
// Plugins.screen_reader.log_messages = true;
// Initialize the plugin
Plugins.screen_reader.init = async function () {
// Check if utils plugin is loaded
if (!Plugins.isLoaded('utils', 0.2)) {
// try to load the utils plugin
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js');
// check again if it was loaded successfully
if (!Plugins.isLoaded('utils', 0.2)) {
console.error('screen_reader plugin depends on "utils >= 0.2".');
return false;
} else {
Plugins._debug('Plugin "utils" has been loaded as dependency.');
}
}
// create new screen reader div if it's not there
if ($("#screen-reader").length < 1)
$("body").append(
"<div id='screen-reader' style='position:absolute; left:-10000px;top:auto;width:1px;height:1px;overflow:hidden;' role='status'></div>"
);
// create speaker function
Plugins.screen_reader.speak = function (text) {
if (document.owrx_initialized) {
clearTimeout(this.timeout);
$("#screen-reader").append(text+"<br/>\n");
this.timeout = setTimeout(function () { $("#screen-reader").html('') }, 3000); // clear after 3 secs
// console.log('DEBUG:', $('#screen-reader').text());
if (Plugins.screen_reader.log_messages) divlog(text);
}
}
// $(document).on('event:profile_changed', function (e, data) {
// Plugins.screen_reader.speak("Profile " + data);
// });
$(document).on('event:owrx_initialized', function (e, data) {
$(document).on('server:config:after', function (e, data) {
if (data.profile_id && data.sdr_id) { // profile was changed
var name = $('#openwebrx-sdr-profiles-listbox')
.find('option[value="' + data.sdr_id + '|' + data.profile_id + '"]').text();
if (name) Plugins.screen_reader.speak("Profile: " + name);
}
});
Plugins.utils.wrap_func(
'setMode',
function (orig, thisArg, args) { // beforeCB -> return true to call the original
if (this.last_mode === args[0]) return true; // same mode, no need to announce
Plugins.screen_reader.speak("Mode: " + args[0]);
this.last_mode = args[0];
return true;
},
function (res) { // afterCB
},
DemodulatorPanel.prototype
);
});
// $(document).on('server:clients:after', function (e, data) {
// });
// $(window).bind('beforeunload', function () {
// });
return true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -0,0 +1,23 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: ScreenShot"
permalink: /receiver/screenshot
---
This `receiver` plugin will allow you to take screenshot of your waterfall.
## Preview
![screenshot](screenshot/screenshot.png "Preview")
## Load
Add this line in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/screenshot/screenshot.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,66 @@
/*
* Plugin: Screenshot - Take a screenshot of the waterfall
*
* License: MIT
* Copyright (c) 2024 Stanislav Lechev [0xAF], LZ2SLL
*/
// no css for this plugin
Plugins.screenshot.no_css = true;
// Initialize the plugin
Plugins.screenshot.init = async function () {
await Plugins._load_script('https://0xaf.github.io/openwebrxplus-plugins/receiver/screenshot/html2canvas.min.js').catch(function() {
throw("Cannot load html2canvas script.");
});
if ($("#screenshot-btn").length < 1) {
$(".openwebrx-record-button").wrap("<div class='openwebrx-button openwebrx-square-button' style='background: none; width:2rem;'></div>");
$(".openwebrx-record-button").after("<div id='screenshot-btn' class='openwebrx-button openwebrx-square-button'>PIC</div>");
$(".openwebrx-record-button, #screenshot-btn").css({ float: 'none', marginTop: 0, width: '2rem', textAlign: 'center', padding: '2px 8px' });
$('#screenshot-btn').click(function () {
var freq = window.center_freq + $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().get_offset_frequency();
freq = parseInt(freq / 1000);
freq = parseFloat(freq / 1000);
console.log(freq);
let cloned_width = document.querySelector('#webrx-canvas-container').style.width;
let cloned_height = 1200;
var iframe_canvas = document.createElement("canvas");
iframe_canvas.setAttribute('width', cloned_width);
iframe_canvas.setAttribute('height', cloned_height);
document.body.appendChild(iframe_canvas);
html2canvas(document.querySelector('.openwebrx-waterfall-container'), {
canvas: iframe_canvas,
width: cloned_width,
height: cloned_height,
windowWidth: cloned_width,
windowHeight: cloned_height,
onclone: (e) => {
// console.log(e);
return e;
}
}).then(function (canvas) {
// console.log(canvas);
var d = new Date();
var a = document.createElement("a");
a.setAttribute('download', `sdr-screenshot-${freq.toFixed(3)}MHz-${d.toISOString()}.png`);
a.setAttribute('href', canvas.toDataURL("image/png").replace("image/png", "image/octet-stream"));
document.body.appendChild(a);
a.click();
setTimeout(function () { document.body.removeChild(a); document.body.removeChild(iframe_canvas) }, 0);
});
});
}
return true;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -0,0 +1,23 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Sort Profiles by NAME"
permalink: /receiver/sort_profiles
---
This `receiver` plugin will sort your profile list by *name* (__NOT__ by frequency)
This plugin is more an example for devs, than useful to users.
The plugin depends on [utils](https://0xaf.github.io/openwebrxplus-plugins/receiver/utils) plugin.
## Load
Add this lines in your `init.js` file:
```js
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js');
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/connect_notify/connect_notify.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,60 @@
/*
* Plugin: sort profiles by name.
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*/
// do not load CSS for this plugin
Plugins.sort_profiles.no_css = true;
// Initialize the plugin
Plugins.sort_profiles.init = async function () {
// Check if utils plugin is loaded
if (!Plugins.isLoaded('utils', 0.1)) {
// try to load the utils plugin
await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js');
// check again if it was loaded successfully
if (!Plugins.isLoaded('utils', 0.1)) {
console.error('soft_profiles plugin depends on "utils >= 0.1".');
return false;
} else {
Plugins._debug('Plugin "utils" has been loaded as dependency.');
}
}
// Catch the event, when server sends us the profiles.
$(document).on('server:profiles:after', function (e, data) {
var sel = $('#openwebrx-sdr-profiles-listbox');
// if the list is empty, return
if (!sel[0] || !sel[0].length)
return;
var selected = sel.val();
var list = sel.find('option');
// sort the list of options, alphanumeric and ignoring the case
list.sort(function (a, b) {
return $(a).text()
.localeCompare(
$(b).text(), undefined, {
numeric: true,
sensitivity: 'base'
}
);
});
// now reset the list and fill it with the new sorted one
sel.html('').append(list);
// set the selected profile from our cached value
sel.val(selected);
});
// return true to validate plugin load
return true;
}

View File

@@ -0,0 +1,21 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Tune Checkbox"
permalink: /receiver/tune_checkbox
---
This a one-line `receiver` plugin to make the 'Hold mouse wheel down to tune' setting enabled by default.
This setting allows you to use the mouse scroll to zoom into the waterfall.
By default, the state of this checkbox is stored in localStorage in your browser. If you often delete your browser's cache and localStorage contents, this plugin might be useful.
## Load
Add this lines in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/tune_checkbox/tune_checkbox.js');
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,14 @@
/*
* Plugin: Make the 'Hold mouse wheel down to tune' option checked by default.
* Allows you to zoom into the waterfall with the mouse scroll.
*
* By default, the state of this checkbox is stored in localStorage in your browser.
* If you often delete your browser's cache and localStorage contents, this plugin might be useful.
*
* License: Apache License 2.0
* Copyright (c) 2024 Dimitar Milkov, LZ2DMV
*/
Plugins.tune_checkbox.no_css = true;
$('#openwebrx-wheel-checkbox').prop('checked', true).change();

View File

@@ -0,0 +1,22 @@
---
layout: page
title: "OpenWebRX+ Receiver Plugin: Utils (utility)"
permalink: /receiver/utils
---
This `utility` plugin will give a function wrapping method and will send some events.
This plugin is a dependency for almost all plugins.
## Load
Add this lines in your `init.js` file:
```js
Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js').then(async function () {
// load the rest of your plugins here
});
```
## init.js
Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins).

View File

@@ -0,0 +1,165 @@
/*
* Utils plugin.
*
* This plugin provides a function wrapping method (read below)
* and adds some events for the rest plugins.
*
* License: MIT
* Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL
*
* Changes:
* 0.1:
* - initial release
* 0.2:
* - add document.owrx_initialized boolean var, once initialized
* - add _DEBUG_ALL_EVENTS
* 0.3:
* - handle return value of AfterCallBack of the wrapper
*/
// Disable CSS loading for this plugin
Plugins.utils.no_css = true;
// Utils plugin version
Plugins.utils._version = 0.3;
/**
* Wrap an existing function with before and after callbacks.
* @param {string} name The name of function to wrap with before and after callbacks.
* @param {function(orig, thisArg, args):boolean} before_cb Callback before original. Return true to call the original.
* @param {function(result, orig, thisArg, args):any} after_cb Callback after original, will receive the result of original
* @param {object} obj [optional] Object to look for function into. Default is 'window'
* @description
* - Before Callback:
* - Params:
* - orig: Original function (in case you want to call it, you have to return false to prevent second calling)
* - thisArg: local 'this' for the original function
* - args: arguments passed to the original function
* - Returns: Boolean. Return false to prevent execution of original function and the after callback.
* - After Callback:
* - Params:
* - res: Result of the original function
* - thisArg: local 'this' for the original function
* - args: arguments passed to the original function
* - Returns: Any. Return anything to the original caller. This can be used to replace the original value.
*
* @example
* // Using before and after callbacks.
* Plugins.utils.wrap_func('sdr_profile_changed',
* function (orig, thisArg, args) { // before callback
* console.log(orig.name);
* if (something_bad)
* console.log('This profile is disabled by proxy function');
* return false; // return false to prevent the calling of the original function and the after_cb()
* }
* return true; // always return true, to call the original function
* },
* function (res, thisArg, args) { // after callback
* console.log(res);
* return res;
* }
* );
*
* @example
* // Using only before callback and handle original.
* Plugins.utils.wrap_func('sdr_profile_changed',
* function (orig, thisArg, args) { // before callback
* // if we need to call the original in the middle of our work
* do_something_before_original();
* var res = orig.apply(thisArg, args);
* do_something_after_original(res);
* return false; // to prevent calling the original and after_cb
* },
* function (res) { // after callback
* // ignored
* return res;
* }
* );
*
*/
Plugins.utils.wrap_func = function (name, before_cb, after_cb, obj = window) {
if (typeof (obj[name]) !== "function") {
console.error("Cannot wrap non existing function: '" + obj + '.' + name + "'");
return false;
}
var fn_original = obj[name];
var proxy = new Proxy(obj[name], {
apply: function (target, thisArg, args) {
if (before_cb(target, thisArg, args)) {
var orgRet = fn_original.apply(thisArg, args);
var ret = after_cb(orgRet, thisArg, args);
return ret !== undefined ? ret : orgRet;
}
}
});
obj[name] = proxy;
}
// Init utils plugin
Plugins.utils.init = function () {
var send_events_for = {};
// function name to proxy.
send_events_for['sdr_profile_changed'] = {
// [optional] event name (prepended with 'event:'). Default is function name.
name: 'profile_changed',
// [optional] data to send with the event (should be function).
data: function () {
return $('#openwebrx-sdr-profiles-listbox').find(':selected').text()
}
};
send_events_for['on_ws_recv'] = {
// if we use handler, it will replace the before_cb
handler: function (orig, thisArg, args) {
if (typeof (args[0].data) === 'string' && args[0].data.substr(0, 16) !== "CLIENT DE SERVER") {
try {
var json = JSON.parse(args[0].data);
if (Plugins.utils._DEBUG_ALL_EVENTS && json.type !== 'smeter')
console.debug("server:" + json.type + ":before", [json['value']]);
$(document).trigger('server:' + json.type + ":before", [json['value']]);
} catch (e) {}
}
// we handle original function here
orig.apply(thisArg, args);
if (typeof (json) === 'object') {
if (Plugins.utils._DEBUG_ALL_EVENTS && json.type !== 'smeter')
console.debug("server:" + json.type + ":after", [json['value']]);
$(document).trigger('server:' + json.type + ":after", [json['value']]);
}
// do not call the after_cb
return false;
}
};
$.each(send_events_for, function (key, obj) {
Plugins.utils.wrap_func(
key,
typeof (obj.handler) === 'function' ? obj.handler : function () {
return true;
},
function (res) {
var ev_data;
var ev_name = key;
if (typeof (obj.name) === 'string') ev_name = obj.name;
if (typeof (obj.data) === 'function') ev_data = obj.data(res);
if (Plugins.utils._DEBUG_ALL_EVENTS) console.debug("event:" + ev_name, ev_data);
$(document).trigger("event:" + ev_name, [ev_data]);
}
);
});
var interval = setInterval(function () {
if (typeof (clock) === 'undefined') return;
clearInterval(interval);
$(document).trigger('event:owrx_initialized');
document.owrx_initialized = true;
}, 10);
return true;
}

View File

@@ -0,0 +1,18 @@
#!/bin/sh
echo "Restarter script started..."
while true; do
while [ $(date +%H:%M) != "02:00" ]; do
sleep 30;
done
# Container time is UTC!!
# sleep 86400 # Sleep for 24 hours
echo "Restarting Container ..."
docker restart openwebrx_docker
sleep 60;
done