mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(ble): Add support for FromRadioSync characteristic (#4609)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
5d2336c092
commit
96d4027f74
10 changed files with 396 additions and 108 deletions
|
|
@ -22,8 +22,8 @@ import io.mockk.clearMocks
|
|||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.mock.mock
|
||||
|
|
@ -139,7 +139,7 @@ class NordicBleInterfaceRetryTest {
|
|||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
delay(100.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
|
|
@ -150,7 +150,7 @@ class NordicBleInterfaceRetryTest {
|
|||
)
|
||||
|
||||
// Wait for connection and stable state
|
||||
delay(2000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
verify(timeout = 5000) { service.onConnect() }
|
||||
|
||||
// Clear initial discovery errors if any (sometimes mock emits empty list initially)
|
||||
|
|
@ -160,8 +160,8 @@ class NordicBleInterfaceRetryTest {
|
|||
val dataToSend = byteArrayOf(0x01, 0x02, 0x03)
|
||||
nordicInterface.handleSendToRadio(dataToSend)
|
||||
|
||||
// Give it time to process retries (500ms delay per retry in code)
|
||||
delay(1500.milliseconds)
|
||||
// Give it time to process retries
|
||||
advanceUntilIdle()
|
||||
|
||||
assert(writeAttempts == 2) { "Should have attempted write twice, but was $writeAttempts" }
|
||||
assert(writtenValue != null) { "Value should have been eventually written" }
|
||||
|
|
@ -244,7 +244,7 @@ class NordicBleInterfaceRetryTest {
|
|||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
delay(100.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
|
|
@ -255,7 +255,7 @@ class NordicBleInterfaceRetryTest {
|
|||
)
|
||||
|
||||
// Wait for connection
|
||||
delay(2000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
verify(timeout = 5000) { service.onConnect() }
|
||||
|
||||
// Clear initial discovery errors
|
||||
|
|
@ -264,8 +264,8 @@ class NordicBleInterfaceRetryTest {
|
|||
// Trigger write which will fail repeatedly
|
||||
nordicInterface.handleSendToRadio(byteArrayOf(0x01))
|
||||
|
||||
// Wait for all 3 attempts + delays (500ms * 2)
|
||||
delay(2500.milliseconds)
|
||||
// Wait for all attempts
|
||||
advanceUntilIdle()
|
||||
|
||||
assert(writeAttempts == 3) {
|
||||
"Should have attempted write 3 times (initial + 2 retries), but was $writeAttempts"
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import co.touchlab.kermit.Severity
|
|||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.mock.mock
|
||||
|
|
@ -42,6 +42,7 @@ import org.junit.Before
|
|||
import org.junit.Test
|
||||
import org.meshtastic.core.ble.BleError
|
||||
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
|
||||
|
|
@ -146,7 +147,7 @@ class NordicBleInterfaceTest {
|
|||
centralManager.getBondedPeripherals().forEach { println("Found bonded peripheral: ${it.address}") }
|
||||
|
||||
// Give it a moment to stabilize
|
||||
delay(100.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Create the interface
|
||||
println("Creating NordicBleInterface")
|
||||
|
|
@ -160,7 +161,7 @@ class NordicBleInterfaceTest {
|
|||
|
||||
// Wait for connection and discovery
|
||||
println("Waiting for connection...")
|
||||
delay(2000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
println("Verifying onConnect...")
|
||||
verify(timeout = 5000) { service.onConnect() }
|
||||
|
|
@ -173,13 +174,13 @@ class NordicBleInterfaceTest {
|
|||
otaPeripheral.simulateValueUpdate(fromNumHandle, byteArrayOf(0x01))
|
||||
|
||||
// Wait for drain to start
|
||||
delay(500.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Simulate a log radio notification
|
||||
val logData = "test log".toByteArray()
|
||||
otaPeripheral.simulateValueUpdate(logRadioHandle, logData)
|
||||
|
||||
delay(500.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Explicitly stub handleFromRadio just in case relaxed mock fails
|
||||
io.mockk.every { service.handleFromRadio(any()) } returns Unit
|
||||
|
|
@ -281,7 +282,7 @@ class NordicBleInterfaceTest {
|
|||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
delay(100.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
|
|
@ -292,7 +293,7 @@ class NordicBleInterfaceTest {
|
|||
)
|
||||
|
||||
// Wait for connection
|
||||
delay(1000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
verify(timeout = 2000) { service.onConnect() }
|
||||
|
||||
// Test writing
|
||||
|
|
@ -300,7 +301,7 @@ class NordicBleInterfaceTest {
|
|||
nordicInterface.handleSendToRadio(dataToSend)
|
||||
|
||||
// Give it time to process
|
||||
delay(500.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
assert(writtenValue != null) { "Value should have been written" }
|
||||
assert(writtenValue!!.contentEquals(dataToSend)) {
|
||||
|
|
@ -374,7 +375,7 @@ class NordicBleInterfaceTest {
|
|||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
delay(100.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
|
|
@ -385,7 +386,7 @@ class NordicBleInterfaceTest {
|
|||
)
|
||||
|
||||
// Wait for connection
|
||||
delay(1000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
verify(timeout = 2000) { service.onConnect() }
|
||||
|
||||
// Find the connected peripheral from CentralManager to trigger disconnect
|
||||
|
|
@ -395,7 +396,7 @@ class NordicBleInterfaceTest {
|
|||
connectedPeripheral.disconnect()
|
||||
|
||||
// Wait for disconnect event propagation
|
||||
delay(1000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Verify onDisconnect was called on the service
|
||||
// NordicBleInterface calls onDisconnect(BleError.Disconnected)
|
||||
|
|
@ -465,7 +466,7 @@ class NordicBleInterfaceTest {
|
|||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
delay(100.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
|
|
@ -476,7 +477,7 @@ class NordicBleInterfaceTest {
|
|||
)
|
||||
|
||||
// Wait for connection and eventual failure
|
||||
delay(1000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Verify that discovery failed
|
||||
verify { service.onDisconnect(any<BleError.DiscoveryFailed>()) }
|
||||
|
|
@ -551,7 +552,7 @@ class NordicBleInterfaceTest {
|
|||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
delay(1000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
|
|
@ -562,7 +563,7 @@ class NordicBleInterfaceTest {
|
|||
)
|
||||
|
||||
// Wait for connection
|
||||
delay(1000.milliseconds)
|
||||
advanceUntilIdle()
|
||||
verify(timeout = 2000) { service.onConnect() }
|
||||
|
||||
// Trigger write which will fail
|
||||
|
|
@ -570,11 +571,99 @@ class NordicBleInterfaceTest {
|
|||
|
||||
// Wait for error propagation (retries take time!)
|
||||
// 3 attempts with 500ms delay between them = ~1000ms+
|
||||
delay(2500.milliseconds)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Verify onDisconnect was called with error
|
||||
verify { service.onDisconnect(any<BleError>()) }
|
||||
|
||||
nordicInterface.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromRadioSync flow prefers Indicate characteristic`() = runTest(testDispatcher) {
|
||||
val mockEnvironment = MockAndroidEnvironment.Api31(isBluetoothEnabled = true)
|
||||
val centralManager = CentralManager.mock(mockEnvironment, scope = backgroundScope)
|
||||
val service = mockk<RadioInterfaceService>(relaxed = true)
|
||||
|
||||
var syncCharHandle: Int = -1
|
||||
val payload = byteArrayOf(0xDE.toByte(), 0xAD.toByte())
|
||||
|
||||
val eventHandler =
|
||||
object : PeripheralSpecEventHandler {
|
||||
override fun onConnectionRequest(preferredPhy: List<no.nordicsemi.kotlin.ble.core.Phy>) =
|
||||
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_Sync")
|
||||
}
|
||||
connectable(
|
||||
name = "Meshtastic_Sync",
|
||||
isBonded = true,
|
||||
eventHandler = eventHandler,
|
||||
cachedServices = {
|
||||
Service(uuid = SERVICE_UUID) {
|
||||
Characteristic(
|
||||
uuid = TORADIO_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.WRITE),
|
||||
permission = Permission.WRITE,
|
||||
)
|
||||
Characteristic(
|
||||
uuid = FROMNUM_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.NOTIFY),
|
||||
permission = Permission.READ,
|
||||
)
|
||||
Characteristic(
|
||||
uuid = FROMRADIO_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.READ),
|
||||
permission = Permission.READ,
|
||||
)
|
||||
Characteristic(
|
||||
uuid = LOGRADIO_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.NOTIFY),
|
||||
permission = Permission.READ,
|
||||
)
|
||||
// NEW: Provide the Sync characteristic
|
||||
syncCharHandle =
|
||||
Characteristic(
|
||||
uuid = FROMRADIOSYNC_CHARACTERISTIC,
|
||||
properties = setOf(CharacteristicProperty.INDICATE),
|
||||
permission = Permission.READ,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
centralManager.simulatePeripherals(listOf(peripheralSpec))
|
||||
advanceUntilIdle()
|
||||
|
||||
val nordicInterface =
|
||||
NordicBleInterface(
|
||||
serviceScope = this,
|
||||
centralManager = centralManager,
|
||||
service = service,
|
||||
address = address,
|
||||
)
|
||||
|
||||
// Wait for connection and discovery
|
||||
advanceUntilIdle()
|
||||
verify(timeout = 2000) { service.onConnect() }
|
||||
|
||||
// Simulate an indication from FROMRADIOSYNC
|
||||
peripheralSpec.simulateValueUpdate(syncCharHandle, payload)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Verify handleFromRadio was called directly with the payload
|
||||
verify(timeout = 2000) { service.handleFromRadio(p = payload) }
|
||||
|
||||
nordicInterface.close()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue