This page describes the current IPSC3-POC wire protocol so a client can be
implemented independently from the server. The current server binary is
ipsc-poc.
For an operator-facing example of the protocol in use, including the current Retevis L71 Android radio profile, see IPSC3 POC Client.
| Plane | Transport | Payload | Purpose |
|---|---|---|---|
| Control | Persistent TCP connection, one JSON object per line | UTF-8 JSON | Login, keepalive, talkgroup selection, group/private PTT signalling, SMS, presence, GPS, status, and config updates. |
| Audio | UDP/RTP | PCMU, RTP payload type 0, 8 kHz mono | Real-time half-duplex voice audio. |
Native TLS and WebSocket transport are planned. Until native TLS is added, the TCP control service should be exposed through a TLS-capable proxy if used on the public internet.
login with its Radio ID, persistent client_instance_id, client_type, and exact version.auth_challenge.auth_response.login_ok, a complete master-backed server_list, a complete talkgroup_list catalogue, and config_update.ping or keepalive.ptt_grant.ptt_release when PTT is released.type field.error response.radio_id is the only client-supplied user identity. The server resolves callsign and name from its RadioID directory.client_instance_id distinguishes physical clients sharing one Radio ID. It is a persistent routing identifier, not a password or user identity.callsign or name fields are ignored for backward compatibility and must not be treated as authoritative.client_type identifies an authorized binary family. version identifies an explicitly authorized release in that family.
Protocol ipsc3-poc/0.4 uses
ipsc3-poc-auth/1, a one-time HMAC-SHA256 challenge during every
control connection. Authentication occurs only at login; no HMAC or key
lookup is performed in the RTP audio path. The server creates no live user,
roaming, presence, PTT, SMS, GPS, or media state until the proof is valid.
Each client family has one random 32-byte key. A server-side allowlist also
records the exact versions permitted for that family. Similar device models
may share a client_type when they use the same controlled binary.
The current test family is ipsc3-poc-android-armv7. Keys are
encoded as RFC 4648 base64url without = padding.
Server challenge:
{
"type": "auth_challenge",
"auth_protocol": "ipsc3-poc-auth/1",
"algorithm": "HMAC-SHA256",
"radio_id": 5052540,
"client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"client_type": "ipsc3-poc-android-armv7",
"version": "0.3.7",
"auth_session_id": "YxKzv1x2zFQ1V2m-random-id",
"nonce": "Kf6h6W2B-random-32-byte-value",
"expires_in_ms": 15000
}
Decode the provisioned client key from base64url, then calculate HMAC-SHA256
over these exact UTF-8 bytes, with a single line-feed byte
(0x0a) between fields and no trailing line feed:
ipsc3-poc-auth/1
AUTH_SESSION_ID
NONCE
RADIO_ID
CLIENT_TYPE
VERSIONFor the example identity, the canonical byte string is:
ipsc3-poc-auth/1
YxKzv1x2zFQ1V2m-random-id
Kf6h6W2B-random-32-byte-value
5052540
ipsc3-poc-android-armv7
0.3.7
RADIO_ID is ordinary unsigned decimal with no leading zeroes.
Use the values echoed by the challenge for every other field. Encode the raw
32-byte HMAC digest as base64url without padding; do not hex-encode it and do
not HMAC serialized JSON.
Client response:
{
"type": "auth_response",
"radio_id": 5052540,
"client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"client_type": "ipsc3-poc-android-armv7",
"version": "0.3.7",
"auth_session_id": "YxKzv1x2zFQ1V2m-random-id",
"proof": "base64url-hmac-without-padding"
}Language-neutral implementation:
key = BASE64URL_DECODE_NO_PADDING(provisioned_key)
canonical = UTF8(
"ipsc3-poc-auth/1" + LF +
challenge.auth_session_id + LF +
challenge.nonce + LF +
DECIMAL(login.radio_id) + LF +
login.client_type + LF +
login.version
)
digest = HMAC_SHA256(key, canonical)
proof = BASE64URL_ENCODE_NO_PADDING(digest)Interoperability test vector:
| Key bytes | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f |
|---|---|
| Key, base64url | AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8 |
| Authentication session | test-session-001 |
| Nonce | test-nonce-002 |
| Radio ID | 5052540 |
| Client type | ipsc3-poc-android-armv7 |
| Version | 0.3.7 |
| Expected proof | gVYcStwCApkdDDk2gobfzPDiKLOhGTQMpZdXPX-BYbI |
The vector key is public test data only and must never be configured on a server or embedded in a release client.
In Qt 6, use QMessageAuthenticationCode with
QCryptographicHash::Sha256. Decode and encode with
QByteArray::Base64UrlEncoding, adding
QByteArray::OmitTrailingEquals when encoding the proof. Build the
canonical value as a QByteArray; do not pass it through locale or
rich-text conversion.
auth_response, successful or not.client_instance_id must exactly match the value echoed in the challenge. It does not alter the ipsc3-poc-auth/1 canonical HMAC bytes because it is a routing identifier rather than a credential.login.Client to server:
{
"type": "login",
"radio_id": 5052540,
"client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"password": "future-use",
"client_type": "ipsc3-poc-android-armv7",
"client": "IPSC3-POC-Android",
"version": "0.3.7",
"platform": "android",
"android_version": "15",
"device_model": "L71",
"manufacturer": "Example Mobile",
"app_package": "net.ipsc3.poc",
"screen": "128x160",
"battery_percent": 73,
"capabilities": {
"rtp_recovery": {
"version": 1,
"uplink_nack": true,
"downlink_nack": true,
"final_sequence": true
}
},
"options": "ScanInclude=505,3801;ScanExclude=8409;TGInclude=505,777,3801,9990;TGExclude=50524"
}
client_type, version, and radio_id are
required for authenticated login. New clients must also send
client_instance_id. Generate it once with a cryptographically
secure random UUID and retain it across normal restarts and upgrades. It must
contain 16 to 64 ASCII letters, digits, hyphens, or underscores. Do not
regenerate it on every connection. The remaining client and device metadata
fields are optional and additive. The server bounds accepted values to 64
characters for client, client_type, and
version, 32 characters for
platform, android_version, and screen,
128 characters for device_model and manufacturer,
and 160 characters for app_package.
battery_percent is an optional integer from 0 to 100 representing
the battery charge remaining when the client logs in. Missing, non-integer,
or out-of-range values are ignored and do not reject the login.
capabilities.rtp_recovery is optional. Version 1 clients may
independently advertise uplink NACK handling, downlink NACK generation, and
final RTP sequence reporting. Omitting this object preserves the original
protocol exactly: the server sends no recovery NACKs, retains no downlink
retransmission cache for that client, and completes PTT release immediately.
options is an optional, semicolon-delimited ASCII string using
the same command style as IPSC3 hotspot options. Option names are
case-insensitive and talkgroup lists are comma-delimited. The preferred wire
representation is a string. For setup tools that already wrap values, the
server also accepts an object containing a text or
options_text string. The field is limited to 1024 characters and
each option is limited to 256 unique, valid 24-bit talkgroup IDs.
| Option | Scope | Behaviour |
|---|---|---|
ScanInclude=505,3801 | Scan only | While this client instance is in scan mode, only listed talkgroups may be followed. An omitted or empty list allows all otherwise eligible scan talkgroups. |
ScanExclude=8409,50524 | Scan only | Prevents listed talkgroups from being followed in scan mode. If a talkgroup appears in both scan lists, ScanExclude wins. |
TGInclude=505,777,3801,9990 | Complete POC client | Acts as an allowlist. Only listed, server-known talkgroups are announced, selectable, transmit-capable, or delivered as group call signalling and RTP audio to this client instance. |
TGExclude=8409,50524 | Complete POC client | Acts as a denylist. Listed talkgroups are omitted from the catalogue and cannot be selected, transmitted on, followed in scan, or delivered as group call signalling and RTP audio to this client instance. |
client_instance_id, not globally to every radio using the same Radio ID.TGExclude takes precedence over TGInclude. A talkgroup present in both lists is excluded.TGInclude, not appear in TGExclude, pass ScanInclude, and finally not appear in ScanExclude.TGInclude and TGExclude policy leaves no server-known talkgroup available, login is rejected rather than silently granting unrestricted access.login_ok.options.ignored.options retain the original unrestricted catalogue and scan behaviour.
password remains reserved for future per-user authentication. It must be
transported only on the protected control connection and is never retained,
logged, included in presence announcements, or exposed by the dashboard.
Server to client:
{
"type": "login_ok",
"protocol": "ipsc3-poc/0.4",
"server_id": "vkdmr-poc-dev",
"radio_id": 5052540,
"client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"multi_instance": {
"enabled": true,
"instance_count": 2,
"max_instances": 8,
"legacy_single_instance": false
},
"callsign": "VK2FLY",
"name": "Matthew",
"session_id": 123456789012345,
"call_talkgroup": 505,
"default_talkgroup": 505,
"selected_talkgroup": 505,
"options": {
"format": "semicolon_v1",
"scan_include": [505, 3801],
"scan_exclude": [8409],
"tg_include": [505, 777, 3801, 9990],
"tg_exclude": [50524],
"ignored": []
},
"authentication": {
"required": true,
"verified": true,
"auth_protocol": "ipsc3-poc-auth/1",
"client_type": "ipsc3-poc-android-armv7"
},
"audio": {
"transport": "udp_rtp",
"codec": "PCMU",
"payload_type": 0,
"sample_rate_hz": 8000,
"channels": 1,
"ptime_ms": 20,
"rtp_host": "ipsc3.example.net",
"rtp_port": 50702,
"ssrc": 1435003188,
"receive_registration": {
"method": "rtp_marker_probe",
"marker": true,
"payload": "PCMU silence",
"payload_bytes": 160,
"keepalive_seconds": 20,
"keep_udp_socket_open": true,
"control_fallback": "rtp_ready"
},
"recovery": {
"version": 1,
"enabled": true,
"uplink_nack": true,
"downlink_nack": true,
"final_sequence": true,
"max_retries": 1,
"normal_jitter_ms": 350,
"degraded_jitter_ms": 700,
"retransmit_cache_ms": 2000,
"max_nack_packets": 33
}
}
}
Callsign and name in login_ok are trusted server results for the
supplied Radio ID. If the Radio ID is not present in the directory, both
fields are null and the Radio ID remains the canonical display
and routing identity.
The client must use login_ok.audio.ssrc for every RTP packet it
sends, including registration probes. The server assigns a different SSRC to
each connected instance so two radios with the same Radio ID can share one
server and one NAT address safely. The assignment is valid only for the
current login session and must not be cached. Older clients that omit
client_instance_id retain legacy single-instance replacement
behaviour and receive their Radio ID as the SSRC. Assigned values are in the
positive signed 32-bit range for compatibility with clients that parse JSON
numbers through a signed integer API.
The login_ok.options object is the server's normalized view of
the policy applied to this session. A null
tg_include means no overall talkgroup allowlist. An empty
tg_exclude means no overall denylist. Existing clients
remain compatible because this is an additive field and receivers already
ignore unknown fields. The authentication challenge and HMAC canonical input
are unchanged.
radio_id remains the callsign lookup, roaming, and DMR routing identity shared by all instances.client_instance_id identifies one installed client. Reconnecting the same instance replaces only its previous connection.
After a successful login, the POC server sends the current IPSC3 server
directory obtained from https://api.ipsc3.net/servers.json. The
directory is refreshed by a background worker and never fetched in the PTT or
RTP path. A changed directory may be broadcast to connected clients.
{
"type": "server_list",
"catalog_version": 1,
"complete": true,
"source": "https://api.ipsc3.net/servers.json",
"generated_at_epoch": 1784344158,
"current_server_id": "ipsc3-vkdmr",
"servers": [
{
"id": "ipsc3-vkdmr",
"hostname": "ipsc3.vkdmr.com",
"callsign_prefix": "VK",
"country": "AU",
"status": "ok",
"ipsc3_version": "1.0.35",
"poc": [
{
"address": "ipsc3.vkdmr.com",
"control_port": 50700,
"transport": "tcp",
"primary": true
}
]
},
{
"id": "ipsc3-zl",
"hostname": "zl.ipsc3.net",
"callsign_prefix": "ZL",
"country": "NZ",
"status": "ok",
"ipsc3_version": "1.0.35",
"poc": [
{
"address": "zl.ipsc3.net",
"control_port": 50700,
"transport": "tcp",
"primary": true
}
]
}
]
}complete is true.complete is false, the master directory was unavailable during startup and the client should retain its previously stored catalogue.current_server_id identifies the server handling the current control session and may be null if the local hostname is not present in the master directory.id is the stable selection key; clients should display the callsign prefix and hostname.primary: true is preferred. The client connects its persistent control socket to address:control_port.login_ok; clients must not infer the RTP endpoint from this directory.config_check request returns both a fresh config_update and the currently cached server_list.Client to server:
{ "type": "ping", "client_time_ms": 1782930000000 }Server to client:
{ "type": "pong", "server_time_ms": 1782930000123 }Server to client:
{
"type": "talkgroup_list",
"catalog_version": 1,
"complete": true,
"filtered": false,
"call_talkgroup": 505,
"default_talkgroup": 505,
"talkgroups": [
{
"id": 505,
"name": "VKDMR Call Channel",
"timeslot": 2,
"dmr_linked": true,
"allow_poc_tx": true,
"allow_poc_rx": true,
"parrot": false,
"call_default": true,
"active_speaker": null
}
]
}The server sends this message immediately after every successful login and may send a replacement when runtime special services change. It contains the complete set of talkgroups that the client may present for selection, including provisioned IPSC3 voice talkgroups and configured POC special groups. In this message, complete means configured and available for selection, not currently carrying audio.
complete is true; it must not append the entries to a stale list.catalog_version versions the catalogue schema, independently of the main POC protocol version.filtered is true when this client supplied TGInclude or at least one valid TGExclude. A complete filtered catalogue intentionally omits all denied or non-included talkgroups.call_talkgroup is the single static talkgroup selected by the server operator as the POC call channel and normal login default. It is server-specific and must never be hard-coded to TG505 by a client.default_talkgroup is the talkgroup selected by the server for a new login and matches login_ok.selected_talkgroup.TGInclude/TGExclude options deny the server call channel, call_talkgroup still identifies the server call channel while default_talkgroup is the first permitted talkgroup.id is unique within the catalogue and is the value used by select_talkgroup and ptt_request.timeslot is the authoritative DMR timeslot used by the server bridge.allow_poc_tx and allow_poc_rx are authoritative client capabilities. A client should disable unavailable actions in its UI.parrot identifies the server-configured parrot service. It is normally false and clients may display a service-specific icon when it is true.call_default is true only on the operator-selected call/default talkgroup.active_speaker is a transient status value and may be null.IPSC3 administrators select the call channel with the POC Call/Default checkbox on a static talkgroup. Exactly one talkgroup is selected per server. For example, VKDMR uses TG505, the UK server uses TG235, and the ZL server uses TG530. Clients must consume the announced values so the same binary works correctly on every IPSC3 server.
Client to server:
{ "type": "select_talkgroup", "talkgroup": 505 }Server to client:
{ "type": "talkgroup_selected", "talkgroup": 505 }
Selection supports late entry. If a DMR transmission is already active on
the selected talkgroup, an RTP-qualified client receives a targeted
call_start and marked RTP audio without waiting for that
transmission to finish. The same behaviour applies when selecting the scan
talkgroup while an eligible stream is active. Clients should start or reset
receive playout on either the targeted call_start or the RTP marker
and must not wait for a new RF transmission header.
IPSC3 maintains presence, liveness, roaming, talkgroup affiliation, and
routing state centrally. POC clients do not exchange peer addresses and the
server does not send a client roster or presence_list. Group
calls, private calls, and SMS are always addressed to the server, which can
route the destination across POC, repeaters, hotspots, and supported trunks.
A valid control ping, RTP registration keepalive, GPS report,
PTT action, or other authenticated activity refreshes the relevant
server-side liveness state. Older clients may also send the following
message; it is accepted as a liveness refresh but produces no presence
digest:
{ "type": "presence", "status": "online" }
A live idle measurement on 23 July 2026 used one authenticated
0.3.8 Android radio, no voice activity, the current 15-second
control keepalive, RTP registration probes, and stationary GPS reporting
approximately every ten minutes. The figures below are per connected radio
and include IP, TCP, UDP, and RTP headers observed at the server.
| Traffic | Approximate usage |
|---|---|
| Control ping/pong and legacy liveness message | 2.95 MB/day |
| RTP/NAT registration keepalive | 0.69 MB/day at the observed 25-second cadence |
| Stationary GPS reporting | 0.1 to 0.2 MB/day |
| Total idle traffic | Approximately 3.8 MB/day or 115 MB per 30 days |
Allow approximately 150 MB per radio per month for normal idle operation, reconnections, occasional catalogue/configuration refreshes, and mobile-network retransmissions. This estimate excludes application downloads, operating-system traffic, and active voice. A protocol-compliant 20-second RTP registration cadence uses about 0.86 MB/day instead of the observed 0.69 MB/day.
Active PCMU audio consumes approximately 0.6 MB per minute for each minute transmitted or received. Moving radios may report location more frequently, but voice normally remains the dominant additional traffic.
The server does not broadcast a POC user digest. Removing the former complete
presence_list prevents idle traffic from growing with the square
of the connected POC client count. Server-side routing and roaming still
retain all required session state.
This section describes a group call. Clients may include
"call_type": "group"; omitting it retains the original group-call
behavior.
Client to server:
{ "type": "ptt_request", "talkgroup": 505 }Server grants if no other user is currently speaking on that talkgroup:
{
"type": "ptt_grant",
"talkgroup": 505,
"stream_id": 123456789,
"scan": "disabled",
"routed_talkgroup": 505,
"routed_timeslot": 2,
"audio": {
"transport": "udp_rtp",
"codec": "PCMU",
"payload_type": 0,
"sample_rate_hz": 8000,
"channels": 1,
"ptime_ms": 20,
"ssrc": 2435003188,
"recovery": {
"enabled": true,
"mode": "normal",
"jitter_ms": 350
}
}
}Server denies if the talkgroup is busy or not available:
{
"type": "ptt_deny",
"talkgroup": 505,
"reason": "busy",
"active_speaker": 5052562
}Client to server when the user releases PTT:
{
"type": "ptt_release",
"final_rtp_sequence": 4217
}
final_rtp_sequence is optional and is used only when final-sequence
recovery was negotiated. Legacy clients continue to send the original
{ "type": "ptt_release" } message and receive the original
immediate release behaviour.
Server to client:
{
"type": "ptt_release",
"call_type": "group",
"talkgroup": 505,
"routed_talkgroup": 505,
"routed_timeslot": 2,
"stream_id": 123456789,
"reason": "client_release",
"duration_ms": 2420,
"rtp_packets": 121,
"sequence_gaps": 0,
"rtp_recovery": {
"mode": "normal",
"received_unique": 121,
"missing_detected": 1,
"recovered": 1,
"unrecovered": 0,
"nack_messages": 1
}
}A private call is addressed to one destination Radio ID. IPSC3 resolves the destination through the same current roaming table used by DMR private calls. The destination may be a POC, Motorola/IPSC, or MMDVM radio. A current POC route requires its UDP presence lease and registered RTP receive endpoint. The destination's selected group talkgroup is not changed or consulted.
Preferred client request:
{
"type": "ptt_request",
"call_type": "private",
"destination_radio_id": 5052562,
"timeslot": 1
}
private_call_request and direct_call_request are
accepted aliases. target_radio_id and destination are
accepted aliases for destination_radio_id. New clients should use
the preferred fields above. Timeslot defaults to 1 when omitted.
Initial server grant:
{
"type": "ptt_grant",
"call_type": "private",
"destination_radio_id": 5052562,
"timeslot": 1,
"stream_id": 123456789,
"audio": {
"transport": "udp_rtp",
"codec": "PCMU",
"payload_type": 0,
"sample_rate_hz": 8000,
"channels": 1,
"ptime_ms": 20,
"ssrc": 2435003188
}
}
The grant permits the client to begin normal PCMU RTP. The local gateway then
confirms the roaming route with the IPSC3 core. If that second-stage route is
unavailable, or the target POC radio is busy, the client receives a
ptt_deny for the same stream and must stop transmitting.
{
"type": "ptt_deny",
"call_type": "private",
"destination_radio_id": 5052562,
"stream_id": 123456789,
"reason": "private_route_unavailable"
}
Release uses the normal { "type": "ptt_release" } request. The
matching response includes call_type: "private",
destination_radio_id, timeslot, stream ID, duration, and RTP
counters. Disconnect and RTP-idle cleanup also release the private stream.
Incoming private voice is delivered to every available client instance of
the destination Radio ID. The
associated call_start and call_end messages have
call_type: "private" and target_radio_id equal to that
destination. RTP SSRC received by clients remains the caller's Radio ID.
POC-to-POC private calls
deliberately traverse the IPSC3 roaming and DMR media boundary rather than
using local group fanout, so their behavior matches cross-network calls.
POC SMS is bidirectional and uses the existing IPSC3 durable store-and-forward service. A POC sender can address a POC or DMR radio, and DMR-origin SMS can be delivered to a current POC roaming destination. Database and retry work runs outside the RTP and DMR packet loops.
Destination Radio ID 9998 is the IPSC3 AI SMS assistant. A POC
SMS sent to 9998 bypasses roaming and ordinary store-and-forward,
enters the same AI history and network-context worker used by DMR handsets,
and returns a normal sms_message from source Radio ID
9998. AI and database work remains on dedicated workers outside
the POC media, RTP, MMDVM, and IPSC voice loops.
Client submission:
{
"type": "sms_send",
"client_message_id": 1042,
"destination_radio_id": 5052562,
"timeslot": 2,
"text": "Testing from POC"
}text_message_send is an accepted type alias.target_radio_id and destination are accepted destination aliases.message is accepted as an alias for text.client_message_id is optional; the server generates one if omitted.The POC daemon first confirms that the request entered its bounded gateway queue:
{
"type": "sms_accepted",
"client_message_id": 1042,
"destination_radio_id": 5052562,
"timeslot": 2,
"status": "queued"
}
The IPSC3 SMS worker then returns durable relay state. The remote Radio ID is
supplied as source_radio_id because the status is sent back from
that destination toward the original sender.
{
"type": "sms_status",
"client_message_id": 1042,
"source_radio_id": 5052562,
"destination_radio_id": 5052540,
"target_client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_scope": "all_instances",
"timeslot": 2,
"detail": {
"status": "queued",
"relay_id": 387,
"message": null
}
}
Known status values are queued, duplicate,
delivered, failed, and notice. A notice
carries explanatory text such as deferred or expired delivery. Retry and
expiry timing use the server's SMS configuration; messages remain durable
while a destination is absent from roaming.
Example AI response to the originating POC handset:
{
"type": "sms_message",
"message_id": 2684354947,
"source_radio_id": 9998,
"destination_radio_id": 5052540,
"target_client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_scope": "all_instances",
"timeslot": 2,
"text": "VK2ABC was last heard at 14:32 via VK2RCG.",
"requires_ack": true
}sms_send message and destination_radio_id: 9998.message_id is opaque and must be preserved as an unsigned 32-bit value.sms_status with detail.status: "failed" and explanatory text.Delivery to a POC destination:
{
"type": "sms_message",
"message_id": 387,
"source_radio_id": 5052540,
"destination_radio_id": 5052562,
"target_client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_scope": "all_instances",
"timeslot": 2,
"text": "From:VK2FLY Time:12:34 Testing from POC",
"requires_ack": true
}
The client must preserve the complete 32-bit message_id and
acknowledge after it has accepted the message for display or local storage:
{
"type": "sms_ack",
"message_id": 387,
"source_radio_id": 5052540,
"timeslot": 2
}Local acknowledgement acceptance:
{ "type": "sms_acknowledged", "message_id": 387 }
text_message_ack is an accepted alias. The backend matches POC
acknowledgements against the exact durable relay ID plus both Radio IDs, so
simultaneous messages cannot acknowledge the wrong record. The original
sender receives a later sms_status with
detail.status: "delivered". Delivered text may exceed 128
characters because IPSC3 can prepend sender and timestamp information. The
same delivery is sent to every connected destination instance; the first
valid acknowledgement completes it and duplicate instance acknowledgements
are suppressed before reaching the core.
An authenticated dashboard user can select a live POC Radio ID on the roaming
page and send an SMS. Dashboard-originated SMS uses the normal durable
store-and-forward path and arrives as the standard sms_message
described above. The client must acknowledge it normally. No additional SMS
message type is required.
Operators and administrators can also send a call alert, disable, or enable command to a live POC session. These commands use the authenticated persistent control connection and never enter the RTP audio path. Server to client:
{
"type": "device_command",
"command_id": 4812,
"source_radio_id": 9998,
"destination_radio_id": 5052540,
"target_client_instance_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_scope": "all_instances",
"command": "call_alert",
"requires_status": true
}Supported command values:
call_alert: present an audible and visible incoming alert without starting PTT.disable: enter an administratively disabled state and block user-originated PTT and other network operations.enable: clear the administratively disabled state and restore normal operation.
A disabled client must keep its authenticated control connection and
keepalives active so it can receive a later enable command. It
must persist the disabled state across application or device restarts. The
client should clearly show that it was disabled by a network operator.
The client reports command handling on the same control connection:
{
"type": "device_command_status",
"command_id": 4812,
"command": "call_alert",
"status": "completed",
"timeslot": 2,
"message": "alert displayed"
}command_id must be copied unchanged from the received command.command must be the received command value.status must be accepted, completed, failed, or unsupported.timeslot is optional, defaults to 2, and must be 1 or 2.message is optional printable ASCII diagnostic text and is limited to 64 characters.The server confirms that it accepted the status report:
{
"type": "device_command_status_acknowledged",
"command_id": 4812
}
Old clients remain compatible because clients are required to ignore unknown
server message types. Such a client will ignore device_command;
the dashboard action will be queued, but no completion status will be
returned until the client implements this section.
login_ok.audio.ssrc.ptt_grant.Recovery version 1 is an additive, negotiated extension. It uses the existing authenticated control connection for compact NACK messages and the existing RTP socket for retransmitted packets. RTP remains PCMU at 20 ms per packet. There is no recovery traffic when no loss is detected.
| Direction | Detector | NACK sender | Retransmission sender |
|---|---|---|---|
| Uplink, client to server | Server | Server over control TCP | Client over its existing RTP socket |
| Downlink, server to client | Client | Client over control TCP | Server over its existing RTP socket |
Server NACK for missing client-to-server RTP:
{
"type": "rtp_nack",
"version": 1,
"direction": "uplink",
"stream_id": 123456789,
"base_sequence": 4100,
"bitmask": 5,
"retry": 1
}Client NACK for missing server-to-client RTP:
{
"type": "rtp_nack",
"version": 1,
"direction": "downlink",
"stream_id": 123456789,
"base_sequence": 9100,
"bitmask": 5,
"retry": 1
}base_sequence is always requested.bitmask requests base_sequence + 1, bit one requests base_sequence + 2, through bit 31 for base_sequence + 32. Sequence arithmetic wraps at 65536.stream_id must match the active call. Requests for another or expired stream are ignored or reported unavailable.
The server acknowledges a downlink request after it has queued available
packets to its bounded recovery worker. queued does not claim that
the UDP datagram reached the handset.
{
"type": "rtp_nack_result",
"version": 1,
"direction": "downlink",
"stream_id": 123456789,
"requested": 3,
"queued": 2,
"unavailable": 1,
"not_cached": 1,
"already_retransmitted": 0
}
Optional diagnostic fields not_cached and
already_retransmitted split the existing unavailable
total. The former includes packets never cached, expired or evicted; the
latter means the one-retry allowance was already consumed. These are packet
request counts, not a unique packet-loss percentage, and do not by themselves
locate network loss. Older clients may ignore these fields. Authorised media
attempts remain eligible for recovery even if the initial local UDP send
fails. Retries retain the original RTP packet; no additional retry or playout
delay is introduced.
ptt_grant.
A capable client includes the last RTP sequence it sent as
final_rtp_sequence in ptt_release. If packets through
that sequence are missing, the server NACKs them and returns:
{
"type": "ptt_release_pending",
"stream_id": 123456789,
"reason": "rtp_recovery",
"mode": "degraded",
"jitter_ms": 700
}
The client stops live RTP but keeps its cache available and services the NACK.
The server completes release as soon as the tail arrives, or after 250 ms in
normal mode and 600 ms in degraded mode. It then sends the ordinary
ptt_release response. This bounded wait prevents a lost final UDP
packet from truncating DMR audio without allowing a call to remain keyed
indefinitely.
Immediately after login, the client should send one RTP registration probe
from the persistent UDP socket it will use to receive audio. The probe is a
valid RTP version 2 packet with payload type 0, SSRC equal to the value in
login_ok.audio.ssrc,
the RTP marker bit set, and 160 bytes of PCMU silence (0xFF).
Send the probe to the rtp_host and rtp_port announced
in login_ok. Do not request PTT for this probe.
The server uses the probe's observed source IP and UDP port as the fanout destination. This is the preferred registration method because it establishes and identifies the correct NAT mapping. The client should keep the UDP socket open and repeat the silent marker probe every 20 seconds while logged in. This keeps the UDP NAT mapping active and lets the server learn a replacement mapping after a mobile network change.
Valid marker probes and voice RTP also maintain the radio's IPSC3 roaming lease. Control login or TCP keepalive alone does not qualify a POC radio for roaming. If no valid RTP is received for 90 seconds, the radio disappears from current roaming and radio-type statistics until the next valid UDP packet. Historical Last Heard records are retained.
A control-channel fallback is available for directly reachable clients. The server always derives the host from the logged-in control connection and ignores any client-supplied host value:
{
"type": "rtp_ready",
"port": 41000
}Server response:
{
"type": "status",
"code": "rtp_ready_ok",
"message": "RTP receive endpoint registered",
"host": "198.51.100.25",
"port": 41000
}allow_poc_rx enabled.talkgroup_status announces the active speaker and stream state on the control channel.
The scan trigger talkgroup and timeslot are supplied by IPSC3 at runtime and
are not hardcoded into a client. On the VKDMR instance this is normally
777 / TS1. Selecting the announced scan talkgroup arms scan for
that POC client instance. Scan state is independent for every connected POC client.
ScanInclude and ScanExclude options are applied before a stream is claimed. TGInclude and TGExclude are also enforced for scan delivery.call_start and call_end messages retain the real talkgroup and timeslot, even though the client remains selected on the scan talkgroup.talkgroup set to the selected scan talkgroup and supplies the real destination in routed_talkgroup and routed_timeslot.scan: "waiting" and null routed fields. Its RTP is not sent to DMR as talkgroup 777.{
"type": "ptt_grant",
"talkgroup": 777,
"stream_id": 123456789,
"scan": "talkback",
"routed_talkgroup": 505,
"routed_timeslot": 2,
"audio": {
"transport": "udp_rtp",
"codec": "PCMU",
"payload_type": 0,
"sample_rate_hz": 8000,
"channels": 1,
"ptime_ms": 20,
"ssrc": 2435003188
}
}
The server-configured IPSC3 parrot talkgroup is announced in the normal
talkgroup_list with parrot: true. The talkgroup number
and timeslot are not hardcoded in the POC protocol. A POC transmission on
this service is converted to AMBE, buffered by the IPSC3 parrot adapter, and
converted back to PCMU for the originating client after the configured
delay. It is not a local PCMU loopback.
call_start and call_end messages with target_radio_id set to the originating Radio ID.
When the server starts delivering audio to a registered POC listener, it
sends a call_start message on the control connection. The message
is used for caller display and applies to both DMR-origin and POC-origin
audio.
{
"type": "call_start",
"source": "dmr",
"radio_id": 5052540,
"target_radio_id": null,
"call_type": "group",
"callsign": "VK2FLY",
"name": "Matthew",
"talkgroup": 505,
"timeslot": 2,
"stream_id": 123456789,
"started_ms": 1782930000000,
"latitude": -33.874288,
"longitude": 151.220588,
"position_source": "caller",
"origin_callsign": "VK2RCG",
"origin_type": "repeater"
}source is dmr for DMR network audio and poc for another POC client.radio_id is authoritative and matches the SSRC in the associated RTP audio.target_radio_id is normally null. For a private call or targeted service response such as parrot, it contains the destination routing Radio ID; all eligible instances of that ID may receive it.call_type is group or private.callsign and name are resolved by the server from its RadioID directory. Either value may be null when the identity is unknown.talkgroup, timeslot, and stream_id identify the exact call.started_ms is the server's Unix epoch start time in milliseconds.latitude and longitude contain the caller's valid position when one has been received within the previous hour. If no recent caller position exists, the server falls back to the configured position of the repeater or hotspot where the caller was last received. Both fields are null when neither position is available.position_source identifies the coordinates as caller, repeater, or hotspot. It is null when coordinates are unavailable.origin_callsign is the callsign of the receiving repeater or hotspot. origin_type is repeater or hotspot. Both are null for calls received from trunks or when the endpoint cannot be resolved.
At the end of the audio stream, the server sends a matching
call_end message:
{
"type": "call_end",
"source": "dmr",
"radio_id": 5052540,
"target_radio_id": null,
"call_type": "group",
"callsign": "VK2FLY",
"name": "Matthew",
"talkgroup": 505,
"timeslot": 2,
"stream_id": 123456789,
"ended_ms": 1782930060000,
"final_rtp_sequence": 9102
}
A client must match stream_id before clearing the caller display.
A delayed call_end for an older stream must not clear a newer
active call. For a recovery-capable client, final_rtp_sequence
identifies the final server-to-client RTP packet. The client should NACK any
missing packets through that sequence and retain playout for its bounded
normal or degraded tail window. The field is null when no RTP
audio was emitted. The Radio ID remains available in RTP SSRC even if the
identity fields are unavailable.
GPS is sent over the logged-in control connection, not over RTP. The server
always associates the report with the Radio ID established for the current
session. A client must not send a separate radio_id in
this message, and any such field is not used as the report identity.
Client to server:
{
"type": "gps_report",
"latitude": -33.874288,
"longitude": 151.220588,
"accuracy_m": 100,
"source": "wifi_cache",
"fix_age_ms": 420000,
"altitude_m": 35.0,
"speed_mps": 13.9,
"bearing_deg": 123.0,
"timestamp_ms": 1782930000000
}Fields:
latitude and longitude are required decimal degrees. lat and lon are accepted aliases.source is optional and must be gps, network, or wifi_cache. It defaults to gps for older clients. Unknown values are rejected.wifi_cache means the client inferred an indoor location from a private, locally stored WiFi fingerprint. IPSC3 displays it as a lower-confidence indoor estimate and adds I after the position on the roaming dashboard.fix_age_ms is the age of the underlying fix or cached position when the report was sent.accuracy_m, fix_age_ms, altitude_m, speed_mps, speed_kmh, bearing_deg, and timestamp_ms are optional.speed_kmh takes precedence. Android clients may send the native Location.getSpeed() value as speed_mps.course_degrees is accepted as an alias for bearing_deg.timestamp_ms is the device fix time as Unix epoch milliseconds. The server uses its own receive time for map freshness and APRS rate limiting.0,0 is rejected.WiFi fingerprints remain entirely on the client. Clients must not send, and IPSC3 does not model, forward, log, or store SSIDs, BSSIDs, access-point MAC addresses, gateway MAC addresses, or similar network identifiers. Only the resulting coordinates, source, accuracy, fix age, and normal optional motion metadata cross the protocol boundary.
Server to client:
{
"type": "status",
"code": "gps_report_ok",
"message": "GPS report accepted",
"radio_id": 5052540,
"received_at_ms": 1782930000123,
"source": "wifi_cache",
"map_update": "queued",
"aprs_is": "queued"
}
The acknowledgment means the normalized event entered the local IPSC3 GPS
processing queue. The main IPSC3 daemon then updates both
radio_gps_positions and radio_gps_position_history,
including the constrained source and confidence metadata, making the latest
position and track available on the map. Database writes
and APRS network I/O run in background workers and do not execute in the POC
control or RTP threads.
Valid reports are also offered to the existing APRS-IS worker. That worker resolves the session Radio ID through the RadioID database, uses the resolved callsign as the APRS source, and applies the server's configured per-radio APRS minimum interval. Unknown Radio IDs remain visible on the IPSC3 map by number but are not sent to APRS-IS because they have no valid callsign.
The server currently accepts at most one POC GPS report per Radio ID every five seconds. If a report arrives sooner, it is not stored or forwarded and the client receives:
{
"type": "status",
"code": "gps_report_rate_limited",
"message": "GPS report received too soon",
"retry_after_ms": 3120
}
Clients should schedule normal background reports substantially less often
than this protective limit and use the value announced in
config_update.location.min_interval_ms as the absolute minimum.
Client to server:
{ "type": "version", "client": "IPSC3-POC-Android", "version": "0.3.7" }Server to client:
{
"type": "config_update",
"protocol": "ipsc3-poc/0.4",
"server_id": "vkdmr-poc-dev",
"audio": {
"codec": "PCMU",
"payload_type": 0,
"sample_rate_hz": 8000,
"channels": 1,
"ptime_ms": 20,
"receive_registration": {
"method": "rtp_marker_probe",
"marker": true,
"payload": "PCMU silence",
"payload_bytes": 160,
"keepalive_seconds": 20,
"keep_udp_socket_open": true,
"control_fallback": "rtp_ready"
},
"recovery": {
"available": true,
"version": 1,
"negotiation_field": "capabilities.rtp_recovery",
"uplink_nack": true,
"downlink_nack": true,
"final_sequence": true,
"max_retries": 1,
"normal_jitter_ms": 350,
"degraded_jitter_ms": 700,
"retransmit_cache_ms": 2000,
"max_nack_packets": 33
}
},
"location": {
"enabled": true,
"message_type": "gps_report",
"min_interval_ms": 5000,
"map_update": true,
"aprs_is": true
},
"features": {
"call_identity": true,
"gps_report": true,
"private_calls": true,
"private_call_instance_fanout": true,
"sms": true,
"sms_max_chars": 128,
"sms_instance_fanout": true,
"multi_instance": true,
"client_instance_id_field": "client_instance_id",
"max_instances_per_radio_id": 8,
"server_assigned_rtp_ssrc": true,
"rtp_recovery_v1": true,
"device_control": true,
"device_commands": ["call_alert", "disable", "enable"],
"scan_groups": true,
"poc_options": true,
"server_recording": false,
"ota_updates": false,
"dmr_gateway": true
},
"scan": {
"enabled": true,
"talkgroup": 777,
"timeslot": 1,
"talkback_hang_ms": 30000
},
"client_options": {
"field": "options",
"format": "semicolon_v1",
"case_sensitive": false,
"supported": ["ScanInclude", "ScanExclude", "TGInclude", "TGExclude"],
"max_chars": 1024,
"max_talkgroups_per_option": 256
}
}{
"type": "error",
"code": "request_failed",
"message": "unknown talkgroup 99999"
}{
"type": "status",
"code": "rtp_ready_ok",
"message": "RTP receive endpoint registered",
"host": "198.51.100.25",
"port": 41000
}presence_list or any other client roster. Legacy client presence messages only refresh server-side liveness and receive no digest response.call_start and call_end control messages containing Radio ID and server-resolved callsign/name.dmr_linked talkgroup, accepted 20 ms PCMU frames can be copied into a bounded background bridge queue. Local POC fanout remains unchanged and never waits for the vocoder.Client applications continue to use PCMU RTP and do not send AMBE. DMR-linked audio conversion is an internal server concern. The POC daemon talks to a separate vocoder process and a separate IPSC3 media adapter over loopback-only versioned datagrams. This keeps software or hardware vocoding and DMR framing outside the public client protocol and outside the POC RTP listener.
See the IPSC3-POC Media Gateway Protocol for the internal AMBE burst format, grant flow, codec service boundary, and current implementation status.
Receivers must ignore unknown fields. This allows later additions for per-user authentication, WebSocket/TLS control transport, Opus app-only talkgroups, OTA app/config updates, hardware PTT state, and server-side recording without breaking early clients.