Skip to content

DMR Reflector Protocol

DMR is Vexillum’s implementation of a Homebrew/HBP-style DMR repeater protocol. A client (typically a hotspot, repeater controller, or gateway) logs in, authenticates with a shared password, sends a configuration block, and then exchanges voice/data frames scoped to a talkgroup and a module.

Field Value
Transport UDP only
Default listen port 62031
Authentication Required - SHA-256 challenge/response
Default talkgroup 7
Module command base 64000

A client should handle:

  • RPTL / RPTACK / RPTK login and authentication handshake
  • RPTC configuration submission
  • Optional RPTO startup module selection
  • RPTPING / MSTPONG keepalive
  • DMRD voice/data frames, gated to slot 2, a specific talkgroup, and a module
  • Optional DMRA talker alias and DMRG GPS position sidecar frames
  • RPTCL explicit logoff

Vexillum’s DMR reflector organizes traffic into modules (channels), each named by a single letter. A connected repeater is assigned a module either by its RPTO startup option or by sending a private call to a virtual “module switch” DMR ID (see Module switching via private calls). Group voice traffic is only forwarded within the same module, on slot 2, on the configured talkgroup.

Vexillum’s DMR authentication follows the classic Homebrew challenge/response shape: a random seed, a SHA-256 digest over seed || password, and constant-time comparison.

  1. On RPTL, the reflector generates a 4-byte seed (an internal counter, incremented per session, offset by the constant 0x9E3779B9) and sends it back in RPTACK.
  2. The client computes SHA256(seed || password) and sends the 32-byte digest in RPTK.
  3. The reflector recomputes the same digest and compares it to the client’s digest using a constant-time comparison. The challenge must be presented within 20 seconds of being issued or it is rejected.
  4. On success, the reflector marks the session authenticated and sends RPTACK again, this time carrying the repeater ID instead of a seed.

Any mismatch - wrong repeater ID, expired challenge, or wrong digest - gets MSTNAK instead.

All integer fields are big-endian. Callsign and text fields are ASCII, space-padded.

Tag Length Layout Purpose
RPTL 8 [0:4] tag RPTL, [4:8] repeater ID (u32) Start login
RPTACK (challenge) 10 [0:6] tag RPTACK, [6:10] 4-byte seed Challenge seed
RPTK 40 [0:4] tag RPTK, [4:8] repeater ID (u32), [8:40] SHA-256 digest Authentication response
RPTACK (auth ok) 10 [0:6] tag RPTACK, [6:10] repeater ID (u32) Authentication confirmed
MSTNAK 6 Literal ASCII MSTNAK Reject
Tag Length Layout Purpose
RPTC 302 See RPTC configuration block below Repeater configuration
RPTO ≥ 8 [0:4] tag RPTO, [4:8] repeater ID (u32), [8:] free-text options Startup module selection
RPTPING 11 [0:7] tag RPTPING, [7:11] repeater ID (u32) Keepalive
MSTPONG 11 [0:7] tag MSTPONG, [7:11] repeater ID (u32) Keepalive reply
RPTCL 9 or 13 [0:5] tag RPTCL, [5:9] repeater ID (u32); a longer 13-byte variant is also accepted, with its trailing 4 bytes unused Logoff
Tag Length Layout Purpose
DMRD 55 [0:4] tag DMRD, [4] reserved (unused by this implementation), [5:8] source ID (u24), [8:11] destination ID (u24), [11:15] repeater ID (u32), [15] flags, [16:55] DMR payload Voice/data frame
DMRA 15 [0:4] tag DMRA, [4:7] source ID (u24), [7] alias type, [8:15] 7-byte alias chunk Talker alias sidecar
DMRG 14 [0:4] tag DMRG, [4:7] source ID (u24), [7:14] 7-byte position chunk GPS position sidecar
Bits Mask Meaning
7 0x80 Slot 2 selector - must be set; this reflector ignores slot 1 frames entirely
6 0x40 Call type: 1 = private call, 0 = group call
5–4 0x30 Frame type (2 = voice LC terminator, used for end-of-stream detection)
3–0 0x0F Burst/voice slot type

