added docker compose and plugins for openwebrx installation
							
								
								
									
										57
									
								
								docker/openwebrx/plugins/receiver/antenna_switcher/README.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
```
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
antenna_pins=[23,24]
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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 = "▾ 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;
 | 
			
		||||
};
 | 
			
		||||
@@ -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()
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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).
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 272 KiB  | 
							
								
								
									
										29
									
								
								docker/openwebrx/plugins/receiver/connect_notify/README.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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).
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 346 KiB  | 
							
								
								
									
										34
									
								
								docker/openwebrx/plugins/receiver/doppler/README.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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.
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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).
 | 
			
		||||
							
								
								
									
										182
									
								
								docker/openwebrx/plugins/receiver/doppler/doppler.css
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										450
									
								
								docker/openwebrx/plugins/receiver/doppler/doppler.js
									
									
									
									
									
										Normal 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 = ">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
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docker/openwebrx/plugins/receiver/doppler/doppler.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.4 MiB  | 
							
								
								
									
										
											BIN
										
									
								
								docker/openwebrx/plugins/receiver/doppler/doppler1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 798 KiB  | 
							
								
								
									
										3263
									
								
								docker/openwebrx/plugins/receiver/doppler/sat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										12
									
								
								docker/openwebrx/plugins/receiver/example/README.md
									
									
									
									
									
										Normal 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)
 | 
			
		||||
							
								
								
									
										153
									
								
								docker/openwebrx/plugins/receiver/example/example.js
									
									
									
									
									
										Normal 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
 | 
			
		||||
							
								
								
									
										11
									
								
								docker/openwebrx/plugins/receiver/example_theme/README.md
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -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'
 | 
			
		||||
  )
 | 
			
		||||
);
 | 
			
		||||
@@ -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).
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								docker/openwebrx/plugins/receiver/init.js
									
									
									
									
									
										Normal 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');
 | 
			
		||||
});
 | 
			
		||||
@@ -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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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).
 | 
			
		||||
@@ -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;
 | 
			
		||||
} */
 | 
			
		||||
@@ -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': '⇧ Shift',
 | 
			
		||||
      'CONTROL': '⌃ Ctrl',
 | 
			
		||||
      'COMMAND': '⌘ Cmd',
 | 
			
		||||
      'META': '⌘ Meta',
 | 
			
		||||
      'ALT': '⌥ Alt',
 | 
			
		||||
      'OPTION': '⌥ Opt',
 | 
			
		||||
      'ENTER': '↵ Enter',
 | 
			
		||||
      'RETURN': '↵ Enter',
 | 
			
		||||
      'DELETE': '⌦ Del',
 | 
			
		||||
      'BACKSPACE': '⌫ BS',
 | 
			
		||||
      'ESCAPE': '⎋ ESC',
 | 
			
		||||
      'ARROWRIGHT': '→',
 | 
			
		||||
      'ARROWLEFT': '←',
 | 
			
		||||
      'ARROWUP': '↑',
 | 
			
		||||
      'ARROWDOWN': '↓',
 | 
			
		||||
      'PAGEUP': '⇞ PgUp',
 | 
			
		||||
      'PAGEDOWN': '⇟ PgDn',
 | 
			
		||||
      'HOME': '↖ Home',
 | 
			
		||||
      'END': '↘ End',
 | 
			
		||||
      'TAB': '⇥ Tab',
 | 
			
		||||
      'SPACE': '␣ Space',
 | 
			
		||||
      'INTERVAL': '␣ 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('<')}|${gen_key('>')}</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;
 | 
			
		||||
}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 569 KiB  | 
							
								
								
									
										23
									
								
								docker/openwebrx/plugins/receiver/magic_key/README.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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).
 | 
			
		||||
							
								
								
									
										63
									
								
								docker/openwebrx/plugins/receiver/magic_key/magic_key.js
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docker/openwebrx/plugins/receiver/magic_key/magic_key.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 MiB  | 
							
								
								
									
										25
									
								
								docker/openwebrx/plugins/receiver/notify/README.md
									
									
									
									
									
										Normal 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).
 | 
			
		||||
							
								
								
									
										21
									
								
								docker/openwebrx/plugins/receiver/notify/notify.css
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								docker/openwebrx/plugins/receiver/notify/notify.js
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								docker/openwebrx/plugins/receiver/screen_reader/README.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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).
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
		 After Width: | Height: | Size: 91 KiB  | 
							
								
								
									
										23
									
								
								docker/openwebrx/plugins/receiver/screenshot/README.md
									
									
									
									
									
										Normal 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
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## 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).
 | 
			
		||||
							
								
								
									
										20
									
								
								docker/openwebrx/plugins/receiver/screenshot/html2canvas.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										66
									
								
								docker/openwebrx/plugins/receiver/screenshot/screenshot.js
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								docker/openwebrx/plugins/receiver/screenshot/screenshot.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.9 MiB  | 
							
								
								
									
										23
									
								
								docker/openwebrx/plugins/receiver/sort_profiles/README.md
									
									
									
									
									
										Normal 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).
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								docker/openwebrx/plugins/receiver/tune_checkbox/README.md
									
									
									
									
									
										Normal 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).
 | 
			
		||||
@@ -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();
 | 
			
		||||
							
								
								
									
										22
									
								
								docker/openwebrx/plugins/receiver/utils/README.md
									
									
									
									
									
										Normal 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).
 | 
			
		||||
							
								
								
									
										165
									
								
								docker/openwebrx/plugins/receiver/utils/utils.js
									
									
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||