No build step. Plain ES modules, served as static files; the Pi never needs Node.
frontend/
index.html style.css manifest.webmanifest sw.js icons/
src/
main.js api.js audio.js tuner.js util.js meter-cal.js
pages/ login.js radio.js admin.js
components/
freq-display.js LCD-style frequency; tap a digit = tuning step, wheel over a digit tunes by it
vfo-b.js VFO B readout, swap / A=B / B=A, SPLIT, quick split, set B
dial.js small round dial (tablet/desktop): drag, wheel, arrow keys; Fine / Coarse x10
tuning-strip.js sliding tuning ruler (phones): swipe, flick, x10
meters.js coloured segment meters: S-meter, power, SWR, ALC, COMP
controls.js colour-coded chips, level sliders, and the filter/DSP panel as tabs per group (Filter, Noise,
Receiver, Transmit; last tab remembered). All generated from the radio's capabilities
ptt.js hold-to-transmit, heartbeat, TX timeout bar
| Width | Layout |
|---|---|
| < 700 px (phone) | Compact top menu on one line, radio bar with a green dot and the control status. Same VFO block as large screens, just smaller: VFO A with the signal meter, the small TX meters under it, VFO B as an attached tab (red TX badge when it is the one transmitting in split); a thin sliding tuning scale; smaller buttons in VFO tools / step / band / mode / chips (side-scrolling rows). Bottom tabs: Radio / Filters / Levels / Audio; PTT sticky on the Radio tab |
| Tablet (touch screen, no hover, >= 700 px wide; e.g. iPad 10") | Phone-style tabs Radio / Filters / Audio & Levels / Digital with the large-screen look: Radio tab = the large-screen arrangement in compact form (smaller buttons, thin tuning scale): VFO block (A + meters + B tab), tuning scale, [VFO tools |
| There is no round dial any more: the sliding tuning scale (swipe or drag, x10 button) is used on every screen size. It shows frequency numbers whose spacing follows the tuning step (bigger step or x10 = wider span). | |
| Look: flat dark-slate buttons with a small LED bar on the left edge (grey = off, yellow = selected/on), controls grouped in labelled | |
| blocks (VFO TOOLS, TUNING STEP, MODE, BAND SELECT, SET FREQUENCY, RECEIVER & TOOLS). The active VFO panel has a cyan border, the transmitting | |
| panel turns red with a TX badge, the transmit VFO in split has an amber border and SPLIT badge. Meter colours keep their meaning | |
(blue/green/yellow/red). VFO B shows no S-meter or mode because the CAT command set has none for VFO B. Respects prefers-reduced-motion |
|
| and safe-area insets. | |
| ## Meters (provisional scales: read this) |
The radio's CAT meters are raw 0-255 values and the manual gives no calibration. So:
S_TABLE in src/meter-cal.js: raw -> dB relative to S9, in the style of
the Hamlib calibration for this radio family. It is an approximation, not measured on your radio (the label says "approx. scale").
S0-S9 fill 60% of the bar like a real meter, S9 to +60 dB the rest. Colours: blue, cyan, green up to S9, yellow and orange
above, red for strong signals.ui.meter_alc_full, ui.meter_comp_full). The ALC default is now 160: with normal speech into a dummy load the radio's ALC meter showed about 50% when the raw peak was 79, so 100% ~ 158 (one data point, assumes a straight line); adjust it so the app's bar matches the radio's own meter.
The COMP default (255) is a placeholder: no data yet.To calibrate properly (bench 5 in 03-bench-checklist.md): feed known signals (S-meter), and compare against the radio's own
meters and a wattmeter (power, SWR, ALC).
set_frequency is in flight at a time; the newest target wins. The display shows the target while tuning and returns to the
radio's own reported frequency shortly after (the radio is authoritative).manifest.webmanifest + sw.js (network-first shell cache, never caches /api or /ws) + generated icons
(python tools/make_icons.py). Static files are served with Cache-Control: no-cache; an open page shows a "Reload now" banner
when a new version has been installed under it.
Device detection: src/main.js sets data-tablet on the page when the screen has no hover (touch) and is 700 px or wider. To force a mode open the page with ?device=tablet, ?device=desktop or ?device=auto (remembered per browser).
?device=desktop to get the old all-in-one page back in a browser (remembered); ?device=auto resets it. Manual notch and auto notch
(DNF) moved to the Noise tab of Filters & DSP.Aligned rows: every control in Filters & DSP, every level and every audio meter is one row [button or label | bar or select | value]
(two columns of rows on wide screens). RIT and XIT are two buttons on one row with the shared clarifier offset bar (double-click the bar
to clear it). The offset bar range is now +-9990 Hz in 10 Hz steps: the old -9999..+9999 range produced values such as 201 Hz that the
server rejects (must be a multiple of 10), which is why the clarifier "did nothing". On the FT-991A, setting an offset while neither RIT
nor XIT is on also switches RIT on (RU/RD only move the clarifier knob). NOT bench-verified yet.
Audio devices: Audio tab, "Speaker / output" and "Microphone / input" lists (this browser only, remembered): choose a Bluetooth
headset or a car hands-free kit. Output selection needs setSinkId (Chrome, Edge, Android Chrome); Safari/iOS cannot choose it, there
the system sound settings decide. Device names appear after the microphone was allowed once. If the chosen microphone is gone, the
default one is used.
The REAR/MIC selector was removed from the PTT area: the microphone button in the VFO panel arms the mic and sets menu 106 to REAR (off = MIC).
VFO A: the band text ("20m") sits next to the mode ("USB") under the frequency. VFO B: [VFO-B] [TX] [SPLIT] [mode] [band] ... frequency
with the frequency right aligned. The VFO B mode comes from OI; (opposite band information, manual p.14, read only; same layout as
IF;), polled with the other VFO B reads. Bench-verified. (Earlier notes saying VFO B has no mode were wrong.) VFO B still has
no S-meter. The Tune button has more space from the tuning step buttons.
Radio meters (raw) row on the Audio tab: live raw 0-255 ALC, COMP, power, SWR and S-meter values, plus the peak ALC and COMP of the
last transmission (reset at each new key-down). Used to calibrate the percentage scales (ui.meter_alc_full, ui.meter_comp_full).
PTT on every tab (Radio, Filters, Audio & Levels, and Digital for FreeDV), pinned at the bottom above the tab bar, so levels and audio can be adjusted while transmitting. A long note on the Audio tab (device names hint) no longer widens the page: the page used to grow wider than the screen and push the tab bar out of view.
AGC (Receiver tab of Filters & DSP): OFF / FAST / MID / SLOW / AUTO via GT0 (documented in all four manuals). The radio answers
AUTO as AUTO-FAST / AUTO-MID / AUTO-SLOW (4 / 5 / 6); the app shows them all as AUTO. Offered on the FT-991A too; NOT bench-verified yet.
ATT is a four-step list (OFF / 6 / 12 / 18 dB) on the newer radios. See docs/08-other-radios.md.
Memory channels (FT-991A and FTDX101D): the Memories button, beside Tune in the tuning step row, opens a list of the radio's stored channels (001-099: number, tag,
frequency, mode) with a filter box; tap one to recall it. While the radio is in memory mode a purple MEM 005 badge shows in the VFO A
header and a Back to VFO button appears (it sends VM;, the radio's V/M key, which is a toggle, so it is only sent while in memory mode).
The list is read with MT ccc; (the READ form, which also returns the tag) and cached by the server; Re-read reads it again after you
changed memories on the radio. Recall is MC ccc;. Export saves the list as a CSV. Add / Edit (FT-991A, administrators; capability memory_edit) writes one channel with the WRITE form of MT
(MT ccc + frequency + +0000 + clarifiers off + mode + 0 + tone mode + 00 + shift + 0 + 12-character tag, manual p.11); the frame is checked first (channel 1..99, frequency range, known
mode, ASCII name of at most 12 characters) and matches the layout the radio itself reports. Because a real FT-991A answers every MT read with channel 001 in P1, a write is never trusted: the
whole list is read before and after, the channel must read back as written (only the frequency may be rounded to the radio's step) and no other channel may have changed; if one did, editing is
switched off until the service restarts. Endpoint POST /api/memories/{channel} (administrators, CSRF token, confirm: true, refused while the radio transmits, audited as memory_write).
Bench findings (FT-991A, 2026-10-08): the MT write has no tone-number field, but the radio stores, per channel, the tone number (CN00 CTCSS 000-049, CN01 DCS 000-103) that VFO-A holds at the moment of
the write; so for a channel with a tone the driver sets VFO-A (mode, frequency, OS0 shift in FM, CT0 tone mode, CN00/CN01), writes the channel, recalls it and reads CN00/CN01 back, then puts VFO-A, its own
shift/tone settings and the V/M state back. GET /api/memories/{channel}/tone returns what a stored channel holds (it recalls the channel and puts the radio back); the editor uses it so that editing a name keeps the tone.
Deleting: recall the channel (checked against IF), then AM; (VFO-A TO MEMORY CHANNEL) empties it (POST /api/memories/{channel}/delete); proved the same way as a write, and AM is never sent unless the recall took.
Not offered: the repeater offset (a radio menu setting). Channels 100-117 (the PMS ranges P-1L..P-9U) are not listed. Empty channels are skipped (the radio answers ?;).
Bench finding (FT-991A on a Raspberry Pi): the answer to MT ccc; always says channel 001 in its P1 field while carrying the data of
the channel asked for. The server therefore accepts the next MT answer as the reply to its own question and takes the channel number from
the request. A channel that stays silent is retried once, then tried with MR ccc; (read without tag), then skipped; the list read only fails
after four silent channels in a row. The status answer IF reports the recalled channel correctly.
Status lights in the VFO A header, after RX / TX / MEM: one preamp light whose text is the setting in use (IPO, AMP1 or
AMP2, from PA0;), ATT (RA0;, or the ATT step of the newer radios), one AGC light whose text is the setting (GT0;:
AGC OFF / FAST / MID / SLOW / AUTO; dark while OFF) and TUNER (AC;; blinks amber while tuning). Green =
active, dark = inactive; they appear only for functions
the radio profile has. Read-only: they mirror the state the server already polls. On phones (< 700 px) they wrap onto their own row.
Each light can be hidden per device (Account > Display, kept in localStorage as rr.lights; the header closes up when all are off).
ui.swr_warn (default 3.0, 0 = off). While tx and the SWR is at or above the limit the SWR bar is drawn fully red and a
line "High SWR: x.x:1" appears under the meters; it is kept 5 s after the transmission and a new transmission starts a new reading. Radios
that report a ratio (swr) are compared directly; the FT-991A reports swr_raw (0..255, RM6), turned into an estimated ratio with
1 + 2 * raw / ui.swr_raw_at_3 (PROVISIONAL, default 100 = "3:1"; the manual has no calibration, set it from the radio's own meter).
The limits are delivered in the hello message (ui.meter), so a change needs a restart like the ALC/COMP calibration.Timings.prefetch_s, 4 s) the server reads the 99 channels in the
background, so Memories opens at once. The cache is dropped and re-read on every reconnect.GET /api/admin/backup returns the config as a .toml download; POST /api/admin/restore applies the
settings the web UI may change (EDITABLE) and ignores the rest (never server.*, storage.*, safety.allow_ptt).GET /api/admin/update; when newer is true and that version was
not dismissed (localStorage rr.updateSeen) a bar offers What's new (the release page), How to update (opens Admin > Config >
Updates, which shows the self_update.sh command) and Dismiss. The server asks GitHub once a day (updates.py; updates.check,
updates.repo; the simulated radio never does); Check now in the Updates card asks at once. Nothing is installed from the page.Locked on - tap TX LOCK or the button to stop); it shows STOP while latched, and a tap on TX LOCK or on the big button un-keys. (The earlier slide-up gesture is gone.) HOLD TO TRANSMIT still transmits only while held. The heartbeat keeps running, so the server still un-keys on silence, disconnect or the time limit; the page also releases when it becomes hidden or the transmission ends. The Lock PTT button still blocks starting one.Lock (screen lock): a round padlock button beside HOLD TO TRANSMIT. While on it locks the WHOLE screen of this device: the PTT button, TUNE and
every other control, the top bar and the tabs are inert (no touch, click, wheel or key press reaches them; scrolling still works) and only the padlock stays live. This device only (remembered in localStorage as rr.pttLock; a running transmission can still be released, and locking while PTT is held
releases it). It is a client-side convenience against stray touches; the server-side safeguards are unchanged.
Top bar on phones shows icons only (labels from 700 px up). The radio tab shows only a status dot: green = radio online, yellow (pulsing) = connecting/reconnecting, red = radio offline.
mic_input_reset). If the page comes back with the microphone still armed it sets REAR again.