{"id":50885931,"url":"https://github.com/kbennett2000/rumble-py","last_synced_at":"2026-06-15T17:01:34.677Z","repository":{"id":359192654,"uuid":"1244772194","full_name":"kbennett2000/rumble-py","owner":"kbennett2000","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-20T22:35:06.000Z","size":136,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-21T00:58:08.784Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kbennett2000.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-20T15:25:37.000Z","updated_at":"2026-05-20T22:35:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kbennett2000/rumble-py","commit_stats":null,"previous_names":["kbennett2000/rumble-py"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/kbennett2000/rumble-py","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kbennett2000%2Frumble-py","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kbennett2000%2Frumble-py/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kbennett2000%2Frumble-py/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kbennett2000%2Frumble-py/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kbennett2000","download_url":"https://codeload.github.com/kbennett2000/rumble-py/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kbennett2000%2Frumble-py/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34372130,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-15T02:00:07.085Z","response_time":63,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-15T17:01:33.753Z","updated_at":"2026-06-15T17:01:34.668Z","avatar_url":"https://github.com/kbennett2000.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 📻 rumble-py\n\nA DTMF-controlled Mumble client for linking amateur radios over the internet.\n\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Status: alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#project-status)\n\n---\n\n\u003e **Status — working, but actively under construction.**\n\u003e\n\u003e Solid and tested today: DTMF detection from a sound card, full Mumble\n\u003e protocol client (connect, join channels, send/receive audio, auto-\n\u003e reconnect), command state machine, TTS announcements, web UI.\n\u003e\n\u003e Coming next (milestone 7): real-time radio audio passthrough into the\n\u003e Mumble channel, CW WAV ident playback on a timer, and proper packaging\n\u003e for a one-line install on a Raspberry Pi. After that: CSRF on the web\n\u003e UI, multi-node admin views, optional bridges to IRLP / EchoLink.\n\n---\n\n## What is this for?\n\nRumble wraps the open-source [Mumble](https://www.mumble.info/) VOIP system\nwith a DTMF listener, so you can control a Mumble client over the air using\nnothing but the touch-tone keypad on your radio. Tap a few keys, hear a\nsynthesized voice confirm the channel change, and you're talking to\noperators on the other side of the planet through your handheld.\n\nThe mental model is the same one IRLP and EchoLink built thirty years of\nham culture around: your radio talks to a small node computer; the node\ntalks to a hub on the internet; the hub talks to other nodes; other nodes\ntalk to other radios. You key up here, somebody keys up there, RF on both\nends.\n\nWhat's different is the plumbing. Rumble doesn't have its own protocol or\nits own directory service. The hub is just a Mumble server — open source\nsoftware you can run on a $5/mo VPS, an old PC, or a Raspberry Pi in your\nshack. The directory of nodes is whatever Mumble channels you choose to\ncreate. The DTMF grammar is yours to configure. None of it depends on a\nproprietary company staying in business, and nothing about it phones home.\n\nIf you've been waiting for \"IRLP, but the modern way\" — that's the goal.\n\n## Why would I want to use this?\n\nIt's worth being explicit about the alternatives, because they're all good\nprojects and the right answer depends on what you're trying to do.\n\n| | Rumble | IRLP | EchoLink | AllStar Link | Hamshack Hotline |\n|---|---|---|---|---|---|\n| Protocol | Mumble (open) | Proprietary | Proprietary | IAX/Asterisk (open) | SIP (open) |\n| Directory | None — you choose | Centralized | Centralized | Centralized | Centralized |\n| Auth | Server-side, your call | Hardware ID per node | Per-callsign | Asterisk-style | Per-extension |\n| Self-hosted hub | Yes (Mumble server) | No | No | Yes (private) | No (Asterisk possible) |\n| OS | Linux / Windows | Linux | Windows / mobile | Linux | Mostly hardware boxes |\n| Hardware floor | Pi 4 (~$45) | Dedicated IRLP node | Sound card | Pi or PC | Cisco/Yealink phone |\n| Use case | Linking, nets, rag-chew | Linking, nets | Casual contacts | Linking, nets, dispatch | Phone-style contacts |\n\nRumble's pitch in one sentence: **fully open, fully self-hosted, runs on\nhardware you already own, and the protocol underneath it is a 20-year-old\nVOIP standard that just keeps working**. There's no central server we can\ntake down. No callsign approval queue. No proprietary client. The trade-\noff is that you do more of the setup yourself — though we've tried hard to\nmake that setup obvious.\n\n## How it works\n\nThe data flow, in one picture:\n\n```\n       ┌─────────────────────┐\n       │   YOUR RADIO        │\n       │ (HT, mobile, etc.)  │\n       └──┬───────────────┬──┘\n          │ RX audio      │ TX audio\n          │ (DTMF + voice)│ (voice + ident)\n          ▼               ▲\n       ┌─────────────────────┐    ┌──────────────────┐\n       │  SOUND-CARD         │    │   WEB UI         │\n       │  INTERFACE          │    │  (browser →      │\n       │  (SignaLink, etc.)  │    │   127.0.0.1:8080)│\n       └──┬───────────────┬──┘    └────────┬─────────┘\n          │ float32 PCM   ▲                │\n          ▼               │ int16 PCM      │\n       ┌──────────────────────────────────────┐\n       │           RUMBLE-PY NODE             │\n       │                                      │\n       │  audio capture  →  DTMF detector ──→ │ commands\n       │                                      │\n       │  TTS synthesis  ──┐                  │\n       │                   ▼                  │\n       │            Mumble client wrapper     │\n       └────────────────┬─────────────────────┘\n                        │ TCP+UDP 64738, TLS\n                        ▼\n       ┌──────────────────────────────────────┐\n       │          MUMBLE SERVER               │\n       │  (self-hosted, VPS, or public)       │\n       └────────────────┬─────────────────────┘\n                        │\n                        ├── other rumble-py nodes\n                        ├── other radios\n                        └── plain Mumble desktop clients\n```\n\nThe DTMF control path is local — operators command **their own node**, not\nsome remote endpoint. When you key your radio and send `#001#1*`, that\nsequence is heard by your node's sound card, decoded by your node's\ndetector, dispatched to your node's command handler, which then tells\n*your* Mumble client where to go. It's the same model as an IRLP touch-\ntone control: the commands never leave your shack until they've been\nparsed.\n\nFor a deeper walk-through of Mumble itself, channels, and how this\ncompares to other linking systems, see [docs/CONCEPTS.md](docs/CONCEPTS.md).\n\n## Requirements\n\n### Hardware\n\n- **A DTMF-capable radio.** Any modern HT or mobile that can send touch\n  tones from its keypad. The original C# project author tested on Baofeng\n  UV-5R / UV-82 handhelds; that's also our primary test platform.\n- **A sound-card interface between the radio and the PC.** A SignaLink\n  USB, DigiRig Mobile, or any of the popular ham digital-mode\n  interfaces will work. See [docs/HARDWARE.md](docs/HARDWARE.md) for\n  specifics, including jumper settings, audio levels, and the SignaLink\n  + Baofeng combination we use.\n- **A computer running Linux or Windows.** A Raspberry Pi 4 is the sweet\n  spot for a permanent node — see [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)\n  for the systemd setup. Anything that can run Python 3.11+ with\n  PortAudio works.\n\n\u003e A laptop's built-in microphone is **not** going to work as a substitute\n\u003e for a radio interface. The audio path is too noisy and the levels are\n\u003e wrong. If you're poking at rumble-py without a radio attached, use the\n\u003e web UI's on-screen keypad to inject DTMF directly — that's exactly what\n\u003e it's for.\n\n### Software\n\n- **Python 3.11 or newer.** 3.12 and 3.13 both work; we ship a small\n  workaround for an [unreleased pymumble bug on 3.12+](CLAUDE.md#known-workarounds).\n- **A Mumble server.** For development, the included\n  `docker/docker-compose.yml` runs one locally on port 64738. For\n  production, you can run [Murmur](https://www.mumble.info/) on the same\n  machine, on a VPS, or on a Pi. See\n  [docs/CONCEPTS.md](docs/CONCEPTS.md) for hosting guidance.\n- **espeak-ng on Linux** for TTS:\n  `sudo apt install espeak-ng`. On Windows, pyttsx3 uses SAPI and\n  needs no extra install.\n- **portaudio19-dev on Linux** for sounddevice:\n  `sudo apt install portaudio19-dev`.\n\n## Quick start\n\nThis is the 5-minute path from `git clone` to \"you're listening for DTMF\non a sound card and connected to a local Mumble server.\"\n\n```bash\n# 1. Install system dependencies (Ubuntu / Debian).\nsudo apt install -y python3.11 python3.11-venv git \\\n                    espeak-ng portaudio19-dev docker.io\n\n# 2. Clone and install rumble-py.\ngit clone https://github.com/kbennett2000/rumble-py.git\ncd rumble-py\npython3.11 -m venv .venv\nsource .venv/bin/activate                # Linux/macOS\n# .venv\\Scripts\\activate                 # Windows PowerShell\npip install -e \".[dev]\"\n\n# 3. Bring up the local dev Mumble server (Docker).\ncd docker \u0026\u0026 docker compose up -d \u0026\u0026 cd ..\n\n# 4. Smoke-test individual components without a radio.\npytest                                   # runs the test suite\npython scripts/listen_for_dtmf.py        # interactive: hear DTMF on a sound card\npython scripts/mumble_smoke.py           # connects to the dev server, sends a tone\npython scripts/web_smoke.py              # starts the web UI without the audio pipeline\n\n# 5. Run the real thing. Copy and edit the example config.\ncp config.example.yaml config.dev.yaml\n$EDITOR config.dev.yaml                  # set your callsign, audio device, etc.\npython -m rumble --config config.dev.yaml\n```\n\nThat last step:\n\n- Connects to the Mumble server defined in your config (the dev one, by\n  default).\n- Opens your configured audio input device and starts listening for DTMF.\n- Brings up the web UI at \u003chttp://127.0.0.1:8080/\u003e.\n- Speaks `\"AE9S rumble-py listening\"` (substituting your callsign) into\n  the Mumble channel as a startup announcement.\n\n`Ctrl-C` shuts it down cleanly.\n\n\u003e **FCC reminder (and equivalent rules elsewhere):** under §97.119 you\n\u003e must identify your station at least every ten minutes while\n\u003e transmitting, and at the end of a communication. Rumble-py plays a\n\u003e WAV-file ident on a timer once you set `ident.wav_path` and run a\n\u003e ident interval under 600 seconds. Until milestone 7 lands the real\n\u003e audio passthrough, that file does not actually transmit; for now you\n\u003e ID with your own voice the way you always did. See the\n\u003e [Project status](#project-status) section for where each piece sits.\n\n## Configuration\n\nThe config lives in a YAML file you pass with `--config`. The structure\nis hierarchical, supports multiple \"banks\" of servers and channels (so\nyou can switch between, say, a morning net configuration and an evening\nrag-chew configuration with one DTMF command), and is validated at load\ntime so a typo never silently breaks the node.\n\nThe top-level shape:\n\n```yaml\ncallsign: \"AE9S\"          # your operator callsign\ninitial_bank: 0            # which bank to load on startup\n\naudio:                     # sound-card settings\n  input_device: null\n  sample_rate: 8000\n  dtmf_min_magnitude: 0.05\n\nident:                     # periodic station ID\n  wav_path: \"./ident.wav\"\n  interval_seconds: 540\n\nweb:                       # built-in web UI\n  enabled: true\n  host: \"127.0.0.1\"\n  port: 8080\n\nbanks:\n  0:                       # bank 0 — local development\n    servers:\n      - name: \"local-dev\"\n        host: \"127.0.0.1\"\n        port: 64738\n        username: \"AE9S\"\n    channels:\n      - server_number: \"001\"\n        channel_number: \"1\"\n        server_ref: \"local-dev\"\n        channel_path: \"Root\"\n        nickname: \"local lobby\"\n  1:                       # bank 1 — somewhere else\n    ...\n```\n\nEvery field, every validation rule, and a fully-annotated example config\nlive in [docs/CONFIGURATION.md](docs/CONFIGURATION.md). The example file\nitself is [`config.example.yaml`](config.example.yaml) — copy it, edit\nit, and point `--config` at the copy.\n\n## Operating\n\nOnce the node is up, the operator's interface is the radio keypad. The\ncommand grammar is small and uniform:\n\n| DTMF | Meaning |\n|---|---|\n| `#*` | Disconnect (move to Root channel) |\n| `#N*` | Load configuration bank N (one digit 0-9) |\n| `#XX#Y*` | Change admin setting XX to value Y |\n| `#XXX#Y*` | Switch to server XXX, channel Y (per the channel map in the active bank) |\n\nA few example sequences, assuming the default bank in `config.example.yaml`:\n\n```\n#001#1*    →  switch to \"local lobby\" (Root channel on local-dev server)\n#001#2*    →  switch to \"Root/Lobby\" on local-dev\n#00#0*     →  enable sticky mute (stays muted across commands)\n#00#1*     →  clear sticky mute\n#*         →  disconnect to Root\n```\n\nAfter every valid command, the node speaks a short confirmation through\nthe Mumble channel: *\"switched to local lobby\"*, *\"muted\"*, *\"loaded bank\n1\"*, etc. The DTMF detector also auto-mutes the Mumble side while a\ncommand is being entered, so the keypad tones don't get relayed to every\nother operator on the channel. Unmuting happens automatically when the\ncommand finishes — unless you've engaged sticky mute, in which case you\nstay muted until you clear it with `#00#1*`.\n\nThe full reference, suitable for printing and clipping to your radio\ndesk, is [docs/CHEAT_SHEET.md](docs/CHEAT_SHEET.md).\n\n### Your first command, narrated\n\nHere's what actually happens, end to end, the first time you key\n`#001#1*` on the radio with a freshly-started rumble-py node:\n\n1. **You key up** and send `#` on your radio's DTMF keypad. The tone\n   pair (697 Hz + 1477 Hz) goes out over the air, into your radio's\n   receiver (yes, your own), out the speaker, through your sound-card\n   interface, and into rumble-py's audio capture.\n2. **The detector hears it.** Within ~25 ms of the tone starting,\n   rumble-py classifies the audio frame as `#`. It auto-mutes the\n   Mumble outbound channel so the tones don't leak to other operators.\n3. **The state machine waits** for the rest of the sequence. Operators\n   on the channel hear silence from your node — perfect, because they\n   shouldn't have to listen to keypad tones.\n4. **You finish the command** — `0`, `0`, `1`, `#`, `1`, `*`. Each tone\n   is recognized as it arrives. When the trailing `*` lands, the state\n   machine emits `ChangeChannel(server=\"001\", channel=\"1\")`.\n5. **The dispatcher looks up the mapping** in your active bank's\n   channel map. `001/1` → server `local-dev`, channel `Root` (per the\n   example config). It tells the Mumble client to move to that channel.\n6. **The TTS engine synthesizes** *\"switched to local lobby\"*, encodes\n   it as 48 kHz mono int16 PCM, and hands it to the Mumble client.\n   The Mumble client wraps it in Opus and ships it to the server.\n7. **The server distributes the announcement** to everyone on\n   `Root/Lobby` — including you, since you just landed there. Your\n   radio's speaker now plays *\"switched to local lobby\"* in a slightly\n   robotic voice. The node also un-mutes its outbound side.\n8. **You're connected.** Anyone else on `Root/Lobby` — whether they're\n   another rumble-py node or a regular Mumble desktop client — can\n   hear you when (once milestone 7 lands) the audio passthrough is\n   live.\n\nEnd to end, steps 1-7 take roughly 1.5-2 seconds — most of that is the\noperator's fingers on the keypad. The detection + dispatch part is\nunder 100 ms.\n\n## The web UI\n\nWhile the node is running, a small web interface is available at\n\u003chttp://127.0.0.1:8080/\u003e by default. It shows:\n\n- **Live status** — connection state, current channel, other operators\n  in that channel, current bank, and the DTMF buffer (what you've keyed\n  so far in a partial command).\n- **An on-screen DTMF keypad** — clicking the buttons sends commands\n  through the same path a real radio's tones would. Handy for testing\n  channel mappings without keying the radio, and for remote control\n  from a phone on the same LAN.\n- **The channel map** — every configured `(server_number,\n  channel_number)` mapping for the active bank, so you don't have to\n  remember which combination goes where.\n- **A live log tail** — server-sent events stream the last 500 log\n  lines, color-coded by level, with new lines appearing in real time.\n- **One-click actions** — switch banks, mute/unmute, disconnect to\n  Root, reload the config file from disk.\n\nTo disable the web UI entirely, set `web.enabled: false` in your config.\n\n\u003e **LAN exposure caveat.** Setting `web.host: 0.0.0.0` makes the UI\n\u003e reachable from other devices on your network. There is no\n\u003e authentication. Anyone who can reach port 8080 can change banks,\n\u003e disconnect you, and inject DTMF commands. This is fine on a trusted\n\u003e home LAN; **don't expose it to the public internet** until CSRF and\n\u003e auth land in a later milestone. If you need broader access in the\n\u003e meantime, put a reverse proxy with HTTP basic auth in front.\n\n## Project status\n\nWe're honest about what works today and what doesn't.\n\n### Working (tested against the dev Mumble server)\n\n- **DTMF state machine.** All four command shapes, full unit-test\n  coverage, recognizes back-to-back commands with no gap, rejects\n  invalid sequences cleanly.\n- **DTMF detection from a sound card.** Goertzel-based per-frame\n  classifier with debounce, validated against synthesized tones for all\n  16 DTMF keys and against light Gaussian noise.\n- **Mumble client wrapper.** Connect/disconnect, channel walking and\n  joining, mute/deaf, send/receive PCM audio, auto-reconnect with\n  exponential backoff, multi-listener event registration.\n- **Config loader.** Validates everything at load time. Supports banks\n  with live switching via `#N*`. Reloadable from the web UI.\n- **TTS announcements** via pyttsx3 (espeak-ng on Linux, SAPI on\n  Windows). Resamples to 48 kHz mono int16 for Mumble.\n- **Web UI.** All routes, partials, SSE log tail, action buttons.\n- **Two-bank end-to-end integration test** runs `python -m rumble`\n  against the docker Mumble server and exercises the dispatch flow.\n\n### Known rough edges\n\n- **Real radio audio is not yet relayed to Mumble.** Today the audio\n  capture path feeds the DTMF detector only; the PCM samples don't get\n  forwarded to `mumble.send_audio()`. Talking through the link\n  end-to-end is milestone 7.\n- **CW WAV ident is not on a timer yet.** `ident.wav_path` and\n  `ident.interval_seconds` are accepted in the config but no scheduler\n  fires them. Until then, ID with your voice as you always have.\n- **PTT keying is by VOX or interface PTT.** Hardware/software PTT\n  (CAT, GPIO, DTR/RTS) is on the roadmap; for now use your\n  interface's PTT line or VOX.\n- **A laptop's built-in microphone won't detect DTMF reliably.**\n  Especially on modern Lenovos and similar where the input has heavy\n  AGC + noise cancellation in firmware. This is expected. Use a USB\n  sound card or a real radio interface; the test rigs all do.\n- **The web UI has no auth.** Loopback-only by default; if you change\n  `web.host`, treat the surface as fully unauthenticated.\n\n### Not yet done\n\n- Hardware PTT (CAT/DTR/GPIO).\n- Real-time radio→Mumble audio passthrough.\n- Mumble→radio audio passthrough with PTT keying.\n- Scheduled CW ident.\n- CSRF on web UI POST endpoints.\n- Packaging (`pipx install rumble-py`, `apt install rumble-py`).\n- Multi-node admin view.\n\n## Roadmap\n\n**Milestone 7 — Make it a real node.**\n\n- Radio audio → Mumble audio passthrough (the missing half of the chain).\n- Mumble audio → radio audio with proper PTT keying.\n- Scheduled CW ident with WAV playback.\n- Hardware PTT: serial DTR/RTS for radios with a COM port, GPIO for\n  Raspberry Pi setups.\n- Proper packaging so installation is one line.\n\n**Beyond that, in no particular order.**\n\n- CSRF tokens on web UI POST endpoints. Then optional HTTP basic auth\n  or per-token auth for LAN exposure.\n- Multi-node admin view (one web UI, multiple connected nodes).\n- IRLP / EchoLink bridges — protocols are documented, just nobody's\n  built the bridge yet. Open question whether the relevant directory\n  services will allow a software-only node.\n- A log/replay buffer of recent QSOs, with metadata search.\n- Per-bank ident schedules (so a Field Day bank IDs every 5 minutes,\n  a casual bank every 9).\n\n## Troubleshooting\n\n| Symptom | First thing to check |\n|---|---|\n| Mumble client says \"certificate not trusted\" | Self-signed cert from your own server. Accept on first-connect; see [Concepts](docs/CONCEPTS.md#authentication-and-certs). |\n| Node connects but channel switches fail | Channel doesn't exist on the server, or path is wrong. Walk the tree in a Mumble desktop client and copy the exact path. |\n| DTMF tones from radio don't register | Almost always audio level or device selection. Run `python scripts/listen_for_dtmf.py` to see the detector's view. Try `dtmf_min_magnitude: 0.02`. |\n| Detected, but only sometimes | Either levels are marginal or the radio's send-DTMF duration is shorter than the detector's debounce. Have the operator hold the key longer. |\n| TTS sounds robotic / slurred on Linux | That's espeak-ng. It's intentionally robotic-clear. SAPI on Windows sounds friendlier; both work over RF. |\n| Web UI is on but pages don't update | HTMX or browser cache. Hard-refresh. Check `journalctl -u rumble -f` if running under systemd. |\n| `ssl.wrap_socket` error on Python 3.12+ | Should be auto-patched by `mumble_client.py`. If you see it anyway, file an issue — see [CLAUDE.md](CLAUDE.md#known-workarounds). |\n| Built-in laptop mic doesn't detect tones | Expected. See [hardware notes](docs/HARDWARE.md). Use a USB sound card. |\n| Reconnect storms after dropping the LAN | Mumble auto-reconnect uses exponential backoff capped at `reconnect_max_backoff` (default 60 s). It's working as designed; check your network. |\n| Bank switch (`#N*`) doesn't seem to do anything | Check the web UI — the active bank field should update immediately. If it does but `#001#1*` still goes to the old place, the new bank doesn't have a mapping for `001/1`. Add one or pick a different code. |\n| `python -m rumble` exits with code 2 immediately | Config error — the loader rejects malformed YAML or failed validation rules. The error message names the offending field; see [docs/CONFIGURATION.md](docs/CONFIGURATION.md#validation-rules-summary). |\n| The web UI loads but the log panel is empty | The SSE stream needs a Mumble or radio event to populate it. Click a DTMF keypad button or change banks; lines should appear. |\n| `espeak-ng` not found on Linux | `sudo apt install espeak-ng`. On other distros: `sudo dnf install espeak-ng`, `sudo pacman -S espeak-ng`, etc. |\n| Random pops or clicks in transmitted audio | Sample-rate mismatch between OS and interface, or a USB hub starving the SignaLink. Plug the SignaLink directly into the PC, not through a hub. |\n\nFor radio-side issues, [docs/HARDWARE.md](docs/HARDWARE.md) has a \"Common\ngotchas\" section that covers RF on the audio cable, ground loops, PTT\nstuck on, and one-way audio.\n\n## Documentation\n\n| File | What's in it |\n|---|---|\n| [README.md](README.md) | This document. Front door. |\n| [docs/CHEAT_SHEET.md](docs/CHEAT_SHEET.md) | One-page printable operating reference card for the shack. |\n| [docs/CONCEPTS.md](docs/CONCEPTS.md) | What Mumble is, channels, certs, comparison to IRLP / EchoLink / AllStar / HHotline. For hams new to VoIP. |\n| [docs/HARDWARE.md](docs/HARDWARE.md) | Radios, sound-card interfaces, audio levels, PTT options. Practical, not theoretical. |\n| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Every field in `config.yaml`, with validation rules and a fully-annotated example. |\n| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Running rumble-py as a 24/7 systemd service on a Raspberry Pi. |\n| [docs/issues-to-file.md](docs/issues-to-file.md) | In-tree notes for issues to file once the repo has active issue tracking. |\n| [CLAUDE.md](CLAUDE.md) | Project conventions, voice, known workarounds. Background for collaborators. |\n\n## Contributing\n\nPRs welcome. We use [Conventional Commits](https://www.conventionalcommits.org/),\nblack for formatting (line length 100), ruff for linting, pytest for tests.\nRun all three locally before sending — there's no CI to catch what you don't:\n\n```bash\nblack src tests\nruff check src tests\npytest                                   # unit suite\nRUMBLE_INTEGRATION=1 pytest              # with docker Mumble server running\n```\n\nThe [CLAUDE.md](CLAUDE.md) file describes the project conventions in more\ndetail, including how to write code comments and where the type-hint\nboundaries are.\n\n## Prior art and acknowledgments\n\nThis project stands on a lot of other people's work.\n\n- **[Mumble](https://www.mumble.info/)** by the Mumble developers —\n  the protocol, the reference server (Murmur), the entire reason this\n  was possible without inventing a VOIP stack.\n- **[pymumble](https://github.com/azlux/pymumble)** by Azlux — the\n  Python client library that lets us speak Mumble directly. Without\n  pymumble, this project would have to drive the Mumble desktop client\n  via UI automation (which is exactly what the C# original did, and\n  exactly what we wanted to escape).\n- **[IRLP](https://www.irlp.net/)** by Dave Cameron VE7LTD — set the\n  conceptual template for radio linking thirty years ago. Most of the\n  mental model in this project is unapologetically borrowed.\n- **[EchoLink](https://www.echolink.org/)** by Jonathan Taylor K1RFD\n  — brought VOIP linking to a broader audience and proved the\n  desktop-client use case.\n- **[AllStar Link](https://allstarlink.org/)** — showed that\n  Asterisk could do real ham linking and built a directory of\n  thousands of nodes.\n- **[Hamshack Hotline](https://hamshackhotline.com/)** — the SIP-on-\n  IP-phone community proved that VOIP-for-hams could be fun and\n  approachable.\n- **The DTMF detection literature** that goes back to Goertzel's 1958\n  paper. We're using the same algorithm AT\u0026T's switches used 60 years\n  ago, in Python.\n\nIf any of those projects sound interesting on their own, they\nabsolutely are. Rumble-py is one point in a much larger design space.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Author\n\n**Kris Bennett — AE9S.** Original C# Rumble (2019-2022) and this Python\nrewrite. [GitHub: kbennett2000](https://github.com/kbennett2000)\n\n---\n\n**73!**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkbennett2000%2Frumble-py","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkbennett2000%2Frumble-py","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkbennett2000%2Frumble-py/lists"}