RADIO HUB

Propagation & Ground-Truth Hardware RSP1A SDR JS8 / GhostNet WWV Time Signal Frequencies & Nets Tools Winlink / HF Modems Knowledge CW Decode CW Keyer Shortwave

Live Conditions

Loading…

Decoded vs Expected — Ground-Truth Log

Live RX decode runs from sdr-decode.py via remote KiwiSDR. Each row = one band+mode sweep. Newest first.

Time (UTC) Band Dial (kHz) Mode Decodes Grids Best SNR Receiver
Loading…

SDRplay RSP1A — Local Wideband Receiver

Loading device…

Confirmed Reception — known frequencies tuned & measured

Each row is a real tune to a known frequency with the live SNR over the local noise floor. This is ground-truth: if it shows here, the RSP1A is hearing it right now.

Freq (MHz)ServiceSNR (dB)SignalFloor (dB)
Loading…

Band Activity Map — broadband discovery sweep

Coarse 88–470 MHz sweep, strongest signal per band. Detections include noise — treat max-SNR as the real indicator, not the count.

Band / ServiceMax SNR (dB)PeakDetectionsExample (MHz)
Loading…

OpenWebRX — Continuous Waterfall Receiver checking…

Full-spectrum waterfall browser UI with live demodulation. Exclusive with the Snapshot Tuner — only one can hold the RSP1A at a time. When started, the host sdrplay_apiService is stopped and the container takes over the USB device.

🔴 Live Waterfall →
Admin login: user admin — password in /var/lib/jarvis/secrets/openwebrx_admin_password
Profiles pre-loaded: 40m JS8/FT8 · 20m FT8 · 80m LSB · 30m WSPR · Shortwave · NOAA wx · Airband · 2m

ADS-B — Aircraft Tracking (1090 MHz) checking…

Decodes Mode S / ADS-B from aircraft transponders and plots them on a live OpenStreetMap (SDRplay dump1090). Exclusive with the Snapshot Tuner and OpenWebRX — only one can hold the RSP1A at a time. Note: the HF antenna is inefficient at 1090 MHz, so expect high-altitude / nearby traffic only until a dedicated 1090 MHz antenna is fitted.

Open live map →

Live Tuner — Snapshot Listen

Tune the RSP1A to any frequency and capture a short live audio clip + spectrum. RX-only. One capture at a time (single-tuner device).

Reading the Data — Caveats

Hardware Catalog + Wiki KB

Searchable, self-updating wiki KB of all station radio hardware and software. Conventions: knowledge/radio/CONVENTIONS.md · FTS5 full-text search is live — try a frequency, callsign, mode, or gear name.

GhostNet JS8 Net Monitor

7.107 MHz USB · JS8 Normal
40m net dial (offset 1500–2000 Hz)
Thursdays 21:00 ET
= 01:00 UTC Fri · window 0100–0130Z
Next net
Groups: @GHOSTNET @GSTFLASH
directed-to targets to watch
Time UTCRXSNRCallsignGridTypeFrame
Loading…

JS8 Activity

Loading…

Decoded Free-text Messages varicode/JSC bodies · newest first

Time UTCCallsignFreq (kHz)BandSNRMessage body
Loading…
Per-frame decoded bodies. Multi-frame messages are stitched by the weekly GhostNet post-net summary. Null = heartbeat / directed / no body.

Most Recent Decodes newest first

Time UTCBandDialRXSNRCallsignGridTypeFrame
Loading…
JS8 frames carry callsign / grid / type + SNR; free-text bodies are frequently varicode fragments, not readable prose (lesson_js8_frame_decode_is_not_readable_message). The signal is who is heard on which band & receiver — not the frame text.

Top Callsigns Heard

CallsignGridDecodesLast heard
Loading…

Decodes by Band & Capture

Loading…

JS8 Reference & Knowledge Base

3.578 80m
7.078 40m
10.130 30m
14.078 20m
18.104 17m
21.078 15m
24.922 12m
28.078 10m

JS8Call + AMRRON + Comstat HF Layer

JS8Call standard dials, AMRRON Comstat format, net check-in procedure, bandwidth & schedule conventions.
knowledge/reticulum/js8call-amrron-comstat-hf-layer.md

GhostNet / (tr)uSDX HF Digital

GhostNet v1.5 codeplug, net schedule, (tr)uSDX transceiver integration for portable JS8/FT8 EMCOMM.
knowledge/reticulum/ghostnet-trusdx-hf-digital.md

JS8 frame decode ≠ readable message

Why decoded JS8 frames surface as callsign/grid/type rather than prose — the varicode-fragment lesson that shaped this dashboard.
memory/lesson_js8_frame_decode_is_not_readable_message.md

