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