mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-07-09 04:45:40 -04:00
feat(discovery): Mesh Beacon client with iOS 014-mesh-beacons parity (#6097)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -206,7 +206,7 @@ class MeshDataHandlerImpl(
|
||||
}
|
||||
|
||||
PortNum.MESH_BEACON_APP -> {
|
||||
handleMeshBeacon(packet)
|
||||
handleMeshBeacon(packet, myNodeNum)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
@@ -224,12 +224,16 @@ class MeshDataHandlerImpl(
|
||||
* low-priority notification when the invitation is first seen (not on every periodic re-broadcast). Only beacons
|
||||
* carrying a join offer (a channel) are actionable; message-only beacons are ignored.
|
||||
*/
|
||||
private fun handleMeshBeacon(packet: MeshPacket) {
|
||||
@Suppress("ReturnCount")
|
||||
private fun handleMeshBeacon(packet: MeshPacket, myNodeNum: Int) {
|
||||
// Ignore our own beacons (spec FR-001) — once broadcast is enabled a node that also listens would self-notify.
|
||||
if (packet.from == myNodeNum) return
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val beacon = MeshBeacon.ADAPTER.decodeOrNull(payload, Logger)
|
||||
// Only actionable beacons (carrying a channel offer) that we haven't already seen warrant a notification.
|
||||
if (beacon?.offer_channel == null) return
|
||||
val offer = MeshBeaconOffer(fromNodeNum = packet.from, beacon = beacon)
|
||||
val offer =
|
||||
MeshBeaconOffer(fromNodeNum = packet.from, beacon = beacon, snr = packet.rx_snr, rssi = packet.rx_rssi)
|
||||
if (meshBeaconRepository.add(offer)) {
|
||||
scope.launch {
|
||||
notificationManager.dispatch(
|
||||
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
@@ -40,6 +41,7 @@ import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeAddress
|
||||
import org.meshtastic.core.model.util.MeshDataMapper
|
||||
import org.meshtastic.core.repository.AdminPacketHandler
|
||||
import org.meshtastic.core.repository.MeshBeaconPrefs
|
||||
import org.meshtastic.core.repository.MeshBeaconRepository
|
||||
import org.meshtastic.core.repository.MeshNotificationManager
|
||||
import org.meshtastic.core.repository.MessageFilter
|
||||
@@ -91,7 +93,6 @@ class MeshDataHandlerTest {
|
||||
private val storeForwardHandler: StoreForwardPacketHandler = mock(MockMode.autofill)
|
||||
private val telemetryHandler: TelemetryPacketHandler = mock(MockMode.autofill)
|
||||
private val adminPacketHandler: AdminPacketHandler = mock(MockMode.autofill)
|
||||
private val meshBeaconRepository = MeshBeaconRepository()
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val testScope = TestScope(testDispatcher)
|
||||
@@ -101,6 +102,18 @@ class MeshDataHandlerTest {
|
||||
// still drives it; cancelled in tearDown.
|
||||
private val geofenceScope = CoroutineScope(testDispatcher)
|
||||
|
||||
// Real repository over an in-memory prefs fake — its persistence write-through is exercised without a DataStore.
|
||||
private val fakeBeaconPrefs =
|
||||
object : MeshBeaconPrefs {
|
||||
private val flow = MutableStateFlow<List<String>>(emptyList())
|
||||
override val storedBeacons: StateFlow<List<String>> = flow
|
||||
|
||||
override fun setStoredBeacons(records: List<String>) {
|
||||
flow.value = records
|
||||
}
|
||||
}
|
||||
private val meshBeaconRepository = MeshBeaconRepository(fakeBeaconPrefs, geofenceScope)
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
geofenceScope.cancel()
|
||||
@@ -416,6 +429,23 @@ class MeshDataHandlerTest {
|
||||
assertEquals(0, meshBeaconRepository.offers.value.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `our own mesh beacon is ignored`() {
|
||||
// Spec FR-001: ignore beacons from the scanning node itself (else a listen+broadcast node self-notifies).
|
||||
val beacon = MeshBeacon(message = "Join us", offer_channel = ChannelSettings(name = "PartyNet"))
|
||||
val packet =
|
||||
MeshPacket(
|
||||
from = 123, // == myNodeNum below
|
||||
decoded = Data(portnum = PortNum.MESH_BEACON_APP, payload = beacon.encode().toByteString()),
|
||||
)
|
||||
every { dataMapper.toDataPacket(packet) } returns
|
||||
DataPacket(from = "!self", bytes = beacon.encode().toByteString(), dataType = PortNum.MESH_BEACON_APP.value)
|
||||
|
||||
handler.handleReceivedData(packet, 123)
|
||||
|
||||
assertEquals(0, meshBeaconRepository.offers.value.size)
|
||||
}
|
||||
|
||||
// --- Store-and-Forward handling ---
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user