sdr-multi-decode Tool

Multi-receiver KiwiSDR JS8/FT8 fan-out decoder that feeds the js8_messages table + this dashboard.
scripts/sdr-multi-decode.mjs · knowledge/radio/tools/sdr-decode-tool.md
Capture cadence: js8-baseline (7.078 + 14.078 daytime, every 30 min) · js8-activity-probe (7.078 + 7.107, every 20 min) · js8-dx-scan (20/17/15/10m, 3×/day) · ghostnet-js8-capture (7.107, Thu net) · ghostnet-pace-tail (Winlink P2P tail).

NIST WWV / WWVH — Standard Time & Frequency

WWV (Fort Collins, CO) and WWVH (Kauai, HI) broadcast UTC on 2.5 / 5 / 10 / 15 / 20 / 25 MHz (WWVH omits 20 & 25). Received AM off a remote KiwiSDR and verified by deterministic DSP — scripts/wwv-decode.py. The station is confirmed by its 1-second tick train (5 ms burst of 1000 Hz WWV / 1200 Hz WWVH at the top of every second, phase-folded to a z-score), corroborated by the 1000/1200 Hz spectral line; the 100 Hz subcarrier carries the BCD time code and the 0.8 s minute marker anchors second :00. Which bands decode = a live propagation ground-truth to Colorado (and Hawaii).

DET tick z≥6, duty≥0.55, line≥4 dB (two agreeing signatures) marginal carrier + tick line present, tick train weak (QSB) quiet below threshold — closed path or off-air

Reception Board

Freq Status Station Tick z Duty Line Subcarrier 100 Hz Min marker SNR proxy Receiver Time (UTC)
Loading…

Higher bands (15/20/25 MHz) are daytime-propagation; expect them quiet after dark. Re-sampled by the wwv-sweep cron; newest capture per frequency shown.

40m — Three Nets, Three Dials

7.078 MHz
JS8Call Standard · USB · continuous
7.107 MHz
GhostNet · USB · Thu 21:00 ET nets
7.110 MHz
AMRRON · USB · net schedule
S2 GhostNet ↗ AMRRON ↗ AMRRON Nets ↗
Loading frequency dataset…

sdr-decode.py

Remote-KiwiSDR FT8/JS8/WSPR decoder. RX-only, no license required. Feeds the ground-truth log. Best RX: Bishop GA (9 km).
scripts/sdr-decode.py --band 40m --mode ft8 --find scripts/sdr-decode.py --list

wwv-decode.py

NIST WWV/WWVH receiver & verifier. Records AM off a remote KiwiSDR and confirms each station by phase-folded 1 s tick + spectral line + 100 Hz subcarrier. Feeds the WWV Reception Board. RX-only. wwv-sweep cron every 4h.
scripts/wwv-decode.py --all --find scripts/wwv-decode.py --freq-mhz 10 --find

HF Conditions Nowcast

NOAA SWPC + HamQSL + wspr.live polled every 30 min. SFI, Kp, X-ray class, per-band WSPR nowcast.

VOACAP Predictor

Local voacapl engine. Circuit reliability heatmaps by band, UTC hour, month. Runs daily at 05:00 UTC.

Remote KiwiSDR Receivers

Nearest live public SDRs to QTH EM83fv. Auto-discovered at run time by sdr-decode.py. Avoids local noise floor.
scripts/sdr-decode.py --list

Time-Sync SOP

chrony + gpsd setup for FT8/JS8 UTC slot alignment. <1s required; <0.5s recommended. GT-U7 GPS dongle target.
knowledge/radio/tools/time-sync.md
Tools Knowledge Base
Loading…

Mercury — Open VARA HF Alternative

Native-Linux OFDM HF modem (Rhizomatica / HERMES, GPL-3.0). Drop-in VARA HF replacement — identical TCP TNC on ports 8300/8301, so pat / Winlink work unchanged — and it carries Reticulum mesh over HF via a KISS broadcast port. Replaces Wine + VARA on the FZ-M1 field tablet (native binary, free, auditable).

Build & Install Status

Loading…

Live from /var/lib/jarvis/data/mercury/build-result.json, written by the mercury-install privileged op — not a hardcoded claim.

PTT Session Readiness

Mercury is a per-session binary — not a persistent service. Status below reflects validated readiness when binary is present. Activate with the commands in the Reference Config cards below.

Loading…
8300 TCP control
VARA-compat ARQ — pat connect
8301 TCP data
VARA ARQ data channel
8100 TCP broadcast
KISS framing — Reticulum (RNS)

