Skip to main content

iOS Low Latency PTT SDK

SDK Version: 1.0.0

Low Latency PTT provides half-duplex live voice over a dedicated RF mode. The LowLatencyPtt Swift product owns audio capture/playback, MELP1200 packetization, RF activation and verified restoration, group isolation, bounded queues, state, events, and statistics.

Add the product and permission​

Add the supplied exact-version Swift package and link only LowLatencyPtt:

import LowLatencyPtt

Do not import codec-support targets. Add NSMicrophoneUsageDescription and request microphone access before startTalking().

Configure and create​

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

let config = try LowLatencyPttConfiguration(
channel: .default,
rateMode: .rate7,
availableGroups: [.everyone, operations],
selectedGroupId: LowLatencyPttGroupProfile.everyone.id
)

let sdk = LowLatencyPttSdk(transport: transport, configuration: config)

The fixed profile contains 16 physical channels. Rates are .rate6 and .rate7. A private group requires a printable ID, group channel 1...161, and a four-digit ASCII key. The group channel contributes to key derivation and does not select physical RF.

Transport contract​

Implement LowLatencyPttTransport.acquireExclusiveSession(). A LowLatencyPttTransportSession supplies stable identity, isReady, readiness AsyncStream<Bool>, deviceDid, executeAt, sendRfNoWait, and idempotent release. All AT and RF operations from acquisition through restoration must use the same underlying BLE/UART object.

Observe and control​

LowLatencyPttSdk is a @MainActor ObservableObject. Read snapshot or observe $snapshot; transient events arrive through events.

let eventTask = Task { @MainActor in
for await event in sdk.events { handle(event) }
}

do {
try await sdk.enable()
try await sdk.startTalking()
try await sdk.stopTalking()
try await sdk.disable()
try await sdk.close()
} catch let error as LowLatencyPttError {
handle(error)
}

eventTask.cancel()

Controls are enable, disable, startTalking, stopTalking, setChannel, setRateMode, setAvailableGroups, setGroup(id:), and close.

Snapshot states are disabled, preparing, idle, talking, receiving, error, and terminal closed. Snapshot fields include enabled/restore flags, selected channel/rate/group, available groups, statistics, and last error. Events report state change, failure, group change, and receive rejection.

Route RF input​

let consumed = sdk.tryHandleInbound(payload, rssi: rssi, snr: snr)
if !consumed { otherRadioProtocols.accept(payload) }

The nonisolated method should be the first RF consumer. It returns false for disabled/closed or unrelated packets. Candidate PTT frames remain consumed even when a later checksum, synchronization, group, rate, echo, duplicate, or queue test rejects them.

RF and audio behavior​

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

Audio is 8 kHz mono PCM16 little-endian. RATE7 uses two MELP frames per packet; RATE6 uses four. Full frames are 31/53 bytes and compact frames are 25/47 bytes. Android and iOS peers must use the same rate and protocol generation.

Errors and recovery​

Domain errors are notConnected, permissionDenied, busy, invalidConfiguration, rfConfigurationFailed, audioFailed, sendFailed, restoreFailed, and closed. Configuration factories throw validation errors; task cancellation remains CancellationError.

When snapshot.restorePending is true, the host must not switch or reassign the radio. Retry disable or use close recovery. A closed snapshot with restoration still pending requires an external disconnect/reconnect and RF verification before reuse.

Private-group four-digit keys provide product group isolation, not high-strength end-to-end identity authentication. Never log the original key or derived material.