Skip to main content

Android End-to-End Integration Example

SDK Version: 1.0.0
Platform: Android

The following XTalkSdkSession owns listeners, BLE state, RadioTransport, authentication, initialization, and cleanup for one device session. The Activity or Fragment must grant runtime permissions before initialize().

Copyable session class

import android.content.Context
import com.xtalk.mesh.sdk.auth.DeviceAuthenticator
import com.xtalk.mesh.sdk.ble.BleRadioTransport
import com.xtalk.mesh.sdk.ble.XTalkBleClient
import com.xtalk.mesh.sdk.ble.XTALK_SCAN_UUID_5632G
import com.xtalk.mesh.sdk.deviceinit.DeviceInitializer
import com.xtalk.mesh.sdk.transport.RadioCrcErrorListener
import com.xtalk.mesh.sdk.transport.RadioInboundPacket
import com.xtalk.mesh.sdk.transport.RadioPacketListener
import com.xtalk.mesh.sdk.transport.RadioPacketSource
import com.xtalk.mesh.sdk.transport.RadioSendResult
import com.xtalk.mesh.sdk.transport.RadioTransport
import com.xtalk.mesh.sdk.transport.RadioTransportEventBridge
import com.xtalk.sdk.core.ConnectResult
import com.xtalk.sdk.core.DataReceivedCallback
import com.xtalk.sdk.core.DeviceEventState
import com.xtalk.sdk.core.DeviceStateCallback
import com.xtalk.sdk.core.InitResult
import com.xtalk.sdk.core.ScanStartResult
import com.xtalk.sdk.core.XTalkDevice
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class XTalkSdkSession(
context: Context,
private val onPacket: (RadioInboundPacket) -> Unit,
) {
private val appContext = context.applicationContext
private val discovered = ConcurrentHashMap<String, XTalkDevice>()

private val deviceListener = DeviceStateCallback { state, devices, _, _ ->
if (state == DeviceEventState.SCAN_RESULT) {
devices.orEmpty().forEach { device -> discovered[device.macAddress] = device }
}
}

private val eventBridge = BleSdkEventBridge()
private val packetListener = RadioPacketListener(onPacket)

val radioTransport: RadioTransport = BleRadioTransport(
eventBridge = eventBridge,
clearRealtimeNoWaitMark = {},
markRealtimeNoWaitSend = { _ -> },
)

fun initialize() {
XTalkBleClient.setApplicationContext(appContext)
val init = XTalkBleClient.ensureInitialized()
check(init == InitResult.INIT_OK) { "xTalk init failed: $init" }
XTalkBleClient.addListener(deviceListener)
radioTransport.addPacketListener(packetListener)
}

fun startScan(): ScanStartResult {
discovered.clear()
return XTalkBleClient.startScan(
timeoutMs = 8_000,
minRssiDbm = -100,
serviceUuids = listOf(XTALK_SCAN_UUID_5632G),
)
}

fun discoveredDevices(): List<XTalkDevice> =
discovered.values.sortedByDescending { it.rssi }

suspend fun connectAndPrepare(
device: XTalkDevice,
frequencyHz: Long,
): Long = withContext(Dispatchers.IO) {
XTalkBleClient.stopScan()
val request = XTalkBleClient.connect(device, 8_000, null)
check(request == ConnectResult.CONNECT_OK) { "connect request failed: $request" }
check(XTalkBleClient.awaitConnected(device.macAddress, 10_000L)) {
"connection did not reach CONNECTED"
}
check(XTalkBleClient.awaitBleReady(8_000L)) { "BLE GATT/AT channel not ready" }

val auth = DeviceAuthenticator.authenticateBleAndReadDid(radioTransport, 30_000L)
check(auth.ok) { "authentication failed: ${auth.reason}" }
val did = requireNotNull(auth.deviceLongId) { "authentication returned no DID" }

val initialized = DeviceInitializer.initializeBleWirelessDefaults(
radioTransport,
DeviceInitializer.BleWirelessDefaults(freqHz = frequencyHz),
)
check(initialized) { "wireless defaults failed" }
check(XTalkBleClient.isReadyForSend()) { "session is not ready for business traffic" }
did
}

suspend fun send(payload: ByteArray): RadioSendResult = withContext(Dispatchers.IO) {
check(XTalkBleClient.isReadyForSend()) { "Auth/Init is incomplete" }
radioTransport.sendRfPayload(payload, timeoutMs = 3_000L)
}

suspend fun close() = withContext(Dispatchers.IO) {
radioTransport.removePacketListener(packetListener)
XTalkBleClient.removeListener(deviceListener)
XTalkBleClient.stopScan()
XTalkBleClient.disconnect(sendRemoteCommand = true)
}
}

private class BleSdkEventBridge : RadioTransportEventBridge {
private val packetAdapters = ConcurrentHashMap<RadioPacketListener, DataReceivedCallback>()
private val realtimeAdapters = ConcurrentHashMap<RadioPacketListener, DataReceivedCallback>()

override fun addPacketListener(listener: RadioPacketListener) = add(listener, packetAdapters)
override fun removePacketListener(listener: RadioPacketListener) = remove(listener, packetAdapters)
override fun addRealtimePacketListener(listener: RadioPacketListener) = add(listener, realtimeAdapters)
override fun removeRealtimePacketListener(listener: RadioPacketListener) = remove(listener, realtimeAdapters)

// The ordinary BLE data callback does not emit structured CRC events. Customers that
// need CRC events should connect the AT/DI parser supplied with their delivery here.
override fun addCrcErrorListener(listener: RadioCrcErrorListener) = Unit
override fun removeCrcErrorListener(listener: RadioCrcErrorListener) = Unit

private fun add(
listener: RadioPacketListener,
adapters: ConcurrentHashMap<RadioPacketListener, DataReceivedCallback>,
) {
val adapter = DataReceivedCallback { data, length, snr, rssi ->
listener.onPacket(
RadioInboundPacket(
payload = data.copyOf(length.coerceIn(0, data.size)),
source = RadioPacketSource.BLE_DIRECT,
snr = snr,
rssi = rssi,
),
)
}
if (adapters.putIfAbsent(listener, adapter) == null) {
XTalkBleClient.addDataListener(adapter)
}
}

private fun remove(
listener: RadioPacketListener,
adapters: ConcurrentHashMap<RadioPacketListener, DataReceivedCallback>,
) {
adapters.remove(listener)?.let(XTalkBleClient::removeDataListener)
}
}

Activity/ViewModel sequence

session.initialize()
check(session.startScan() == ScanStartResult.SCAN_START_OK)

// After the user selects a device from session.discoveredDevices():
val did = session.connectAndPrepare(selectedDevice, approvedFrequencyHz)
check(session.send(payload) == RadioSendResult.SEND_OK)

// From ViewModel.clear, screen exit, or device/account switch:
session.close()

Required boundaries

  • connectAndPrepare, send, and close run on Dispatchers.IO because they wait on locks, GATT, or device responses.
  • Use only the frequency approved for the customer project and region.
  • clearRealtimeNoWaitMark/markRealtimeNoWaitSend may be no-ops unless the application performs realtime local-echo recognition.
  • Remove every add...Listener registration with the same listener instance.
  • After disconnect, reboot, or OTA, repeat Connect → Auth → Init; do not reuse a previous ready state.

Complete API Reference · Error Handling