# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this project is

Browser-based file transfer over audio. The sender page encodes files as AFSK/MFSK audio tones; the receiver page (`remote.html`) decodes them using the Web Audio API. The protocol is HDLC framing + a ZModem-lite Go-Back-N sliding-window ARQ.

Two host-side transports are supported:

| Page | Transport | Audio path |
|------|-----------|-----------|
| `kvm.html` | WebRTC/Janus | GL-RM10 KVM device — tones travel over the KVM's USB audio path |
| `avd.html` | Local audio (virtual cable) | Azure Virtual Desktop — tones travel via RDP audio redirection |

`remote.html` is transport-agnostic — it always uses the local audio hardware of the machine it runs on.

## Running

```bash
python3 serve.py          # HTTPS on 0.0.0.0:8443, auto-generates cert.pem/key.pem
node sliding-window-test.js          # full protocol test suite
node sliding-window-test.js --quick  # faster subset
node sliding-window-test.js --verbose
node modem-dsp-test.js               # DSP-level mod/demod loopback (real audio)
```

`serve.py` prints two URLs on start: one for the host browser and one for the remote Windows browser (`remote.html`). Both require accepting the self-signed cert warning.

### KVM mode (`kvm.html`)

Open `kvm.html` on the operator's machine. Enter the GL-RM10's IP and password and click Connect. On the machine behind the KVM, open `remote.html` in Chrome and start the modem.

### AVD mode (`avd.html`)

1. Install a virtual audio cable on the operator's machine (e.g. [VB-Audio Virtual Cable](https://vb-audio.com/Cable/)).
2. Open the [AVD web client](https://client.wvd.microsoft.com/arm/webclient) and connect to the Azure VM session. Configure it to use the virtual cable for mic input and speaker output.
3. On the Azure VM, open `remote.html` in Chrome. Select the RDP-redirected audio devices (the AVD client maps them into the VM as standard Windows audio devices).
4. Open `avd.html` on the operator's machine. Select the virtual cable as input and output, then click Connect.

Start with **600 baud AFSK** — it uses tones at 1200/2200 Hz which are well within RDP audio's passband. Higher baud rates and large M values push tones above 8 kHz where RDP codecs may attenuate them significantly.

## Shared library

`host-common.js` contains all code shared between `kvm.html` and `avd.html`:

- Logging (`log`, `setBadge`, `fmtSpeed`)
- LED indicators (`Leds`)
- Shared `App` state object
- `initModem(audioCtx, audioSource, modDest)` — builds mod/demod/ZModem, wires HDLC tick and monitor gains
- `teardownModem()` — tears down the modem stack; call before transport-specific cleanup
- `setConnUI(on)`, `applyMonitorMode(mode)`, `checkSampleRate(audioCtx)`
- `clampM()`, `loadSettings(extraIds)`, `saveSettings(extraIds)`
- `wireFileTransferUI()`, `wireMonitorSelector()`, `wireMfskOpts()`

Exposed as `window.HostCommon`.

## Browser requirement

**Chrome only.** Firefox does not implement `setSinkId()`, which is required for audio device routing in both KVM and AVD modes.

## Critical constraints

**Baud/M/W must match on both sides.** If sender and receiver are misconfigured, the receiver will decode nothing. The RSSI bar and HDLC frame counter (`frames ok=N bad=N`) in the modem card are the primary diagnostics — signal present but frames=0 means a modem mismatch.

**Decode-lock indicator.** Each demod exposes `getLockState()` (`{ locked, carrier, rssi }`) and `tickLock()` (call ~1/s; advances the time-based state and emits diagnostics). `locked` means a CRC-OK frame arrived within `LOCK_HOLD_MS`. The **CD LED** encodes the three states the operator needs to tell apart: off = no carrier, **amber = carrier present but nothing decoding** (the mismatch/lock-loss case — previously a silent failure), green = decode-locked. When a carrier is present with sustained CRC failures and no good frame, `tickLock()` logs *"carrier present but no frames decoding — check baud / M / modem-type match"* instead of failing silently. Lock acquire/loss is logged too. The host HDLC tick drives this and appends `· LOCK` to the frame counter.

**MFSK Nyquist limit.** Tone formula: `freqs[i] = (i+1) × baud`. Highest tone = `M × baud` must be < 24 000 Hz (SR/2). This constrains valid combinations:
- 1200 baud → M ≤ 16 (top tone 19 200 Hz)
- 2400 baud → M ≤ 8  (top tone 19 200 Hz)
- 4800 baud → M ≤ 4  (top tone 19 200 Hz)

`clampM()` in `host-common.js` enforces this in the UI (both host pages).

## Protocol details

- **Sequence numbers** wrap at 256 (`nextSeq()` helper). Don't assume 32-bit seq in ACK/NAK frames.
- **NAK staleness check** uses `nakOff < ackedOff` (strict less-than). The `<=` form is wrong — it incorrectly discards valid NAKs when `nakOff === ackedOff`, causing 6-second timeouts instead of immediate rewinds. Do not change this back.
- **Compression**: files are deflate-compressed before framing; `meta.sizeDeflate` in the ZFILE header carries the compressed length. The receiver inflates after reassembly.
- **Adaptive ACK timeout**: the sender starts from a baud-aware default, then tracks RTT with a Jacobson/Karn estimator (`RTO = srtt + 4·rttvar`, clamped to `[frameTxMs+500, 30000]` ms) and backs off exponentially (×2, cap ×8) on loss. RTT is only sampled from frames sent exactly once (Karn) — retransmits carry a `retx` flag and are skipped. Pin a fixed timeout by passing `opts.ackTimeoutMs`, which disables adaptation. The estimator is unit-sensitive: it measures RTT with the wall clock but compares against `ackTimeoutMs`, so under the test harness's `SPEEDUP` it scales the sample by `_timeScale` (1 in production).

## Test harness

`sliding-window-test.js` runs in Node.js. It simulates the KVM OPUS audio path with configurable loss, burst loss, and latency. All timeouts are divided by `SPEEDUP` (200) so tests complete in seconds — this is intentional, not a bug. All 11 scenarios must pass (no ✗ lines) before shipping protocol changes.

**It is frame-level only** — sender and receiver are wired together with `{onFrame}` mocks, so it never runs a sample through `MfskMod`/`MfskDemod`. Clock-recovery or symbol-decision regressions can ship with all 11 protocol scenarios green while real audio decodes nothing.

`modem-dsp-test.js` covers that gap. It renders genuine HDLC+NRZI+MFSK/AFSK audio with the real modulator, adds white noise to a target SNR, and asserts the real demodulator decodes — across MFSK/AFSK configs, both the single-frame path (blocking `_send`: ZFILE/ACK/NAK) and the phase-continuous pipelined stream (`enqueueBits`: back-to-back ZDATA). Run it (no ✗ lines) before shipping changes to the modulators, demodulators, or the DPLL clock-recovery gate.

## localStorage persistence

All HTML pages persist modem settings (`baudSel`, `windowSel`, `modemTypeSel`, `mSel`) to localStorage via `loadSettings()` / `saveSettings()` in `host-common.js`. Saved values are validated against actual `<select>` options and stale entries are removed — if you add or remove a baud/M option, the stored value is cleared on next load rather than silently applying an invalid value.

`avd.html` additionally persists `avdInSel` / `avdOutSel` (audio device IDs) separately, since device IDs are dynamic and not validated against static option lists.
