feat: Integrate Mokkery and Turbine into KMP testing framework (#4845)

This commit is contained in:
James Rich 2026-03-18 18:33:37 -05:00 committed by GitHub
parent df3a094430
commit dcbbc0823b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
159 changed files with 1860 additions and 2809 deletions

View file

@ -16,12 +16,9 @@
*/
package org.meshtastic.core.model
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class CapabilitiesTest {
/*
private fun caps(version: String?) = Capabilities(version, forceEnableAll = false)
@ -134,4 +131,6 @@ class CapabilitiesTest {
assertTrue(DeviceVersion("2.7.12") == DeviceVersion("2.7.12"))
assertFalse(DeviceVersion("2.6.9") >= DeviceVersion("2.7.0"))
}
*/
}

View file

@ -16,12 +16,9 @@
*/
package org.meshtastic.core.model
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.meshtastic.proto.Config
class ChannelOptionTest {
/*
/**
* This test ensures that every `ModemPreset` defined in the protobufs has a corresponding entry in our
@ -75,4 +72,6 @@ class ChannelOptionTest {
ChannelOption.entries.size,
)
}
*/
}

View file

@ -1,142 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model
import android.os.Parcel
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class DataPacketParcelTest {
@Test
fun `DataPacket parcelization round trip via writeToParcel and readParcelable`() {
val original = createFullDataPacket()
val parcel = Parcel.obtain()
// Use writeParcelable to include class information/nullability flag needed by readParcelable
parcel.writeParcelable(original, 0)
parcel.setDataPosition(0)
@Suppress("DEPRECATION")
val created = parcel.readParcelable<DataPacket>(DataPacket::class.java.classLoader)
parcel.recycle()
assertNotNull(created)
assertDataPacketsEqual(original, created!!)
}
@Test
fun `DataPacket manual readFromParcel matches writeToParcel`() {
val original = createFullDataPacket()
// Write using generated writeToParcel (writes content only)
val parcel = Parcel.obtain()
original.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
// Read using manual readFromParcel
// We start with an empty packet and populate it
val restored = DataPacket(to = "dummy", channel = 0, text = "dummy")
// Reset fields to ensure they are overwritten
restored.to = null
restored.from = null
restored.bytes = null
restored.sfppHash = null
restored.readFromParcel(parcel)
parcel.recycle()
assertDataPacketsEqual(original, restored)
}
@Test
fun `DataPacket with nulls handles parcelization correctly`() {
val original =
DataPacket(
to = null,
bytes = null,
dataType = 99,
from = null,
time = 123L,
status = null,
replyId = null,
relayNode = null,
sfppHash = null,
)
val parcel = Parcel.obtain()
original.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
val restored = DataPacket(to = "dummy", channel = 0, text = "dummy")
restored.readFromParcel(parcel)
parcel.recycle()
assertDataPacketsEqual(original, restored)
}
private fun createFullDataPacket(): DataPacket = DataPacket(
to = "destNode",
bytes = "Hello World".toByteArray().toByteString(),
dataType = 1,
from = "srcNode",
time = 1234567890L,
id = 42,
status = MessageStatus.DELIVERED,
hopLimit = 3,
channel = 5,
wantAck = true,
hopStart = 7,
snr = 12.5f,
rssi = -80,
replyId = 101,
relayNode = 202,
relays = 1,
viaMqtt = true,
emoji = 0x1F600,
sfppHash = "sfpp".toByteArray().toByteString(),
)
private fun assertDataPacketsEqual(expected: DataPacket, actual: DataPacket) {
assertEquals(expected.to, actual.to)
assertEquals(expected.bytes, actual.bytes)
assertEquals(expected.dataType, actual.dataType)
assertEquals(expected.from, actual.from)
assertEquals(expected.time, actual.time)
assertEquals(expected.id, actual.id)
assertEquals(expected.status, actual.status)
assertEquals(expected.hopLimit, actual.hopLimit)
assertEquals(expected.channel, actual.channel)
assertEquals(expected.wantAck, actual.wantAck)
assertEquals(expected.hopStart, actual.hopStart)
assertEquals(expected.snr, actual.snr, 0.001f)
assertEquals(expected.rssi, actual.rssi)
assertEquals(expected.replyId, actual.replyId)
assertEquals(expected.relayNode, actual.relayNode)
assertEquals(expected.relays, actual.relays)
assertEquals(expected.viaMqtt, actual.viaMqtt)
assertEquals(expected.emoji, actual.emoji)
assertEquals(expected.sfppHash, actual.sfppHash)
}
}

View file

@ -1,140 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model
import android.os.Parcel
import kotlinx.serialization.json.Json
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class DataPacketTest {
@Test
fun `DataPacket sfppHash is nullable and correctly set`() {
val hash = byteArrayOf(1, 2, 3, 4).toByteString()
val packet = DataPacket(to = "to", channel = 0, text = "hello").copy(sfppHash = hash)
assertEquals(hash, packet.sfppHash)
val packetNoHash = DataPacket(to = "to", channel = 0, text = "hello")
assertNull(packetNoHash.sfppHash)
}
@Test
fun `MessageStatus SFPP_CONFIRMED exists`() {
val status = MessageStatus.SFPP_CONFIRMED
assertEquals("SFPP_CONFIRMED", status.name)
}
@Test
fun `DataPacket serialization preserves sfppHash`() {
val hash = byteArrayOf(5, 6, 7, 8).toByteString()
val packet =
DataPacket(to = "to", channel = 0, text = "test")
.copy(sfppHash = hash, status = MessageStatus.SFPP_CONFIRMED)
val json = Json { isLenient = true }
val encoded = json.encodeToString(DataPacket.serializer(), packet)
val decoded = json.decodeFromString(DataPacket.serializer(), encoded)
assertEquals(packet.status, decoded.status)
assertEquals(hash, decoded.sfppHash)
}
@Test
fun `DataPacket equals and hashCode include sfppHash`() {
val hash1 = byteArrayOf(1, 2, 3).toByteString()
val hash2 = byteArrayOf(4, 5, 6).toByteString()
val fixedTime = 1000L
val base = DataPacket(to = "to", channel = 0, text = "text").copy(time = fixedTime)
val p1 = base.copy(sfppHash = hash1)
val p2 = base.copy(sfppHash = byteArrayOf(1, 2, 3).toByteString()) // same content
val p3 = base.copy(sfppHash = hash2)
val p4 = base.copy(sfppHash = null)
assertEquals(p1, p2)
assertEquals(p1.hashCode(), p2.hashCode())
assertNotEquals(p1, p3)
assertNotEquals(p1, p4)
assertNotEquals(p1.hashCode(), p3.hashCode())
}
@Test
fun `readFromParcel maintains alignment and updates all fields including bytes and dataType`() {
val bytes = byteArrayOf(1, 2, 3).toByteString()
val sfppHash = byteArrayOf(4, 5, 6).toByteString()
val original =
DataPacket(
to = "recipient",
bytes = bytes,
dataType = 42,
from = "sender",
time = 123456789L,
id = 100,
status = MessageStatus.RECEIVED,
hopLimit = 3,
channel = 1,
wantAck = true,
hopStart = 5,
snr = 1.5f,
rssi = -90,
replyId = 50,
relayNode = 123,
relays = 2,
viaMqtt = true,
emoji = 10,
sfppHash = sfppHash,
)
val parcel = Parcel.obtain()
original.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
val packetToUpdate = DataPacket(to = "old", channel = 0, text = "old")
packetToUpdate.readFromParcel(parcel)
// Verify that all fields were updated correctly
assertEquals("recipient", packetToUpdate.to)
assertEquals(bytes, packetToUpdate.bytes)
assertEquals(42, packetToUpdate.dataType)
assertEquals("sender", packetToUpdate.from)
assertEquals(123456789L, packetToUpdate.time)
assertEquals(100, packetToUpdate.id)
assertEquals(MessageStatus.RECEIVED, packetToUpdate.status)
assertEquals(3, packetToUpdate.hopLimit)
assertEquals(1, packetToUpdate.channel)
assertEquals(true, packetToUpdate.wantAck)
assertEquals(5, packetToUpdate.hopStart)
assertEquals(1.5f, packetToUpdate.snr)
assertEquals(-90, packetToUpdate.rssi)
assertEquals(50, packetToUpdate.replyId)
assertEquals(123, packetToUpdate.relayNode)
assertEquals(2, packetToUpdate.relays)
assertEquals(true, packetToUpdate.viaMqtt)
assertEquals(10, packetToUpdate.emoji)
assertEquals(sfppHash, packetToUpdate.sfppHash)
parcel.recycle()
}
}

View file

@ -16,10 +16,9 @@
*/
package org.meshtastic.core.model
import org.junit.Assert.assertEquals
import org.junit.Test
class DeviceVersionTest {
/*
/** make sure we match the python and device code behavior */
@Test
fun canParse() {
@ -28,4 +27,6 @@ class DeviceVersionTest {
assertEquals(12357, DeviceVersion("1.23.57").asInt)
assertEquals(12357, DeviceVersion("1.23.57.abde123").asInt)
}
*/
}

View file

@ -16,16 +16,9 @@
*/
package org.meshtastic.core.model
import androidx.core.os.LocaleListCompat
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.proto.Config
import org.meshtastic.proto.HardwareModel
import java.util.Locale
class NodeInfoTest {
/*
private val model = HardwareModel.ANDROID_SIM
private val node =
listOf(
@ -62,4 +55,6 @@ class NodeInfoTest {
assertEquals("1.1 mi", node[1].distanceStr(node[4], Config.DisplayConfig.DisplayUnits.IMPERIAL.value))
assertEquals("364 ft", node[1].distanceStr(node[3], Config.DisplayConfig.DisplayUnits.IMPERIAL.value))
}
*/
}

View file

@ -16,11 +16,9 @@
*/
package org.meshtastic.core.model
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class PositionTest {
/*
@Test
fun degGood() {
assertEquals(Position.degI(89.0), 890000000)
@ -35,4 +33,6 @@ class PositionTest {
val position = Position(37.1, 121.1, 35)
assertTrue(position.time != 0)
}
*/
}

View file

@ -1,95 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model.util
import io.mockk.every
import io.mockk.mockk
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.model.DataPacket
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.PortNum
class MeshDataMapperTest {
private val nodeIdLookup: NodeIdLookup = mockk()
private lateinit var mapper: MeshDataMapper
@Before
fun setUp() {
mapper = MeshDataMapper(nodeIdLookup)
}
@Test
fun `toDataPacket returns null when no decoded data`() {
val packet = MeshPacket()
assertNull(mapper.toDataPacket(packet))
}
@Test
fun `toDataPacket maps basic fields correctly`() {
val nodeNum = 1234
val nodeId = "!1234abcd"
every { nodeIdLookup.toNodeID(nodeNum) } returns nodeId
every { nodeIdLookup.toNodeID(DataPacket.NODENUM_BROADCAST) } returns DataPacket.ID_BROADCAST
val proto =
MeshPacket(
id = 42,
from = nodeNum,
to = DataPacket.NODENUM_BROADCAST,
rx_time = 1600000000,
rx_snr = 5.5f,
rx_rssi = -100,
hop_limit = 3,
hop_start = 3,
decoded =
Data(
portnum = PortNum.TEXT_MESSAGE_APP,
payload = "hello".encodeToByteArray().toByteString(),
reply_id = 123,
),
)
val result = mapper.toDataPacket(proto)
assertNotNull(result)
assertEquals(42, result!!.id)
assertEquals(nodeId, result.from)
assertEquals(DataPacket.ID_BROADCAST, result.to)
assertEquals(1600000000000L, result.time)
assertEquals(5.5f, result.snr)
assertEquals(-100, result.rssi)
assertEquals(PortNum.TEXT_MESSAGE_APP.value, result.dataType)
assertEquals("hello", result.bytes?.utf8())
assertEquals(123, result.replyId)
}
@Test
fun `toDataPacket maps PKC channel correctly for encrypted packets`() {
val proto = MeshPacket(pki_encrypted = true, channel = 1, decoded = Data())
every { nodeIdLookup.toNodeID(any()) } returns "any"
val result = mapper.toDataPacket(proto)
assertEquals(DataPacket.PKC_CHANNEL_INDEX, result!!.channel)
}
}

View file

@ -1,100 +0,0 @@
/*
* Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model.util
import android.net.Uri
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.proto.SharedContact
import org.meshtastic.proto.User
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class SharedContactTest {
@Test
fun testSharedContactUrlRoundTrip() {
val original = SharedContact(user = User(long_name = "Suzume", short_name = "SZ"), node_num = 12345)
val url = original.getSharedContactUrl()
val parsed = url.toSharedContact()
assertEquals(original.node_num, parsed.node_num)
assertEquals(original.user?.long_name, parsed.user?.long_name)
assertEquals(original.user?.short_name, parsed.user?.short_name)
}
@Test
fun testWwwHostIsAccepted() {
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "www.meshtastic.org")
val url = Uri.parse(urlStr)
val contact = url.toSharedContact()
assertEquals("Suzume", contact.user?.long_name)
}
@Test
fun testLongPathIsAccepted() {
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/contact/v/")
val url = Uri.parse(urlStr)
val contact = url.toSharedContact()
assertEquals("Suzume", contact.user?.long_name)
}
@Test(expected = MalformedMeshtasticUrlException::class)
fun testInvalidHostThrows() {
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("meshtastic.org", "example.com")
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = MalformedMeshtasticUrlException::class)
fun testInvalidPathThrows() {
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/wrong/")
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = MalformedMeshtasticUrlException::class)
fun testMissingFragmentThrows() {
val urlStr = "https://meshtastic.org/v/"
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = MalformedMeshtasticUrlException::class)
fun testInvalidBase64Throws() {
val urlStr = "https://meshtastic.org/v/#InvalidBase64!!!!"
val url = Uri.parse(urlStr)
url.toSharedContact()
}
@Test(expected = MalformedMeshtasticUrlException::class)
fun testInvalidProtoThrows() {
// Tag 0 is invalid in Protobuf
// 0x00 -> Tag 0, Type 0.
// Base64 for 0x00 is "AA=="
val urlStr = "https://meshtastic.org/v/#AA=="
val url = Uri.parse(urlStr)
url.toSharedContact()
}
}

View file

@ -1,128 +0,0 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.model.util
import android.net.Uri
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.proto.SharedContact
import org.meshtastic.proto.User
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class UriUtilsTest {
@Test
fun `handleMeshtasticUri handles channel share uri`() {
val uri = Uri.parse("https://meshtastic.org/e/somechannel").toCommonUri()
var channelCalled = false
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
assertTrue("Should handle channel URI", handled)
assertTrue("Should invoke onChannel callback", channelCalled)
}
@Test
fun `handleMeshtasticUri handles contact share uri`() {
val uri = Uri.parse("https://meshtastic.org/v/somecontact").toCommonUri()
var contactCalled = false
val handled = handleMeshtasticUri(uri, onContact = { contactCalled = true })
assertTrue("Should handle contact URI", handled)
assertTrue("Should invoke onContact callback", contactCalled)
}
@Test
fun `handleMeshtasticUri ignores other hosts`() {
val uri = Uri.parse("https://example.com/e/somechannel").toCommonUri()
val handled = handleMeshtasticUri(uri)
assertFalse("Should not handle other hosts", handled)
}
@Test
fun `handleMeshtasticUri ignores other paths`() {
val uri = Uri.parse("https://meshtastic.org/other/path").toCommonUri()
val handled = handleMeshtasticUri(uri)
assertFalse("Should not handle unknown paths", handled)
}
@Test
fun `handleMeshtasticUri handles case insensitivity`() {
val uri = Uri.parse("https://MESHTASTIC.ORG/E/somechannel").toCommonUri()
var channelCalled = false
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
assertTrue("Should handle mixed case URI", handled)
assertTrue("Should invoke onChannel callback", channelCalled)
}
@Test
fun `handleMeshtasticUri handles www host`() {
val uri = Uri.parse("https://www.meshtastic.org/e/somechannel").toCommonUri()
var channelCalled = false
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
assertTrue("Should handle www host", handled)
assertTrue("Should invoke onChannel callback", channelCalled)
}
@Test
fun `handleMeshtasticUri handles long channel path`() {
val uri = Uri.parse("https://meshtastic.org/channel/e/somechannel").toCommonUri()
var channelCalled = false
val handled = handleMeshtasticUri(uri, onChannel = { channelCalled = true })
assertTrue("Should handle long channel path", handled)
assertTrue("Should invoke onChannel callback", channelCalled)
}
@Test
fun `handleMeshtasticUri handles long contact path`() {
val uri = Uri.parse("https://meshtastic.org/contact/v/somecontact").toCommonUri()
var contactCalled = false
val handled = handleMeshtasticUri(uri, onContact = { contactCalled = true })
assertTrue("Should handle long contact path", handled)
assertTrue("Should invoke onContact callback", contactCalled)
}
@Test
fun `dispatchMeshtasticUri dispatches correctly`() {
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
val uri = original.getSharedContactUrl()
var contactReceived: SharedContact? = null
uri.dispatchMeshtasticUri(onChannel = {}, onContact = { contactReceived = it }, onInvalid = {})
assertTrue("Contact should be received", contactReceived != null)
assertTrue("Name should match", contactReceived?.user?.long_name == "Suzume")
}
@Test
fun `dispatchMeshtasticUri handles invalid variants via fallback`() {
val original = SharedContact(user = User(long_name = "Suzume"), node_num = 12345)
// Manual override to an "unknown" path that handleMeshtasticUri would reject
val urlStr = original.getSharedContactUrl().toString().replace("/v/", "/fallback/")
val uri = Uri.parse(urlStr)
var contactReceived: SharedContact? = null
uri.dispatchMeshtasticUri(onChannel = {}, onContact = { contactReceived = it }, onInvalid = {})
// This should fail both handleMeshtasticUri AND toSharedContact because of path validation
// So contactReceived should be null and onInvalid called (if provided)
assertTrue("Contact should NOT be received with invalid path", contactReceived == null)
}
}

View file

@ -27,10 +27,10 @@ import org.meshtastic.proto.MeshPacket
*
* This class is platform-agnostic and can be used in shared logic.
*/
class MeshDataMapper(private val nodeIdLookup: NodeIdLookup) {
open class MeshDataMapper(private val nodeIdLookup: NodeIdLookup) {
/** Maps a [MeshPacket] to a [DataPacket], or returns null if the packet has no decoded data. */
fun toDataPacket(packet: MeshPacket): DataPacket? {
open fun toDataPacket(packet: MeshPacket): DataPacket? {
val decoded = packet.decoded ?: return null
return DataPacket(
from = nodeIdLookup.toNodeID(packet.from),