Android Device Authentication SDK
SDK Version: 1.0.0
Package:com.xtalk.mesh.sdk.auth
Authentication selects a working AT encoding, verifies the challenge response, and reads the DID. Frequency, rate, and work-mode values belong to the Init SDK and are not authentication parameters.
Create the BLE RadioTransport
BleRadioTransport requires an event bridge and realtime-send marker callbacks. A no-op implementation is sufficient when the transport is used only for Auth/Init:
object NoOpRadioTransportEventBridge : RadioTransportEventBridge {
override fun addPacketListener(listener: RadioPacketListener) = Unit
override fun removePacketListener(listener: RadioPacketListener) = Unit
override fun addRealtimePacketListener(listener: RadioPacketListener) = Unit
override fun removeRealtimePacketListener(listener: RadioPacketListener) = Unit
override fun addCrcErrorListener(listener: RadioCrcErrorListener) = Unit
override fun removeCrcErrorListener(listener: RadioCrcErrorListener) = Unit
}
val radioTransport: RadioTransport = BleRadioTransport(
eventBridge = NoOpRadioTransportEventBridge,
clearRealtimeNoWaitMark = {},
markRealtimeNoWaitSend = { _ -> },
)
If the application consumes RF packets or CRC events, replace the no-op bridge with a customer implementation of RadioTransportEventBridge.
BLE authentication
After the connection and GATT path are ready, authenticate on a background dispatcher:
val auth = withContext(Dispatchers.IO) {
DeviceAuthenticator.authenticateBleAndReadDid(
radioTransport = radioTransport,
timeoutMs = 30_000L,
)
}
if (!auth.ok) {
handleAuthenticationFailure(auth.reason)
} else {
val did = auth.deviceLongId
onAuthenticated(did)
}
Use the exclusive API to coalesce concurrent authentication requests:
val ok = DeviceAuthenticator.authenticateBleAndReadDidExclusive(
radioTransport = radioTransport,
timeoutMs = 30_000L,
forceReauth = false,
)
After a device restart, OTA completion, or invalidated authentication state, set forceReauth = true or call markAuthenticationRequired(reason, resetAtMode) first.
UART authentication
The customer implements AtCommandTransport; the SDK does not own the customer's UART driver:
val uartTransport = object : DeviceAuthenticator.AtCommandTransport {
override val label: String = "uart"
override fun runAtCommand(
command: String,
timeoutMs: Long,
log: Boolean,
): List<String> = myUart.runAtCommand(command, timeoutMs, log)
}
val auth = DeviceAuthenticator.authenticateUartAndReadDid(
transport = uartTransport,
challengeTimeoutMs = 10_000L,
didTimeoutMs = 6_000L,
)
Do not log challenge secrets or publish complete authentication responses. Initialize the device only after authentication succeeds.
Complete interface contract
AtCommandTransport
interface AtCommandTransport {
val label: String
fun runAtCommand(command: String, timeoutMs: Long, log: Boolean = true): List<String>
}
label is a readable non-secret log tag. runAtCommand serializes one transaction, returns complete CR/LF-delimited lines, and throws for a closed link, device error, or timeout. Auth converts exceptions to Result.reason. RadioAtCommandTransport(radioTransport) is recommended: it collects a complete Challenge response and fails fast for other commands.
Authentication methods
| API | Precondition/thread | Parameters | Return/failure |
|---|---|---|---|
authenticateBleAndReadDid(radioTransport, timeoutMs = 30000) | BLE CONNECTED; background | Overall budget | Select AT mode, optional Challenge, read/apply DID; full Result |
authenticateBleAndReadDidExclusive(radioTransport, timeoutMs = 30000, forceReauth = false) | Same | forceReauth clears state | Coalesces calls; joiners wait at most timeoutMs + 8000; Boolean only |
authenticateUartAndReadDid(transport, challengeTimeoutMs = 10000, didTimeoutMs = 6000) | UART open; background | Separate deadlines | Challenge + DID, applies successful DID; Result |
markAuthenticationRequired(reason, resetAtMode = true) | Before reboot/OTA/recovery | Reason and BLE AT reset | Clears Auth/wireless-ready state |
readDeviceLongId(transport, timeoutMs, log) | Authenticated/readable device | Transport/deadline/log | Tries AT+EFUSESN? then AT+SN?; structured DID or null |
applyDeviceLongId(deviceLongId, source) | Verified DID | 32-bit ID/source tag | true when stored in core identity state |
Result.reason
| reason | Meaning | Safe recovery |
|---|---|---|
ok | Auth and DID succeeded | Continue to Init |
not_connected | Not CONNECTED | Inspect state and reconnect |
ble_not_ready | GATT/AT not ready | Disconnect/reconnect; do not spin Auth |
at_path_unavailable | Normal/XOR probes both failed | Check firmware/link compatibility |
challenge_command_failed | Send, device error, or timeout | Stay unauthenticated; retry once after link validation |
challenge_verify_failed | Signature verification failed | Never bypass; disconnect/contact support |
did_read_failed | No valid structured DID | Do not Init; reconnect/check firmware |
Only ok == true authorizes Init. A DID in a failed intermediate result is not authenticated identity. Android Auth has no independent cancel API; coroutine cancellation does not guarantee interruption of a blocking call. Stop later steps, wait for return, then disconnect. An exclusive joiner timeout returns false and does not start a parallel Challenge.