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

286 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.