RNS loopback validated 2026-07-14: KISS frame decoded, 510 B DATAC1, PTT ON/OFF cycle. Loopback config: /var/lib/jarvis/data/reticulum-loopback/ · Full bridge design: plans/mercury-reticulum-bridge.md

ardopcf — Open ARDOP Modem

Loading…

Open ARDOP (pflarue) — free, native-Linux modem with broader Winlink RMS reach than Mercury; our open fallback (or primary) below VARA HF. Live from /var/lib/jarvis/data/ardopcf/build-result.json (ardopcf-install op). pat TNC on 8515.

Layered HF-Modem Stack — robustness first

Principle (Rob 2026-07-11): be as robust as possible — independent modems, graceful fallback, one pat client switching transports by connect-URL scheme.

Email-reach order: VARA HF (future, Wine — widest RMS)ardopcf (open, common on RMS) → Mercury (open, Linux-native, emerging RMS) → JS8Call (keyboard, last-ditch).
Fully-open / grid-down order: ardopcf → Mercury → JS8Call. Full matrix + failure-mode analysis: knowledge/radio/tools/hf-modem-comparison.md.

Buildout Roadmap

Plan: plans/mercury-winlink-buildout.md. Build state above is machine-verified; the phase checklist tracks project progress (updated per session).

done Verify build system from source (branch mercuryv2, root Makefile, no cmake)
done mercury-build-deps op — apt deps + headers verified
done Compile Mercury 1.9.9 in userspace (~18s, x86_64)
done Install /usr/bin/mercury via mercury-install op — verified -l (6 modes)
done /radio Mercury sub-tab + live status endpoint
done Loopback / CLI TNC validation — pat→Mercury handshake confirmed (S211)
done Point pat at Mercury TCP TNC 8300/8301
done Handshake dry-run confirmed: VARA modem initialized → VARA version 4.9.0 registered
gated Deploy to FZ-M1 (Ubuntu 24.04.4) — tablet offline; connect to proceed
gated Winlink RMS session over Mercury ARQ — General-class license for HF TX
done Mercury broadcast port 8100 → RNS TCPClientInterface (DATAC1, kiss_framing) — loopback PASS 2026-07-14: KISS frame decoded, 510 B DATAC1, PTT ON/OFF confirmed
gated RF over-air test — two stations, DATAC1 decode from far end (pre-license: RX only)
gated Two-station QSO — RNS announce round-trip confirmed · gate: General license + radio + antenna
gated Benchmark: Mercury DATAC1 vs ardopcf on live path (SNR floor + goodput per mode) · gate: General license
Part-97 RNS encrypts all traffic → amateur HF only under declared EMCOMM activation with served-agency MOU. Full legal analysis: plans/mercury-reticulum-bridge.md §7

pat → Mercury (Winlink)

Point pat's VARA TNC at Mercury — same TCP interface. Then start Mercury with the audio/rig flags.
config.json → "varahf": { "addr":"localhost:8300", "bandwidth":2300, "rig":"", "ptt_ctrl":false } mercury -i plughw:0,0 -o plughw:0,0 -R 3073 -A /dev/ttyUSB0

pat → ardopcf (Winlink)

Open ARDOP fallback. pat's ardop TNC on 8515 (data auto 8516). Start ardopcf with real audio + PTT (pat drives PTT via hamlib when ptt_ctrl:true).
config.json → "ardop": { "addr":"localhost:8515", "arq_bandwidth":{"Max":500}, "rig":"", "ptt_ctrl":false } ardopcf 8515 plughw:1,0 plughw:1,0 -p /dev/ttyUSB1 connect ardop:///RMSCALL · WebGUI: -G 8514

Reticulum over HF (KISS) — loopback PASS 2026-07-14

Mercury DATAC1 broadcast port → RNS TCPClientInterface. Validated: KISS frame decoded, 510 B DATAC1, PTT ON/OFF cycle. RNS venv: /var/lib/jarvis/rns-env. ⚠ Part-97: RNS encrypts — EMCOMM-waiver or Part 90 only. Full bridge design: plans/mercury-reticulum-bridge.md
source /var/lib/jarvis/rns-env/bin/activate && mercury -m 0 -i plughw:1,0 -o plughw:1,0 -b 8100 & rnsd --config ~/.config/reticulum -v & config: TCPClientInterface kiss_framing=True target_port=8100 bitrate=980

HF Modem Knowledge Base

Deep KB per modem + the layered-robustness comparison matrix and Winlink network architecture.
knowledge/radio/tools/mercury.md
knowledge/radio/tools/ardopcf.md
knowledge/radio/tools/vara-hf.md
knowledge/radio/tools/hf-modem-comparison.md
knowledge/radio/winlink-architecture.md
Freshness: loading…
KB Health
loading…
Loading…

