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, andcloserun onDispatchers.IObecause they wait on locks, GATT, or device responses.- Use only the frequency approved for the customer project and region.
clearRealtimeNoWaitMark/markRealtimeNoWaitSendmay be no-ops unless the application performs realtime local-echo recognition.- Remove every
add...Listenerregistration with the same listener instance. - After disconnect, reboot, or OTA, repeat Connect → Auth → Init; do not reuse a previous ready state.