Digital Radio Hotspot

A small home-built box that lets my handheld digital radio talk to radio amateurs around the world, using the internet to cover the distance.

On its own, a handheld radio reaches a few kilometres at best. A hotspot is a tiny personal gateway: the handheld talks to it over a short radio link, and the hotspot carries the conversation over the internet to BrandMeister, a network that links thousands of digital radios worldwide. Mine is built from a Raspberry Pi (a credit-card-sized computer) with a radio board on top, in a 3D-printed case with a cooling fan. It runs under my amateur radio callsign, ZR6KW.

Where it's at

  • Working day to day: it links my radio to the network and passes calls both ways.
  • Power problem fixed (it seems): the Pi was running short of power, which caused glitches. A proper 3 A-rated cable appears to have solved it.
  • Still an occasional freeze: now and then the radio board locks up until its software is restarted. The automatic watchdog that should do this by itself still needs fixing.
  • Fine-tuning left: the receive side is calibrated; checking the transmit side properly needs a cheap software-defined radio (SDR) to measure it.
The hotspot's web dashboard showing an incoming DMR call from the Netherlands on timeslot 2, with the caller's callsign and name covered
The hotspot's dashboard during a call from the Netherlands (the caller's details are hidden)

Photos

Build your own: step by step

A rough outline of how I set mine up. Use frequencies allowed by your own licence and local band plan; the ones below are mine.

  1. Get licensed and registered. You need an amateur radio licence and callsign (ICASA in South Africa), a DMR ID from radioid.net, and a BrandMeister SelfCare account, where you set a hotspot password.
  2. Gather the parts. A Raspberry Pi 3B, an MMDVM_HS_Dual_Hat (the dual version allows full duplex), a microSD card, a 5 V / 3 A power supply with a 3 A-rated Micro-USB cable, and a DMR handheld (mine is a Baofeng DM-32UV). A case with a fan is optional.
  3. Assemble. Fit the hat onto the Pi's GPIO header and screw on both antennas. Always have antennas attached before the board can transmit.
  4. Install WPSD. Download the WPSD image from wpsd.radio and write it to the microSD card with Raspberry Pi Imager or balenaEtcher. Set up Wi-Fi (or plug in a network cable) as WPSD's instructions describe, then boot the Pi.
  5. Open the dashboard. From a browser on the same network, open the hotspot's address (WPSD's instructions give it) and go to the configuration page.
  6. Configure the hotspot.
    • Callsign and DMR ID, and your timezone
    • Modem: MMDVM_HS_Dual_Hat on the Pi's GPIO, in duplex mode
    • Frequencies: RX 433.300 MHz, TX 438.300 MHz
    • DMR mode on, Color Code 1
    • Network: BrandMeister, the master for your country, and your hotspot password
  7. Secure it. Change the default passwords, turn on WPSD's firewall option, and keep the system updated.
  8. Program the radio. Add a digital channel that mirrors the hotspot: receive 438.300 MHz, transmit 433.300 MHz, Color Code 1, Timeslot 2, TX Admit Always. Then add the talkgroups you want to use.
  9. Calibrate receive. Use WPSD's calibration tool to find the RX offset with the lowest error rate (details under Technical details).
  10. Test. Make a call to BrandMeister's echo ("Parrot") service and check you hear yourself back, then watch calls come in on the dashboard.
  11. Make it reliable. Check the Pi isn't short of power (vcgencmd get_throttled should show 0x0) and turn on the WPSD Services Watchdog so a stuck modem restarts itself.

Technical details

Hardware

ComponentDetail
Single-board computerRaspberry Pi 3B
ModemMMDVM_HS_Dual_Hat: dual ADF7021 (one RX, one TX) for true duplex
Modem firmwarev1.6.1
Reference oscillator14.7456 MHz TCXO
Pi to modem linkGPIO UART, /dev/ttyAMA0 at 115200 baud
Handheld radioBaofeng DM-32UV
Power5 V / 3.5 A wall supply with a 3 A-rated Micro-USB cable
NetworkBrandMeister, master BM_6551_South_Africa
Block diagram: a 5 V supply powers the Raspberry Pi 3B running WPSD, which talks to the MMDVM dual hat over UART and to BrandMeister over the internet. The hat's RX chip hears the handheld on 433.300 MHz and its TX chip transmits to it on 438.300 MHz.
System block diagram (click to enlarge)

Software

  • WPSD, current Debian Trixie-based image (fresh reinstall)
  • Set up during the reinstall: modem type, timezone Africa/Johannesburg, BrandMeister network, firewall and password hardening, persistent journald logging
  • Pi 3B UART: Bluetooth must not hold the hardware UART. Check dtoverlay=disable-bt (or pi3-disable-bt) in /boot/firmware/config.txt and that hciuart is disabled. WPSD sets this by default, but it's worth confirming after updates.

RF configuration

ParameterHotspotHandheld (mirror image)
RX frequency433.300 MHz438.300 MHz
TX frequency438.300 MHz433.300 MHz
Color Code11
Timeslot22
RXOffset−75 Hz (calibrated)
TXOffset−75 Hz (estimated, not measured)
DMRTXLevel45 (trial value)
TX AdmitAlways (Color Code Idle can cause "Channel Busy" while network traffic is passing)

Relevant MMDVMHost settings (credentials omitted):

[General]
Callsign=ZR6KW
Id=6550447
Duplex=1

[Info]
RXFrequency=433300000
TXFrequency=438300000

[Modem]
Port=/dev/ttyAMA0
UARTSpeed=115200
RXOffset=-75        ; calibrated, BER 0.09%
TXOffset=-75        ; estimated from RXOffset - needs SDR verification
DMRTXLevel=45       ; trial value - needs SDR verification

[DMR]
Enable=1
ColorCode=1

