A custom repeater controller for the Retevis RT97L repeater, built in C11 for Raspberry Pi and Linux/macOS. Interfaces with the repeater via its DB9 accessory port through a RIM-Lite v2 or AIOC (All-In-One-Cable) USB radio interface (CM119 chipset). All CTCSS/DCS/DTMF decoding and CW ID generation is handled in software using libplcode. Supports GMRS (Part 95E), Amateur (Part 97), and Business/Industrial (Part 90) operation.
33 modules — repeater state machine, CW ID, caller identification, DTMF commands, voicemail, weather, time, NWS alerts, TTS (ElevenLabs / Wyoming), ASR (Wyoming speech recognition), AI voice assistant (OpenAI-compatible LLM with tool calling), parrot/echo, CDR, statistics, system stats, recording, burst tones, emergency mode, OTP authentication, courtesy tones, GPIO, logging, web dashboard, webhook notifications, voice scrambler, SDR channel monitor, FreeSWITCH autopatch, POCSAG paging, FLEX paging, APRS position/telemetry, PoC radio bridge, Zello Channel bridge (mod_zello — full-duplex audio relay between RF and a Zello channel via libzello), repeater linking (mod_link bridges to a kerchunk-reflectd reflector via SRTP/Opus with per-talkgroup floor control).
318 tests — unit + integration test coverage, including 35 tests for the pure audio-thread functions (kerchunk_audio_tick_rx, kerchunk_audio_tick_tx, ring-commit + paInputUnderflow, repeat-last fill) and 11 tests for the fused TX-activity detector (kerchunk_txactivity).
Embedded CLI — interactive console with tab completion and history when running in foreground mode (kerchunkd -f). Log output streams above the prompt.
Native JSON API — every CLI command returns structured JSON via kerchunk -j. Event streaming via kerchunk -e -j (NDJSON). Response system (kerchunk_resp_t) provides both formats from a single handler.
Web Dashboard — embedded HTTP/HTTPS server with split public/admin architecture. Public site (/) provides live audio monitoring, weather, NWS alerts, and repeater status with zero authentication. Admin site (/admin/) provides the full dashboard, user management, config editor, coverage planner, and PTT — protected by HTTP Basic Auth + optional IP-based ACL (admin_acl). TLS/HTTPS with Let's Encrypt. Set public_only = on to block admin access entirely for internet-facing deployments. Admin ACL returns 404 for non-matching IPs — no auth challenge, no hint the admin exists. Dynamic UI: modules register controls via CLI metadata (/admin/api/commands).
Burst tones — DTMF sequences, two-tone paging, Selcall, MDC-1200, CW ID, and tone burst generation via the tones CLI command.
Wall-clock scheduler — schedule_aligned for periodic tasks (CW ID), schedule_at for future one-shot events. Managed thread pool with 5 modules migrated to supervised threads.
Configurable sample rate — internal audio pipeline defaults to 48kHz, configurable to 8000/16000/32000/48000 Hz. Git hash embedded in version string and deb package metadata.
Heartbeat event — 5-second keepalive for SSE/WebSocket clients.
19 core CLI commands, 29 module CLI commands (including ai, ai tools, ai history, ai ask <text>, ai reset, and zello status|connect|disconnect|say|wav) with full inline help and tab completion.
- GMRS / Amateur / Part 90 Feature Matrix
- Architecture
- Hardware
- Dependencies
- Building
- Running
- CLI
- DTMF Commands
- Modules
- mod_repeater — RX State Machine
- mod_cwid — CW Callsign Identification
- mod_courtesy — Courtesy Tone
- mod_caller — Caller Identification
- mod_dtmfcmd — DTMF Command Router
- mod_voicemail — Voicemail
- mod_weather — Weather Announcements
- mod_time — Time Announcements
- mod_recorder — Transmission Recording
- mod_tones — Burst Tones
- mod_emergency — Emergency Mode
- mod_otp — TOTP Authentication
- mod_parrot — Echo/Parrot
- mod_cdr — Call Detail Records
- mod_tts — Text-to-Speech (ElevenLabs / Wyoming)
- mod_asr — Automatic Speech Recognition
- mod_ai — AI Voice Assistant
- mod_nws — NWS Weather Alert Monitor
- mod_stats — Statistics and Metrics
- mod_web — Web Dashboard
- mod_webhook — Webhook Notifications
- mod_scrambler — Voice Scrambler (Part 90 only)
- mod_sdr — SDR Channel Monitor
- mod_freeswitch — FreeSWITCH AutoPatch
- mod_sysstats — System Stats
- mod_poc — PoC Radio Bridge
- mod_zello — Zello Channel Bridge
- mod_gpio — GPIO Relay Control
- mod_logger — Event Logger
- mod_pocsag — POCSAG Paging
- mod_flex — FLEX Paging
- mod_link — Repeater Linking
- mod_aprs — APRS Position/Telemetry
- General Config
- Audio Config
- HID Config
- User and Group Database
- GMRS Coverage Planner
- Event Types
- FCC Compliance
- Testing
- License
kerchunkd supports GMRS (Part 95E), Amateur (Part 97), and Business/Industrial (Part 90) repeater operation. Some features have regulatory restrictions depending on the service type. The operator is responsible for compliance.
| Feature | GMRS | Amateur | Part 90 | Notes |
|---|---|---|---|---|
| Repeater state machine | Y | Y | Y | Core functionality |
| CW ID | Y | Y | Y | FCC-required station identification |
| Voice ID (TTS) | Y | Y | Y | Speaks frequency and PL tone |
| CTCSS/DCS encode/decode | Y | Y | Y | Tone squelch |
| DTMF commands | Y | Y | Y | Remote control via radio keypad |
| Caller identification | Y | Y | Y | ANI and DTMF login |
| Courtesy tones | Y | Y | Y | |
| Time/weather announcements | Y | Y | Y | On-demand via DTMF |
| NWS severe weather alerts | Y | Y | Y | Emergency public information |
| Emergency mode (*911#) | Y | Y | Y | Extended TX, suppress TOT |
| Voicemail | Y | Y | Y | |
| Parrot/echo test | Y | Y | Y | Audio quality check |
| Transmission recording | Y | Y | Y | FCC 95.1705 activity logging |
| Call detail records | Y | Y | Y | |
| Statistics and metrics | Y | Y | Y | |
| GPIO relay control | Y | Y | Y | |
| Web dashboard (listen) | Y | Y | Y | Public status, audio monitor |
| Webhook notifications | Y | Y | Y | |
| SDR channel monitor | Y | Y | Y | |
| OTP authentication | Y | Y | Y | |
| Web PTT (transmit) | N | Y | Y | GMRS: no remote/internet TX |
| Voice scrambler | N | N | Y | Part 90 only — prohibited on GMRS (FCC 95.333) and Amateur (FCC 97.113(a)(4)) |
| AutoPatch (FreeSWITCH) | N | Y | Y | GMRS: interconnection ambiguous |
| PoC radio bridge (mod_poc) | N | Y | Y | Full-duplex audio to PoC clients via libpoc. GMRS: interconnection ambiguous |
| Zello channel bridge (mod_zello) | N | Y | Y | Full-duplex audio to a Zello channel via libzello. GMRS: interconnection ambiguous |
| POCSAG paging | Y | Y | Y | Brief data transmission |
| FLEX paging | Y | Y | Y | Brief data transmission |
| APRS position/telemetry | Y | Y | Y | Brief data transmission, COR gated |
| Speech recognition (ASR) | Y | Y | Y | Wyoming ASR — transcribes inbound RF, no outbound TX |
| AI voice assistant | Y | Y | Y | LLM + tool calling; user keys up with wake phrase or *99#, AI responds via TTS |
GMRS (Part 95 Subpart E):
- Station identification required (CW ID handles this)
- No encryption or scrambling (FCC 95.333)
- No unsolicited one-way transmissions (auto-announce defaults off)
- Interconnection (autopatch) not explicitly addressed post-2017 reform
- Web PTT constitutes remote control/internet linking — not permitted
Amateur (Part 97):
- Station identification per 97.119 (CW ID handles this)
- Autopatch permitted (97.113) — no business calls, third-party rules apply
- Scrambling/encryption prohibited (97.113(a)(4)) — this includes frequency inversion; do not use mod_scrambler on Amateur frequencies
- Internet linking and remote control permitted with proper identification
- Web PTT permitted with control operator oversight
Business/Industrial (Part 90):
- Encryption and scrambling are permitted — mod_scrambler is legal for Part 90 operation
- Station identification per 90.425 (CW ID or voice announcement)
- Remote control and interconnection (autopatch) permitted
- Web PTT permitted
- No prohibition on one-way transmissions — auto-announce features may be enabled
GMRS — ensure these modules are not loaded in [modules]:
; Remove from load list for GMRS:
; mod_scrambler — no encryption/scrambling on GMRS (FCC 95.333)
; mod_freeswitch — no autopatch on GMRSAnd disable Web PTT:
[web]
ptt_enabled = off ; no remote TX on GMRSAmateur — same as GMRS, except Web PTT and autopatch are permitted:
; Remove from load list for Amateur:
; mod_scrambler — no encryption/scrambling on Amateur (FCC 97.113(a)(4))Part 90 (Business/Industrial) — all modules may be loaded, including mod_scrambler:
[modules]
load = mod_scrambler ; encryption permitted on Part 90
load = mod_freeswitch ; interconnection permitted on Part 90
[web]
ptt_enabled = on ; remote control permitted on Part 90Modular design: a lightweight core with an event bus, dynamically loadable modules (.so), an outbound audio queue with priority, an interactive CLI, and an embedded web server.
┌───────────────────────────────────────────────────────────────┐
│ kerchunkd (daemon) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ PortAudio│ │ HID │ │ Event │ │ Module │ │
│ │ Audio │ │ COR/PTT │ │ Bus │ │ Loader │ │
│ │ (callback│ │ (hidraw) │ │ (mutex) │ │ (dlopen) │ │
│ │ +ring buf│ │ │ │ │ │ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ ┌────┴─────────────┴─────────────┴─────────────┴────────┐ │
│ │ DSP Pipeline (libplcode) │ │
│ │ CTCSS dec . DCS dec . DTMF dec . CW ID enc . Tones │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Outbound │ │ Control │ │ HTTP/SSE │ │
│ │ Queue │ │ Socket │ │ mod_web │ │
│ │ (priority│ │ (per- │ │ (port │ │
│ │ sorted) │ │ client) │ │ 8080) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Loaded Modules (33) │ │
│ │ │ │
│ │ mod_repeater RX state machine (IDLE/RECV/TAIL/HANG) │ │
│ │ mod_cwid Morse CW ID + voice ID via TTS │ │
│ │ mod_courtesy Courtesy tone on COR drop │ │
│ │ mod_caller Caller ID (DTMF ANI / DTMF login) │ │
│ │ mod_dtmfcmd DTMF command router (*XX#) │ │
│ │ mod_voicemail Record/play/delete voice messages │ │
│ │ mod_gpio GPIO relay control via DTMF │ │
│ │ mod_logger Event logging + rotation │ │
│ │ mod_weather Weather via weatherapi.com + TTS │ │
│ │ mod_time Time announcements via TTS │ │
│ │ mod_recorder Per-transmission WAV recording │ │
│ │ mod_tones Burst tone toolbox (DTMF, Selcall, etc.) │ │
│ │ mod_emergency Emergency mode (*911#/*910#) │ │
│ │ mod_otp TOTP authentication (*68<code>#) │ │
│ │ mod_parrot Echo/parrot for audio quality check │ │
│ │ mod_cdr Call detail records (daily CSV) │ │
│ │ mod_tts Text-to-speech (ElevenLabs / Wyoming) │ │
│ │ mod_asr Speech recognition (Wyoming ASR) │ │
│ │ mod_ai AI voice assistant (LLM + tool calling) │ │
│ │ mod_poc PoC radio server bridge (libpoc) │ │
│ │ mod_zello Zello channel bridge (libzello) │ │
│ │ mod_nws NWS weather alert monitor │ │
│ │ mod_stats Statistics, metrics, persistence │ │
│ │ mod_sysstats System stats (CPU, mem, temp, uptime) │ │
│ │ mod_web HTTP server + SSE + web dashboard │ │
│ │ mod_webhook HTTP POST notifications on events │ │
│ │ mod_scrambler Frequency inversion voice scrambler │ │
│ │ mod_sdr RTL-SDR single-channel monitor │ │
│ │ mod_freeswitch FreeSWITCH AutoPatch (Ham only) │ │
│ │ mod_pocsag POCSAG paging encoder │ │
│ │ mod_flex FLEX paging encoder │ │
│ │ mod_aprs APRS position reporting/telemetry │ │
│ │ mod_link Bridge to kerchunk-reflectd network │ │
│ └──────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
- Audio thread (20ms) — captures audio, runs libplcode decoders (CTCSS/DCS/DTMF), software relay with drain, drains outbound queue, manages queue-driven PTT. Decisions live in pure functions (
kerchunk_audio_tick_rx,kerchunk_audio_tick_tx) over a singlekerchunk_audio_state_t, so they're unit-tested without PortAudio. PortAudio callbacks are thin wrappers overkerchunk_audio_ring_commit(), which handlespaInputUnderflowdrop and capture resample. - Main thread (20ms) — fused TX-activity detector (COS bit OR DTMF decoder detected →
COR_ASSERT/DROP), timers, control socket, config reload. See ARCH-COR-DTMF.md for the COR/DTMF design. - Web thread — accepts HTTP connections, serves API/SSE/static files
- Audio flush thread (5ms) — drains the web-audio SPSC ring and wakes mongoose via
mg_wakeupso WebSocket listeners get steady frames - TTS thread — async synthesis via ElevenLabs cloud or Wyoming local server (non-blocking)
- ASR thread — async transcription via Wyoming ASR server (non-blocking)
- AI thread — LLM + tool-calling worker (non-blocking)
- NWS thread — async weather alert polling (non-blocking)
- SDR thread — async channel monitor capture (non-blocking)
- Config access — protected by
pthread_mutex(kerchunk_core_lock_config/kerchunk_core_unlock_config) for safe reads/writes across threads - Audio ring buffer — lock-free with
atomic_size_thead/tail pointers for producer/consumer between PortAudio callback and main thread - Audio taps — mutex-protected tap registration/unregistration with snapshot pattern to avoid holding locks during callbacks
- Control socket — per-client mutex protects concurrent writes and deferred event flushing
- Signal handling —
volatile sig_atomic_tforg_runningandg_reloadflags
The repeater tracks two independent state machines:
RX (Inbound) — what the repeater is hearing:
COR assert
┌──────────┬──────────┐
│ │ │
▼ │ │
┌──────┐ (debounce) │
│ IDLE │─────►┐ │
└──────┘ │ │
▲ ▼ │
│ ┌───────────┐ │
│ │ RECEIVING │◄───┤ COR re-assert (rekey)
│ └───────────┘ │
│ │ │
│ COR drop │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ TAIL_WAIT │────┘ COR re-assert (rekey)
│ └───────────┘
│ │
│ tail expires
│ │
│ ▼
│ ┌───────────┐
│ │ HANG_WAIT │────┐ COR re-assert (rekey)
│ └───────────┘
│ │
│ hang expires
│ │
└─────────┘
TIMEOUT: fires after timeout_time in RECEIVING
TX (Outbound) — what the repeater is transmitting:
┌─────────┐
│ TX_IDLE │ Not transmitting
└─────────┘
│
├─── COR assert (software_relay=on) ────┐
│ ▼
│ ┌─────────┐
├─── Queue has items │ TX_RELAY│ Relaying RX audio
│ └─────────┘
▼ │
┌─────────┐ COR drop
│ TX_QUEUE│ Playing queued audio │
└─────────┘ (TTS, weather, CW ID) ▼
│ ┌─────────┐
├─── queue empties │ TX_TAIL │ Drain + tail silence
│ └─────────┘
▼ │
┌─────────┐ PTT drop
│ TX_TAIL │ TX tail silence (CTCSS) │
└─────────┘ ▼
│ ┌─────────┐
PTT drop │ TX_IDLE │
│ └─────────┘
▼
┌─────────┐
│ TX_IDLE │
└─────────┘
Both state machines are visible in the web dashboard and reported via /api/status:
{"rx_state":"RECEIVING","tx_state":"TX_RELAY","ptt":true,"cor":true,...}RX Timers:
- Tail (
tail_time, default 2s) — silence after COR drop, courtesy tone plays - Hang (
hang_time, default 500ms) — PTT held after tail for quick rekey - TOT (
timeout_time, default 180s) — max continuous receive - Debounce (
cor_debounce, default 150ms) — kerchunk filter
TX Timers:
- TX delay (
tx_delay, default 100ms) — silence after PTT assert (skipped if PTT already held) - TX tail (
tx_tail, default 200ms) — silence after audio, CTCSS continues - Relay drain (
relay_drain, default 500ms) — continue relaying after COR drops
- Software relay — when enabled, kerchunkd captures RX audio and retransmits in software. Live voice preempts queued announcements.
- Relay drain — on COR drop, relay continues for configurable period (default 500ms) to avoid cutting speech mid-word. Playback ring also fully drains before PTT releases.
- PTT refcounting — multiple modules can hold PTT simultaneously; hardware releases only when all refs drop to zero
- Queue auto-PTT — audio thread asserts PTT when draining, releases when empty
- CTCSS/DCS continuous — tone mixed into TX delay, all audio, TX tail, and relay
- Configurable sample rate — internal sample rate defaults to 48kHz (
sample_ratein[audio], valid: 8000/16000/32000/48000).hw_rateforces PortAudio to device-native rate with automatic resampling - WAV resampling — queued WAV files are automatically resampled at load time via
kerchunk_resample()to match the configured sample rate - Full-duplex stream — single PortAudio stream when capture and playback are the same device (shared clock, no drift)
RT97L DB9 <──> RIM-Lite v2 / AIOC (CM108AH USB) <──> Raspberry Pi / Linux / Mac
├── PortAudio: audio I/O
└── HID (hidraw): COR input / PTT output
| RIM-Lite | Signal | RT-97S | Notes |
|---|---|---|---|
| 2 | TX Voice Audio -> | 2 | < 100mV |
| 3 | <- COS in | 3 | Active Low |
| 5 | PTT -> | 9 | Active Low |
| 6 | <- RX Audio | 5 | De-emph. discriminator |
| 8,9 | Ground | 7 |
The RIM-Lite v2 uses a C-Media CM108AH chip (vendor 0d8c, product 013a).
COR and PTT are controlled via GPIO pins exposed through the Linux hidraw interface.
| GPIO | Bit | CM108 HID Usage | RIM-Lite Function |
|---|---|---|---|
| GPIO0 | 0 | Volume Up | (unused) |
| GPIO1 | 1 | Volume Down | COR input |
| GPIO2 | 2 | Mute | PTT output |
| GPIO3 | 3 | (reserved) | (unused) |
Config values use 0-based GPIO/bit numbers directly — no conversion needed.
COR polarity note: The CM108 internally inverts GPIO inputs (active-low GPIO pin
= bit HIGH in the HID report). Use cor_polarity = active_high in the config because
the CM108 has already performed the inversion. Using active_low will double-invert
and COR drops will never be detected.
[hid]
device = /dev/rimlite ; udev symlink (see udev rule below)
cor_bit = 1 ; GPIO1 = COR
cor_polarity = active_high ; CM108 inverts internally, do not double-invert
ptt_bit = 2 ; GPIO2 = PTTThe udev rule in debian/kerchunk.udev creates the /dev/rimlite symlink automatically:
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", ATTRS{idProduct}=="013a", \
GROUP="audio", MODE="0660", SYMLINK+="rimlite"
| Dependency | Purpose | Install (macOS) | Install (Linux) |
|---|---|---|---|
| libplcode | CTCSS/DCS/DTMF/CWID codec | make install or .deb |
sudo dpkg -i libplcode-dev_*.deb |
| PortAudio | Audio I/O | brew install portaudio |
apt install portaudio19-dev |
| libcurl | HTTP (weather, NWS, TTS) | brew install curl |
apt install libcurl4-openssl-dev |
| OpenFst | Optional: TTS text normalization | brew install openfst |
apt install libfst-dev |
| libnemo_normalize | Optional: NeMo text normalization | make install or .deb |
sudo dpkg -i libnemo-normalize_*.deb |
| librtlsdr | Optional: SDR channel monitor | brew install librtlsdr |
apt install librtlsdr-dev |
| libpocsag | Optional: POCSAG paging | make install or .deb |
sudo dpkg -i libpocsag-dev_*.deb |
| libflex | Optional: FLEX paging | make install or .deb |
sudo dpkg -i libflex-dev_*.deb |
| libaprs | Optional: APRS position/telemetry | make install or .deb |
sudo dpkg -i libaprs-dev_*.deb |
| libpoc | Optional: PoC radio bridge | make install or .deb |
sudo dpkg -i libpoc-dev_*.deb |
| libzello | Optional: Zello channel bridge | make install or .deb |
sudo dpkg -i libzello-dev_*.deb |
| libwyoming | Optional: Wyoming TTS/ASR | make install or .deb |
sudo dpkg -i libwyoming-dev_*.deb |
| pkg-config | Build system | (included with Xcode) | apt install pkg-config |
Install dependencies first:
# Required (Debian/Ubuntu)
sudo apt install build-essential pkg-config autoconf automake libtool \
portaudio19-dev libcurl4-openssl-dev libplcode-dev
# Optional: RTL-SDR channel monitor
sudo apt install librtlsdr-dev
# Optional: TTS text normalization (libnemo-normalize)
sudo apt install libnemo-normalize-dev libfst-dev
# Optional: Paging, APRS, PoC, Wyoming (detected by pkg-config)
sudo dpkg -i libpocsag-dev_*.deb # POCSAG paging (mod_pocsag)
sudo dpkg -i libflex-dev_*.deb # FLEX paging (mod_flex)
sudo dpkg -i libaprs-dev_*.deb # APRS position/telemetry (mod_aprs)
sudo dpkg -i libpoc-dev_*.deb # PoC radio bridge (mod_poc)
sudo dpkg -i libzello-dev_*.deb # Zello channel bridge (mod_zello)
sudo dpkg -i libwyoming-dev_*.deb # Wyoming TTS/ASR (mod_tts, mod_asr)Build with autotools:
git clone https://github.com/briankwest/kerchunk.git
cd kerchunk
autoreconf -fi
./configure
make
make check # Run test suite (318 tests)
sudo make installBuild outputs:
kerchunkd— the daemonkerchunk— interactive CLImodules/*.so— up to 33 loadable modules (optional ones skipped if deps are missing)test_kerchunk— test suite
On a fresh Linux install (Ubuntu/Debian), install all build dependencies:
sudo apt install build-essential pkg-config autoconf automake libtool \
portaudio19-dev libcurl4-openssl-dev libplcode-dev
# Optional: for TTS text normalization (libnemo-normalize)
sudo apt install libnemo-normalize-dev libfst-dev
# Optional: for SDR channel monitor
sudo apt install librtlsdr-dev
# Optional: for paging, APRS, PoC, Zello (detected by pkg-config)
sudo dpkg -i libpocsag-dev_*.deb libflex-dev_*.deb libaprs-dev_*.deb \
libpoc-dev_*.deb libzello-dev_*.debAudio group — PortAudio needs access to ALSA devices (/dev/snd/*), which are owned by the audio group:
sudo usermod -aG audio $USER
# Log out and back in for the group change to take effectUSB radio interface (RIM-Lite / CM119) — the HID device (/dev/hidraw*) used for COR/PTT is only accessible by root by default. Install the included udev rule:
sudo cp 99-rimlite.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=hidrawThis grants audio group access to the C-Media CM119 HID interface (vendor 0d8c, product 013a) and creates a stable SYMLINK+="rimlite" device path at /dev/rimlite.
Without the audio group membership, PortAudio will report 0 devices and the daemon will run without audio. Without the udev rule, HID access requires running as root.
./kerchunkd -d # List audio devices
./kerchunkd -c kerchunk.conf -f # Start in foregroundPublic dashboard: https://localhost:8080/ (when [web] enabled = on). Admin: https://localhost:8080/admin/ (HTTP Basic Auth required).
./kerchunk # Enter interactive modeFeatures: tab completion, command history, live log streaming, auto-reconnect.
kerchunk> status # Daemon status (RX/TX state, PTT, COR)
kerchunk> help # Show all commands
kerchunk> version # Version and git hash (e.g. 1.0.1+abc1234)
kerchunk> uptime # Daemon uptime
kerchunk> audio # Audio device and sample rate info
kerchunk> hid # HID device status
kerchunk> user # Current user info
kerchunk> log # Log level control
kerchunk> diag # Diagnostics
kerchunk> play <file> # Play a WAV file
kerchunk> tone <freq> <dur> # Generate a tone
kerchunk> /log debug # Start log streaming
kerchunk> /nolog # Stop log streaming
kerchunk> exit # Exit console
19 core commands: status, help, version, uptime, audio, hid, user, log, diag, play, tone, sim, tts, cwid, caller, emergency, dtmfcmd, threads, schedule.
Key module commands:
kerchunk> tones dtmf 1234 # Send DTMF sequence
kerchunk> tones twotone 1000 1500 # Send two-tone page
kerchunk> tones selcall 12345 # Send Selcall sequence
kerchunk> tones mdc 1234 # Send MDC-1200 burst
kerchunk> tones burst 1000 500 # Send tone burst (freq, duration_ms)
kerchunk> tones cwid # Send CW ID burst
kerchunk> threads # Show managed thread pool status
kerchunk> schedule # Show wall-clock scheduler status
kerchunk> pocsag send 1234 "Test" # Send POCSAG page
kerchunk> flex send 1234 "Test" # Send FLEX page
kerchunk> aprs beacon # Force APRS beacon
kerchunk> zello status # Show Zello bridge state
kerchunk> zello say hello channel # Send a text message to the channel
kerchunk> zello wav /var/lib/kerchunk/test.wav # Stream a WAV to the channel
./kerchunk status # One-shot command
./kerchunk -x 'sim dtmf *95#' # Execute from script
./kerchunk -x 'tts say hello' # TTS from cron./kerchunk -j status
{"rx_state":"IDLE","tx_state":"TX_IDLE","ptt":false,"cor":false,"queue":0,"modules":29,"users":2,"emergency":false}
./kerchunk -j stats | jq .channel.duty_pct
./kerchunk -e -j # Structured event stream (NDJSON)
./kerchunk -e -j | jq 'select(.type=="rx_state_change" or .type=="tx_state_change")'kerchunk> sim cor on # Simulate COR assert
kerchunk> sim cor off # Simulate COR drop
kerchunk> sim dtmf *95# # Simulate DTMF sequence
kerchunk> sim tx sounds/test.wav # Queue a WAV file
| Sequence | Action | Module |
|---|---|---|
*87# |
Voicemail status | mod_voicemail |
*86# |
Record voicemail (own mailbox) | mod_voicemail |
*86<id># |
Record voicemail for user ID | mod_voicemail |
*85# |
Voicemail play | mod_voicemail |
*84# |
Voicemail list | mod_voicemail |
*83# |
Voicemail delete | mod_voicemail |
*41<pin># |
GPIO on | mod_gpio |
*40<pin># |
GPIO off | mod_gpio |
*93# |
Current weather | mod_weather |
*94# |
Weather forecast | mod_weather |
*95# |
Time check | mod_time |
*911# |
Emergency mode on | mod_emergency |
*910# |
Emergency mode off | mod_emergency |
*88# |
Parrot/echo mode | mod_parrot |
*96# |
NWS weather alerts | mod_nws |
*68<code># |
OTP authenticate (6-digit TOTP) | mod_otp |
*97# |
Toggle scrambler on/off | mod_scrambler |
*970# |
Disable scrambler | mod_scrambler |
*971#-*978# |
Set scrambler code 1-8 | mod_scrambler |
*0<digits># |
AutoPatch dial | mod_freeswitch |
*0# |
AutoPatch hangup | mod_freeswitch |
*99# |
Arm AI voice assistant for next TX | mod_ai |
*990# |
Stop AI: cancel arm and clear this caller's conversation | mod_ai |
*73<n># |
Switch link talkgroup (e.g. *731# → TG 1) |
mod_link |
Note: APRS beacon and status (
aprs beacon/aprs status) are CLI-only commands; there is no dialable DTMF pattern for them.Overrides: Every pattern above can be remapped via
[dtmf] <config_key> = <pattern>inkerchunk.conf. Theconfig_keyis the third argument each module passes todtmf_register(e.g.,scrambler_toggle,autopatch,dtmf_ai). See the[dtmf]section inkerchunk.conf.examplefor the full list.
Controls the IDLE/RECEIVING/TAIL_WAIT/HANG_WAIT/TIMEOUT RX state machine and closed repeater access control.
| Key | Type | Default | Description |
|---|---|---|---|
tail_time |
ms | 2000 |
Tail timer after COR drop |
hang_time |
ms | 500 |
Hang timer (PTT held for quick rekey) |
timeout_time |
ms | 180000 |
Time-out timer (3 min max) |
cor_debounce |
ms | 150 |
Kerchunk filter (0 to disable) |
tx_delay |
ms | 100 |
Silence after PTT assert before audio |
tx_tail |
ms | 200 |
Silence after audio before PTT release |
software_relay |
on/off | off |
Relay RX audio to TX in software |
relay_drain |
ms | 500 |
Continue relaying after COR drop (0-5000) |
cor_drop_hold |
ms | 1000 |
DEPRECATED — use [txactivity] end_silence_ms. Read as fallback for back-compat. |
require_identification |
on/off | off |
Closed repeater: deny unless identified |
voice_id |
on/off | on |
Speak frequency/PL via TTS after CW ID |
Config section: [repeater]
The KERCHEVT_COR_ASSERT / KERCHEVT_COR_DROP events are no longer
derived directly from a single HID bit — they're emitted by a fused
detector that OR's the raw COS bit with the DTMF decoder's
detected state. This makes Retevis-class radios (which drop COS
during DTMF tones) work correctly without the old 1 s drop-hold
mask.
| Key | Type | Default | Description |
|---|---|---|---|
end_silence_ms |
ms | 300 |
Voice-mode unkey latency. All inputs must be quiet this long before TX_END fires. |
end_silence_dtmf_ms |
ms | 1000 |
DTMF-mode silence window (longer to absorb inter-tone gaps on tight-squelch radios). |
dtmf_grace_ms |
ms | 3000 |
How long after the last DTMF tone we stay in DTMF-patient mode. |
trust_cos_bit |
on/off | on |
Set off for radios whose COS bit lies; falls back to dtmf_active only. |
Config section: [txactivity]. See ARCH-COR-DTMF.md for the full design rationale.
Morse CW ID + voice ID ("WRDP519 repeater, 462.550, PL 131.8") via TTS. Two modes:
- always (default) — Fixed wall-clock timer. IDs every N minutes regardless of activity.
- on_call — Event-driven (RT97L-style). IDs only during and after activity: initial ID on first key-up, repeating ID during conversation, final ID after last TX, then silent.
Interval capped at 15 min (FCC 95.1751).
| Key | Type | Default | Description |
|---|---|---|---|
cwid_mode |
string | always |
always = fixed timer, on_call = activity-triggered. on_call is the FCC-aligned default: a quiet repeater isn't "in operation" so doesn't need to ID. |
cwid_interval |
duration | 10m |
ID interval. Accepts 15m, 300s, 600000 (ms). Capped at 15 min per FCC 95.1751. |
cwid_tail |
duration | (interval) | on_call: delay before final ID after last activity |
cwid_wpm |
int | 20 |
Words per minute (min 5) |
cwid_freq |
Hz | 800 |
Tone frequency |
tx_ctcss |
int | -- | CTCSS freq x10 (e.g., 1318 = 131.8 Hz). Announced in voice ID as "PL 131.8" |
tx_dcs |
int | -- | DCS code (e.g., 23). Announced in voice ID as "DCS 023" |
pl_tone |
string | -- | Explicit PL/DCS string for voice ID (overrides tx_ctcss/tx_dcs) |
Config section: [repeater]. Callsign from [general] callsign. Frequency from [general] frequency. Set voice_id = on and one of tx_ctcss, tx_dcs, or pl_tone to announce the access tone after the CW ID.
| Key | Type | Default | Description |
|---|---|---|---|
freq |
Hz | 800 |
Tone frequency |
duration |
ms | 100 |
Tone duration |
amplitude |
int | 4000 |
Amplitude (0-32767) |
Config section: [courtesy]
| Key | Type | Default | Description |
|---|---|---|---|
methods |
string | Comma-separated: dtmf_ani,dtmf_login |
|
ani_window |
duration | 1s |
Window after COR for ANI digits |
login_timeout |
duration | 30m |
Login session timeout |
Config section: [caller]
| Key | Type | Default | Description |
|---|---|---|---|
inter_digit_timeout |
duration | 3s |
Reset timeout between digits |
hits_to_begin |
blocks (20 ms) | 1 |
Consecutive tone blocks before decoder lock-on |
misses_to_end |
blocks (20 ms) | 3 |
Consecutive silent blocks before tone-end |
min_off_frames |
blocks (20 ms) | 1 |
Silence required before SAME digit can re-fire. Bump to 5–15 if a single keypress on your radio (especially BTECHs) registers as 2+ digits — radios that chop the carrier mid-press will trigger duplicate detections. |
Config section: [dtmf]. Defaults are tuned loose for tight-squelch
radios; raise on noisy lines if you see spurious double-detects.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable voicemail |
voicemail_dir |
string | /var/lib/kerchunk/voicemail |
Storage directory |
max_messages |
int | 20 |
Max per user |
max_duration |
s | 60 |
Max recording length |
Config section: [voicemail]
Uses TTS ("Current weather. Partly cloudy. Temperature 72 degrees. Wind from the south at 12 miles per hour.") with WAV fallback.
| Key | Type | Default | Description |
|---|---|---|---|
api_key |
string | weatherapi.com API key | |
location |
string | ZIP code or city | |
fetch_interval |
duration | 5m |
Dashboard refresh cadence — runs whenever an api_key is set, regardless of auto_announce, so the weather card stays current on FCC-compliant default configs |
interval |
duration | 30m |
On-air announce cadence (only fires when auto_announce = on) |
auto_announce |
on/off | off |
Periodic on-air announcement (FCC 95.1733) |
announce_temp |
on/off | on |
Include temperature |
announce_conditions |
on/off | on |
Include conditions |
announce_wind |
on/off | on |
Include wind |
Config section: [weather]
Uses TTS ("The time is 2:30 PM central.") with WAV fallback.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Periodic auto-announce |
interval |
ms | 900000 |
Interval |
timezone |
string | central, eastern, mountain, pacific |
Config section: [time]
Records RX (per COR cycle) and TX (per queue drain) to timestamped WAV files. Recording filenames use the username (not display name).
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable recording |
directory |
string | recordings |
Output directory |
max_duration |
s | 300 |
Max recording length |
Config section: [recording]
DTMF sequences, two-tone paging, Selcall, MDC-1200, CW ID, and tone burst generation.
*911# activates, *910# deactivates. Suppresses TOT and auto-announcements.
| Key | Type | Default | Description |
|---|---|---|---|
timeout |
ms | 1800000 |
Auto-deactivate timeout |
Config section: [emergency]
RFC 6238 TOTP with embedded SHA-1/HMAC-SHA1 (no external crypto dependencies). Users dial *68<6-digit code># to authenticate via Google Authenticator, Authy, or any TOTP app. Grants time-limited elevated access for privileged commands.
Other modules gate commands via kerchunk_core_get_otp_elevated(user_id).
| Key | Type | Default | Description |
|---|---|---|---|
session_timeout |
ms | 120000 |
Elevated session duration (2 min) |
time_skew |
int | 1 |
Accept +/- N time steps (each 30s) |
Config section: [otp]
User config: add totp_secret = <base32 key> to [user.N] sections.
*88# arms. Records next transmission (max 10s), plays back for audio quality check. After playback, reports the peak sample level (dBFS) and the speech-active average RMS (measured only on frames above a ~-40 dBFS noise floor, so long user pauses don't dilute the reading). This makes it a cheap on-radio audio-level check: "your peak was -6 dBFS, average -18 dBFS" is actionable; "average 3%" from a long-silence dilution was not.
| Key | Type | Default | Description |
|---|---|---|---|
max_duration |
s | 10 |
Max recording (capped at 30) |
Config section: [parrot]
Daily CSV files (<directory>/YYYY-MM-DD.csv) with caller, method,
duration, emergency flag, recording path. Filename + date columns
are local time; the leading timestamp column is UTC epoch seconds.
The dashboard's today_calls / today_seconds counters tally
voice transmissions only — announcements (CW ID, weather, time,
ASR/AI, parrot, voicemail prompts) still get a CSV row for audit
but don't bump the counters. On daemon restart, today's CSV is
replayed (skipping system-author rows) so the counters survive
reboots and deb upgrades mid-day.
| Key | Type | Default | Description |
|---|---|---|---|
directory |
string | cdr |
Output directory |
Config section: [cdr]
Async worker thread. Two engines: ElevenLabs (cloud API) or Wyoming (local/network via libwyoming). Wyoming connects to a wyoming-server running Piper TTS — no subprocess, no Python. Responses cached as WAV files keyed by text hash in <sounds_dir>/cache/tts/. Use tts cache-clear to flush.
Optional text normalization via libnemo_normalize (requires OpenFst). Normalizes numbers, times, dates, and abbreviations before synthesis so TTS speaks them correctly (e.g., "3:45 PM" → "three forty five PM"). Configure normalize_far_dir in [tts] to enable.
| Key | Type | Default | Description |
|---|---|---|---|
engine |
string | elevenlabs |
elevenlabs or wyoming |
api_key |
string | ElevenLabs API key (elevenlabs engine) | |
voice_id |
string | 21m00Tcm4TlvDq8ikWAM |
ElevenLabs voice ID |
model |
string | eleven_turbo_v2_5 |
ElevenLabs model ID |
wyoming_host |
string | 127.0.0.1 |
Wyoming server host (wyoming engine) |
wyoming_port |
int | 10200 |
Wyoming server port |
wyoming_voice |
string | Voice name (empty = server default) | |
normalize_far_dir |
string | Path to NeMo FAR grammars (optional) |
Config section: [tts]
Transcribes all inbound RF transmissions via a Wyoming ASR server (libwyoming). Supports batch mode (Whisper — best accuracy, ~1s delay after COR drop) and streaming mode (Zipformer — instant transcript on COR drop). Transcripts are logged, stored in a rolling history, and available via asr history.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable ASR |
mode |
string | batch |
batch (Whisper) or streaming (Zipformer) |
wyoming_host |
string | 127.0.0.1 |
Wyoming ASR server host |
wyoming_port |
int | 10200 |
Wyoming ASR server port |
language |
string | en |
Language code |
max_capture |
int | 30 |
Max seconds to capture per transmission |
min_duration |
duration | 500 |
Min duration to transcribe (skip kerchunks) |
Config section: [asr]
LLM-driven on-air assistant. Transcripts from mod_asr go through an OpenAI-compatible /v1/chat/completions endpoint (Ollama, llama.cpp, vLLM, LM Studio, or any hosted OpenAI-API-compatible backend), the LLM calls structured tools to fetch real-time data, and the final response is spoken via mod_tts. All work runs on a dedicated worker thread — the audio path and main loop are never blocked.
Trigger modes:
wake_phrase(default) — transcripts prefixed with[ai] wake_phrase(defaultkerchunk) go to the AI. Rest is ignored.dtmf— caller dials*99#before transmitting. One-shot; consumed after the next TX.always— every transcript goes through (dedicated AI channel).
Multi-turn conversations — after a response, the same caller can follow up within conversation_timeout (default 5 min) without repeating the wake phrase.
Built-in tools (10): get_time, get_weather, get_forecast, get_repeater_status, get_stats, get_nws_alerts, get_user_info, get_asr_history, set_emergency (admin), send_page (admin). CLI-backed tools dispatch via kerchunk_dispatch_command. Admin-gated tools check kerchunk_core_get_otp_elevated or user.access >= KERCHUNK_ACCESS_ADMIN.
System prompt is loaded from a standalone markdown file (default /etc/kerchunk/system_prompt.md) so personality can be iterated without restarting the daemon — re-read on config reload. A sensible default is shipped as system_prompt.md.example and seeded on first install.
Failure modes handled cleanly: connect refused, HTTP errors, auth failures, model-not-found, timeouts, empty responses. A circuit breaker disables the AI after max_consecutive_failures (default 5) for disable_after_fail_s (default 300s).
Reasoning models (qwen3.x, deepseek-r1) are supported via disable_reasoning = on (default), which passes "think": false to Ollama — skips chain-of-thought so the full token budget becomes content. Harmless on non-reasoning models.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Master switch |
llm_url |
string | OpenAI-compatible endpoint, e.g. http://ollama.lan:11434/v1/chat/completions |
|
llm_model |
string | Model tag (required for Ollama) | |
llm_api_key |
string | Optional Bearer token for auth proxies | |
llm_timeout_s |
int | 30 |
HTTP timeout for inference |
llm_verify_tls |
on/off | on |
Verify cert when llm_url is https:// |
system_prompt_file |
path | /etc/kerchunk/system_prompt.md |
Markdown file read verbatim as the system prompt |
max_tokens |
int | 500 |
Response budget. Reasoning models may need 1000-1500 |
temperature |
float | 0.3 |
LLM sampling temperature |
disable_reasoning |
on/off | on |
Pass think:false — skips chain-of-thought in qwen3/r1 |
trigger |
string | wake_phrase |
wake_phrase | dtmf | always |
wake_phrase |
string | kerchunk |
First word(s) that arm the AI |
conversation_timeout |
duration | 5m |
Idle before conversation resets |
max_tool_rounds |
int | 3 |
Max tool_call → response loops per request |
standby_delay_ms |
ms | 2000 |
Queue a standby cue if the LLM takes longer than this |
standby_cue |
string | sound |
sound | tts | none |
standby_sound |
path | system/standby |
WAV played when standby_cue = sound |
sound_offline |
path | system/ai_offline |
Fallback when LLM unreachable |
sound_error |
path | system/ai_error |
Fallback on HTTP error |
sound_timeout |
path | system/ai_timeout |
Fallback on HTTP timeout |
max_consecutive_failures |
int | 5 |
Circuit breaker threshold |
disable_after_fail_s |
s | 300 |
Circuit breaker cooldown |
Config section: [ai]. Dependencies: libcurl, libcjson, mod_asr, mod_tts, a reachable OpenAI-compatible endpoint. DTMF: *99# arms the AI (offset 18, override via [dtmf] dtmf_ai = <pattern>). CLI: ai, ai tools, ai history, ai ask <text>, ai reset.
Model choice matters. Models below ~7B parameters can struggle with reliable tool call emission. Try qwen2.5:7b, llama3.1:8b, or mistral-nemo on Ollama. qwen3.5:0.8b works but lives at the edge of the capability cliff.
Bridges Push-to-Talk over Cellular radios (Retevis L71, TYT, etc.) to the RF repeater via libpoc. Bidirectional audio bridging, user/group sync from kerchunk DB, TLS support, SOS alerts, and text messaging.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
int | 1 |
Enable PoC server |
port |
int | 29999 |
Listen port |
rf_bridge_group |
int | 0 |
Kerchunk group ID bridged to RF (0=none) |
rf_to_poc |
int | 1 |
Forward RF audio to PoC clients |
poc_to_rf |
int | 1 |
Forward PoC audio to RF TX |
Config section: [poc]. Per-user access: add poc_password to [user.N] sections.
Bridges audio between the RF repeater and a Zello
channel via libzello. One Zello
account (Friends & Family or Zello Work) joins the configured channel and
relays audio in both directions: RF RX → Zello channel, and Zello speakers
→ RF TX. Each remote Zello speaker shows up as a VCOR with the source tag
zello and the speaker's Zello username, so mod_recorder, mod_cdr, and
mod_asr can attribute correctly (recordings land as
*_TX_zello_<remote_username>.wav).
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable the Zello bridge |
server_url |
url | wss://zello.io/ws |
Channel API endpoint. For Zello Work use wss://zellowork.io/ws/<network> |
username |
string | Zello account username | |
password |
string | Zello account password | |
channel |
string | Channel to join (case-sensitive) | |
auth_token |
string | Developer JWT (Friends & Family only). Inline string | |
auth_token_file |
path | Alternative: load JWT from a file | |
listen_only |
on/off | off |
Connect listen-only (server rejects start_stream from us) |
rf_to_zello |
on/off | on |
Forward RF RX audio to the Zello channel |
zello_to_rf |
on/off | on |
Forward Zello speaker audio to RF TX |
priority |
int | 3 |
Queue priority for Zello → RF audio |
virtual_user_id |
int | 998 |
kerchunk user_id stamped on VCOR events for inbound Zello audio |
Config section: [zello]. CLI: zello status|connect|disconnect|say <text>|wav <path>.
The zello wav <path> command streams a 16-bit mono WAV file straight to
the channel (resamples to 16 kHz if needed) — useful as a known-good
reference when diagnosing RF-capture audio issues. The file must be
readable by the kerchunk user; systemd's ProtectHome=true blocks
/home, so stage test files under /var/lib/kerchunk or use the bundled
/usr/share/kerchunk/sounds tree.
Debug knob: set ZELLO_TX_DUMP_DIR=/var/lib/kerchunk/recordings in the
kerchunkd service environment and mod_zello will append every
RF → Zello transmission's post-resample 16 kHz mono PCM to a per-stream
WAV file in that directory. Lets you audit what mod_zello is actually
feeding libzello without involving the Zello server.
Polls api.weather.gov, tracks alerts by ID, announces via TTS. EAS-style tones for extreme.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable monitoring |
latitude |
float | Location latitude | |
longitude |
float | Location longitude | |
contact |
string | Email for User-Agent | |
poll_interval |
ms | 300000 |
Poll interval (5 min) |
reannounce_interval |
ms | 900000 |
Re-announce (15 min) |
min_severity |
string | moderate |
Minimum severity |
auto_announce |
on/off | on |
Auto-announce new alerts |
attention_tones |
on/off | on |
EAS tones for extreme |
Config section: [nws]
Channel, per-user, and system metrics. 24h histogram. Persistence across restarts.
| Key | Type | Default | Description |
|---|---|---|---|
rrd_file |
path | (none) | RRD database file (created if missing) |
Config section: [stats]. CLI: stats, stats user <name>, stats reset
Embedded HTTP/HTTPS server with split public/admin architecture, JSON API, SSE event stream, WebSocket audio streaming and PTT, and static file serving.
Public (/) — no authentication required:
- Dashboard (
index.html) — repeater status, live audio (listen-only), weather, NWS alerts - Registration (
register.html) — self-registration (whenregistration_enabled = on)
Admin (/admin/) — HTTP Basic Auth required:
- Dashboard (
admin/index.html) — real-time SSE event stream, controls, TTS, statistics - Users (
admin/users.html) — user/group CRUD with TOTP QR codes - Config (
admin/config.html) — live config editor with reload - Coverage (
admin/coverage.html) — GMRS RF coverage planner with terrain analysis - PTT (
admin/ptt.html) — WebSocket push-to-talk with mic capture and RX audio playback
Public API routes (/api/status, /api/weather, /api/nws, /api/audio WebSocket listen-only, POST /api/register) require no authentication and do not expose sensitive data (API keys, etc). Admin API routes (/admin/api/*) require HTTP Basic Auth. The public /api/audio WebSocket is listen-only; PTT commands are only accepted on /admin/api/audio.
Set public_only = on to block all /admin/ access — for internet-facing deployments where admin is accessed via VPN.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable web server |
port |
int | 8080 |
Listen port |
bind |
string | 127.0.0.1 |
Bind address (0.0.0.0 for external) |
auth_user |
string | admin |
HTTP Basic Auth username for /admin/ |
auth_password |
string | HTTP Basic Auth password for /admin/ | |
admin_acl |
string | CIDR list for admin IP restriction (empty = allow all) | |
public_only |
on/off | off |
Block all /admin/ access |
static_dir |
string | Path to HTML/JS/CSS files | |
tls_cert |
string | Path to TLS certificate (PEM). Enables HTTPS | |
tls_key |
string | Path to TLS private key (PEM). Required with tls_cert | |
cors_origin |
string | * |
CORS Access-Control-Allow-Origin header |
ptt_enabled |
on/off | off |
Enable WebSocket PTT (admin only) |
ptt_max_duration |
duration | 30s |
Max PTT duration |
ptt_priority |
int | 2 |
Queue priority for PTT audio |
registration_enabled |
on/off | off |
Enable public user self-registration |
Config section: [web]
Public API: GET /api/status, GET /api/weather, GET /api/nws, /api/audio WebSocket (listen-only), POST /api/register
Admin API: GET /admin/api/status (includes sensitive fields), GET /admin/api/stats, GET /admin/api/users, GET /admin/api/groups, GET /admin/api/config, GET /admin/api/commands, GET /admin/api/events (SSE), GET /admin/api/sounds (sound-tree for Play picker), GET /admin/api/cdr/days, GET /admin/api/cdr?date=YYYY-MM-DD, GET /admin/api/recording?path=…&download=1 (Range-aware WAV streaming, sandboxed to recordings_dir), /admin/api/audio WebSocket (PTT), POST /admin/api/cmd, POST /admin/api/config/reload, CRUD POST/PUT/DELETE /admin/api/users/{id}, POST/PUT/DELETE /admin/api/groups/{id}
Admin pages: /admin/ (dashboard), /admin/users.html, /admin/cdr.html (historical CDR browser with date picker, filters, inline WAV playback), /admin/config.html, /admin/coverage.html, /admin/ptt.html, /admin/bulletin.html
Fires HTTP POST to a configured URL when specified events occur. Background thread with configurable timeout and retry. Events: COR, PTT, caller identification, announcements, recordings, state changes, shutdown.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable webhook |
url |
string | Destination URL | |
secret |
string | Shared secret (sent as X-Webhook-Secret header) |
|
events |
string | Comma-separated event list | |
timeout_ms |
int | 5000 |
HTTP request timeout |
retry_count |
int | 2 |
Retry failed requests |
Config section: [webhook]
Frequency inversion voice scrambler for Part 90 (Business/Industrial) operation only. Prohibited on GMRS (FCC 95.333) and Amateur (FCC 97.113(a)(4)). Self-inverse: same operation scrambles and descrambles. 8 codes mapping to carrier frequencies 2700-3400 Hz (100 Hz steps). CW ID and emergency mode bypass scrambling.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable scrambler |
code |
int | 4 |
Code 1-8 (carrier = 2600 + code*100 Hz) |
frequency |
int | Optional explicit carrier Hz (overrides code) |
Config section: [scrambler]. DTMF: *97# toggle, *970# off, *971#-*978# set code.
Uses an RTL-SDR dongle (librtlsdr) to monitor a single channel. Tunes to the channel frequency at 240 kHz sample rate, FM demodulation with de-emphasis, CTCSS/DCS/DTMF decoding via libplcode, FM noise squelch, and CSV activity logging.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable SDR monitor |
device_index |
int | 0 |
RTL-SDR device index |
channel |
int | 1 |
Channel number (1-22) |
log_file |
string | sdr_activity.csv |
Activity log file |
Config section: [sdr]. CLI: sdr. Requires librtlsdr-dev.
Ham-only (*0<digits># to dial, *0# to hang up). Connects to a local FreeSWITCH via ESL for outbound calls — e.g., phone patch for emergencies, or a SIP bridge to cellular. Supports VOX-keyed audio, jitter buffer, DTMF dial/hangup events, admin-only mode, and dial/inactivity timeouts. ESL connection uses exponential backoff + a circuit breaker (1s/2s/4s/8s/16s/30s cap, stops after 16 failed attempts) so a stopped FreeSWITCH doesn't flood the log with reconnect noise.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable AutoPatch |
esl_host |
string | 127.0.0.1 |
FreeSWITCH ESL host |
esl_port |
int | 8021 |
ESL port |
esl_password |
string | ClueCon |
ESL password |
sip_gateway |
string | Dial prefix for outbound bridge | |
admin_only |
on/off | off |
Require admin access to use |
dial_timeout |
duration | 30s |
Max dial+connect time |
inactivity_timeout |
duration | 60s |
Max idle time during call |
max_call_duration |
duration | 3m |
Hard cap on call length |
vad_threshold |
int | 800 |
RMS floor to qualify as phone-side speech |
vad_hold_ms |
duration | 500ms |
Keep PTT held this long after last speech frame |
vad_attack_frames |
int | 5 |
Consecutive 20 ms frames before VAD asserts (filters out clicks/short noise) |
phone_gain |
float | 0.5 |
Phone audio gain (0.0–2.0). 0.5 = –6 dB; drop further if phone audio clips on the radio |
See FREESWITCH.md for the full ESL + dialplan setup. Config section: [freeswitch]. CLI: freeswitch.
Reports host CPU load, memory usage, disk temperature, and uptime to the web dashboard and via CLI. Purely informational; no RF activity. Useful for headless Raspberry Pi deployments where you want a status glance.
Config section: (none; no knobs). CLI: sys.
DTMF *41<pin># on, *40<pin># off. Only pins listed in allowed_pins can be controlled. All GPIO pins are 3.3V logic — use a relay board or transistor driver for loads requiring more current.
| Key | Type | Default | Description |
|---|---|---|---|
allowed_pins |
string | Comma-separated GPIO pins |
Config section: [gpio]
Raspberry Pi recommended GPIO pins:
| GPIO | Header Pin | Notes |
|---|---|---|
| 5 | 29 | General purpose |
| 6 | 31 | General purpose |
| 13 | 33 | General purpose, PWM capable |
| 16 | 36 | General purpose |
| 17 | 11 | General purpose (default in example config) |
| 19 | 35 | General purpose |
| 20 | 38 | General purpose |
| 21 | 40 | General purpose |
| 22 | 15 | General purpose (default in example config) |
| 26 | 37 | General purpose |
| 27 | 13 | General purpose (default in example config) |
Pins to avoid: GPIO 0-1 (HAT EEPROM I2C), 2-3 (I2C with pull-ups), 4 (1-Wire), 7-11 (SPI), 14-15 (UART serial console).
COR/PTT uses the CM119 USB HID interface, not GPIO — no pin conflicts.
| Key | Type | Default | Description |
|---|---|---|---|
file |
string | events.log |
Log file path |
max_size_mb |
int | 10 |
Rotation threshold (MB) |
Config section: [logger]
Encodes and transmits POCSAG paging messages via the repeater's TX audio path. Supports baseband and FSK modulation modes, with optional de-emphasis. Requires libpocsag (detected by pkg-config at build time).
Experimental: POCSAG encoding and transmission work but have not been extensively tested over the air. Use with caution.
Config section: [pocsag]. CLI: pocsag send <capcode> <message>, pocsag numeric <capcode> <digits>, pocsag tone <capcode>, pocsag status.
Encodes and transmits FLEX paging messages via the repeater's TX audio path. Supports 1600/3200/6400 bps speeds, baseband and FSK modulation modes, with optional de-emphasis. Requires libflex (detected by pkg-config at build time).
Experimental: FLEX encoding and transmission work but have not been extensively tested over the air. Use with caution.
Config section: [flex]. CLI: flex send <capcode> <message>, flex numeric <capcode> <digits>, flex tone <capcode>, flex status.
Bridges this repeater to a central reflector (kerchunk-reflectd,
shipped as a separate deb) so transmissions on one site are heard on
every other site sharing a talkgroup. Per-session HMAC-SHA256 auth over
TLS-WebSocket, audio carried as Opus 24 kHz / 32 kbps with FEC over
SRTP/UDP. Reflector enforces per-talkgroup floor with a 1.5 s lease;
the loser of a contest gets floor_denied (rate-limited 1/500 ms).
Reconnect uses exponential backoff with ±20 % jitter and permanent
codes (bad_key, unknown_node, banned, version_mismatch) move
the module to state:"stopped" until an operator runs link reconnect
or link clear-alarm.
DTMF *73<n># switches talkgroup at runtime. Dashboard tab at
/admin/link.html shows live state, current talker, and counters via
SSE. CLI: link status / tg <n> / reconnect / clear-alarm.
Config section: [link]. Requires the reflector to be configured with
a matching [node.<id>] block + a 32-byte preshared key. See
LINK-PROTOCOL.md for the wire protocol and USAGE.md for an
end-to-end setup walkthrough.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
on/off | off |
Enable mod_link |
reflector_ws |
URL | — | wss://host:port/link (or ws://) |
node_id |
string | — | Must match a [node.<id>] on the reflector |
preshared_key_hex |
64 hex | — | 32-byte key from openssl rand -hex 32 |
default_tg |
int | reflector default | Talkgroup to land on after login |
verify_peer |
on/off | on |
TLS peer cert verification (mongoose now serves the full chain from a multi-cert PEM, so Let's Encrypt fullchain.pem validates cleanly) |
link_tail_ms |
ms | 500 |
PTT hold after last received frame |
opus_bitrate |
bps | 32000 |
Opus encoder bitrate |
opus_loss_perc |
int | 10 |
Expected loss %, drives FEC strength |
reconnect_min_ms |
ms | 1000 |
Initial reconnect backoff |
reconnect_max_ms |
ms | 60000 |
Cap on reconnect backoff |
APRS position reporting and packet decoding. TX path generates AFSK 1200 position beacons queued into the audio pipeline. RX path decodes APRS packets from mod_sdr audio. Requires libaprs (detected by pkg-config at build time).
Config section: [aprs]. CLI: aprs beacon, aprs send <message>, aprs status. See APRS.md for architecture details.
| Key | Type | Default | Description |
|---|---|---|---|
callsign |
string | FCC callsign for CW ID | |
frequency |
string | Output frequency (for display/voice ID) | |
offset |
string | Input offset (for display) | |
sample_rate |
int | 48000 |
Internal audio sample rate (valid: 8000, 16000, 32000, 48000). Moved to [audio] section — see Audio Config |
log_level |
string | info |
error, warn, info, debug |
sounds_dir |
string | ./sounds |
WAV file base path |
socket_path |
string | /tmp/kerchunk.sock |
CLI socket path |
pid_file |
string | /tmp/kerchunkd.pid |
PID file (prevents duplicate instances) |
users_file |
string | Separate user/group database file (see below) | |
address |
string | Site street address (for dashboard display) | |
latitude |
float | Site latitude (decimal degrees, for coverage planner) | |
longitude |
float | Site longitude (decimal degrees, for coverage planner) | |
elevation |
int | Site elevation (ft ASL, for coverage planner) | |
google_maps_api_key |
string | Google Maps API key (for coverage planner map) |
Config section: [general]
| Key | Type | Default | Description |
|---|---|---|---|
sample_rate |
int | 48000 |
Internal audio sample rate (valid: 8000, 16000, 32000, 48000) |
capture_device |
string | default |
PortAudio capture device |
playback_device |
string | default |
PortAudio playback device |
hw_rate |
int | 0 |
Force hardware sample rate (0=auto, 48000 recommended for USB) |
preemphasis |
on/off | off |
Pre-emphasis filter |
preemphasis_alpha |
float | 0.95 |
Pre-emphasis filter coefficient |
speaker_volume |
int | -1 |
ALSA speaker playback volume (0-151, -1=don't set) |
mic_volume |
int | -1 |
ALSA mic capture volume (0-16, -1=don't set) |
agc |
on/off | off |
Auto Gain Control |
Config section: [audio]
| Key | Type | Default | Description |
|---|---|---|---|
device |
string | /dev/rimlite |
HID device path (udev symlink) |
cor_bit |
int | 1 |
GPIO number for COR input (0-7) |
cor_polarity |
string | active_high |
active_high (CM108 inverts internally) |
ptt_bit |
int | 2 |
GPIO number for PTT output (0-7) |
Config section: [hid]
Use kerchunk-diag -C to dump raw HID reports and verify which GPIO bit
corresponds to COR on your interface. Use kerchunk-diag -T to test PTT.
Users and groups can be defined in the main kerchunk.conf or in a separate file via users_file in [general]. When users_file is set (e.g., users_file = users.conf), all [user.N] and [group.N] sections are loaded from that file instead. The web UI writes changes to the separate file only, keeping the main config untouched.
[group.1]
name = Family
[user.1]
username = bwest # Lowercase, no spaces — login identity
name = Brian West # Display name
callsign = KD0SBW # Amateur/GMRS callsign
email = brian@example.com
dtmf_login = 101 # Or dial *101#
ani = 5551
access = 2 # admin
voicemail = 1
group = 1
totp_secret = JBSWY3DPEHPK3PXP # Base32 TOTP secret (for mod_otp)Users without a username field in config get one auto-derived from their name (lowercase, spaces replaced with underscores).
See kerchunk.conf.example for complete annotated reference.
The public dashboard (index.html) includes a GMRS coverage map. The full coverage planner is at coverage.html. Uses site location (latitude, longitude, elevation from [general]) and Google Maps API (google_maps_api_key) to calculate and display estimated RF coverage with terrain analysis.
| Event | Fired when |
|---|---|
COR_ASSERT |
User started transmitting (fused: COS bit OR DTMF active) |
COR_DROP |
User finished transmitting (fused detector silent for [txactivity] end_silence_ms) |
VCOR_ASSERT |
Virtual carrier — web PTT / PoC / phone keyed up |
VCOR_DROP |
Virtual carrier — network caller released |
PTT_ASSERT |
Transmitter keyed |
PTT_DROP |
Transmitter unkeyed |
RX_STATE_CHANGE |
RX FSM state transition (mod_repeater) |
TX_STATE_CHANGE |
TX FSM state transition (audio thread) |
TAIL_START |
Tail timer started |
TAIL_EXPIRE |
Tail timer expired |
RX_TIMEOUT |
Time-out timer fired |
CALLER_IDENTIFIED |
Caller identified |
CALLER_CLEARED |
Caller cleared |
CTCSS_DETECT |
CTCSS tone detected/lost |
DCS_DETECT |
DCS code detected/lost |
DTMF_DIGIT |
DTMF digit onset |
DTMF_END |
DTMF digit released |
QUEUE_DRAIN |
TX queue playback started |
QUEUE_COMPLETE |
TX queue empty, tail starts |
QUEUE_PREEMPTED |
Active queue drain interrupted by incoming COR |
RECORDING_SAVED |
Recording WAV saved |
ANNOUNCEMENT |
Module-generated announcement (courtesy, ASR transcript, AI response, CWID, pager) |
CONFIG_RELOAD |
Config file reloaded |
SHUTDOWN |
Daemon shutting down |
HEARTBEAT |
5-second keepalive for SSE/WebSocket clients |
TICK |
Main loop tick (20ms) |
AUDIO_FRAME |
20ms audio frame captured |
Custom events (KERCHEVT_CUSTOM + N) are used by mod_dtmfcmd to dispatch DTMF commands to their subscriber modules. See the DTMF table above for the assigned offsets.
Event dispatch recursion cap — kerchevt_fire() enforces KERCHEVT_MAX_DEPTH = 16 per-thread to break fire → handler → fire cycles. Over the limit, the fire is dropped with a loud error log naming the event type.
Part 95E (GMRS) and Part 97 (Amateur):
- CW ID — interval capped at 15 min max (FCC 95.1751 / 97.119)
- Voice ID — speaks frequency and PL tone after CW ID
- Auto-announce — weather/time default off (FCC 95.1733 — no unsolicited one-way TX)
- Kerchunk filter — COR debounce prevents brief key-ups
- TOT — time-out timer prevents stuck transmissions
- Emergency mode — suppresses TOT and auto-announcements
- Recording — activity log for FCC 95.1705 cooperative use
- CDR — structured transmission logging for compliance
- Scrambler disabled by default — encryption prohibited on GMRS (FCC 95.333) and Amateur (FCC 97.113(a)(4))
Part 90 (Business/Industrial):
- Scrambler permitted — frequency inversion encryption is legal on Part 90 frequencies
- Station ID — CW ID or voice announcement per FCC 90.425
- Auto-announce permitted — no one-way transmission restriction
- All other compliance features (TOT, recording, CDR) apply equally
make check # 318 tests across 2 binaries (test_kerchunk + test_web_acl)- Unit tests: event bus, config parser, queue, repeater state events, CW ID encoding, response system, admin ACL, scheduler, recursion cap, fused TX-activity detector (
kerchunk_txactivity), SPSC audio ring + PA-callback commit +paInputUnderflowdrop + repeat-last fill, RX audio sub-tick (decoder reset-on-assert-edge, DTMF event edges, relay drain + early-stop), TX audio sub-tick (queue-pause gate, tx_delay/tail silence budgets, drain cadence, tail-cancel-on-requeue, PTT release with hold-ticks) - Integration tests: repeater state machine (including closed repeater), DTMF dispatch, caller identification, voicemail, timers, user database, DSP decoders, recorder, TX encoder, emergency, parrot, CDR, CWID, stats (including persistence), OTP authentication, voice scrambler, FreeSWITCH bridge, admin IP restriction
Tests come in two styles. Pure unit tests drive the extracted functions directly (no mocks, no module includes) — this is how kerchunk_audio_tick_rx/tx, kerchunk_audio_ring_commit, and kerchunk_txactivity are covered. Integration tests use a mock core vtable (test_integ_mock.h) that records every call, and include module .c files directly into the test binary after redefining KERCHUNK_MODULE_DEFINE, giving tests access to static globals for full introspection.
MIT