A terminator frame - the signal that ends a transmission - has frame type 2 and burst/slot value 2, which packs into a single flags byte value of 0xA2 (1010 0010: slot2 set, group call, frame type 2, burst 2).

RPTC is always exactly 302 bytes. Text fields are ASCII and are trimmed of non-printable bytes and surrounding whitespace when parsed; unused space within a field should be padded with spaces.

Offset Bytes Field Notes
0 4 Tag ASCII RPTC
4 4 Repeater ID Unsigned 32-bit integer
8 8 Callsign Uppercased, letters/digits///- only
16 9 RX frequency ASCII text, e.g. 438800000
25 9 TX frequency ASCII text, e.g. 431200000
34 2 Power ASCII text, e.g. 25
36 2 Color code ASCII text, e.g. 01
38 8 Latitude ASCII text, e.g. 40.7128
46 9 Longitude ASCII text, e.g. -74.0060
55 3 Reserved Not parsed by this implementation
58 20 Site name ASCII text
78 19 Description ASCII text
97 1 Slots 1=Slot 1 only, 2=Slot 2 only, 3=Duplex, 4=Simplex/DMO
98 124 URL ASCII text
222 40 Software ID ASCII text, e.g. MMDVM
262 40 Package ID ASCII text, e.g. MMDVMHost

RPTC is only accepted after successful authentication. Its repeater ID must match the ID established during login.

Parameter Value
Challenge TTL 20 seconds
Session timeout 120 seconds
Cleanup tick interval 10 seconds
Stream idle timeout 3 seconds
Session cap 4096

Follows the general session/stream lifecycle described in the reference overview. A session must complete both authentication (RPTL/RPTK) and configuration (RPTC) before any DMRD, DMRA, or DMRG frame is accepted from it - an authenticated-but-unconfigured session is otherwise fully tracked and can still RPTPING.

A DMRD frame is only forwarded if all of the following hold:

  • The session is authenticated and configured.
  • The flags byte has the slot-2 bit set (0x80). Slot 1 traffic is dropped unconditionally by this implementation.
  • The call type is a group call (flags bit 0x40 clear).
  • The destination ID equals the reflector’s configured talkgroup (default 7).

When forwarded, the destination ID and repeater ID fields are rewritten per recipient: the destination ID is rewritten to the reflector’s configured talkgroup, and the repeater ID is rewritten to each recipient’s own repeater ID. The source ID and voice payload are left untouched.

Private calls (flags bit 0x40 set) never carry voice; they are reserved entirely for the module-switching control-plane trick described next.

This is a Vexillum-specific extension, not part of any external Homebrew specification. When UserControl is enabled (the default), a private call’s destination ID is interpreted as a command rather than a talkgroup:

Destination ID Effect
ModuleCommandBase (default 64000) Unlink - clears the session’s current module
ModuleCommandBase + 1ModuleCommandBase + 26 Switch to module AZ, e.g. 64001 → module A, 64026 → module Z
StatusCommand (default 65000) Reserved; currently accepted and silently ignored

A repeater’s active module can also be set directly via the RPTO startup option, using either startup=<letter>, module=<letter>, module=<numeric ID> (interpreted the same way as a private-call module-switch ID), or a bare single letter.

Client                               Server
  |                                    |
  | RPTL                               |
  | repeaterID                         |
  |----------------------------------->|
  |                                    |
  | RPTACK                             |
  | seed                               |
  |<-----------------------------------|
  |                                    |
  | RPTK                               |
  | repeaterID, sha256(seed||password) |
  |----------------------------------->|
  |                                    |
  | RPTACK                             |
  | repeaterID                         |
  |<-----------------------------------|
  |                                    |
  | RPTC                               |
  | 302-byte configuration block       |
  |----------------------------------->|
  |                                    |
  | RPTACK                             |
  | repeaterID                         |
  |<-----------------------------------|

Any step that fails validation gets MSTNAK in place of RPTACK, and the client should restart from RPTL.