Calibration

RX offset (done), with the WPSD calibration tool:

  1. Select DMR mode, click Start and wait for the status indicator to go green.
  2. On the radio, set TX Admit to Always temporarily and hold PTT on the normal hotspot channel (not VFO).
  3. Step the offset in 100 Hz, then 50 Hz, then 25 Hz steps, chasing the lowest BER.
  4. Run long enough to check the best offset doesn't drift as the board warms up.
  5. Save Offset, Stop, then set TX Admit back.

Result: BER 0.09% at RXOffset −75 Hz. DMR is comfortable up to about 1%.

TX offset and deviation (open): the calibration tool uses the radio as the signal source, so it only measures RX. TXOffset was set to −75 on the assumption that both ADF7021s share the same TCXO and the two frequencies are close, and DMRTXLevel was trialled at 45. Neither is a measurement. Proper TX calibration needs an RTL-SDR (or a spectrum analyser) to check the actual transmitted frequency and deviation.

Troubleshooting log

Issue A: modem lock-up (intermittent, still open)

  • Symptom: the dashboard freezes on TX: DMR, the radio shows "link timeout" on PTT, and network traffic on the dashboard doesn't reach the radio. It tends to happen around transmitting.
  • Diagnosis: the modem reports it is transmitting and never reports completion, so it is stuck at the STM32/firmware level.
  • Workaround: sudo systemctl restart mmdvmhost restores it every time.
  • Power: fixing the under-voltage (below) looks to have cut the problem down, but lock-ups still happen now and then.
  • To do: fix the WPSD Services Watchdog so it restarts the modem automatically. A systemd timer that checks the modem and restarts it is a fallback.

Issue B: carrier but no DMR decode on the handheld (open)

  • Symptom: the radio's RX LED and signal meter respond to the hotspot's 438.300 MHz transmission, but no audio or callsign is decoded, sometimes as intermittent drop-outs.
  • Key point: carrier detect and the S-meter only prove RF is present, not that the modulation can be decoded.
  • Tried: TXOffset −75 and DMRTXLevel 45, with no fix.
  • Next: measure TX frequency and deviation with an RTL-SDR, and correlate drop-outs with BER on the dashboard.

Resolved: the SVC LED flashing red is the normal firmware heartbeat, not a fault. An earlier forum-based diagnosis was wrong.

Power investigation

CheckResult
vcgencmd get_throttled0x50000: under-voltage and throttling had occurred since boot
After swapping to another cableStill 0x50000
Live check0x50005: under-voltage and throttling active at that moment
GPIO power injectionTried, then removed (see below)
3 A-rated Micro-USB cableAppears to have solved the under-voltage

Why it mattered: load spikes (at boot, and from the dual hat while transmitting) caused brief voltage sags at the Pi. These can glitch the UART to the STM32 and leave the modem wedged, which fits Issue A. Cheap USB cables typically have thin 28–30 AWG power wires that drop too much voltage.

Reading get_throttled: bits 0–3 mean "happening now" (0x1 under-voltage, 0x2 frequency capped, 0x4 throttled, 0x8 soft temperature limit); bits 16–19 mean "has happened since boot". The flags only clear on reboot. Watch them live with watch -n 1 vcgencmd get_throttled.

GPIO power injection (tried, not kept): feeding regulated 5 V straight into header pins 2/4 (5 V) and 6 (GND) avoids the Micro-USB connector and cable losses, but it also bypasses the Raspberry Pi's built-in input protection (its polyfuse). That risk wasn't worth keeping once a proper cable did the job, so the Pi is back on Micro-USB power.

Wiring for GPIO power injection, which was tried and then removed: a regulated 5.1 V, 3 A supply's positive lead to header pins 2 and 4, negative lead to pin 6. It bypasses the Micro-USB input protection.
GPIO 5 V power injection: tried, then removed because it bypasses the Pi's protection

Mobile version (12 V vehicle power)

A mobile version of the Pi 3B and hat, powered from a vehicle's 12 V system, has also been explored.

  • Didn't work: LM2596 and XL4015 buck modules, and a homebrew LM317 regulator. They caused RF interference in the receiver or board resets.
  • Planned: vehicle 12 V → fuse → LC/ferrite input filter → Pololu D24V50F5 (or equivalent) synchronous 5 V buck → LC/ferrite output filter → Pi 3B and hat.
  • No-solder alternative: a quality USB car charger with clip-on ferrites on the cable.

Useful commands

sudo systemctl restart mmdvmhost        # clear a stuck modem
sudo systemctl status mmdvmhost         # PID / uptime - is it the same process?
journalctl -u mmdvmhost -f              # live MMDVMHost journal only
ls /var/log/pi-star/MMDVM-*.log         # MMDVMHost's own detailed log
vcgencmd get_throttled                  # power / throttling flags
watch -n 1 vcgencmd get_throttled       # catch under-voltage live
systemctl status hciuart                # should be disabled (UART free for modem)

Lessons learned

  • Carrier and S-meter response are not the same as a successful DMR decode. Verify the modulation.
  • Guessing DMRTXLevel and TXOffset isn't calibration. Measure with an SDR.
  • A clean get_throttled at one moment doesn't rule out power; the "since boot" bits and a live watch tell the real story.
  • The cable matters as much as the power supply. A proper 3 A-rated cable fixed what a 3.5 A supply couldn't.
  • Don't bypass the Pi's power protection when a better cable will do.
  • Don't trust forum diagnoses of LED patterns without checking the firmware's actual behaviour.

Next steps

  • Fix the WPSD Services Watchdog so a stuck modem restarts itself
  • Track down the cause of the remaining intermittent lock-ups
  • Get an RTL-SDR and calibrate TX frequency and deviation
  • Keep watching get_throttled to confirm it stays 0x0 under load