Ctrl K
ComunidadGuías…

README.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Hearthwave

A GMRS hub that turns a home server or x86 mini PC into a shared radio
operating station for every member of your household or neighborhood watch group.
Incoming transmissions are transcribed by speech-to-text and streamed to all
connected devices; outgoing messages are synthesized to speech, automatically
wrapped with the FCC station callsign (§95.1751), and transmitted over the air.
Each family member or watch volunteer signs in from their own phone, tablet, or
laptop — no app install required.

Net Control Station (NCS) mode is built in, with a live check-in roster, six
traffic priority levels, and SKYWARN weather alerts sourced directly from the
National Weather Service. On top of that, Hearthwave has a true installable plugin
system: drop a plugin into `/data/plugins` (or upload a `.zip` from the admin
Settings) and it hot-loads into the radio pipeline without touching core server
logic. MeshCore — a bridge that forwards every transmission onto a LoRa mesh for
off-grid reach — ships as an example plugin, alongside a Meshtastic equivalent, to
show how it's done.

Hearthwave is a fork of GMRS-TTY that replaces the desktop PySide6 UI with a
browser-based React frontend communicating over WebSocket.

> **Latest release:** v2.25.0

## Who uses it

- **Families** — one GMRS license covers the whole household. Each person gets
  their own account with their own voice and preferences. Kids on tablets, parents
  on laptops, grandparents with large, high-contrast type — everyone on the same channel.
- **Neighborhood watch groups** — deploy Hearthwave as the watch's base station.
  Assign listen-only accounts to patrol volunteers and TX-capable accounts to
  designated net control operators. The NCS roster tracks who is active on each
  patrol; SKYWARN alerts auto-announce over the air when severe weather approaches,
  and trained spotters can file standardized SKYWARN spot reports straight from the net.
- **Non-speaking operators** — the AAC interface turns Hearthwave into a
  communication device that transmits: tap large symbol buttons to build a
  message, press SEND, and it is spoken over the air in your own chosen voice.
  Deaf and hard-of-hearing operators read every transmission as live text.

## Features

- **FCC license badges at check-in** — every check-in (account or radio caller)
  triggers a background FCC lookup on the callsign; the roster card then shows
  **FCC ✓** for an active license, a red **License expired** flag, or **Not in FCC**.
  Lookups never block the check-in, are skipped offline, and are cached per
  callsign; the verdict rides into the saved session record
- **Approximate map pins from a license** — an admin opts a contact in (**Show on
  map when checked in**) and a check-in with no GPS position anywhere plots them at
  their FCC-licensed *city*, drawn as a hollow dashed ring and labelled *Approximate*
  so it never reads as a real fix. Off by default, per contact, admin-only to turn
  on, and skipped entirely when a mesh or APRS position for that callsign is already
  on the map
- **ICS-214 export** — any past net exports as an ICS-214 Activity Log for an
  ARES/RACES section: the numbered boxes of the paper form, with the operational
  period, the checked-in stations as resources assigned, and a timed activity log
  built from the net opening, each check-in, and the net closing. A short dialog
  collects the four boxes a net record has no answer for (incident name, preparer,
  ICS position, home agency) and remembers them for the next export
- **Printable roster** — **PRINT ROSTER** puts a past net on paper as a plain
  numbered sheet: heading, operational period, one row per station, and a
  prepared-by/date sign-off line. Prints the whole roster in check-in order
  regardless of how the on-screen table is filtered or sorted
