diff --git a/docker/openwebrx/docker-compose.yml b/docker/openwebrx/docker-compose.yml new file mode 100644 index 0000000..6801fec --- /dev/null +++ b/docker/openwebrx/docker-compose.yml @@ -0,0 +1,42 @@ +services: + openwebrx: + image: 'slechev/openwebrxplus-softmbe:latest' + container_name: openwebrx_docker + restart: unless-stopped + ports: + - '8073:8073' + - '5678:5678' + environment: + TZ: Europe/Berlin + FORWARD_LOCALPORT_1234: 5678 + # OPENWEBRX_ADMIN_USER: admin + # OPENWEBRX_ADMIN_PASSWORD: 123465 + HEALTHCHECK_USB_1df7_3000: 1 # SDRPlay Healthcheck + HEALTHCHECK_USB_0bda_2838: 1 # NeSDR Smart Healthcheck + devices: + - /dev/bus/usb:/dev/bus/usb + volumes: + - ./etc:/etc/openwebrx + - ./var:/var/lib/openwebrx + - ./plugins:/usr/lib/python3/dist-packages/htdocs/plugins + +# if you want your container to restart automatically if the HEALTHCHECK fails +# (see here: https://stackoverflow.com/a/48538213/420585) + openwebrx_autoheal: + restart: unless-stopped + container_name: openwebrx_docker_autoheal + image: willfarrell/autoheal + environment: + - AUTOHEAL_CONTAINER_LABEL=all + volumes: + - /var/run/docker.sock:/var/run/docker.sock + + restarter: + image: docker:cli + container_name: openwebrx_docker_restarter + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./restarter:/restarter + entrypoint: ["/bin/sh", "-c"] + command: ["chmod +x /restarter/script.sh && /restarter/script.sh"] + restart: unless-stopped diff --git a/docker/openwebrx/plugins/receiver/antenna_switcher/README.md b/docker/openwebrx/plugins/receiver/antenna_switcher/README.md new file mode 100644 index 0000000..7680851 --- /dev/null +++ b/docker/openwebrx/plugins/receiver/antenna_switcher/README.md @@ -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 +``` diff --git a/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.cfg b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.cfg new file mode 100644 index 0000000..dea138c --- /dev/null +++ b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.cfg @@ -0,0 +1 @@ +antenna_pins=[23,24] diff --git a/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.css b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.css new file mode 100644 index 0000000..2ea6c40 --- /dev/null +++ b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.css @@ -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; + } +} diff --git a/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.js b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.js new file mode 100644 index 0000000..a2b1d3f --- /dev/null +++ b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.js @@ -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; +}; diff --git a/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.py b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.py new file mode 100644 index 0000000..f35d08c --- /dev/null +++ b/docker/openwebrx/plugins/receiver/antenna_switcher/antenna_switcher.py @@ -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() diff --git a/docker/openwebrx/plugins/receiver/antenna_switcher/install_backend.sh b/docker/openwebrx/plugins/receiver/antenna_switcher/install_backend.sh new file mode 100644 index 0000000..9b82544 --- /dev/null +++ b/docker/openwebrx/plugins/receiver/antenna_switcher/install_backend.sh @@ -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 diff --git a/docker/openwebrx/plugins/receiver/colorful_spectrum/README.md b/docker/openwebrx/plugins/receiver/colorful_spectrum/README.md new file mode 100644 index 0000000..7c076c5 --- /dev/null +++ b/docker/openwebrx/plugins/receiver/colorful_spectrum/README.md @@ -0,0 +1,23 @@ +--- +layout: page +title: "OpenWebRX+ Receiver Plugin: Colorful Spectrum" +permalink: /receiver/colorful_spectrum +--- + +This `receiver` plugin will colorify your spectrum analyzer. + +## Preview + +![spectrum](colorful_spectrum/colorful_spectrum.png "Preview") + +## Load + +Add this line in your `init.js` file: + +```js +Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/colorful_spectrum/colorful_spectrum.js'); +``` + +## init.js + +Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins). diff --git a/docker/openwebrx/plugins/receiver/colorful_spectrum/colorful_spectrum.js b/docker/openwebrx/plugins/receiver/colorful_spectrum/colorful_spectrum.js new file mode 100644 index 0000000..ad9ad1b --- /dev/null +++ b/docker/openwebrx/plugins/receiver/colorful_spectrum/colorful_spectrum.js @@ -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; +} diff --git a/docker/openwebrx/plugins/receiver/colorful_spectrum/colorful_spectrum.png b/docker/openwebrx/plugins/receiver/colorful_spectrum/colorful_spectrum.png new file mode 100644 index 0000000..da40b71 Binary files /dev/null and b/docker/openwebrx/plugins/receiver/colorful_spectrum/colorful_spectrum.png differ diff --git a/docker/openwebrx/plugins/receiver/connect_notify/README.md b/docker/openwebrx/plugins/receiver/connect_notify/README.md new file mode 100644 index 0000000..95dda43 --- /dev/null +++ b/docker/openwebrx/plugins/receiver/connect_notify/README.md @@ -0,0 +1,29 @@ +--- +layout: page +title: "OpenWebRX+ Receiver Plugin: Connect/Disconnect notifications" +permalink: /receiver/connect_notify +--- + +This `receiver` plugin will: + +* Send a chat message to all users when you connect/disconnect to SDR +* Show notification when another user is connected/disconnected to SDR + +The plugin depends on [notify](https://0xaf.github.io/openwebrxplus-plugins/receiver/notify) plugin. + +## Preview + +![connect](connect_notify/connect_notify.png "Preview") + +## Load + +Add this line in your `init.js` file: + +```js +Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/notify/notify.js'); +Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/connect_notify/connect_notify.js'); +``` + +## init.js + +Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins). diff --git a/docker/openwebrx/plugins/receiver/connect_notify/connect_notify.js b/docker/openwebrx/plugins/receiver/connect_notify/connect_notify.js new file mode 100644 index 0000000..37ef54f --- /dev/null +++ b/docker/openwebrx/plugins/receiver/connect_notify/connect_notify.js @@ -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; +} diff --git a/docker/openwebrx/plugins/receiver/connect_notify/connect_notify.png b/docker/openwebrx/plugins/receiver/connect_notify/connect_notify.png new file mode 100644 index 0000000..8ef087a Binary files /dev/null and b/docker/openwebrx/plugins/receiver/connect_notify/connect_notify.png differ diff --git a/docker/openwebrx/plugins/receiver/doppler/README.md b/docker/openwebrx/plugins/receiver/doppler/README.md new file mode 100644 index 0000000..77288ab --- /dev/null +++ b/docker/openwebrx/plugins/receiver/doppler/README.md @@ -0,0 +1,34 @@ +--- +layout: page +title: "OpenWebRX+ Receiver Plugin: Doppler" +permalink: /receiver/doppler +--- + +This `receiver` plugin will track the Doppler shift frequency of a chosen satellite. Useful for SSTV/Packet. + +This plugin started as a port of [work](https://github.com/studentkra/OpenWebRX-Doppler) by [Sergey Osipov](https://github.com/studentkra). +Then I switched to [CelesTrak JSON API](https://celestrak.org/) and created Satellite Finder modal window. + +## Preview + +![doppler](doppler/doppler.png "Preview") + +## Usage + + 1. Open the Satellite finder window to choose a satellite or enter the SatID if you know it. + 2. Click TRACK. + +The Satellite Finder window will help you find a satellite and will give useful information on each satellite. +![doppler1](doppler/doppler1.png "FindSat") + +## Load + +Add this line in your `init.js` file: + +```js +Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/doppler/doppler.js'); +``` + +## init.js + +Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins). diff --git a/docker/openwebrx/plugins/receiver/doppler/doppler.css b/docker/openwebrx/plugins/receiver/doppler/doppler.css new file mode 100644 index 0000000..ebcb284 --- /dev/null +++ b/docker/openwebrx/plugins/receiver/doppler/doppler.css @@ -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; +} \ No newline at end of file diff --git a/docker/openwebrx/plugins/receiver/doppler/doppler.js b/docker/openwebrx/plugins/receiver/doppler/doppler.js new file mode 100644 index 0000000..8d7fe6b --- /dev/null +++ b/docker/openwebrx/plugins/receiver/doppler/doppler.js @@ -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(` +
+ +
Open SAT Finder
+
TRACK
+
+ `); + + var modalTabs = ` +
+
+ `; + + var groups = Object.keys(Plugins.doppler.satelliteGroups); + for (let i = 0; i < groups.length; i++) { + modalTabs += ` +
+ + +
+
+ + +
+ + + +
+
+
+ + + + + + + + + + + + + +
SatIDNameElevAzDistVisible
+
+
+
+ `; + } + + modalTabs += ` +
+
+ `; + + $('#satellite-row').append(` + + `); + + $('#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 = `