Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import org.meshtastic.core.ble.BleConnection
import org.meshtastic.core.ble.BleError
import org.meshtastic.core.ble.BleScanner
import org.meshtastic.core.ble.MeshtasticBleConstants.FROMNUM_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIOSYNC_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
Expand Down Expand Up @@ -112,6 +113,7 @@ constructor(
private var toRadioCharacteristic: RemoteCharacteristic? = null
private var fromNumCharacteristic: RemoteCharacteristic? = null
private var fromRadioCharacteristic: RemoteCharacteristic? = null
private var fromRadioSyncCharacteristic: RemoteCharacteristic? = null
private var logRadioCharacteristic: RemoteCharacteristic? = null

init {
Expand Down Expand Up @@ -270,6 +272,7 @@ constructor(
toRadioCharacteristic = chars[TORADIO_CHARACTERISTIC]
fromNumCharacteristic = chars[FROMNUM_CHARACTERISTIC]
fromRadioCharacteristic = chars[FROMRADIO_CHARACTERISTIC]
fromRadioSyncCharacteristic = chars[FROMRADIOSYNC_CHARACTERISTIC]
logRadioCharacteristic = chars[LOGRADIO_CHARACTERISTIC]

Logger.d { "[$address] Characteristics discovered successfully" }
Expand All @@ -288,25 +291,44 @@ constructor(

// --- Notification Setup ---

@Suppress("LongMethod")
private suspend fun setupNotifications() {
val fromNumReady = CompletableDeferred<Unit>()
val fromRadioSyncReady = CompletableDeferred<Unit>()
val logRadioReady = CompletableDeferred<Unit>()

fromNumCharacteristic
if (fromRadioSyncCharacteristic == null) {
fromNumCharacteristic
?.subscribe {
Logger.d { "[$address] FromNum subscription active" }
fromNumReady.complete(Unit)
}
?.onEach { notifyBytes ->
Logger.d { "[$address] FromNum Notification (${notifyBytes.size} bytes), draining queue" }
connectionScope.launch { drainPacketQueueAndDispatch() }
}
?.catch { e ->
if (!fromNumReady.isCompleted) fromNumReady.completeExceptionally(e)
Logger.w(e) { "[$address] Error in fromNumCharacteristic subscription" }
service.onDisconnect(BleError.from(e))
}
?.launchIn(connectionScope) ?: fromNumReady.complete(Unit)
}

fromRadioSyncCharacteristic
?.subscribe {
Logger.d { "[$address] FromNum subscription active" }
fromNumReady.complete(Unit)
Logger.d { "[$address] FromRadioSync subscription active" }
fromRadioSyncReady.complete(Unit)
}
?.onEach { notifyBytes ->
Logger.d { "[$address] FromNum Notification (${notifyBytes.size} bytes), draining queue" }
connectionScope.launch { drainPacketQueueAndDispatch() }
Logger.d { "[$address] FromRadioSync Notification (${notifyBytes.size} bytes), dispatching packet" }
dispatchPacket(notifyBytes)
}
?.catch { e ->
if (!fromNumReady.isCompleted) fromNumReady.completeExceptionally(e)
Logger.w(e) { "[$address] Error in fromNumCharacteristic subscription" }
Logger.w(e) { "[$address] Error subscribing to fromRadioSyncCharacteristic" }
service.onDisconnect(BleError.from(e))
}
?.launchIn(connectionScope) ?: fromNumReady.complete(Unit)
?.launchIn(scope = connectionScope) ?: fromRadioSyncReady.complete(Unit)

logRadioCharacteristic
?.subscribe {
Expand Down Expand Up @@ -414,6 +436,7 @@ constructor(
toRadioCharacteristic = null
fromNumCharacteristic = null
fromRadioCharacteristic = null
fromRadioSyncCharacteristic = null
logRadioCharacteristic = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,92 @@ class NordicBleInterfaceTest {

nordicInterface.close()
}

@Test
fun `fromRadioSync notification delivers packet directly`() = runTest(testDispatcher) {
val centralManager = CentralManager.Factory.mock(scope = backgroundScope)
val service = mockk<RadioInterfaceService>(relaxed = true)

var fromRadioSyncHandle: Int = -1

val eventHandler =
object : PeripheralSpecEventHandler {
override fun onConnectionRequest(
preferredPhy: List<no.nordicsemi.kotlin.ble.core.Phy>,
): ConnectionResult = ConnectionResult.Accept

override fun onReadRequest(characteristic: MockRemoteCharacteristic): ReadResponse =
ReadResponse.Success(byteArrayOf())
}

val peripheralSpec =
PeripheralSpec.simulatePeripheral(identifier = address, proximity = Proximity.IMMEDIATE) {
advertising(
parameters = LegacyAdvertisingSetParameters(connectable = true, interval = 100.milliseconds),
) {
CompleteLocalName("Meshtastic_1234")
}
connectable(
name = "Meshtastic_1234",
isBonded = true,
eventHandler = eventHandler,
cachedServices = {
Service(uuid = BleConstants.BTM_SERVICE_UUID.toKotlinUuid()) {
Characteristic(
uuid = BleConstants.BTM_TORADIO_CHARACTER.toKotlinUuid(),
properties = setOf(CharacteristicProperty.WRITE),
permission = Permission.WRITE,
)
Characteristic(
uuid = BleConstants.BTM_FROMNUM_CHARACTER.toKotlinUuid(),
properties = setOf(CharacteristicProperty.NOTIFY),
permission = Permission.READ,
)
Characteristic(
uuid = BleConstants.BTM_FROMRADIO_CHARACTER.toKotlinUuid(),
properties = setOf(CharacteristicProperty.READ),
permission = Permission.READ,
)
fromRadioSyncHandle =
Characteristic(
uuid = BleConstants.BTM_FROMRADIOSYNC_CHARACTER.toKotlinUuid(),
properties = setOf(CharacteristicProperty.INDICATE),
permission = Permission.READ,
)
Characteristic(
uuid = BleConstants.BTM_LOGRADIO_CHARACTER.toKotlinUuid(),
properties = setOf(CharacteristicProperty.NOTIFY),
permission = Permission.READ,
)
}
},
)
}

centralManager.simulatePeripherals(listOf(peripheralSpec))
delay(100.milliseconds)

val nordicInterface =
NordicBleInterface(
serviceScope = this,
centralManager = centralManager,
service = service,
address = address,
)

// Wait for connection
delay(1000.milliseconds)
verify(timeout = 2000) { service.onConnect() }

// Simulate notification (Indication) on FromRadioSync
val syncData = byteArrayOf(0xAA.toByte(), 0xBB.toByte())
peripheralSpec.simulateValueUpdate(fromRadioSyncHandle, syncData)

delay(500.milliseconds)

// Verify that handleFromRadio was called with the data
verify(timeout = 2000) { service.handleFromRadio(syncData) }

nordicInterface.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ object MeshtasticBleConstants {
/** Characteristic for reading data from the radio. */
val FROMRADIO_CHARACTERISTIC: Uuid = Uuid.parse("2c55e69e-4993-11ed-b878-0242ac120002")

val FROMRADIOSYNC_CHARACTERISTIC: Uuid = Uuid.parse("888a50c3-982d-45db-9963-c7923769165d")

/** Characteristic for receiving log notifications from the radio. */
val LOGRADIO_CHARACTERISTIC: Uuid = Uuid.parse("5a3d6e49-06e6-4423-9944-e9de8cdf9547")
}
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class BleOtaTransport(
bleConnection.disconnect()
isConnected = false
transportScope.cancel()
responseChannel.close()
}

private suspend fun sendCommand(command: OtaCommand) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ class BleOtaTransportErrorTest {
if (command.startsWith("OTA")) {
backgroundScope.launch {
delay(50.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "ERR Hash Rejected\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "ERR Hash Rejected\n".toByteArray())
}
}
}
return WriteResponse.Success
Expand Down Expand Up @@ -130,7 +132,9 @@ class BleOtaTransportErrorTest {
): WriteResponse {
backgroundScope.launch {
delay(50.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
}
}
return WriteResponse.Success
}
Expand Down Expand Up @@ -204,15 +208,19 @@ class BleOtaTransportErrorTest {
): WriteResponse {
backgroundScope.launch {
delay(50.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
}
}
return WriteResponse.Success
}

override fun onWriteCommand(characteristic: MockRemoteCharacteristic, value: ByteArray) {
backgroundScope.launch {
delay(10.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "ACK\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "ACK\n".toByteArray())
}
}
}
}
Expand Down Expand Up @@ -252,7 +260,9 @@ class BleOtaTransportErrorTest {
// Setup final response to be a Hash Mismatch error after chunks are sent
backgroundScope.launch {
delay(1000.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "ERR Hash Mismatch\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "ERR Hash Mismatch\n".toByteArray())
}
}

val data = ByteArray(1024) { it.toByte() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ class BleOtaTransportNordicMockTest {
}
backgroundScope.launch(testDispatcher) {
delay(50.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
}
}
}
return WriteResponse.Success
Expand All @@ -106,12 +108,16 @@ class BleOtaTransportNordicMockTest {
println("Mock: Received chunk size=${value.size}, total=$currentTotal/$expected")
backgroundScope.launch(testDispatcher) {
delay(5.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "ACK\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "ACK\n".toByteArray())
}

if (currentTotal >= expected && expected > 0) {
delay(10.milliseconds)
println("Mock: Sending final OK")
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ class BleOtaTransportTest {
backgroundScope.launch(testDispatcher) {
// Use a very small delay to simulate high speed
delay(1.milliseconds)
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
if (otaPeripheral.isConnected) {
otaPeripheral.simulateValueUpdate(txCharHandle, "OK\n".toByteArray())
}
}
return WriteResponse.Success
}
Expand Down
Loading