6.5 KiB
DJ7NTs QO100 Webconsole
Disclaimer: This is a proof-of-concept project. Use at your own risk — no guarantees or warranties of any kind. The source code is not fully open-source at this time.
Prerequisites
- Docker (any recent version) and Docker Compose
- Analog Devices PlutoSDR (original rev.B/C - 2(!) Channel-Version (one TX, one RX)) or AD9363-based clone (e.g. LibreSDR) — device type is selected during setup
- USB-to-Ethernet adapter (100 Mbit) connected to the Pluto's USB OTG port — Gigabit adapters are not supported and may cause issues
- LNB connected to the PlutoSDR
- Original(!) 2channel-Pluto should be GPS-DO stabilized. Currently there's only a XIT to compensate it. As an additional Feature there's a global TX-Offset-Setting.
- The Pluto and the host running Docker must be on the same network
The prebuilt image is available for x86_64 (PCs/servers) and arm64 (Raspberry Pi 4+, Rock 5 ITX). Docker automatically pulls the correct variant for your hardware.
Quick Start with Docker Compose
Create a docker-compose.yml:
services:
sdrc:
image: git.dj7nt.de/dj7nt/qo100wc:latest
ports:
- "3004:3000"
volumes:
- ./data:/app/data
environment:
SESSION_SECRET: ${SESSION_SECRET}
DATABASE_PATH: /app/data/qo100.db
PLUTO_CONNECTED: "1"
restart: unless-stopped
Generate a session secret and create a .env file:
echo "SESSION_SECRET=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)" > .env
Create the data directory before starting (Docker would create it as root otherwise):
mkdir -p data
Start:
docker compose up -d
First-Time Setup
- Open
http://<your-host>:3004in a browser - Log in with admin / admin (you will be forced to change the password)
- You will be redirected to the Setup wizard (
/setup) - Configure:
- Device Type — select your SDR hardware: ADALM Pluto (AD9364) or AD9363 Clone (LibreSDR)
- Operating Mode —
qo100for satellite operation orsimplefor general-purpose SDR use - PlutoSDR IP — the IP address of your Pluto (default:
192.168.6.122) - LNB LO Frequency — the actual local oscillator frequency of your LNB in Hz. This is not the nominal 9750 MHz — e.g. my Bullseye TCXO LNB runs at
9749971700Hz (9750 MHz minus ~28.3 kHz offset). If you use a different LNB, measure or look up its exact LO frequency. (QO-100 mode only) - TX Calibration — frequency calibration offset in Hz for transmitter accuracy
- TX Offset — fixed frequency offset applied to TX (e.g. for transverter configurations)
- Click Save and Connect
To change these later, go to Setup as admin (via the sidebar or /setup).
Admin Page (/admin)
The admin page provides full management of the webconsole. Only users with the admin role can access it.
User Management
- View all users with their role, status, last seen, and last TX timestamps
- Create, edit, disable, or delete users
- Reset user passwords
- Three roles: admin (full access), user (TX/RX), guest (RX only)
- Safety: cannot delete yourself or the last remaining admin
SDR Settings
- Device Type — select SDR hardware profile (Pluto or AD9363 clone)
- Operating Mode —
qo100(satellite) orsimple(general-purpose) - PlutoSDR IP — IP address of the Pluto (default:
192.168.6.122) - LNB LO Frequency — exact local oscillator frequency in Hz (e.g.
9749971700for a Bullseye TCXO) - S-Meter Offset — calibration offset from dBFS to dBm
- TX Calibration — transmitter frequency calibration in Hz
- TX Offset — fixed TX frequency offset in Hz
Changes trigger a reconnect to the SDR bridge.
TX Lock
A global transmit lock that, when enabled, blocks all users from transmitting (PTT, two-tone, etc.) and stops any active transmissions.
Activity Log
- Paginated log (200 entries per page) of all user activity
- Filterable by user and event type (login, logout, TX start/stop, disconnect, etc.)
Networking: Reaching the Pluto
By default Docker uses bridge networking. If the Pluto is on a separate Ethernet interface the container can't route to, use host networking (Linux only):
services:
sdrc:
image: git.dj7nt.de/dj7nt/qo100wc:latest
network_mode: host
# no ports: block needed — app listens on 3000 directly
volumes:
- ./data:/app/data
environment:
SESSION_SECRET: ${SESSION_SECRET}
DATABASE_PATH: /app/data/qo100.db
PLUTO_CONNECTED: "1"
restart: unless-stopped
HTTPS / Microphone Access
The browser's getUserMedia() API (used for TX microphone capture) requires a secure context. This means one of:
- Access via
http://localhost(works for local testing only) - Access via HTTPS (required for remote access)
The Docker image does not include HTTPS. You need a reverse proxy.
HAProxy Example
Put HAProxy in front to terminate TLS (e.g. with Let's Encrypt via certbot):
services:
sdrc:
image: git.dj7nt.de/dj7nt/qo100wc:latest
# no ports exposed publicly — only haproxy talks to it
volumes:
- ./data:/app/data
environment:
SESSION_SECRET: ${SESSION_SECRET}
DATABASE_PATH: /app/data/qo100.db
PLUTO_CONNECTED: "1"
restart: unless-stopped
networks:
- internal
haproxy:
image: haproxy:3
ports:
- "80:80"
- "443:443"
volumes:
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- ./certs:/usr/local/etc/haproxy/certs:ro
restart: unless-stopped
networks:
- internal
networks:
internal:
haproxy/haproxy.cfg:
frontend http
bind *:80
http-request redirect scheme https unless { ssl_fc }
frontend https
bind *:443 ssl crt /usr/local/etc/haproxy/certs/sdrc.pem
default_backend sdrc
backend sdrc
server sdrc sdrc:3000
Place your fullchain + privkey as certs/sdrc.pem (concatenated PEM). With Let's Encrypt:
certbot certonly --standalone -d sdrc.example.com
cat /etc/letsencrypt/live/sdrc.example.com/fullchain.pem \
/etc/letsencrypt/live/sdrc.example.com/privkey.pem > certs/sdrc.pem
After this setup, open https://sdrc.example.com — the browser will allow microphone access for TX.
Alternative: Chrome Flag (Not Recommended for Production)
For testing without HTTPS, launch Chrome with:
chrome --unsafely-treat-insecure-origin-as-secure=http://<host>:3004
This grants media permissions on that HTTP origin. Only use for development.