Notes, scripts and configuration for my MMDVM HS Hat hotspot running on a
Raspberry Pi 4 (hostname: mmdvm).
- MMDVM HS Hat rev 2.0 (SM6WJM), rev 1.6 (SM6WLH)
- 12.288 MHz TCXO onboard (works a bit better than the more common 14.7456 MHz)
- Raspberry Pi 4
Because of the 12.288 MHz TCXO, PiStar cannot update the firmware with a pre-compiled binary — the firmware must be built from source with the correct config (see #firmware-build).
- Hardware: https://github.com/mathisschmieder/MMDVM_HS_Hat
- Firmware: https://github.com/juribeparada/MMDVM_HS
- Host software: https://github.com/g4klx/MMDVMHost
- DMR gateway: https://github.com/g4klx/DMRGateway
- MQTT broker (Debian/Raspberry Pi OS):
mosquittopackage
git clone https://github.com/juribeparada/MMDVM_HS.git
cd MMDVM_HS
# Pick the matching board/clock config — this one is for the 12 MHz HS Hat.
cp configs/MMDVM_HS_Hat-12mhz.h Config.h
makeThere is also an upstream installer script if you prefer:
curl -OL https://raw.github.com/juribeparada/MMDVM_HS/master/scripts/install_fw_hshat-12mhz.shOn newer Raspberry Pi models the autoboot logic in stm32flash no longer
toggles BOOT0/RESET correctly via /sys/class/gpio (see
stm32flash hints). The
script in scripts/flash.sh drives the pins manually using pinctrl:
- GPIO 20 → BOOT0
- GPIO 21 → RESET
Usage on the Pi:
# Default binary path is bin/mmdvm_f1.bin
./scripts/flash.sh
# Or pass an explicit path
./scripts/flash.sh path/to/firmware.binRequires stm32flash and pinctrl installed on the Pi.
The active MMDVMHost configuration lives in MMDVM.ini. Highlights:
| Setting | Value |
|---|---|
| Callsign | SM6WJM |
| DMR ID | 2406237 |
| Mode | Simplex (Duplex=0) |
| RX/TX | 433.6375 MHz |
| Modes enabled | DMR |
| DMR network | Brandmeister BM2402 (44.5.24.178:62031) |
| Modem port | /dev/ttyAMA0 @ 115200 |
The Brandmeister password is no longer in MMDVM.ini — newer MMDVMHost
hands off to a separate DMR gateway process (see next section), which holds
the BM password.
As of mid-2026 MMDVMHost dropped the Type=Direct mode in [DMR Network]
that let it connect straight to a Brandmeister master. The new model:
RF ──► MMDVMHost ──UDP 127.0.0.1──► DMRGateway ──► BM2402 master
MMDVMHost now only knows about a local UDP gateway:
[DMR Network]
LocalAddress=127.0.0.1
LocalPort=62032
GatewayAddress=127.0.0.1
GatewayPort=62031
You must run DMRGateway alongside
MMDVMHost on the Pi. Its config is the place where the Brandmeister
password and the BM2402 endpoint (44.5.24.178:62031) now live. Sketch:
[DMR Network 1]
Enabled=1
Name=BM_2402
Address=44.5.24.178
Port=62031
Password=YOUR_BM_PASSWORD
# ...
Newer MMDVMHost always tries to publish status to an MQTT broker — there is
no Enable=0 switch. Install mosquitto on the Pi to silence the
“Connection refused” warning at startup:
sudo apt install mosquitto
sudo systemctl enable --now mosquittoThe [MQTT] section in MMDVM.ini points at 127.0.0.1:1883 with
Auth=0, which matches a stock mosquitto install.
The Name= field in [MQTT] is used as both the MQTT client_id and the
**topic prefix** — every topic below is published as mmdvm/<topic> with
the current config. Set Name=hotspot-uhf if you want a distinct
namespace per node.
| Topic | Content |
|---|---|
mmdvm/log | Plaintext log lines, level-gated by [Log] MQTTLevel. Same as stdout. |
mmdvm/json | Structured per-event JSON (see below) — the interesting telemetry feed. |
mmdvm/display-out | Raw display-protocol bytes for an external renderer (replaces OLED/Nextion/HD44780). |
mmdvm/response | Replies to remote-control commands. |
One JSON envelope per RF or network event:
- Call start (DMR / D-Star / YSF / P25 / NXDN) — src DMR ID, callsign
(looked up via
DMRIds.dat), dst ID, group-vs-private flag - Call end — frame count, BER %, min/max/avg RSSI
lost— RF dropout / timeoutrejected/invalid/late_entry— access-control denials or malformed frames- Periodic
rssiandberduring a transmission - Mode switch — which mode is currently active
- POCSAG — paging RIC, type (
alphanumeric/numeric/alert_1/alert_2), text, source (local/network) - FM state-machine transitions —
listening,kerchunk_rf,relaying_rf,timeout_rf, etc. - Lifecycle —
"MMDVMHost is starting","Opening network connections","Sending CW ID", etc.
mmdvm/display-in— input events from an external display appmmdvm/command— remote-control commands, registered only when[Remote Control] Enable=1
Every call your hotspot handles publishes the caller’s DMR ID, callsign,
destination TG, RSSI and BER to MQTT in real time. Anyone subscribed to
mmdvm/# can watch the activity live. Fine for the default localhost +
Auth=0 setup. If you ever bind mosquitto to a routable interface,
enable auth and add a listener config that keeps port 1883 off the LAN.
mosquitto_sub -h 127.0.0.1 -t 'mmdvm/#' -vFrom upstream g4klx/MMDVMHost@master:
MQTTConnection.cpp:128,175,214— topic prefixing (Name/topic)Log.cpp:86—logtopic;Log.cpp:104—jsontopicMMDVMHost.cpp:341–344— subscriptions;:1200—display-outRemoteControl.cpp:207—responsetopicwriteJSONRF/writeJSONNet/writeJSONcall sites inDMRSlot.cpp,DStarControl.cpp,YSFControl.cpp,P25Control.cpp,NXDNControl.cpp,FMControl.cpp,POCSAGControl.cpp
Useful for development / debugging — observe the HBP frames that
MMDVMHost would normally send to DMRGateway, or echo them to a custom
process bridging somewhere else.
MMDVM-testing.ini is a copy of MMDVM.ini with [DMR Network]
pointed at a different host (192.168.1.40:62031 as committed — change to
your listener’s IP) and LocalAddress=0.0.0.0 so MMDVMHost can receive
replies from a non-localhost peer:
MMDVMHost MMDVM-testing.ininc -u -l -p 62031 | xxdPayload is binary — pipe through xxd or hexdump -C. First 4 bytes of
each datagram are an ASCII magic identifying the packet type.
Gotchas with nc -u:
- OpenBSD
nc(Debian default) locks onto the first peer it sees. nccan’t reply, so the channel becomes observe-only andMMDVMHostwon’t TX anything on RF because nothing answers.
Doesn’t terminate the UDP stream, so the hotspot keeps thinking it’s talking to a real gateway:
sudo tcpdump -i any -nXX 'udp port 62031 or udp port 62032'For Wireshark capture (no built-in HBP dissector, but the magics are readable in the byte view):
sudo tcpdump -i any -w mmdvm.pcap 'udp port 62031 or udp port 62032'Echo every datagram back to the sender so MMDVMHost sees a live peer:
socat -v UDP4-RECVFROM:62031,fork \
UDP4-SENDTO:'$SOCAT_PEERADDR:$SOCAT_PEERPORT'HomeBrew Repeater Protocol (HBP) over UDP. Same wire format
BrandMeister masters and DMRGateway speak. No authentication on this
socket. Packet type is identified by a 4-byte ASCII magic at offset 0.
| From → To | Magic | Purpose |
|---|---|---|
| Pi → 192.168.1.40 | DMRD | RF voice/data burst (55 B) |
| Pi → 192.168.1.40 | DMRG | GPS embedded in radio’s LC |
| Pi → 192.168.1.40 | DMRA | Talker Alias block |
| Pi → 192.168.1.40 | DMRC | Repeater config heartbeat (~every 10 s) |
| 192.168.1.40 → Pi | DMRD | Network frames to TX on RF |
| 192.168.1.40 → Pi | DMRP | Ping / keepalive |
| 192.168.1.40 → Pi | DMRB | Beacon request |
DMRD packet layout (55 bytes):
| Offset | Field |
|---|---|
| 0–3 | Magic DMRD |
| 4 | Sequence |
| 5–7 | Source DMR ID (24-bit) |
| 8–10 | Destination DMR ID (24-bit) |
| 11–14 | Repeater ID (Id from [General], 32-bit BE) |
| 15 | Flags: bit7=slot, bit6=group/private, bit5/4=data/voice sync, bits0–3=frame N or data type |
| 16–19 | Stream ID (random per call/CSBK/data header) |
| 20–52 | 33-byte DMR payload (AMBE voice frames or data) |
| 53 | BER |
| 54 | RSSI |
hblink3(Python): https://github.com/HBLink-org/hblink3DMRGateway(C++): https://github.com/g4klx/DMRGatewayMMDVMHost/DMRNetwork.cppfor the exact byte layouts
Sanity check: with the link up and no RF activity you should still see a
DMRC packet roughly every 10 seconds. If those are flowing but no
DMRD, nothing is being keyed on RF.
Rust binary that will eventually bridge MMDVMHost’s [DMR Network] UDP
stream (HBP) to a SIP PBX such as Asterisk. Today it has a working SIP
REGISTER client and a passive DMR listener.
Build, configuration and per-subcommand usage live in mmdvm_sip/README.org.
To flash an MD-380 from Windows 10 you need to install the STM Device in DFU mode driver.
.
├── Cargo.toml # Workspace manifest
├── README.org # This file
├── MMDVM.ini # Production MMDVMHost configuration (→ local DMRGateway)
├── MMDVM-testing.ini # Testing variant — streams DMR frames to a LAN host
├── scripts/
│ └── flash.sh # STM32 flashing script (uses pinctrl)
└── mmdvm_sip/ # Rust DMR ↔ SIP bridge (experimental)