Android RadioTransport Abstraction
SDK Version: 1.0.0
Package:com.xtalk.mesh.sdk.transport
RadioTransport lets Auth, Init, and business code access BLE or customer UART through one contract. It does not open the physical link. Connect BLE or open UART before constructing/using it.
1. Models and enums
| Type | Values/fields | Purpose |
|---|---|---|
RadioTransportMode | BLE, UART | Selected physical channel |
RadioSendResult | SEND_OK, SEND_FAIL_NOT_CONNECTED, SEND_FAIL_INTERNAL | RF submission result |
AtCommandErrorPolicy | FAIL_FAST, COLLECT_UNTIL_COMPLETE | Fail on an error line or let a custom completion condition collect a full response |
RadioPacketSource | BLE_DIRECT, BLE_PASSTHROUGH_DI, UART_DI, UART_HEX | Inbound packet origin |
RadioInboundPacket | payload, source, snr, rssi, receivedAtNanos | Inbound RF data; payload is the business byte array |
RadioCrcErrorEvent | source, slot, snr, rssi, rawLine, receivedAtNanos | Structured CRC error; unavailable metadata is null |
RadioPacketListener | onPacket(packet) | Normal or realtime packet callback |
RadioCrcErrorListener | onCrcError(event) | CRC callback |
Listener threads are transport-defined high-frequency I/O paths. Return quickly and dispatch UI updates to the main thread.
2. Every RadioTransport member
Common contract: invoke AT and RF operations off the main thread. Do not overlap Auth, Init, or custom AT transactions. timeoutMs is milliseconds. A timeout does not prove that the device did not execute the command; query state before retrying.
| Member | Parameters/defaults | Return, errors, and paired operation |
|---|---|---|
mode, label | None | Current channel and log label |
isOpenOrConnected() | None | Physical link state; not Auth/Init readiness |
isReadyForSend() | None | Business-send gate |
localDeviceLongId() | None | Authenticated DID or null |
runAtCommand(command, timeoutMs = 3000, log = true, errorPolicy = FAIL_FAST, stopWhen = null) | AT text and optional completion predicate | Response lines; disconnected, send, device error, and timeout paths may throw |
runAtCommandDrainingResponseTail(...) | stopWhen required | Keeps listener/lock through terminal OK/ERROR or deadline after payload match; unsupported transports throw UnsupportedOperationException |
sendAtCommand(command) | AT text | true when submitted; no response lines |
sendAtCommandNoWait(command) | AT text | Submission only; does not prove device execution |
sendRfPayload(payload, timeoutMs = 3000) | Non-empty frame | RadioSendResult; recommended for normal reliable traffic |
sendRfPayloadAndWaitSendFinish(payload, timeoutMs = 3000) | Non-empty frame | Waits for device SEND_FINISH; true on success |
sendRfPayloadNoWait(payload) | Non-empty realtime frame | Low-latency submission without completion; caller rate-limits |
addPacketListener / removePacketListener | Same listener instance | Normal packet lifecycle pair |
addRealtimePacketListener / removeRealtimePacketListener | Same listener instance | Realtime packet lifecycle pair |
addCrcErrorListener / removeCrcErrorListener | Same listener instance | CRC lifecycle pair; bridge needs a CRC parser source |
setExpectedRealtimeSourceShortId(shortId) | null clears | UART/advanced source filter; BLE may be a no-op |
isRecentLocalRealtimeEcho(payload) | Complete frame | Local realtime echo check; unsupported implementations return false |
setFrequencyHz(freqHz) | Approved Hz value | true only when all channel values were applied |
setRateMode(rateMode) | Device-supported mode | true on success |
queryRateMode(timeoutMs = 1500) | Timeout | Current value or null on response/parse failure |
queryWorkMode(timeoutMs = 1500) | Timeout | Current value or null on response/parse failure |
restoreTextPttDefaults(defaultRateMode, defaultAddtl = 1, defaultWorkMode = 21) | Three settings | true only if all restore operations succeed; call after call/file modes |
val lines = withContext(Dispatchers.IO) {
radio.runAtCommand("AT+RATE?", timeoutMs = 1_500L)
}
val sent = withContext(Dispatchers.IO) {
radio.sendRfPayload(payload, timeoutMs = 3_000L)
}
3. BleRadioTransport
val radio: RadioTransport = BleRadioTransport(
eventBridge = customerEventBridge,
clearRealtimeNoWaitMark = { echoTracker.clear() },
markRealtimeNoWaitSend = { bytes -> echoTracker.mark(bytes) },
)
Initialize XTalkBleClient first. Connect BLE before transport operations and complete Auth/Init before business sends. eventBridge maps lower-level inbound events; see End-to-End Integration. The realtime callbacks may be empty when local-echo tracking is not needed. The transport does not own BLE; customers still remove listeners and disconnect.
4. RadioTransportEventBridge
The interface has three strict add/remove pairs: normal packet, realtime packet, and CRC error. Keep a listener → underlying callback map and remove the original callback instance. The complete BLE mapping example is in End-to-End Integration.
5. Customer UART adapter
Wrap an existing driver when a ready-made UART transport is not included in the delivery. This is the contract skeleton only; the implementation must add response framing, DI decoding, CRC handling, exclusive locking, and deadlines:
class CustomerUartRadioTransport(private val uart: CustomerUartDriver) : RadioTransport {
override val mode = RadioTransportMode.UART
override val label = "customer-uart"
override fun isOpenOrConnected() = uart.isOpen
override fun isReadyForSend() = uart.isOpen && uart.did != null && uart.initialized
override fun localDeviceLongId() = uart.did
override fun runAtCommand(
command: String,
timeoutMs: Long,
log: Boolean,
errorPolicy: AtCommandErrorPolicy,
stopWhen: ((List<String>) -> Boolean)?,
) = uart.runExclusiveAt(command, timeoutMs, errorPolicy, stopWhen)
override fun sendAtCommand(command: String) = uart.sendAt(command, waitWrite = true)
override fun sendAtCommandNoWait(command: String) = uart.sendAt(command, waitWrite = false)
override fun sendRfPayload(payload: ByteArray, timeoutMs: Long) =
if (uart.sendRf(payload, timeoutMs)) RadioSendResult.SEND_OK
else RadioSendResult.SEND_FAIL_INTERNAL
override fun sendRfPayloadAndWaitSendFinish(payload: ByteArray, timeoutMs: Long) =
uart.sendRfAndAwaitFinish(payload, timeoutMs)
override fun sendRfPayloadNoWait(payload: ByteArray) = uart.sendRfNoWait(payload)
// Delegate listener pairs, radio settings/queries, and defaults to the customer driver.
// Return false/null or throw UnsupportedOperationException for unsupported operations.
}
For Auth only, implement DeviceAuthenticator.AtCommandTransport. For Init only, implement DeviceInitializer.AtCommandTransport. Implement full RadioTransport only when unified business switching is required.
6. RadioTransportRouter
val router = RadioTransportRouter(bleTransport, uartTransport, RadioTransportMode.BLE)
router.addPacketListener(packetListener)
router.setMode(RadioTransportMode.UART)
val active = router.currentTransport()
setMode moves the Router's three listener sets from the old transport to the new one. Open and Auth/Init the new physical channel before switching. Remove listeners during cleanup; the Router does not close transports.
7. Advanced inbound parsers
These public low-level tools are for transport authors; ordinary BLE customers do not call them directly:
| API | Contract |
|---|---|
AtLineBuffer.hasBufferedData() | Whether an unterminated partial line remains |
AtLineBuffer.append(bytes, onLine) | CR/LF framing; the two-argument callback also receives assembly milliseconds |
AtResponsePolicy.isOkLine/isErrorLine/isSendFinishLine | Compatible terminal recognition |
AtResponsePolicy.isResponseTailOkLine/isResponseTailErrorLine | Strict tail-drain terminal recognition |
AtResponsePolicy.isFreqProbeAccepted/freqProbeStatus | NORMAL_FREQ, NEEDS_VERIFY, or null |
AtResponseTailDrainTracker.observe | CONTINUE/COMPLETE/ERROR; payloadMatched is sticky |
BinaryEscape.escape/unescape | Reversible CR/LF/escape-byte framing without mutating input |
DiFrameParser.parseLine/parseBytes | Parse +DI/AT+DI; null on failure; drop frames with lengthMismatch |
DiFrameParser.parseDiMeta | Extract count, length, slot, SNR/RSSI, and TXP; missing fields are null |
DiMeta.hasInvalidRemoteFeedback() | Detect invalid remote feedback combination |
CrcErrorParser.parseLine/parseBytes | Parse +CRCERR, optionally through an AT decoder; null on failure |
AsciiPrefix.findDiPrefixIndex/findCrcErrPrefixIndex/findAsciiPrefixIndex | Find an ASCII prefix after possible leading noise |
Parsers do not own serial reads, threads, reconnection, or business retries.