2026-06-12 07:48:02 +02:00
2026-06-12 07:48:02 +02:00

DJ7NTs QO-100 Web SDR Transceiver Console

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. FT8/FT4 digital modes are experimental.

Features

  • Voice SSB — USB with full DSP chain (NR, AGC, 5-band EQ, notch filter)
  • CW (experimental)— TX with 10 ms envelope ramp
  • FT8 / FT4 (experimental) — server-side encode/decode, WSJT-X-style autosequencer, dual-mode decode
  • Multi-user — admin, user, and guest roles; concurrent users with independent VFO
  • Wavelog integration — automatic logging
  • Web-based — no desktop app needed; all DSP runs server-side, browser handles audio I/O

Supported Operating Modes

Mode Description
QO-100 Satellite operation with LNB conversion, beacon tracking (AFC), transponder frequency offset, and bandpass clamping
Simple General-purpose SDR — direct frequency tuning, no LNB, no beacon tracking

Supported Hardware

Device Profile Notes
Analog Devices PlutoSDR (rev B/C, 2-channel) pluto GPIO + ADM1177 power monitor, native 576 kHz sample rate. GPS-DO recommended for TX stability; XIT compensates drift. Global TX offset configurable.
AD9363-based clone (e.g. LibreSDR) ad9363-clone No GPIO/ADM1177; requires linear-interpolation resampling to 576 kHz; auto-probes multiple sample rates.

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) — selected during setup
  • USB-to-Ethernet adapter (100 Mbit) connected to the Pluto's USB OTG port, if using a Board without Ethernet — Gigabit adapters are not supported and may cause issues
  • LNB connected to the PlutoSDR
  • Pluto should be GPS-DO stabilized. Currently there's only XIT to compensate drift. A global TX offset is available as an additional 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.

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

Sidecar Architecture

The sidecar is a Go binary that bridges the server and the Pluto SDR hardware via libiio. It handles all I/Q streaming, TX/RX control, and hardware-specific configuration.

Deployment Modes

Mode Description Usage
Local (default) Sidecar runs on the host machine, communicates with Pluto over the network ./pluto-sidecar <ip> --socket <path> --profile pluto
On-Pluto Cross-compiled ARM binary runs directly on the Pluto device. Lower latency, uses local IIO context. Auto-reconnects on errors. ./pluto-sidecar-arm --local --profile pluto --listen :4242

The Docker image includes the sidecar and runs it in local mode automatically.

On-Pluto Deployment

For lowest latency, the sidecar can run directly on the Pluto's ARM Cortex-A9. The ARM binary (pluto-sidecar-arm) is cross-compiled with a Docker buildx step and deployed to the Pluto via SSH/SCP. In this mode, the sidecar opens a local IIO context (no network I/O for IQ data) and listens on a TCP port for the server to connect.

Binary Protocol

Control and data flow over separate channels using a binary frame protocol: [type:u8][length:u32le][payload]. The on-Pluto mode additionally supports async transport for low-latency TX.

FT8 / FT4 Digital Modes (Experimental)

Server-side weak-signal digital modes using the @e04/ft8ts library (pure TypeScript port of WSJT-X). Both FT8 (15 s slots) and FT4 (7.5 s slots) are decoded simultaneously under a single "FT8/FT4" RX mode.

Capabilities

  • Dual decode — FT4 decoded every 7.5 s, FT8 every 15 s; messages tagged with mode
  • Autosequencer — WSJT-X-style automatic QSO flow (CQ → reply → signal report → 73)
  • Auto mode — when replying, automatically matches the decoded station's mode (FT8 or FT4)
  • Slot-aligned TX — transmissions start at slot boundaries; early-TX window (≤2 s FT8 / ≤1.2 s FT4) allows quick autosequence replies
  • Dedicated UI — waterfall display (2.93 Hz/bin), decode list with color coding, TX panel with message templates

Signal Path

RX: SSB demod (3.2 kHz bandwidth) → HPF → LPF → decimate to 12 kHz → ring buffer → slot-boundary decode jobs (separate workers per mode, depth-3 OSD).

TX: @e04/ft8ts encode → 12 kHz audio → interpolate to 48 kHz → slot-scheduled → TX worker.

AGC, NR, EQ, and notch are bypassed for FTx — constant-amplitude GFSK signals must not be squashed.

DSP Pipeline

All signal processing runs server-side (TypeScript/Bun). The browser only handles audio I/O and UI.

RX

576 kHz I/Q
  → frequency shift → ×12 decimation → Hilbert transform → sideband select
  → DC blocker → notch filter → Ephraim-Malah noise reduction
  → AGC → 5-band EQ → low-pass filter → ×6 decimation → 8 kHz PCM → browser

TX

48 kHz mic (browser)
  → jitter buffer → TX worker thread
  → mic gain → pre-filter → EQ → pre-emphasis → compressor → limiter → post-filter
  → SSB modulator → ×12 interpolation → 576 kHz I/Q → sidecar → Pluto

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. You will be redirected to the Setup wizard (/setup)
  4. Configure:
    • Device Type — select your SDR hardware: ADALM Pluto (AD9364) or AD9363 Clone (LibreSDR)
    • Operating Modeqo100 for satellite operation or simple for 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 9749971700 Hz (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)
  5. 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 Modeqo100 (satellite) or simple (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. 9749971700 for 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, FTx, 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