CW Human-Style LLM Decoder

Human operators beat rigid DSP on real HF by re-deriving sender timing, recognising QSB dropouts as impairments (not data), and predicting through fades using callsign + QSO + English priors. This pipeline makes an LLM do exactly that.
✓ Ph0 Eval harness live · baseline measured
✓ Ph1 Prompt-only eval done · qwen3.5:9b wins · gate near-miss
✓ Ph1.5 Few-shot eval done · gate still missed (0.497) · prompt-tuning plateaued
✓ Ph2A Fade-aware ruler: honest re-score passes gate (0.4688 ≤ 0.469) · zero-training win
✓ Ph2B Per-tone lock-on: 0% contamination · model-stage eval complete · QSB recall 0.76 (gate miss 0.85) → training path activated
✓ Ph2D Live flywheel + gold corpus appender built · downtime cron active (02:10 ET nightly)
→ Ph2C LoRA training — Rob picks compute path (C0 ROCm spike vs C1 rent-GPU ~$1)
→ Ph3 Decode-on-demand: backend pipeline + /radio WAV form DONE · live SDR capture pending (hardware integration)

Phase 1 Results — prompt-only local model eval (2026-07-15)

ModelCERWER ▲Heavy-QSB WERGateSpeed
DSP baseline 0.2590.452 0.670 ~0s
qwen3.5:9b ★ winner 0.2300.345 0.491 ✗ −5%~14s
gemma3:12b 0.2720.362 0.521 ~9s
qwen3:1.7b 0.3980.497 0.739 ~2s
Gate: heavy-QSB WER ≤ 0.469 (30% reduction from baseline 0.670). qwen3.5:9b achieves 27% reduction — near-miss. deepseek-r1 models excluded: thinking mode fills token budget before any response appears. Clean signals: all models preserve parity (WER ≤ 0.125), no regression.

Phase 1.5 Results — few-shot prompt engineering (2026-07-15)

ModelCERWER ▲Heavy-QSB WERGate
qwen3.5:9b Ph1 winner 0.2160.348 0.497 ✗ +6%
gemma3:12b · qwen3:1.7b A/B incomplete — background eval SIGKILLed on session resume (both were worse than qwen3.5:9b in Ph1, so non-decisive)
Verdict: Few-shot did not close the gate. 3 exemplars (CQ-garbled-prefix, signoff-corruption, random-groups-fade→[?]) improved CER (0.230→0.216) and taught the model not to hallucinate callsigns from noise — but heavy-QSB WER regressed slightly (0.491→0.497): correct [?] fade-markers count as word-errors vs. a truth string without them. Prompt-tuning has plateaued. Next: Phase 2 QLoRA fine-tune on qwen3.5:9b (deferred to nightly), OR accept 27% reduction and wire Phase 3 decode-on-demand.

DSP Baseline by Stratum — test/20 (2026-07-15)

StratumnCERWERField
Overall20 0.2590.4520.625
QSB none/light8 0.0000.1251.000
QSB heavy (≥0.4)12 0.4310.6700.375
Tight fist (<0.7×)3 0.4160.8890.667
Normal fist17 0.2310.3750.618
WPM slow (<15)3 0.1180.2221.000
WPM fast (>25)8 0.2160.2960.688

Phase 1 Analysis & Next Steps

