What is zFM?
zFM is a small, self-hosted “push-to-talk style” voice system. Users connect to a single server (TCP), join a talkgroup, and transmit audio either by pressing-to-talk or using VOX (voice-activated transmit). The server keeps track of who is currently speaking per talkgroup, routes audio to listeners, and exposes a live dashboard in the browser.
- Ham / hobby groups (private voice room)
- Community coordination (multiple talkgroups)
- Lab / workshop intercom
- Bridging talkgroups between servers (“peers”)
- server.cpp - voice server + HTTP/HTTPS dashboard + admin API
- client.cpp - console client (PortAudio, VOX/PTT, squelch, ADPCM)
- client_gui.cpp - optional SDL2 GUI wrapper
- index.html/dashboard.js - live status + spectrum/waterfall
- admin.html/admin.js - admin panel (users, TGs, bridges, peers)
What’s possible with zFM?
Here is what zFM can do today, based on the current codebase:
- Talkgroups (public/hidden/admin-only visibility in the dashboard)
- Single active speaker per talkgroup with time-limit protection (max_talk_ms)
- VOX mode (hands-free transmit) or classic push-to-talk
- Hardware PTT options (GPIO / CM108 style control if available, serial RTS/DTR, and command hooks)
- Web dashboard: active speaker, talkgroup list, bridged TG graph, connected users, audio level + spectrum/waterfall view
- Admin UI in the browser: manage users, talkgroups, bridges, peers, time/weather settings
- Admin commands from the console client: kick/mute/ban, list users, last-heard, etc.
- Bridge peers: connect multiple servers together using rule-based talkgroup forwarding
- Optional announcements (time + katwarn + weather) mixed into a talkgroup, when enabled
- Optional ADPCM (bandwidth saver) with jitter buffering and PLC options
- RX / TX squelch options (auto / level / hang time) to suppress noise
zFM is intentionally lightweight. It is not a full “radio network stack” and does not implement codecs like Opus. It’s designed for easy self-hosting and clear operator controls.
Quick start (recommended workflow)
This is the shortest path to “I can talk and I can see the dashboard”.
Place server.json next to the server binary and run it. The server also starts the web dashboard automatically.
# Linux
./zfm_server
# Windows (PowerShell)
.\zfm_server.exe
In your browser, open the HTTP dashboard port (default 8080):
http://<server-ip>:8080/
Admin panel lives at:
http://<server-ip>:8080/admin.html
The console client loads client.json. You can also pass a custom path as the first argument.
# Uses client.json
./zfm_client
Then press t to talk, or enable VOX in the config.
Software
Choose the version of the software that matches your platform.
Screenshot
Reference screenshots related to configuration, layout, or issues.
Server / Client
Web Dashboard / Admin Control
Client GUI
Install / Build on Linux
zFM is plain C++ and does not require heavy frameworks. You mainly need a compiler, PortAudio for the console client, and optionally OpenSSL + SDL2 (for HTTPS / GUI).
- Compiler: g++ or clang++ (C++17)
- Server: standard sockets; optional openssl for HTTPS
- Client: portaudio dev headers and library
- GUI client (optional): SDL2 + SDL2_ttf
# Debian/Ubuntu
sudo apt update
sudo apt install -y build-essential portaudio19-dev libssl-dev
# Optional GUI client
sudo apt install -y libsdl2-dev libsdl2-ttf-dev
# Server (HTTP dashboard)
g++ -O2 -std=c++17 -pthread server.cpp -o zfm_server
# Server with HTTPS (requires OpenSSL, compile with USE_OPENSSL=1)
g++ -O2 -std=c++17 -pthread -DUSE_OPENSSL=1 server.cpp -lssl -lcrypto -o zfm_server
# Console client (PortAudio)
g++ -O2 -std=c++17 -pthread client.cpp -lportaudio -o zfm_client
Tip: place your web files in a folder named like dashboard (or change http_root in server.json).
Install / Build on Windows
On Windows, the server and client use Winsock. You can build using Visual Studio (MSVC) or MinGW-w64. The only “external” dependency you typically need is PortAudio for the client (and OpenSSL if you want HTTPS).
- Create a new “Console App” (C++) project
- Add server.cpp and/or client.cpp
- Set C++ Language Standard to C++17
- Server: link already includes Ws2_32.lib (code sets this)
- Client: add PortAudio include/lib paths and link portaudio.lib
:: Server
g++ -O2 -std=c++17 server.cpp -lws2_32 -o zfm_server.exe
:: Client (needs PortAudio)
g++ -O2 -std=c++17 client.cpp -lws2_32 -lportaudio -o zfm_client.exe
If PortAudio is not installed system-wide, use -I and -L to point to its include/lib directories.
If you enable HTTPS, you must provide cert.pem and key.pem and link OpenSSL libraries. If you don’t need HTTPS, keep USE_OPENSSL off.
Optional GUI client (SDL2)
The GUI wrapper (client_gui.cpp) includes the console client code and adds a windowed UI using SDL2 and SDL2_ttf. This is optional - the console client already supports all core features.
g++ -O2 -std=c++17 -pthread -DGUI client_gui.cpp -lportaudio -lSDL2 -lSDL2_ttf -o zfm_client_gui
Note: the include paths in client_gui.cpp reference SDL headers; you may need to adjust include paths depending on your setup.
Web dashboard
The server hosts a live dashboard (HTML/CSS/JS) from http_root (default: dashboard). It periodically queries server status and renders:
- Server time, katwarn and weather (if enabled)
- Number of connected clients
- Now speaking (active speaker)
- Talkgroups: speaker, duration, listeners, audio level, activity “heat”
- Talkgroup links (bridges) and peer connection state
- Connected users list
- Spectrum / waterfall focused on a talkgroup (click a TG row to focus; optional auto-follow)
The dashboard pages already use responsive CSS. On phones, prefer landscape for the best view of the waterfall.
Admin panel (web)
Open /admin.html in your browser and sign in with a user that has OPERATOR or ADMIN role. The panel is built for touch/mobile too (drawer menu, modal editors).
| Section | What you can do |
|---|---|
| Users | Create/edit users, roles, ban status, priority, talkgroup allow-list, permissions |
| Talkgroups | Create/edit TGs and set visibility mode (public / hide / admin) |
| Bridges | Link talkgroups (show who is bridged to who) |
| Peers | Configure cross-server peers and rules for forwarding |
| Time / KATWARN / Weather | Enable announcements and set API/location/volume options |
| Server | Change ports and server-wide options, saved to server.json |
The admin panel applies changes immediately and persists them by writing server.json. Keep backups of your config in production.
Client commands (console)
When VOX is disabled, the console client waits for short commands:
| Command | Meaning | Notes |
|---|---|---|
| t or /talk | Start a talk session (push-to-talk) | Hold/release depends on PTT mode; audio is sent while active |
| q or /quit | Exit the client | Also possible via Ctrl+C |
| /… | Send an admin command to the server | Requires OPERATOR/ADMIN role (see below) |
If vox_enabled is true, you don’t need t. Transmit starts automatically when your mic level crosses vox_threshold.
Admin commands (from the client)
In the console client you can send admin commands by typing /command …. The client will send ADMIN messages to the server.
| Command | Who can use it | What it does |
|---|---|---|
| /kick CALLSIGN | Operator+ | Disconnect a user |
| /mute CALLSIGN | Operator+ | Mute a user (server-side) |
| /unmute CALLSIGN | Operator+ | Unmute a user |
| /ban CALLSIGN | Operator+ | Ban a user and disconnect them if online |
| /unban CALLSIGN | Operator+ | Remove ban |
| /list_users | Operator+ | List all configured users (with muted/banned markers) |
| /list_tgs | Admin | List known talkgroups |
| /last_heard | Operator+ | Show last-heard table (callsign@talkgroup:timestamp) |
| /add_user CALLSIGN PASS | Admin | Create a new user |
| /remove_user CALLSIGN | Admin | Remove a user (and disconnect if online) |
| /set_pass CALLSIGN PASS | Admin | Change a user password |
| /add_tg CALLSIGN TG | Admin | Add a talkgroup to a user’s allow-list (also registers TG) |
| /drop_tg CALLSIGN TG | Admin | Remove a talkgroup from a user’s allow-list |
| /set_admin CALLSIGN 0|1 | Admin | Admin flag (note: implementation may evolve; prefer editing role in admin UI) |
Operators can manage regular users, but cannot override admins. Admin-only actions include adding users and editing talkgroup access lists.
server.json (server configuration)
The server loads server.json at startup and rewrites it when you change settings in the admin panel (or via admin commands). Below is an example layout with notes.
{
"server_name": "Server",
"peer_secret": "password",
"server_port": 26613,
"max_talk_ms": 600000,
"http_root": "dashboard",
"http_port": 8080,
"https_port": 8443,
"https_cert_file": "cert.pem",
"https_key_file": "key.pem",
"time_announcement": {
"enabled": true,
"folder": "audio",
"volume_factor": 0.35
},
"weather_enabled": false,
"weather_host_ip": "api.openweathermap.org",
"weather_talkgroup": "Weather",
"weather_interval_sec": 600,
"weather_api_key": "api_key",
"weather_lat": "50.1109",
"weather_lon": "8.6821",
"weather_city_key": "Frankfurt",
"katwarn_enabled": false,
"katwarn_host": "nina.api.proxy.bund.dev",
"katwarn_talkgroup": "KATWARN",
"katwarn_interval_sec": 3600,
"katwarn_ars": "064120000000",
"katwarn_city_key": "katwarn_frankfurt",
"users": [
{
"callsign": "Admin",
"password": "password",
"role": "admin",
"banned": false,
"priority": 100,
"talkgroups": ["Gateway","Public","Weather","Operation","Admin"]
},
{
"callsign": "Operator",
"password": "password",
"role": "operator",
"banned": false,
"priority": 50,
"talkgroups": ["Gateway","Public","Weather","Operation"]
},
{
"callsign": "Guest",
"password": "password",
"role": "user",
"banned": false,
"priority": 0,
"talkgroups": ["Gateway","Public"]
}
],
"talkgroups": [
{ "name": "Gateway", "mode": "public" },
{ "name": "Public", "mode": "public" },
{ "name": "Weather", "mode": "public" },
{ "name": "Operation", "mode": "hide" },
{ "name": "Admin", "mode": "admin" }
],
"bridges": {
"Weather": ["Gateway","Public"],
"Gateway": ["Public"],
"Public": ["Gateway"]
},
"peers": [
{
"name": "Remote Server",
"host": "127.0.0.1",
"port": 26613,
"secret": "premote_password",
"rules": ["Gateway=Gateway:both", "Public=Public:both"]
}
]
}
- server_port: main voice server TCP port
- http_root: folder containing index.html, dashboard.js, etc.
- talkgroups[].mode: dashboard visibility (public, hide, admin)
- bridges: talkgroup link map (audio forwarded across linked TGs)
- peers: cross-server connections + forwarding rules
Put the server behind a firewall, restrict admin access, and use strong passwords. If you expose the dashboard publicly, consider HTTPS and a reverse proxy.
client.json (client configuration)
The console client reads client.json and uses it to pick the server, audio devices, talkgroup, VOX/PTT behavior, and audio processing options.
{
"mode": "Server",
"server_ip": "127.0.0.1",
"server_port": 26613,
"callsign": "Guest",
"password": "password",
"talkgroup": "Gateway",
"sample_rate": 22050,
"frames_per_buffer": 960,
"channels": 1,
"input_device_index": 0,
"output_device_index": 0,
"gpio_ptt_enabled": false,
"gpio_ptt_pin": 18,
"gpio_ptt_active_high": true,
"gpio_ptt_hold_ms": 250,
"keyboard_ptt_enabled": true,
"keyboard_ptt_keycode": 32,
"vox_enabled": false,
"vox_threshold": 5000,
"input_gain": 100,
"output_gain": 100,
"ptt_cmd_on": "",
"ptt_cmd_off": "",
"ptt_serial_port": "",
"ptt_serial_line": "RTS",
"ptt_serial_invert": false,
"roger_sound": 1,
"use_adpcm": false,
"adpcm_adaptive": true,
"adpcm_jitter_frames": 3,
"adpcm_plc_ms": 120,
"rx_squelch_enabled": false,
"rx_squelch_auto": true,
"rx_squelch_level": 55,
"rx_squelch_voice_pct": 55,
"rx_squelch_hang_ms": 450,
"tx_squelch_enabled": false,
"tx_squelch_auto": true,
"tx_squelch_level": 55,
"tx_squelch_voice_pct": 55,
"tx_squelch_hang_ms": 450
}
- keyboard_ptt_enabled: use keyboard keycode (default 32 = Space)
- gpio_ptt_enabled: GPIO/CM108 style control where supported
- ptt_serial_port: toggle RTS/DTR on a COM/tty device
- ptt_cmd_on/off: run external commands when PTT toggles
- input_gain/output_gain: 0–200 (%)
- frames_per_buffer: latency vs CPU (lower = snappier)
- rx_squelch_*: suppress noise / open only on voice
- use_adpcm: reduce bandwidth, may reduce quality
License
This software is experimental and provided "AS IS". Use at your own risk.
MIT License
Copyright (c) 2025 zFM
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHOR OR COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Katwarn Info
Below is a complete list of all 16 German federal states with their capital city, latitude/longitude, and the corresponding "ARC" for Katwarn.
| State | Capital | Latitude | Longitude | ARC |
|---|---|---|---|---|
| Schleswig-Holstein | Kiel | 54.321329 | 10.134888 | 010000000000 |
| Hamburg | Hamburg | 53.551086 | 9.993682 | 020000000000 |
| Lower Saxony | Hanover | 52.370516 | 9.733222 | 030000000000 |
| Bremen | Bremen | 53.075820 | 8.807170 | 040000000000 |
| North Rhine-Westphalia | Düsseldorf | 51.221723 | 6.776161 | 050000000000 |
| Hesse | Wiesbaden | 50.086013 | 8.244352 | 060000000000 |
| Rhineland-Palatinate | Mainz | 49.981851 | 8.280081 | 070000000000 |
| Baden-Württemberg | Stuttgart | 48.782324 | 9.177017 | 080000000000 |
| Bavaria | Munich | 48.137433 | 11.575491 | 090000000000 |
| Saarland | Saarbrücken | 49.232624 | 7.009818 | 100000000000 |
| Berlin | Berlin | 52.524368 | 13.410530 | 110000000000 |
| Brandenburg | Potsdam | 52.398858 | 13.065662 | 120000000000 |
| Mecklenburg-Western Pomerania | Schwerin | 53.629370 | 11.413160 | 130000000000 |
| Saxony | Dresden | 51.050891 | 13.738317 | 140000000000 |
| Saxony-Anhalt | Magdeburg | 52.131293 | 11.631888 | 150000000000 |
| Thuringia | Erfurt | 50.978700 | 11.032830 | 160000000000 |
Troubleshooting
- Make sure server.json / client.json is next to the binary (or pass a path)
- Check JSON formatting (commas/quotes). zFM uses a lightweight parser.
- Adjust input_device_index and output_device_index
- Try a standard sample rate: 22050 or 48000
- Increase frames_per_buffer if you hear crackles
- Confirm server HTTP port (http_port) is reachable
- Confirm http_root points to your dashboard folder
- Check firewall rules for ports server_port and http_port
- Check if your user is banned or muted (admin UI)
- Check talkgroup permissions (your user’s talkgroups allow-list)
- Time limit may revoke the mic after max_talk_ms
Security & best practices
- Use strong passwords for operator/admin accounts.
- Don’t expose admin endpoints publicly without protection (VPN / reverse proxy auth).
- Keep backups of server.json before large changes.
- Use HTTPS (or a reverse proxy) if you access the dashboard over the internet.
- Prefer running the server under a dedicated user account with limited permissions.
FAQ
No. You can run the server + clients without it. It just makes management easier.
Yes - that’s a common setup. Bind behind a firewall and use private IPs.
Use the admin panel (Talkgroups tab) or add them into server.json. You can also create/register a TG by adding it to a user via /add_tg or the admin UI.
Follow the instruction below on terminal.
sudo tee /etc/udev/rules.d/99-cm108-ptt.rules >/dev/null <<'RULE'
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", ATTRS{idProduct}=="013c", MODE="0660", GROUP="plugdev"
RULE
sudo usermod -aG plugdev "$USER"
sudo udevadm control --reload-rules
sudo udevadm trigger
or this.
sudo systemctl stop hciuart
sudo systemctl disable hciuart
sudo tee /etc/udev/rules.d/90-cm108.rules >/dev/null <<'RULE'
# block pulseaudio using the soundcard for SVXLINK
ATTRS{idVendor}=="0d8c", ENV{PULSE_IGNORE}="1"
# create a symlink /dev/hidrawX to /dev/cm108gpio
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", SYMLINK+="cm108gpio", MODE="0666"
RULE
sudo usermod -aG audio $USER
sudo usermod -aG plugdev "$USER"
sudo udevadm control --reload-rules
sudo udevadm trigger