Skip to main content

Android Low Latency PTT SDK

SDK Version: 1.0.0

Low Latency PTT provides half-duplex live voice over a dedicated RF mode. It owns microphone capture/playback, MELP1200 packetization, group isolation, RF activation/restoration, bounded queues, state, events, and statistics. It is independent of Mesh messages, ordinary recorded PTT, realtime calls, and file transfer.

Dependency and permission

implementation("com.xtalk:low-latency-ptt-sdk:1.0.0")

Consume the dependency metadata supplied with the release because the artifact has coroutine and codec runtime dependencies. Request android.permission.RECORD_AUDIO at runtime before talking.

Configure and create

val operations = LowLatencyPttGroupProfile.group(
groupId = "ops",
title = "Operations",
channel = 7,
key4 = "7007",
)

val config = LowLatencyPttConfiguration(
channel = LowLatencyPttChannel.default,
rateMode = LowLatencyPttRateMode.RATE_7,
availableGroups = listOf(LowLatencyPttGroupProfile.everyone(), operations),
selectedGroupId = LowLatencyPttGroupProfile.everyone().id,
queryTimeoutMs = 1_500,
writeTimeoutMs = 2_000,
)

val sdk = LowLatencyPttSdk(applicationContext, transport, config)

LowLatencyPttChannel.channels contains the fixed 16-channel product profile. LowLatencyPttRateMode supports RATE_6 and RATE_7. everyone() uses keyId=0; group(...) requires printable group ID, channel 1..161, and exactly four ASCII digits for key4. This group channel is key material and does not select physical RF.

Transport contract

Implement LowLatencyPttTransport.acquireExclusiveSession(). The returned LowLatencyPttTransportSession must keep one stable device connection until release and expose:

  • identity, isReady, and readiness StateFlow<Boolean>
  • deviceDid
  • executeAt(command, timeoutMs)
  • sendRfNoWait(payload)
  • idempotent release()

The session must not switch between BLE/UART while active. It must return original device AT responses without rewriting FREQ, RATE, or BCNID values.

State, events, and control

val stateJob = scope.launch { sdk.snapshot.collect(::renderSnapshot) }
val eventJob = scope.launch { sdk.events.collect(::handleEvent) }

try {
sdk.enable()
sdk.startTalking()
sdk.stopTalking()
sdk.disable()
} finally {
sdk.close()
stateJob.cancel()
eventJob.cancel()
}

All controls are suspending: enable, disable, startTalking, stopTalking, setChannel, setRateMode, setAvailableGroups, setGroup, and close.

LowLatencyPttSnapshot exposes enabled/restore flags, state, talking/receiving flags, selected channel/rate/group, available groups, statistics, and last error. States are DISABLED, PREPARING, IDLE, TALKING, RECEIVING, ERROR, and terminal CLOSED.

Events are StateChanged, Failure, GroupChanged, and RxRejected. Rejection reasons are invalid frame, rate mismatch, group mismatch, checksum, authentication, local echo, duplicate, and queue full.

Route RF input

val consumed = sdk.tryHandleInbound(payload, rssi, snr)
if (!consumed) otherRadioProtocols.accept(payload, rssi, snr)

Call this first for every RF payload. Disabled/closed or clearly unrelated frames return false. A candidate frame returns true even if later rejected, preventing it from being misrouted to Mesh. The callback path copies into a bounded queue and does not decode or update UI inline.

RF transaction and audio format

enable() acquires the session, reads original FREQ/RATE/BCNID, writes and verifies the selected PTT channel and rate, then initializes audio. disable() and close() restore and verify the original triple before releasing the session.

Audio is 8 kHz mono signed PCM16 little-endian. RATE7 aggregates two MELP frames; RATE6 aggregates four. Full synchronization frames are 31 bytes for RATE7 and 53 for RATE6; compact frames are 25 and 47 bytes. Both endpoints must select the same rate and SDK generation.

Errors and recovery

Domain failures throw LowLatencyPttException; inspect error for NOT_CONNECTED, PERMISSION_DENIED, BUSY, INVALID_CONFIGURATION, RF_CONFIGURATION_FAILED, AUDIO_FAILED, SEND_FAILED, RESTORE_FAILED, or CLOSED. Configuration factory validation throws IllegalArgumentException; coroutine cancellation remains CancellationException.

If snapshot.restorePending is true, do not reassign or switch the underlying radio. Retry disable() or execute close() recovery. A closed snapshot that still reports restore pending requires host disconnect/reconnect and RF verification before reuse.

Security note

Private groups derive packet isolation material from the normalized group ID, group channel, and four-digit key. The four-digit key is not high-strength end-to-end authentication; use it only for the product group-isolation purpose described here. Never log key4 or derived key bytes.