What worked: qwen3.5:9b correctly preserves clean signals, recovers partial QSO structure (fills "DE" exchanges, recognises CQ format), and beats the baseline on every clean stratum. 24% overall WER reduction is real signal.
Where it fails: Heavily garbled signals (QSB≥0.8) — model can't recover callsigns from fragments like "T U K E DE T2 KU K" → truth "CQ CQ DE NK2T NK2T K". Random character groups (non-QSO) are irreducible for any language model.
Gate miss analysis: 5% above gate (WER 0.491 vs 0.469). The 12 heavy-QSB samples dominate; 2-3 better corrections would clear it. Options: (A) few-shot examples in the prompt, (B) QLoRA fine-tune on qwen3.5:9b, (C) accept 27% as "good enough" and proceed to Ph3.
DONESynthesizer + keying-tape extractor + corpus + eval harness
DONEPhase 0 baseline: 200 samples (160/20/20)
DONEPhase 1: prompt-only — qwen3.5:9b, gemma3:12b, qwen3:1.7b · script cw-phase1-eval.py
DONEPhase 1.5: few-shot on qwen3.5:9b — gate still missed (0.497); prompt-tuning plateaued · script cw-phase15-eval.py
DONEPhase 2A: fade-aware ruler → honest re-score → gate passed (0.4688); zero-training win · cw-eval.py --fade-aware
DONEPhase 2B DSP: per-tone find_tones() + independent keying tapes — zero contamination verified · cw-keying-tape.py --n-tones N
DONEPhase 2B eval: 40-sample 2-sender corpus · baseline DSP scored · 100% recall on clean/low-SNR · 0% cross-contamination · cw-eval.py --two-sender
DONEPhase 2D: gold corpus appender (cw-gold-append.py) + downtime cron (02:10 ET) + focus-lock wired
DONEPhase 2B model: qwen3.5:9b on 40 2-sender tapes · QSB recall 0.7647 · gate MISS (0.85) · +7% on qsb-one · 0% contamination · cw-2sender-model-eval.py
DONEPhase 2C corpus: 500 single-sender + 200 2-sender ready at /var/lib/jarvis/data/cw-corpus-2c/ (seeds 50k–60k; 0 errors)
BLOCKEDPhase 2C training — Rob picks compute path (C0 ROCm-iGPU fp16 spike vs C1 rent-GPU ~$1 QLoRA); 8.5% QSB recall gap to close (gate 0.85, current 0.765)
DONEPhase 3a: decode-on-demand WAV form live on /radio CW tab · POST /api/radio/cw-decode backend · per-sender copy+confidence + auto-gold + P4 rx-obs sidecar (2026-07-21)
NEXTPhase 3b: live SDR capture (tune → 30s WAV → decode) — gated on SoapySDR RSP1A integration

Phase 2A — Fade-Aware Re-Score (2026-07-15)

The Ph1.5 "miss" (WER 0.497 vs gate 0.469) was 100% measurement artifact: the standard ruler penalised [?] honest abstentions as substitution errors. A DP-based fade-aware ruler (cw-eval.py --fade-aware) exempts [?]/[fade] tokens aligned to real words in heavy-QSB samples, keeping the strict WER track alongside.
Gate criterionValueThresholdStatus
heavy-QSB WER(fa)0.4688≤ 0.469 ✓ margin 0.0002
clean WER vs DSP baseline0.125≤ DSP baseline ✓ tied
rel. WER reduction heavy-QSB30.0%≥ 30% ✓ exact
Margins are razor-thin (0.0002 / 0% / 0%) — the model is at the capability threshold, not comfortably above it. This confirms qwen3.5:9b is the right base; training (Ph2C) is deferred pending Ph2B results. Decision: fix the ruler, not the model — a zero-training win.

Phase 2B — Per-Tone Lock-On: Source Separation Baseline (2026-07-16)

DSP-only baseline: find_tones() FFT peak-picking → per-tone narrowband envelope → independent keying tapes. Scored on 40 synthetic 2-sender mixes across 4 noise variants. Cross-contamination is zero across all conditions — frequency-domain separation is solid.
VariantnRecall (mean)ContaminationWER (mean)
Overall40 0.8875 0.0000 0.122
Clean (SNR 30 dB)10 1.0000.00.0
Low-SNR (8 dB)13 1.0000.00.0
QSB on one sender (0.6)7 0.6790.00.408
QSB on both senders (0.5)10 0.7750.00.201
Zero contamination across all conditions — each sender's callsigns stay in its own tape. DSP floor is strong (100% recall clean + noisy). QSB drops recall to 68–78% — the model stage (cw-phase15-eval.py) fills this gap via fade reasoning + QSO priors. Gate for 2-sender: recall ≥ 0.85 on clean/low-SNR (PASSED) AND contamination = 0 (PASSED). QSB robustness extends to model stage in subsequent iterations.

Phase 2B — Model Stage: qwen3.5:9b on 2-Sender Corpus (2026-07-18)

Model stage ran after DSP separation: each per-sender keying tape fed to qwen3.5:9b for QSO-prior-based callsign recovery. Scored on same 40-sample 2-sender corpus. Zero cross-contamination maintained.
VariantnDSP recallModel recallΔ recallContamWER
Overall40 0.8875 0.9000 +0.0125 0.0000 0.054
Clean (30 dB)10 1.0001.000 0.00.0
Low-SNR (8 dB)13 1.0001.000 0.00.0
QSB one sender (0.6)7 0.679 0.750 +0.071 0.0 0.158
QSB both senders (0.5)10 0.775 0.775 0 0.0 0.104
Gate criterion (QSB variants, n=17)ValueThresholdStatus
QSB callsign recall (model stage) 0.7647≥ 0.850 ✗ gap 0.085 → 2C
Cross-contamination (model stage) 0.0000= 0.0
What worked: qsb-one improved +7%: model correctly recovered "CQ CQ DE W9TN W9TN K" from garbled tape "C K K ET DE A9 E WM ITN T" using CQ-pattern prior. QSO structure priors help when enough structure survives the fade. What didn't: qsb-both showed zero improvement — when both senders fade simultaneously, no anchor fragments remain for recovery (e.g., "NE#R", "TT0PR" can't be reconstructed without the callsign being present). Hallucination risk rises (WD4EM instead of WD9E). Decision: Prompt-only stage has plateaued. 8.5% QSB recall gap requires LoRA training (Ph2C). Training data: synthetic + accumulated real-air gold corpus. Gate remains ≥ 0.85 combined QSB recall.

