Android 完整接入示例
SDK Version: 1.0.0
Platform: Android
下面的 XTalkSdkSession 把一个设备会话的监听器、BLE 连接、RadioTransport、认证、初始化和清理放在同一生命周期所有者中。权限必须在调用 initialize() 前由 Activity/Fragment 授予。
可复制的会话类
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 = device,
timeoutMs = 8_000,
maxReconnectAttempts = 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 = radioTransport,
timeoutMs = 30_000L,
)
check(auth.ok) { "authentication failed: ${auth.reason}" }
val did = requireNotNull(auth.deviceLongId) { "authentication returned no DID" }
val initialized = DeviceInitializer.initializeBleWirelessDefaults(
radioTransport = radioTransport,
defaults = 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)
// BLE SDK 的普通数据回调不产生结构化 CRC 事件;需要 CRC 事件的客户
// 应把交付通道提供的 AT/DI 解析器接到这两个方法。
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 调用顺序
session.initialize()
check(session.startScan() == ScanStartResult.SCAN_START_OK)
// 用户选择 session.discoveredDevices() 中的设备后:
val did = session.connectAndPrepare(selectedDevice, approvedFrequencyHz)
check(session.send(payload) == RadioSendResult.SEND_OK)
// ViewModel.clear、页面结束或切换账号/设备时:
session.close()
必须保留的边界
connectAndPrepare、send、close在Dispatchers.IO执行,因为内部会等待锁、GATT 或设备响应。- 频率必须是客户项目/地区获准值;不要直接把示例值当作全球生产配置。
clearRealtimeNoWaitMark/markRealtimeNoWaitSend只用于客户需要实时本地回声识别时;普通数据业务可保持无操作。- 每个
add...Listener必须用同一个实例调用对应remove...Listener。 - 设备断线、重启或 OTA 后,重新执行 Connect → Auth → Init,不要沿用旧
isReadyForSend状态。