- **Stations on a map** — positions heard over Meshtastic, MeshCore, or APRS on
  RF (via a KISS TNC such as direwolf) are plotted on a **MAP** panel, with a
  keyboard- and screen-reader-friendly **List** view giving each station's
  distance, bearing, and how long ago it was heard. Distance and bearing are
  measured from a fixed station latitude/longitude set in **Settings → Station**,
  and are resolved on the server so a kiosk needs neither a trustworthy clock nor
  any geography of its own. Map tiles come from an offline pack dropped into
  `/data/tiles`, so the map keeps working with the internet down; a remote tile
  URL is an optional fallback. Wall displays show the map too, and e-ink panels
  get the nearest-first list instead. Every position source is **receive-only** —
  none of them has a transmit path, and each is off by default (see
  [Position reports](https://xpiatio.github.io/Hearthwave/legality.html#positions)
  for why "APRS on GMRS" is not what it sounds like)
- **Dated message timestamps** — a message from an earlier day now carries its
  date next to the time, so a transcript left up overnight, a long net log, or a
  wall display that has been running for a week no longer reads as though
  everything arrived in the last few minutes. Timestamps roll over on their own
  at midnight without a reload.
- **Audio devices remembered by name** — device selections are stored by name
  rather than by index, so they survive new hardware being plugged in or a card
  that was busy at boot instead of silently shifting the station onto the wrong
  device; existing settings migrate automatically, and a **Rescan audio devices**
  button picks up hardware added while the server is running.
- **Coordinator dashboard** — coordinators on a wide screen (≥1200px) now get a
  single-viewport ops console in place of the stacked Neighborhood view: RX
  transcript and transmit box on the left (radio check-in form docked underneath),
  roster and incident log on the right, with net controls in a persistent command
  bar — no more scrolling between the roster and the controls at the bottom, or
  alt-tabbing to read radio traffic. It's automatic by role and screen width, with
  no toggle to set: phones, Kid accounts, participants, and narrower windows keep
  the existing stacked view, and a coordinator on a phone still gets full
  coordinator controls there. The round-table also gains a per-row **Call** button
  to call any checked-in station out of order (while a net is running), and
  **No answer**, which flags a station that didn't respond so it's skipped for the
  rest of the round — the flag clears when the coordinator clicks it or a new round
  starts. Past Nets and its CSV export both gain a **No answer** column so "checked
  in, never reached" survives the net
- **Radio-caller check-in** — a coordinator can check in a neighbor who called in
  on the air but has no Hearthwave account, from a short callsign/name/location
  form with a "pick a neighbor" shortcut over known contacts and an optional
  save-to-contacts so a recurring caller is a pick, not a retype, next week. These
  rows take their turn in the round-table exactly like an account holder's, and
  are marked wherever identity matters: a **By radio** chip in the live roster, and
  a `via` column in the Past Nets table and the per-session CSV export
- **Net session history** — every net that ends is recorded as a structured roster
  under `/data/net_sessions`. Browse past nets, per-station attendance (totals,
  recent turnout, streaks), and export any net or the whole history as CSV from the
  **Past Nets** tab of the Journal panel. Records are private and never published to
  the public journal page. Deleting a record requires an admin.
- **The careful second pass is on from the first install** — every setup script now
  stages `large-v3-turbo` alongside the streaming model, so the two-tier transcription
  that cleans up long overs and callsigns works out of the box instead of waiting for
  a model you had to know to download. Adds about 1.6 GB to the install; opt out with
  `--final-model none`
- **Mesh bridges carry every spoken message** — with MeshCore or Meshtastic enabled,
  family check-ins, wall-display quick messages, neighborhood incident reports and
  street alerts, and Net Control's scripts, round-table calls and SKYWARN spot
  reports all reach the mesh, not just typed chat overs. The message-box cap now
  counts UTF-8 bytes like the radio does, so an accented name no longer lets you
  type past what fits in one packet
- **Boards that scale to their size** — the Neighborhood roster and incident log
  tier their density the way the family presence board already did: a six-person net
  keeps roomy rows, a twenty-person net packs into columns instead of one long
  scroll, and narrowing the incident log by category gives those cards the room the
  shorter list earned. An admin (not just a coordinator) can now clear either board
- **"I'm OK" leads the presence board** — the household's check-in tile sits first
  on the family panel instead of below the roster, so the button everyone actually
  reaches for is the first thing on screen
- **E-ink wall display** — a per-display setting (admin toggle, per device token)
  renders the `/display` kiosk for real e-ink panels: fixed grayscale black-on-white
  that ignores the day/dusk theme, no gradients, and every animation switched off. It
  suppresses live word-by-word partials so a message only appears once it's final, and
  pins the layout in place (the burn-in drift that normal panels use is pure ghosting
  fuel on e-ink) — a low-power, glanceable family board that won't smear or flicker.
  Drag-to-sort is off on these panels for the same reason
- **Station ID on safety messages** — "I'm OK" family check-ins, neighborhood
  incident reports, street alerts, and wall-display quick messages all end with
  the call sign and name, so every keyed transmission identifies the station on
  air per GMRS Part 95 (wall displays ID with the station's own license)
- **Wall display (kiosk)** — point any tablet at `/display`, type the six-digit
  pairing code from Settings, and it becomes a glanceable family board: a scrollable
  radio log newest-first in the top left (received transmissions stream in as they're
  decoded), the clock, weather and street alerts and the next net beside it, and the
  family presence tiles along the bottom, auto-sized to the household and long-press
  draggable into whatever order suits the room. Tap to mark someone OK or send a
  household quick message — no login screen, no settings exposed, and every send is
  server-checked against the admin's allowlist. The pairing survives restarts: a
  display only unpairs when an operator says so
- **Hearthwave Home** — the app opens on a home screen of large activity cards
  (Chat, Family, Neighborhood, Net Control) instead of dropping everyone into
  the operator console; a per-user interface level (Simple / Operator) hides
  radio-operator controls for family members, and new accessibility options add
  adjustable text size, a high-contrast theme, and visible keyboard focus rings
- **Accessibility: switch scanning & visual alerts** — drive the whole app from a
  single adaptive button (auto-scan with selectable speed and select-to-activate),
  flash the screen on incoming messages, control everything from the keyboard
  (press `?` for the shortcut list), and confirm important actions with large-button
  dialogs; kid accounts can send from the AAC board
- **Neighborhood activity** — a dedicated home-screen card / mobile tab for
  watch groups: one-tap roster check-in (identity comes from your profile, and
  an early check-in survives the net actually starting), the next scheduled net
  at a glance, standardized incident reporting (suspicious activity, hazard,
  medical, lost pet or person, utility outage) that's spoken on air and logged
  to a filterable, capped incident log, and street alerts that banner and send
  a browser notification to everyone. Admins grant a **Neighborhood coordinator**
  per user in Settings → Users, who gets Start/End net, round-table calling,
  and street alerts; Kid accounts can check in and read the log but can't file
  reports or coordinate
- **AAC interface** — a per-user toggle swaps the entire UI for a full-screen
  AAC (Augmentative and Alternative Communication) screen, like the
  symbol-button speech devices used by non-speaking people: category tabs of
  large emoji + label buttons build a message in a sentence strip, and one big
  SEND button speaks it over the radio. The button grid is fully user-editable
  (buttons, categories, `{Name}`/`{callsign}` placeholders) and follows the
  user to any device
- **STT calibration wizard** — read a fixed reference passage into the radio
  and Hearthwave automatically sweeps gain mode, noise-profile denoise, and
  Whisper model against it, ranks each combination by word-error-rate, and
  applies whichever one you choose
- **RX audio level meter** — a live receive-audio level meter sits above the
  chat, so you can see signal strength and confirm the radio is feeding audio at
  a glance
- **Stations & Journal open full-size** — on tablets and desktop, the Stations
  and Journal views now open as centered modal dialogs (the same treatment as
  Contacts), filling the window instead of a cramped inline strip. Phones keep
  their bottom-navigation tabs
- **Noise-profile denoise** — samples the channel's own noise floor while the
  squelch is closed and hands it to the denoiser as a stationary noise estimate,
  instead of letting it guess from the speech-bearing audio. Off by default
  pending word-error-rate proof on real recordings; A/B it offline with the
  eval harness's `--noise-profile auto`
- **Callsign auto-correct** — optionally rewrites a misheard callsign in the
  final transcript to your roster's canonical call (single-character corrections
  only; if two known calls are equally close, nothing changes). Corrected chips
  get a dotted underline and a tooltip showing what was actually heard, and the
  journal, read-aloud, and plugins all see the corrected text
- **Deeper eval harness** — the offline word-error-rate harness can now A/B
  decode tuning (beam size, repetition penalty, hotwords, per-word confidence),
  VAD timing, prompt phrasing, and the noise profile against labelled recordings
- **Selectable audio gain stage** — choose how receive audio is leveled before
  transcription, right from admin Settings: dynamic AGC (default), a gentler
  one-shot RMS normalize, or off. The offline STT eval harness can A/B all three
  against word-error-rate, so you can tune for your radios instead of guessing.
- **Installable plugins** — extend the radio pipeline without touching core code:
  drop a plugin folder into `/data/plugins` or upload a `.zip` from admin Settings,
  and it hot-loads with its own declarative config UI. Install, reload, and remove
  plugins live from the Settings dialog; namespaced config keeps each plugin's
  settings isolated. MeshCore and Meshtastic ship as example plugins; NCS stays
  built in
- **Speech-recognition vocabulary biasing** — transcription is weighted toward radio vocabulary (NATO alphabet, procedure words, Q-codes) and your contacts' callsigns, with auto-refresh on contact changes and a manual Rescan button.
- **Monitoring beacon** — an optional, periodic presence call
  (`{callsign} Hearthwave base, monitoring.`) on a fixed timer. Off by default;
  suppressed during NCS mode, transmits only on a clear channel, and doubles as
  your FCC station ID when it airs (configurable interval and phrase)
- **Refreshed brand** — a new Hearthwave logo (a home sheltering a radio set,
  with signal waves cresting off the roof) across the login screen, top bar,
  About dialog, and favicon, plus a redesigned project website that defaults to
  a warm light theme with a one-click dark-mode toggle
- **Two-tier transcription** — a fast model streams live partials while a
  larger model (`large-v3-turbo`, `distil-large-v3`, or `large-v3`) re-transcribes
  each completed transmission in full, replacing the live text with a
  higher-accuracy final — without truncating long overs or dropping a callsign
  the live pass already heard. New installs default to **Auto**: stage a final
  model and it's picked up on the next Listen toggle, no configuration needed
- **GPU-accelerated final pass (ROCm)** — the whole-utterance final pass can run
  on an AMD GPU via ROCm while streaming stays on CPU, eliminating the CPU
  contention that otherwise stalls live transcription during a final pass (measured:
  streaming latency during a final pass dropped from +105% to +3% on a Radeon 680M).
  Configurable via `stt_final_device` (`auto` / `gpu` / `cpu`); falls back to CPU
  automatically if no ROCm GPU is present. ROCm deployment is build-from-source only
  (see [deployment profiles](#quick-start))
- **Cuts through static** — adaptive squelch tracks the channel noise floor so
  weak carriers pre-trigger capture, a 1-second pre-roll plus carrier-drop
  finalize stops the first and last words from being clipped
- **Clearer transmit audio** — optional TX conditioning band-limits, compresses,
  and levels synthesized speech for intelligibility over narrowband FM
- **STT accuracy tuning** — built-in debug capture and an offline word-error-rate
  eval harness for measuring and improving recognition on real recordings
- **Live transcription** — Whisper STT converts every received transmission to
  text in real time and broadcasts it to all connected users; admin-configurable
  saved-phrases list biases recognition toward group-specific vocabulary
- **Multi-user auth** — PBKDF2 password hashing, per-user session tokens, and
  per-user preferences stored server-side
- **Family activity & roles** — a presence board (OK / on air / no word /
  missed check-in) with a giant **I'm OK** button that speaks, chats, and
  updates status in one tap; admin-configurable daily check-in reminders per
  member; and three account roles (Admin / Adult / Kid) — Kid accounts can
  only transmit their preset quick messages, with the profanity filter and
  Simple interface locked on
- **Voice PTT** — browser microphone button (or Space bar) captures and transmits
  audio; pre-roll buffer captures the first syllable even before PTT is pressed
- **Receive audio conditioning** — a per-segment DSP chain (band-pass, spectral-gate
  denoise, a selectable gain stage — AGC / RMS / off — and a low-pass filter) cleans
  narrowband FM audio before it reaches Whisper
- **Won't talk over the channel** — automatic transmissions (like the station-ID
  beacon) hold until the squelch closes, so Hearthwave never keys up on top of a
  received signal; an operator-initiated Transmit still overrides — the operator
  decides when to key
- **CW decode** — Morse code receive mode alongside voice
- **NCS mode** — Net Control Station plugin with roster management, six traffic
  levels (Routine, Priority, Emergency, General, Short Term, In-n-Out), and
  one-click callsign check-in/out
- **Net scripts & round-table** — configurable opening preamble and closing
  scripts read over the air, plus a round-table caller that cycles the roster
  prompting each station for traffic
- **SKYWARN alerts** — live National Weather Service alerts pushed to all users
  with browser notification support
- **SKYWARN spot reports** — file a severe-weather report from a criteria-gated
  form (hail ≥1″, wind ≥40 mph, tornado/funnel/wall cloud, flooding) that builds
  a standardized on-air report and transmits, logs, and journals it
- **Installable plugin system** — drop a plugin directory under `/data/plugins`
  or upload it as a `.zip` from the admin **Settings → Plugins** tab; plugins
  hot-load (install/reload/uninstall with no restart) and declare their settings
  declaratively, so the frontend renders a generic settings form automatically —
  no browser code in a plugin. MeshCore (a USB Companion LoRa-mesh bridge) and
  Meshtastic ship as mutually-exclusive **example plugins** you can study and
  copy; see the authoring guide at [docs/plugins.md](docs/plugins.md)
- **Journals** — AI-assisted session summaries with full transcript export
- **Contacts** — shared contact book with FCC license lookup; multiple family members sharing the same GMRS callsign are stored as separate records and addressed individually by name
- **Spectrogram** — real-time frequency display (voice or full range, viridis or
  grayscale colormaps)
- **Attendance panel** — automatic log of every station heard this session
- **Responsive layout** — phone, tablet, and desktop layouts chosen automatically; tablets get the full desktop view with larger, touch-friendly controls
- **WCAG 2.2 AA** — full keyboard navigation, screen reader support, and ARIA
  labelling throughout the interface
- **Chat vs Transmit split** — a CHAT action broadcasts a message to all operators' displays without keying the radio; TRANSMIT is the over-the-air action; chat lines are marked `[CHAT]` and are profanity-filtered per recipient
- **Shared message stream** — the message log (RX, TX, and CHAT entries) is held server-side and shared across the base station and every web/mobile login, so a client that signs in or refreshes later sees the history accumulated since the last clear. The stream is kept in memory (a backend restart starts fresh) and capped to the most recent messages. Clearing the log is **admin-only and global** — an admin clear wipes the chat for everyone at once, after a confirmation prompt
- **Unified Settings** — one **Settings** dialog for everyone: a **Preferences** tab available to all users (audio input/output devices, profanity filter, fuzzy callsign match, callsign auto-correct, spectrogram options) plus admin-only **Station** and **System** tabs. A single **Save** in the footer commits changes across every tab at once; non-admins see only the Preferences tab
- **VOX primer tone** — an optional short tone prepended to each transmission so a VOX-keyed radio is fully keyed before the message starts; configurable on/off and tone duration in milliseconds (System tab, off by default)
- **Logo, About & version** — a Hearthwave logo on the login screen and in the top bar, plus an About dialog (opened from the account menu, the login-screen footer, or the top-bar logo) showing the running version, project links, and FCC information. The version is sourced from the backend `/health` endpoint
- **Docker install** — single `docker compose up -d` gets you running

## UI Overview

The interface uses a navy/blue design language that matches the
[Hearthwave website](https://xpiatio.github.io/Hearthwave/):

- **Dark mode** — deep navy backgrounds (`#0F2540` page, `#1A3A5C` panels) with
  blue primary actions (`#60A5FA`) and white text
- **Light mode** — navy-tinted backgrounds (`#E8EEF7` page, `#C8D8EC` panels)
  with blue primary actions (`#2563EB`) and dark navy text
- **Green** is reserved exclusively for radio status indicators: connected dot,
  transmitting state, PTT active, and received-message labels
- **Gradient panel headers** — each panel type has a typed gradient (NCS uses a
  deeper blue; Journals and Attendance use base navy)
- **WCAG 2.2 AA compliant** — all colour pairs meet 4.5:1 contrast for normal
  text and 3:1 for large text and UI components

### Desktop layout

```
┌──────────────────────────── TopBar ─────────────────────────────┐
│ Callsign · Status · PTT · ABORT TX · Waterfall · Account        │
├─────────────────────────────────────────────────────────────────┤
│  NCS panel (when opened) · Stations/Journal open as modal dialogs │
├─────────────────────────────────────────────────────────────────┤
│  Pending Stations bar                                             │
├──────────────────────────┬──────────────────────────────────────┤
│  Spectrogram (waterfall)  │   Chat Display (scrollable log)       │
├──────────────────────────┴──────────────────────────────────────┤
│  StatusRow · QuickMessages · Message Input                        │
└──────────────────────────────────────────────────────────────────┘
```

### Mobile layout

Sticky TopBar with hamburger menu → SwipeableDrawer for settings and account.
PTT and ABORT TX in the top bar. Chat display fills the viewport.
Bottom navigation bar for panel switching.

## How it works

```
Browser (any device)
      │  WebSocket :8765 (?token=…)
      ▼
FastAPI Backend  ──►  PulseAudio / sounddevice
      │                     │
   Piper TTS            Whisper STT / CW Decoder
      │                     │
   Serial PTT          Silero VAD
      ▼                     ▼
    Radio               Spectrogram
```

- **RX pipeline**: audio capture → VAD → squelch → segmentation → Whisper STT
  (or CW decoder) → callsign span detection → text broadcast to all clients
- **TX pipeline**: text input → abbreviation expansion → profanity filter →
  FCC ID wrapper → Piper TTS → PTT → audio output → `tx_echo` broadcast
- **Auth**: PBKDF2-hashed passwords, session tokens validated on WebSocket
  connect; unauthenticated connections are rejected

## FCC compliance

Hearthwave is designed as a **remote control point** for a single local station,
not an internet repeater gateway or RoIP bridge. All transmissions originate
from the licensed station's transceiver under direct operator control. The system
automatically prepends and appends the station callsign per §95.1751.

Remote access over the internet is the operator's responsibility. Hearthwave
provides no port-forwarding, relay, or TURN/STUN infrastructure — use a VPN or
private tunnel.

Position reporting is **receive-only**. Hearthwave plots what it hears over
Meshtastic, MeshCore, and APRS on RF; no position source has a transmit path, so
Part 95E's digital-data limits — which govern what a station *sends* — are not
engaged. "APRS on GMRS" is not a thing you can lawfully do with this or any other
software: see [Position reports](https://xpiatio.github.io/Hearthwave/legality.html#positions)
for the citations.

For the full rule-by-rule breakdown — verbatim Part 95E citations, the
remote-control vs. repeater-linking distinction, and every automated function
disclosed — see [Legality & FCC compliance](https://xpiatio.github.io/Hearthwave/legality.html).

## Hardware requirements

| Component | Requirement |
|---|---|
| Server | x86 mini PC or NUC (e.g. Intel N100; N305 or better recommended when running the two-tier final pass); ARM not supported |
| RAM | 8 GB minimum, 16 GB recommended — Whisper STT is memory-intensive, and the optional two-tier final-pass model adds ~1.5 GB while active |
| Disk | ~15 GB free for the CPU path: backend image ~5.8 GB, staged Whisper models ~2.1 GB (`small.en` + `large-v3-turbo`), Piper voices ~0.7 GB, plus journals and Docker build cache. The ROCm path needs ~40 GB — its image is built locally (~15.6 GB) and the final-pass model is staged twice, CT2 and HF transformers, ~1.6 GB each |
| GPU (optional) | AMD GPU with amdgpu / ROCm kernel driver — offloads the final pass to the GPU, eliminating CPU contention during transcription. CPU-only operation is fully supported without a GPU |
| Audio | A two-way audio path to the radio's combo (speaker/mic) jack — either the computer's built-in 3.5 mm jack or a USB audio adapter (see [Connecting the radio](#connecting-the-radio)) |
| PTT | VOX (no wiring — uses the VOX primer tone), or a USB serial dongle (RTS/DTR) |
| OS | Ubuntu 22.04+ or Debian 12+ recommended; Docker required |
| Radio | Any GMRS transceiver with an external speaker/mic (combo) jack |

### Connecting the radio

Hearthwave needs a two-way audio path between the computer and the radio's
**combo (speaker/mic) jack** — most handhelds and mobiles expose this as a
Kenwood-style **K1** connector (a 3.5 mm + 2.5 mm plug pair). **A USB connection
to the radio is _not_ required** — USB, when present, is only the computer-side
sound card. Two paths both work:

1. **Direct 3.5 mm (no USB)** — run the K1 cable from the radio's combo jack into
   the computer's built-in 3.5 mm headset/combo jack. Simplest setup; key the
   radio with **VOX** and enable the **VOX primer tone** so the radio is fully
   keyed before speech begins.
2. **USB audio adapter** — run the K1 cable into a USB sound card (a DigiRig or
   similar). Useful when the computer has no usable analog jack. Here you can key
   PTT over a USB serial dongle (RTS/DTR) instead of VOX.

Pick the input/output devices and PTT mode on first run from the Setup screen.
Audio devices can be changed later on the **Preferences** tab of **Settings**;
PTT mode lives on the admin-only **System** tab. Devices are remembered by name,
so a selection survives new hardware being plugged in or a card that was busy at
boot; use **Rescan audio devices** to pick up hardware added while running.

## Quick start

Hearthwave ships with three deployment profiles. Choose the one that matches your hardware:

| Profile | Compose file | Setup script | Notes |
|---|---|---|---|
| **CPU** (default) | `docker-compose.yml` | `setup-cpu.sh` (or `setup.sh`) | Prebuilt image — recommended for most users |
| **AMD GPU (ROCm)** | `docker-compose.rocm.yml` | `setup-rocm.sh` | Runs the final pass on an AMD GPU; **image built locally** (~16 GB, not published to registry) |
| **NVIDIA GPU (CUDA)** | `docker-compose.cuda.yml` | `setup-cuda.sh` | **Stub — not yet validated**; structure is in place but untested |

### CPU install (default)

```bash
# 1. Clone the repo
git clone https://github.com/Xpiatio/Hearthwave
cd Hearthwave

# 2. Run setup (creates .env and configures audio)
./setup.sh          # forwards to setup-cpu.sh

# 3. Start the stack
docker compose up -d

# 4. Open in your browser
http://your-server-ip
```

### AMD GPU (ROCm) install

```bash
./setup-rocm.sh     # builds the local ROCm image and stages the HF-format final-pass model
docker compose -f docker-compose.rocm.yml up -d
```

Requires an AMD GPU with the amdgpu / ROCm kernel driver, the host `render` group, and
`HSA_OVERRIDE_GFX_VERSION` set appropriately (default `10.3.0`, needed for APUs such as
the Radeon 680M that are not officially listed in the ROCm supported-GPU table). See
[USER_MANUAL.md](USER_MANUAL.md) for full prerequisites.

On first launch the Setup screen appears — create the admin account and configure
your callsign, audio devices, and PTT interface.

## Development

```bash
# Backend (Python 3.13 in the images; 3.11+ works)
pip install -r backend/requirements.txt -r backend/requirements-dev.txt
uvicorn backend.server:app --reload --port 8765
cd backend && python -m pytest        # 2026 tests

# Frontend (Node 22, matching the image builder stage)
cd frontend
npm ci
npm run dev        # dev server on :5173, proxies /ws + /auth to :8765
npm run test       # vitest, 1221 tests
npm run build      # production build
```

The dev server's proxy target is `BACKEND_URL` (default `http://localhost:8765`), so
you can point a hot-reloading frontend at a backend running in Docker:

```bash
BACKEND_URL=http://<backend-container-ip>:8765 npm run dev -- --host 0.0.0.0
```

## Plugin system

Hearthwave has a true installable plugin system. A plugin is a directory
containing a `plugin.py` placed under `/data/plugins/<id>/` (the directory name is
the plugin id), or uploaded as a `.zip` from the admin **Settings → Plugins** tab.
Plugins hot-load — install, reload, and uninstall happen live with no restart (a
server restart is always a safe fallback). **Plugins ship no browser/React code:**
they declare their settings via a config schema and the frontend renders a generic
settings form automatically.

### Writing a plugin

Plugins import everything from the stable SDK at `backend/plugins/sdk.py`:

```python
from backend.plugins.sdk import BasePlugin, PluginManifest, ConfigField, PluginContext
```

A plugin subclasses `BasePlugin` and declares a `PluginManifest`:

```python
from backend.plugins.sdk import BasePlugin, PluginManifest, ConfigField


class WordGuard(BasePlugin):
    manifest = PluginManifest(
        id="word-guard",
        name="Word Guard",
        description="Blocks outgoing transmissions containing a banned word.",
        version="1.0.0",
        default_enabled=False,
        config_schema=(
            ConfigField(key="banned_word", label="Banned word", type="text", default="classified"),
        ),
    )

    async def on_audio_tx_pre_queue(self, payload: dict) -> dict | None:
        cfg = self.ctx.get_config().plugin_config("word-guard")
        word = cfg.get("banned_word", "")
        if word and word in payload.get("text", "").lower():
            return None            # None blocks the transmit
        return payload             # return the (optionally modified) payload to allow it
```

The loader binds a `PluginContext` to `self.ctx`, calls `setup()`, registers the
plugin, then dispatches `on_config_changed`. Core services are reached through
`self.ctx` — `broadcast`, `enqueue_tx`, `get_config`, `channel_clear`,
`report_position`, `data_dir`, and `logger`. The `PluginManifest` fields are `id`, `name`, `description`,
`version`, `default_enabled`, `conflicts_with`, `config_schema`, and
`tx_composition`.

### Configuration

Each plugin's settings live under `config["plugins"][<id>]` — an `enabled` flag
plus one key per `config_schema` field — and are managed from the admin-only
**Settings → Plugins** tab: a list of installed plugins each with an enable
toggle, an auto-generated settings form, version, a conflicts note, and
Reload/Uninstall buttons, plus an **Install plugin (.zip)** button. Because
plugins run with full server access, the tab carries a warning to that effect.

### Hooks

Every hook is a no-op by default — override only the ones you need. They fire in
registration order, and dispatch is exception-isolated (a raising hook is logged
and skipped; for `on_audio_tx_pre_queue` a raising plugin is treated as
pass-through, not a block).

| Hook | Sync/async | Fires when | Return value |
| --- | --- | --- | --- |
| `setup()` | async | Once when the plugin is loaded, after `ctx` is bound | Ignored. Acquire resources here. |
| `on_unload()` | async | When the plugin is reloaded or uninstalled | Ignored. Release resources here. |
| `on_config_changed(config)` | async | Server config is (re)loaded — once at load and again after every admin save | Ignored. React to setting changes here: open/close connections, restart pollers, re-read tunables. MeshCore uses it to connect or disconnect its serial link when its settings change. |
| `on_client_message_received(payload, reply=None)` | async | Any connected client sends a WebSocket message | Ignored. `payload` is a copy — mutating it has no effect. `reply(msg: dict)` is an optional async callable that sends `msg` back to the originating client. |
| `on_audio_rx_start()` | async | The squelch detector opens (incoming carrier detected) | Ignored. Bridged from the STT worker thread to the event loop automatically. |
| `on_audio_rx_chunk(chunk)` | **sync** | Each raw audio chunk is captured from the input device | Ignored. `chunk` is a float32 numpy array at 16 kHz. **Hot path on the STT worker thread — keep it fast; do not `await` or call asyncio APIs.** |
| `on_rx_final(text)` | async | Each finalized (non-partial) RX transcript is broadcast | Ignored. |
| `on_audio_tx_pre_queue(payload)` | async | Before TX text enters the synthesis queue | Return `payload` (optionally modified) to allow the transmit, or `None` to block it. Plugins run in registration order and the **first to return `None` wins**. Modifiable fields: `text`, `_filter_profanity`, `_voice_name`, `_length_scale`. |

A plugin can also constrain the core message input without the input knowing
anything about it, by declaring the `tx_composition` field on its manifest. The
input honours the most restrictive declaration across enabled plugins (live
byte counter + length cap, counted in UTF-8 bytes to match what the mesh radios
measure). MeshCore declares one so its packet-length budget is enforced as you type.

### Examples and built-ins

MeshCore, Meshtastic, and APRS (RF receive) are **example plugins**, shipped
under `examples/plugins/` and seeded into `/data/plugins` on first run — reference
implementations for writing your own, not core features.

MeshCore and Meshtastic are mutually exclusive (enabling one disables the other
via `conflicts_with`), because both want the one serial mesh radio. MeshCore
bridges to a USB Companion radio over serial (with a configurable baud rate);
Meshtastic is serial too (no baud setting). Both build on a shared
`MeshForwarderPlugin` base (`backend/plugins/mesh_forwarder.py`, re-exported by
the SDK) so a concrete forwarder supplies only its config mapping and transport,
and both can additionally report the positions of nodes they hear — off by
default, enabled per plugin with **Show node positions on the map**.

APRS (RF receive) is a different radio, so it conflicts with nothing and can run
alongside either mesh plugin. It reads a KISS TNC over TCP (direwolf, default
`127.0.0.1:8001`) or over a serial device, decodes AX.25 UI frames, and parses
the position with `aprslib` — an optional dependency, already in
`backend/requirements.txt`, that the plugin reports a clear error about if it is
enabled without it. An optional callsign allow/deny filter keeps a busy band down
to the stations you care about. It is receive-only and has no transmit path.

All three feed positions through `ctx.report_position(...)`, and each owns a
`PositionPoller` rather than inheriting one — see [docs/plugins.md](docs/plugins.md).

**NCS / SKYWARN remains built-in** — it is registered by the app rather than
loaded from `/data/plugins`, because it is deeply integrated (contacts, FCC
lookup, journals, and a rich panel). It still appears in the Plugins tab as a
toggle (enabled by default), and its UI hides when disabled.

See `backend/plugins/sdk.py` and `backend/plugins/loader.py` for the contract and
loader, `examples/plugins/` for the example plugins, and
[docs/plugins.md](docs/plugins.md) for the full authoring guide (package layout,
SDK API, config schema, install/reload, trust model, and a copy-paste
hello-world).

## License

MIT