286 lines
11 KiB
Markdown
286 lines
11 KiB
Markdown
# 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`:
|
||
|
||
```yaml
|
||
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:
|
||
|
||
```bash
|
||
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):
|
||
|
||
```bash
|
||
mkdir -p data
|
||
```
|
||
|
||
Start:
|
||
|
||
```bash
|
||
docker compose up -d
|
||
```
|
||
|
||
## Sidecar Architecture
|
||
|
||
The **sidecar** is a Go binary that bridges the server and the Pluto SDR hardware via [libiio](https://github.com/analogdevicesinc/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 Mode** — `qo100` 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 Mode** — `qo100` (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):
|
||
|
||
```yaml
|
||
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):
|
||
|
||
```yaml
|
||
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:
|
||
|
||
```bash
|
||
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.
|