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)) — this image does not work with Pluto Plus, Hamgeek AD9363, LibreSDR, or other AD9363-based clones
  • 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. Future Version may have a TX-Offset
  • 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

  1. Open http://<your-host>:3004 in a browser
  2. Log in with admin / admin (you will be forced to change the password)
  3. Navigate to Setup (or go to /setup)
  4. Configure:
    • 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 9749971700 Hz (9750 MHz minus ~28.3 kHz offset). If you use a different LNB, measure or look up its exact LO frequency.
  5. Click Save and Connect

To change these later, go to Setup as admin.

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

  • PlutoSDR IP — IP address of the Pluto (default: 192.168.6.122)
  • LNB LO Frequency — exact local oscillator frequency in Hz (e.g. 9749971700 for a Bullseye TCXO)
  • S-Meter Offset — calibration offset from dBFS to dBm

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.

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.

S
Description
QO100 WebConsole
Readme 43 KiB