Phase 2D — Live Improvement Flywheel (2026-07-16)

Iterative loop: real-air captures → multi-tone tape → decode → adjudicate → gold corpus → retrain. Gold rows accumulate at /var/lib/jarvis/data/cw-corpus/gold-real.jsonl.
DONEcw-gold-append.py — verified-decode → gold corpus with adjudicator guard (opus/human/rob only; model's own output never trains)
DONEDowntime cron: cw-phase2-downtime fires 02:10 ET · focus-lock 2.5 h · defers non-essential Ollama crons
DONECorpus-poisoning guard: only Opus/human-verified rows enter gold — never unverified model output
DONE/radio CW tab → "Decode WAV" form: WAV path + sender count + band/dial → POST /api/radio/cw-decode → per-sender copy+confidence displayed · auto-gold appends high-conf decodes to corpus
NEXTLive SDR capture: "tune to freq → capture 30s → decode" (requires SoapySDR WAV output from RSP1A — hardware integration)
DONEDashboard trend line wired — WER + callsign recall over iterations · feeds from cw-eval-CHANGELOG.jsonl
Gold corpus: loading…

CW Eval Trend — WER + 2-Sender Recall by Model

Single-sender WER (lower = better · gate ≤ 0.469)
2-Sender callsign recall (higher = better · gate ≥ 0.85)
Loading…

Decode WAV — On Demand

Run the full 3-stage pipeline (DSP → qwen3.5:9b → confidence routing) on any saved WAV. File must be under /var/lib/jarvis/data/ (service path constraint). Allow ~30–60 s.

Three-Stage Architecture

audio (WAV / live SDR)
  │ Stage 1: cw-keying-tape.py (DSP front-end)
  ▼
KEYING TAPE (tone · SNR · dit · on/off runs · histograms · hint)
  │ Stage 2: code pre-segments → local model reasons (QSO priors)
  ▼
COPY (text + confidence + [fade] markers)
Cascade: DSP baseline (free) → local model if low-confidence (~14s) → Opus on hard/interesting signals. Ph1 winner: qwen3.5:9b via Ollama (think:false, /api/chat, no training required). Offline-Mode target: qwen3:1.7b (too small at 1.7b for reliable QSO correction — Ph2 may close the gap).

Harness Training — CW Decoder 📄 Draft v1 (frozen) 📓 Working v1

Gradient-free scaffold optimization — three untried inference-time levers applied to CW decoding. Build order: Lever 1 (self-consistency vote) → Lever 3 (grammar constrain) → Lever 2 (arena routing). Dual-ledger: optimization log (keep-what-wins) + learning log (successes AND failures).
Phase
0 — Baseline lock
DSP QSB recall
0.9636 ✓ gate
Model stage anchor
Running…
Experiments
Experiment Ledger
Loading…

⚡ CW Keyer — Nano → transistor → G90 KEY jack

The K3NG Arduino Nano generates all Morse timing; a single transistor wired as a low-side switch shorts the G90 KEY line to ground to key the transmitter. One key line + shared ground is the whole interface (ptt=0 — the G90 does its own T/R). Full spec & sources: plans/cw-keyer/hardware-assembly.md.

key line D11 ptt=0 · G90 T/R daemon jarvis-cw-keyer cwctl control
firmware pin 11 ✅ bench P4 LED ✅ on-air GATED (D-KEY-4)

Related docs

plan.md decisions.md hardware-assembly.md scripts/cw-keyer/README.md tone-generator-cpo.md

Bill of materials

PartValueNote
Arduino NanoATmega328P / CH340Have it · K3NG, D11, ptt=0
Transistor (pick one)2N2222 NPN or 2N7000 MOSFETSame wiring · 2N3904 also OK
Series resistor1 kΩ ¼Wbase/gate
Pulldown resistor10 kΩ ¼Wcontrol→GND · recommended (NPN) / required (MOSFET)
Cable to rig3.5 mm TRSmatches G90 KEY jack
Board + wireperfboard, solder, heatshrinkor in-line "dead-bug"

Added parts: 1 transistor + 2 resistors + 1 TRS lead. Everything else in hand.

Why this works

The G90 holds its KEY line at a small positive voltage (internal pull-up), low current. Keying = pull that line to ground. The Nano's 5 V logic can't do that directly across an isolated rig, so the transistor does the pulling: D11 HIGH turns it on and grounds the Tip; D11 LOW turns it off and the line floats back up.

The 10 kΩ pulldown matters for safety: while the Nano resets, D11 floats high-impedance — the pulldown holds the transistor off so the rig can't be keyed by accident. False keying = unwanted TX, exactly what the on-air gate guards.

Wiring — low-side switch (2N2222 NPN or 2N7000 MOSFET, identical)

   Nano D11 ──[ 1 kΩ ]──●──────────►  B/G   (Base / Gate — control)
                        │
                     [ 10 kΩ ]  ← pulldown: holds Q OFF while the Nano resets
                        │
   Nano GND ────────────●──────────►  E/S   (Emitter / Source — common)
       │
       └──── common ground ───────►  G90 Sleeve

   G90 Tip + Ring (jumpered) ───────►  C/D   (Collector / Drain — switched)

   Q = ONE transistor.   2N2222 / 2N3904 (NPN):  B/G=Base  C/D=Collector  E/S=Emitter
                         2N7000 (N-MOSFET):       B/G=Gate  C/D=Drain      E/S=Source
   (B/G, C/D, E/S are three SEPARATE pins — never wire them to each other.)

   KEY-DOWN:  D11 HIGH → Q conducts → C/D↔E/S closes → G90 Tip pulled to GND → TX keys
   KEY-UP:    D11 LOW  → Q off       → Tip floats (rig's pull-up)             → no key

Common ground is essential — Nano GND, transistor Emitter/Source, and G90 Sleeve must all meet at one node, or the switch has no return path.

Transistor pinout — VERIFY your part

TO-92, flat face toward you, legs down, left→right:

PartPin1Pin2Pin3
P2N2222A / PN2222A (NPN)EBC
2N3904 (NPN)EBC
2N7000 (MOSFET)SGD

Some off-brand "2N2222" TO-92 are ECB, not EBC. TO-92 pinouts vary by vendor — read your part's datasheet, or buzz it out with a meter (diode mode) before soldering. Swapped B/C keys backwards or not at all.

G90 KEY jack — 3.5 mm TRS manual-verified

ContactSignalConnect to
TipDIT (dot)jumper together →
Collector/Drain
RingDAH (dash)
SleeveGroundcommon GND

G90 manual (p.9): "When using a manual CW key, connect 'dit' and 'da' together." So Tip+Ring are jumpered into one key node, driven by the transistor.

Arduino Nano pinout — D11 (key line) + GND

    Left side                 Right side
  (top → bottom)            (top → bottom)
  ─────────────             ──────────────
    D13 (LED)                 D12
    3V3                    ★  D11 (MOSI) ← KEY LINE to 1 kΩ
    REF                       D10 (SS)
    A0                        D9
    A1                        D8
    A2                        D7
    A3                        D6
    A4                        D5
    A5                        D4
    A6                        D3
    A7                        D2
    5V                     ★  GND ← common ground (transistor E + G90 Sleeve)
    RST                       RST
  ★ GND ← (also GND)          RX0
    VIN                       TX1

  ★ = pins used in this build
  D11 is the 2nd pin from top on the RIGHT side.
  GND is the 12th pin from top on the right, and
       the 14th pin from top on the left.

D11 is the K3NG key line (tx_key_line_1 11). It goes HIGH for the duration of each Morse element and drives the transistor Base through the 1 kΩ resistor.

GND ties to the transistor Emitter and to the G90 Sleeve — all three must share a single node. Use either GND pin (right-side pin 12 is convenient when both D11 and GND are on the right).

Local mirror: Nano pinout PNG (Arduino official) — works offline on LAN.

Arduino Nano official pinout

2N2222 NPN — concrete wiring (TO-92, E-B-C)

Part in hand: TO-92 marked 2N2222 A331 → pinout is E-B-C (flat face toward you, legs down, left→right). This schematic uses those pin names directly — no generic B/G aliases.

  Nano D11 ──[ 1 kΩ ]──●──── Base (B)  ← pin 2 of TO-92 (center leg)
                       │
                    [ 10 kΩ ]  ← pulldown: Base → GND, holds transistor OFF while Nano resets
                       │
  Nano GND ────────────●──── Emitter (E) ← pin 1 of TO-92 (left leg, flat face toward you)
      │
      └──── common GND ──────── G90 KEY jack Sleeve (3.5 mm)

  G90 KEY Tip + Ring (jumpered) ─── Collector (C) ← pin 3 of TO-92 (right leg)

  TO-92 orientation:  ┌─────┐   flat face toward you, pins pointing down
                      │ 2N2222│
                      └─┬─┬─┘
                        E B C   ← left=E, center=B, right=C

  KEY-DOWN: D11 HIGH → B-E forward biased → C-E conducts → G90 Tip pulled to GND → TX keys
  KEY-UP:   D11 LOW  → 10k pulls B to GND → transistor off → Tip floats → no key

  ⚠ Some off-brand TO-92 "2N2222" are ECB, not EBC. Always buzz it out with a meter
    in diode mode first: B→E and B→C both read ~0.6 V forward; reverse both read OL.
    Date code A331 follows P2N2222A spec (EBC) — trust the meter, not the label.

TRS plug: Tip = DIT, Ring = DAH, Sleeve = GND (G90 manual p.9). Tip+Ring are jumpered into one key wire — both connect to Collector (C). Meter the cable conductors before soldering; wire colours are not standardised.

Solder / build steps

  1. Breadboard dry-fit first — no solder until the bench test passes.
  2. 1 kΩ from Nano D11 → transistor Base/Gate.
  3. 10 kΩ from Base/GateGND (pulldown).
  4. Transistor Emitter/Source → Nano GND.
  5. TRS lead: strip Tip/Ring/Sleeve; solder Ring to Tip (jumper).
  6. Tip+Ring key wire → transistor Collector/Drain.
  7. TRS Sleeve → common GND node.
  8. Heatshrink joints; strain-relieve; keep leads short.

Bench test — no rig, no antenna

Multimeter in continuity/Ω across TRS Tip↔Sleeve (rig unplugged):

  1. hostx scripts/cw-keyer/cwctl statusonair:false.
  2. Meter Tip↔Sleeve idle → open (key up).
  3. cwctl send "E" 5 → brief close (a dit) then open.
  4. cwctl send "R" 5 → di-dah-dit, three clean closures.

Pass = the meter tracks the Morse. Only then connect the rig — dummy load first, no antenna.

On-air — GATED (D-KEY-4) · do NOT transmit until cleared

aBench interface test passes (this tab) + P4 LED gate ✅ (cleared 7-26).
bRob's explicit per-run go.
cBand / power / antenna / license confirmed for that run — dummy load first.

G90 config (verify at the rig): set CW key mode = manual / straight key (internal keyer OFF) so it keys 1:1 with our formed Morse. A continuous tone on key-down = the internal iambic keyer is still on — switch it off. Start into a dummy load at low power, confirm output, then antenna.

Build progress

doneFirmware: D11 key line + ptt=0 flashed (P6 prereq, 05598ec4).
doneBench P4 LED gate cleared — Rob confirmed 5× R @10 WPM.
todoInterface soldered (transistor + 2 resistors + TRS lead).
todoBench continuity test passed (meter tracks Morse, no rig).
gatedG90 connected, dummy load, key-mode = manual verified.
gatedFirst on-air key under KN4IJL (Rob go + band/power/antenna).

Resources & local mirrors

All local mirrors served from this dashboard — accessible on LAN without internet.

ResourceLocal mirrorExternal (canonical)
Xiegu G90 operation manual
KEY jack pinout p.9; CW mode p.11–14
✅ G90 manual PDF manualslib.com
2N2222A / P2N2222A datasheet
ON Semiconductor — TO-92 pinout + absolute max ratings
✅ 2N2222A PDF onsemi.com
Arduino Nano pinout diagram
Official Arduino.cc — all 30 pins labelled
✅ Nano pinout PNG arduino.cc
EI4LF G90 CW primer
Straight-key workaround, QSK/semi-break-in setup
⚠ Unavailable (obriain.com parked/404 as of 2026-08-12) Wayback Machine
K3NG CW keyer firmware
Repo config + feature flags (flashed commit 05598ec4)
scripts/cw-keyer/README.md github.com/k3ng

Shortwave Broadcast Catalog — EM83 (SE US / GA)

EiBi A26 season schedule (Mar 29 – Oct 25, 2026) filtered for stations receivable from EM73–EM83. Includes domestic US transmitters (WRMI, WWCR, WBCQ, WEWN), Caribbean, and Latin American targets. Source: EiBi (eibispace.de) · A26 (summer 2026) · 2026-08-09 · 550 broadcast entries (deduped)
kHz Station UTC Window ET Window Days Language Target SE-US Note