Client       Server
  |            |
  | RPTPING    |
  | repeaterID |
  |----------->|
  |            |
  | MSTPONG    |
  | repeaterID |
  |<-----------|

RPTPING requires an existing session whose repeater ID matches; a mismatch gets MSTNAK.

Send a final DMRD frame with the flags byte set to 0xA2 (slot 2, group call, frame type 2, burst 2). The reflector releases the module’s active stream lock immediately on seeing this terminator, allowing the next sender on that module to start without waiting for the 3-second idle timeout.

DISCONNECTED
  |
  v
LOGGING_IN (sent RPTL)
  |
  +-- MSTNAK --> DISCONNECTED
  |
  +-- RPTACK (seed) --> AUTHENTICATING (sent RPTK)
                          |
                          +-- MSTNAK --> DISCONNECTED
                          |
                          +-- RPTACK (peerID) --> CONFIGURING (sent RPTC)
                                                    |
                                                    +-- MSTNAK --> DISCONNECTED
                                                    |
                                                    +-- RPTACK --> READY
                                                                    |
                                                                    +-- PTT active --> TRANSMITTING
                                                                    |
                                                                    +-- incoming DMRD --> RECEIVING

Terminal conditions: transport failure, session timeout (120 seconds without traffic), inbound RPTCL acknowledgment, or user shutdown.

This reflector silently drops every DMRD frame that doesn’t have the 0x80 slot-2 bit set in its flags byte - there is no slot 1 support at all. A client built against real slot 1/slot 2 dual-slot hardware must map its “active” traffic onto slot 2 for this reflector, or it will see nothing forwarded and no error.

RPTC is rejected with MSTNAK if the session hasn’t completed the RPTK challenge yet, even if the repeater ID is correct.

A RPTK response sent more than 20 seconds after RPTL’s RPTACK is rejected regardless of whether the digest is correct. Compute and send the digest immediately.

Private calls never forward voice on this reflector. Sending a private-call DMRD frame expecting one-to-one voice routing will only work if the destination ID happens to match a module-switch or unlink command ID, and even then no voice is exchanged.

Assuming the RPTC reserved gap is available

Section titled “Assuming the RPTC reserved gap is available”

Bytes 55-57 of the RPTC block are not parsed by this implementation. Don’t rely on the reflector reading anything placed there.

An RPTL login for repeater ID 312000:

Field Value
Tag RPTL
Repeater ID 312000

A DMRD group-call voice frame on slot 2, source 123456, destination (talkgroup) 7, repeater 1001:

Field Value
Tag DMRD
Source ID 123456
Destination ID 7
Repeater ID 1001
Flags 0x80 (slot 2, group call, mid-stream)

The same stream’s terminator frame:

Field Value
Flags 0xA2 (slot 2, group call, frame type 2, burst 2)

DMR is implemented as a Vexillum mode runtime (internal/modes/dmr). The login/authentication/configuration handshake and packet tag names follow the conventions of the Homebrew/HBP-family DMR repeater protocol; the module-switching-via-private-call mechanism, session timeouts, and session caps described above are Vexillum-specific.

  • Slot 1 is not supported; only slot-2 group-call traffic is forwarded.
  • One shared password for all repeaters connecting to an instance - no per-repeater credentials.
  • One active voice stream per module at a time.
  • DMRA/DMRG sidecar data updates session metadata but triggers no reply packet.
  • The StatusCommand private-call ID is accepted but currently has no effect.
  1. Implement RPTL / RPTACK / RPTK and verify the SHA-256 digest against a known seed and password.
  2. Add RPTC and confirm the 302-byte field layout round-trips.
  3. Add RPTPING / MSTPONG.
  4. Add DMRD with the slot-2 flag set, targeting the configured talkgroup and module.
  5. Add the terminator frame (0xA2) and confirm the next sender is admitted immediately afterward.
  6. Add RPTCL logoff.
  7. If needed, add RPTO startup module selection and the module-switch private-call commands.
  8. Test against a running Vexillum DMR instance with the actual configured password before assuming compatibility with any other Homebrew-protocol implementation.