mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat/decoupling (#4685)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
40244f8337
commit
2c49db8041
254 changed files with 5132 additions and 2666 deletions
|
|
@ -31,7 +31,7 @@ class ChannelSetTest {
|
|||
val url = Uri.parse("https://meshtastic.org/e/#CgMSAQESBggBQANIAQ")
|
||||
val cs = url.toChannelSet()
|
||||
Assert.assertEquals("LongFast", cs.primaryChannel!!.name)
|
||||
Assert.assertEquals(url, cs.getChannelUrl(false))
|
||||
Assert.assertEquals(url.toString(), cs.getChannelUrl(false).toString())
|
||||
}
|
||||
|
||||
/** validate against the host or path in a case-insensitive way */
|
||||
|
|
|
|||
|
|
@ -56,11 +56,43 @@ class SharedContactTest {
|
|||
assertEquals("Suzume", contact.user?.long_name)
|
||||
}
|
||||
|
||||
@Test(expected = java.net.MalformedURLException::class)
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,9 +103,6 @@ enum class RegionInfo(
|
|||
val freqEnd: Float,
|
||||
val wideLora: Boolean = false,
|
||||
) {
|
||||
/** This needs to be last. Same as US. */
|
||||
UNSET(RegionCode.UNSET, "Please set a region", 902.0f, 928.0f),
|
||||
|
||||
/**
|
||||
* United States
|
||||
*
|
||||
|
|
@ -288,6 +285,9 @@ enum class RegionInfo(
|
|||
* @see [Firmware Issue #7399](https://github.com/meshtastic/firmware/pull/7399)
|
||||
*/
|
||||
BR_902(RegionCode.BR_902, "Brazil 902MHz", 902.0f, 907.5f, wideLora = false),
|
||||
|
||||
/** This needs to be last. Same as US. */
|
||||
UNSET(RegionCode.UNSET, "Please set a region", 902.0f, 928.0f),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -32,3 +32,12 @@ data class Contact(
|
|||
val isUnmessageable: Boolean,
|
||||
val nodeColors: Pair<Int, Int>? = null,
|
||||
) : CommonParcelable
|
||||
|
||||
data class ContactSettings(
|
||||
val contactKey: String,
|
||||
val muteUntil: Long = 0L,
|
||||
val lastReadMessageUuid: Long? = null,
|
||||
val lastReadMessageTimestamp: Long? = null,
|
||||
val filteringDisabled: Boolean = false,
|
||||
val isMuted: Boolean = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/** Address identifiers for all supported radio backend implementations. */
|
||||
enum class InterfaceId(val id: Char) {
|
||||
BLUETOOTH('x'),
|
||||
MOCK('m'),
|
||||
NOP('n'),
|
||||
SERIAL('s'),
|
||||
TCP('t'),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun forIdChar(id: Char): InterfaceId? = entries.firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/** Represents activity on the mesh network. */
|
||||
sealed class MeshActivity {
|
||||
/** Data is being sent to the radio. */
|
||||
data object Send : MeshActivity()
|
||||
|
||||
/** Data is being received from the radio. */
|
||||
data object Receive : MeshActivity()
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.delivery_confirmed
|
||||
import org.meshtastic.core.resources.error
|
||||
import org.meshtastic.core.resources.message_delivery_status
|
||||
import org.meshtastic.core.resources.message_status_enroute
|
||||
import org.meshtastic.core.resources.message_status_queued
|
||||
import org.meshtastic.core.resources.message_status_sfpp_confirmed
|
||||
import org.meshtastic.core.resources.message_status_sfpp_routing
|
||||
import org.meshtastic.core.resources.routing_error_admin_bad_session_key
|
||||
import org.meshtastic.core.resources.routing_error_admin_public_key_unauthorized
|
||||
import org.meshtastic.core.resources.routing_error_bad_request
|
||||
import org.meshtastic.core.resources.routing_error_duty_cycle_limit
|
||||
import org.meshtastic.core.resources.routing_error_got_nak
|
||||
import org.meshtastic.core.resources.routing_error_max_retransmit
|
||||
import org.meshtastic.core.resources.routing_error_no_channel
|
||||
import org.meshtastic.core.resources.routing_error_no_interface
|
||||
import org.meshtastic.core.resources.routing_error_no_response
|
||||
import org.meshtastic.core.resources.routing_error_no_route
|
||||
import org.meshtastic.core.resources.routing_error_none
|
||||
import org.meshtastic.core.resources.routing_error_not_authorized
|
||||
import org.meshtastic.core.resources.routing_error_pki_failed
|
||||
import org.meshtastic.core.resources.routing_error_pki_send_fail_public_key
|
||||
import org.meshtastic.core.resources.routing_error_pki_unknown_pubkey
|
||||
import org.meshtastic.core.resources.routing_error_rate_limit_exceeded
|
||||
import org.meshtastic.core.resources.routing_error_timeout
|
||||
import org.meshtastic.core.resources.routing_error_too_large
|
||||
import org.meshtastic.core.resources.unrecognized
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Routing
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun getStringResFrom(routingError: Int): StringResource = when (routingError) {
|
||||
Routing.Error.NONE.value -> Res.string.routing_error_none
|
||||
Routing.Error.NO_ROUTE.value -> Res.string.routing_error_no_route
|
||||
Routing.Error.GOT_NAK.value -> Res.string.routing_error_got_nak
|
||||
Routing.Error.TIMEOUT.value -> Res.string.routing_error_timeout
|
||||
Routing.Error.NO_INTERFACE.value -> Res.string.routing_error_no_interface
|
||||
Routing.Error.MAX_RETRANSMIT.value -> Res.string.routing_error_max_retransmit
|
||||
Routing.Error.NO_CHANNEL.value -> Res.string.routing_error_no_channel
|
||||
Routing.Error.TOO_LARGE.value -> Res.string.routing_error_too_large
|
||||
Routing.Error.NO_RESPONSE.value -> Res.string.routing_error_no_response
|
||||
Routing.Error.DUTY_CYCLE_LIMIT.value -> Res.string.routing_error_duty_cycle_limit
|
||||
Routing.Error.BAD_REQUEST.value -> Res.string.routing_error_bad_request
|
||||
Routing.Error.NOT_AUTHORIZED.value -> Res.string.routing_error_not_authorized
|
||||
Routing.Error.PKI_FAILED.value -> Res.string.routing_error_pki_failed
|
||||
Routing.Error.PKI_UNKNOWN_PUBKEY.value -> Res.string.routing_error_pki_unknown_pubkey
|
||||
Routing.Error.ADMIN_BAD_SESSION_KEY.value -> Res.string.routing_error_admin_bad_session_key
|
||||
Routing.Error.ADMIN_PUBLIC_KEY_UNAUTHORIZED.value -> Res.string.routing_error_admin_public_key_unauthorized
|
||||
Routing.Error.RATE_LIMIT_EXCEEDED.value -> Res.string.routing_error_rate_limit_exceeded
|
||||
Routing.Error.PKI_SEND_FAIL_PUBLIC_KEY.value -> Res.string.routing_error_pki_send_fail_public_key
|
||||
else -> Res.string.unrecognized
|
||||
}
|
||||
|
||||
data class Message(
|
||||
val uuid: Long,
|
||||
val receivedTime: Long,
|
||||
val node: Node,
|
||||
val text: String,
|
||||
val fromLocal: Boolean,
|
||||
val time: String,
|
||||
val read: Boolean,
|
||||
val status: MessageStatus?,
|
||||
val routingError: Int,
|
||||
val packetId: Int,
|
||||
val emojis: List<Reaction>,
|
||||
val snr: Float,
|
||||
val rssi: Int,
|
||||
val hopsAway: Int,
|
||||
val replyId: Int?,
|
||||
val originalMessage: Message? = null,
|
||||
val viaMqtt: Boolean = false,
|
||||
val relayNode: Int? = null,
|
||||
val relays: Int = 0,
|
||||
val filtered: Boolean = false,
|
||||
/** The transport mechanism this packet arrived over (see [MeshPacket.TransportMechanism]). */
|
||||
val transportMechanism: Int = 0,
|
||||
) {
|
||||
fun getStatusStringRes(): Pair<StringResource, StringResource> {
|
||||
val title = if (routingError > 0) Res.string.error else Res.string.message_delivery_status
|
||||
val text =
|
||||
when (status) {
|
||||
MessageStatus.RECEIVED -> Res.string.delivery_confirmed
|
||||
MessageStatus.QUEUED -> Res.string.message_status_queued
|
||||
MessageStatus.ENROUTE -> Res.string.message_status_enroute
|
||||
MessageStatus.SFPP_ROUTING -> Res.string.message_status_sfpp_routing
|
||||
MessageStatus.SFPP_CONFIRMED -> Res.string.message_status_sfpp_confirmed
|
||||
else -> getStringResFrom(routingError)
|
||||
}
|
||||
return title to text
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
* 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 okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.common.util.GPSFormat
|
||||
import org.meshtastic.core.common.util.bearing
|
||||
import org.meshtastic.core.common.util.latLongToMeter
|
||||
import org.meshtastic.core.model.util.UnitConversions.celsiusToFahrenheit
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.model.util.toDistanceString
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.PowerMetrics
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
/**
|
||||
* Domain model representing a node in the mesh network.
|
||||
*
|
||||
* This class aggregates user information, position data, and hardware metrics.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
data class Node(
|
||||
val num: Int,
|
||||
val metadata: DeviceMetadata? = null,
|
||||
val user: User = User(),
|
||||
val position: Position = Position(),
|
||||
val snr: Float = Float.MAX_VALUE,
|
||||
val rssi: Int = Int.MAX_VALUE,
|
||||
val lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
|
||||
val deviceMetrics: DeviceMetrics = DeviceMetrics(),
|
||||
val channel: Int = 0,
|
||||
val viaMqtt: Boolean = false,
|
||||
val hopsAway: Int = -1,
|
||||
val isFavorite: Boolean = false,
|
||||
val isIgnored: Boolean = false,
|
||||
val isMuted: Boolean = false,
|
||||
val environmentMetrics: EnvironmentMetrics = EnvironmentMetrics(),
|
||||
val powerMetrics: PowerMetrics = PowerMetrics(),
|
||||
val paxcounter: Paxcount = Paxcount(),
|
||||
val publicKey: ByteString? = null,
|
||||
val notes: String = "",
|
||||
val manuallyVerified: Boolean = false,
|
||||
val nodeStatus: String? = null,
|
||||
/** The transport mechanism this node was last heard over (see [MeshPacket.TransportMechanism]). */
|
||||
val lastTransport: Int = 0,
|
||||
) {
|
||||
val capabilities: Capabilities by lazy { Capabilities(metadata?.firmware_version) }
|
||||
|
||||
val isOnline: Boolean
|
||||
get() = lastHeard > onlineTimeThreshold()
|
||||
|
||||
val colors: Pair<Int, Int>
|
||||
get() { // returns foreground and background @ColorInt for each 'num'
|
||||
val r = (num and 0xFF0000) shr 16
|
||||
val g = (num and 0x00FF00) shr 8
|
||||
val b = num and 0x0000FF
|
||||
val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
|
||||
val foreground = if (brightness > 0.5) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
|
||||
val background = (0xFF shl 24) or (r shl 16) or (g shl 8) or b
|
||||
return foreground to background
|
||||
}
|
||||
|
||||
val isUnknownUser
|
||||
get() = user.hw_model == HardwareModel.UNSET
|
||||
|
||||
val hasPKC
|
||||
get() = (publicKey ?: user.public_key)?.size?.let { it > 0 } == true
|
||||
|
||||
val mismatchKey
|
||||
get() = (publicKey ?: user.public_key) == ERROR_BYTE_STRING
|
||||
|
||||
val hasEnvironmentMetrics: Boolean
|
||||
get() = environmentMetrics != EnvironmentMetrics()
|
||||
|
||||
val hasPowerMetrics: Boolean
|
||||
get() = powerMetrics != PowerMetrics()
|
||||
|
||||
val batteryLevel
|
||||
get() = deviceMetrics.battery_level
|
||||
|
||||
val voltage
|
||||
get() = deviceMetrics.voltage
|
||||
|
||||
val batteryStr
|
||||
get() = if ((batteryLevel ?: 0) in 1..100) "$batteryLevel%" else ""
|
||||
|
||||
val latitude
|
||||
get() = (position.latitude_i ?: 0) * 1e-7
|
||||
|
||||
val longitude
|
||||
get() = (position.longitude_i ?: 0) * 1e-7
|
||||
|
||||
private fun hasValidPosition(): Boolean = latitude != 0.0 &&
|
||||
longitude != 0.0 &&
|
||||
(latitude >= -90 && latitude <= 90.0) &&
|
||||
(longitude >= -180 && longitude <= 180)
|
||||
|
||||
val validPosition: Position?
|
||||
get() = position.takeIf { hasValidPosition() }
|
||||
|
||||
// @return distance in meters to some other node (or null if unknown)
|
||||
fun distance(o: Node): Int? = when {
|
||||
validPosition == null || o.validPosition == null -> null
|
||||
else -> latLongToMeter(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
}
|
||||
|
||||
// @return formatted distance string to another node, using the given display units
|
||||
fun distanceStr(o: Node, displayUnits: Config.DisplayConfig.DisplayUnits): String? =
|
||||
distance(o)?.toDistanceString(displayUnits)
|
||||
|
||||
// @return bearing to the other position in degrees
|
||||
fun bearing(o: Node?): Int? = when {
|
||||
validPosition == null || o?.validPosition == null -> null
|
||||
else -> bearing(latitude, longitude, o.latitude, o.longitude).toInt()
|
||||
}
|
||||
|
||||
fun gpsString(): String = GPSFormat.toDec(latitude, longitude)
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun EnvironmentMetrics.getDisplayStrings(isFahrenheit: Boolean): List<String> {
|
||||
val temp =
|
||||
if ((temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
"%.1f°F".format(celsiusToFahrenheit(temperature ?: 0f))
|
||||
} else {
|
||||
"%.1f°C".format(temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val humidity = if ((relative_humidity ?: 0f) != 0f) "%.0f%%".format(relative_humidity) else null
|
||||
val soilTemperatureStr =
|
||||
if ((soil_temperature ?: 0f) != 0f) {
|
||||
if (isFahrenheit) {
|
||||
"%.1f°F".format(celsiusToFahrenheit(soil_temperature ?: 0f))
|
||||
} else {
|
||||
"%.1f°C".format(soil_temperature)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val soilMoistureRange = 0..100
|
||||
val soilMoisture =
|
||||
if ((soil_moisture ?: Int.MIN_VALUE) in soilMoistureRange && (soil_temperature ?: 0f) != 0f) {
|
||||
"%d%%".format(soil_moisture)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val voltage = if ((this.voltage ?: 0f) != 0f) "%.2fV".format(this.voltage) else null
|
||||
val current = if ((current ?: 0f) != 0f) "%.1fmA".format(current) else null
|
||||
val iaq = if ((iaq ?: 0) != 0) "IAQ: $iaq" else null
|
||||
|
||||
return listOfNotNull(
|
||||
paxcounter.getDisplayString(),
|
||||
temp,
|
||||
humidity,
|
||||
soilTemperatureStr,
|
||||
soilMoisture,
|
||||
voltage,
|
||||
current,
|
||||
iaq,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Paxcount.getDisplayString() =
|
||||
"PAX: ${(ble ?: 0) + (wifi ?: 0)} (B:${ble ?: 0}/W:${wifi ?: 0})".takeIf { (ble ?: 0) != 0 || (wifi ?: 0) != 0 }
|
||||
|
||||
fun getTelemetryStrings(isFahrenheit: Boolean = false): List<String> =
|
||||
environmentMetrics.getDisplayStrings(isFahrenheit)
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_ID_SUFFIX_LENGTH = 4
|
||||
private const val RELAY_NODE_SUFFIX_MASK = 0xFF
|
||||
|
||||
val ERROR_BYTE_STRING: ByteString = ByteArray(32) { 0 }.toByteString()
|
||||
|
||||
fun getRelayNode(relayNodeId: Int, nodes: List<Node>, ourNodeNum: Int?): Node? {
|
||||
val relayNodeIdSuffix = relayNodeId and RELAY_NODE_SUFFIX_MASK
|
||||
|
||||
val candidateRelayNodes =
|
||||
nodes.filter {
|
||||
it.num != ourNodeNum &&
|
||||
it.lastHeard != 0 &&
|
||||
(it.num and RELAY_NODE_SUFFIX_MASK) == relayNodeIdSuffix
|
||||
}
|
||||
|
||||
val closestRelayNode =
|
||||
if (candidateRelayNodes.size == 1) {
|
||||
candidateRelayNodes.first()
|
||||
} else {
|
||||
candidateRelayNodes.minByOrNull { it.hopsAway }
|
||||
}
|
||||
|
||||
return closestRelayNode
|
||||
}
|
||||
|
||||
/** Creates a fallback [Node] when the node is not found in the database. */
|
||||
fun createFallback(nodeNum: Int, fallbackNamePrefix: String): Node {
|
||||
val userId = DataPacket.nodeNumToDefaultId(nodeNum)
|
||||
val safeUserId = userId.padStart(DEFAULT_ID_SUFFIX_LENGTH, '0').takeLast(DEFAULT_ID_SUFFIX_LENGTH)
|
||||
val longName = "$fallbackNamePrefix $safeUserId"
|
||||
val defaultUser =
|
||||
User(id = userId, long_name = longName, short_name = safeUserId, hw_model = HardwareModel.UNSET)
|
||||
return Node(num = nodeNum, user = defaultUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Config.DeviceConfig.Role?.isUnmessageableRole(): Boolean = this in
|
||||
listOf(
|
||||
Config.DeviceConfig.Role.REPEATER,
|
||||
Config.DeviceConfig.Role.ROUTER,
|
||||
Config.DeviceConfig.Role.ROUTER_LATE,
|
||||
Config.DeviceConfig.Role.SENSOR,
|
||||
Config.DeviceConfig.Role.TRACKER,
|
||||
Config.DeviceConfig.Role.TAK_TRACKER,
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.node_sort_alpha
|
||||
import org.meshtastic.core.resources.node_sort_channel
|
||||
import org.meshtastic.core.resources.node_sort_distance
|
||||
import org.meshtastic.core.resources.node_sort_hops_away
|
||||
import org.meshtastic.core.resources.node_sort_last_heard
|
||||
import org.meshtastic.core.resources.node_sort_via_favorite
|
||||
import org.meshtastic.core.resources.node_sort_via_mqtt
|
||||
|
||||
enum class NodeSortOption(val sqlValue: String, val stringRes: StringResource) {
|
||||
LAST_HEARD("last_heard", Res.string.node_sort_last_heard),
|
||||
ALPHABETICAL("alpha", Res.string.node_sort_alpha),
|
||||
DISTANCE("distance", Res.string.node_sort_distance),
|
||||
HOPS_AWAY("hops_away", Res.string.node_sort_hops_away),
|
||||
CHANNEL("channel", Res.string.node_sort_channel),
|
||||
VIA_MQTT("via_mqtt", Res.string.node_sort_via_mqtt),
|
||||
VIA_FAVORITE("via_favorite", Res.string.node_sort_via_favorite),
|
||||
}
|
||||
|
|
@ -19,67 +19,299 @@ package org.meshtastic.core.model
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
|
||||
/**
|
||||
* Central interface for controlling the radio and mesh network.
|
||||
*
|
||||
* This component provides an abstraction over the underlying communication transport (e.g., BLE, Serial, TCP) and the
|
||||
* low-level mesh protocols. It allows feature modules to interact with the mesh without needing to know about
|
||||
* platform-specific service details or AIDL interfaces.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface RadioController {
|
||||
/** Reactive connection state of the radio. */
|
||||
val connectionState: StateFlow<ConnectionState>
|
||||
|
||||
/**
|
||||
* Flow of notifications from the radio client.
|
||||
*
|
||||
* These represent high-level events like "Handshake completed" or "Channel configuration updated."
|
||||
*/
|
||||
val clientNotification: StateFlow<ClientNotification?>
|
||||
|
||||
/**
|
||||
* Sends a data packet to the mesh.
|
||||
*
|
||||
* @param packet The [DataPacket] containing the payload and routing information.
|
||||
*/
|
||||
suspend fun sendMessage(packet: DataPacket)
|
||||
|
||||
/** Clears the current [clientNotification]. */
|
||||
fun clearClientNotification()
|
||||
|
||||
// Abstracted ServiceActions
|
||||
/**
|
||||
* Toggles the favorite status of a node on the radio.
|
||||
*
|
||||
* @param nodeNum The node number to favorite/unfavorite.
|
||||
*/
|
||||
suspend fun favoriteNode(nodeNum: Int)
|
||||
|
||||
/**
|
||||
* Sends our shared contact information (identity and public key) to a remote node.
|
||||
*
|
||||
* @param nodeNum The destination node number.
|
||||
*/
|
||||
suspend fun sendSharedContact(nodeNum: Int)
|
||||
|
||||
// Radio configuration
|
||||
/**
|
||||
* Updates the local radio configuration.
|
||||
*
|
||||
* @param config The new configuration [org.meshtastic.proto.Config].
|
||||
*/
|
||||
suspend fun setLocalConfig(config: org.meshtastic.proto.Config)
|
||||
|
||||
/**
|
||||
* Updates a local radio channel.
|
||||
*
|
||||
* @param channel The channel configuration [org.meshtastic.proto.Channel].
|
||||
*/
|
||||
suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel)
|
||||
|
||||
/**
|
||||
* Updates the owner (user info) on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param user The new user info [org.meshtastic.proto.User].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int)
|
||||
|
||||
/**
|
||||
* Updates the general configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param config The new configuration [org.meshtastic.proto.Config].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int)
|
||||
|
||||
/**
|
||||
* Updates a module configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param config The new module configuration [org.meshtastic.proto.ModuleConfig].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int)
|
||||
|
||||
/**
|
||||
* Updates a channel configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param channel The new channel configuration [org.meshtastic.proto.Channel].
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int)
|
||||
|
||||
/**
|
||||
* Sets a fixed position on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param position The position to set.
|
||||
*/
|
||||
suspend fun setFixedPosition(destNum: Int, position: Position)
|
||||
|
||||
/**
|
||||
* Updates the notification ringtone on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param ringtone The name/ID of the ringtone.
|
||||
*/
|
||||
suspend fun setRingtone(destNum: Int, ringtone: String)
|
||||
|
||||
/**
|
||||
* Updates the canned messages configuration on a remote node.
|
||||
*
|
||||
* @param destNum The destination node number.
|
||||
* @param messages The canned messages string.
|
||||
*/
|
||||
suspend fun setCannedMessages(destNum: Int, messages: String)
|
||||
|
||||
// Admin get operations
|
||||
/**
|
||||
* Requests the current owner (user info) from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getOwner(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests a specific configuration section from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param configType The numeric type of the configuration section.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getConfig(destNum: Int, configType: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests a module configuration section from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param moduleConfigType The numeric type of the module configuration section.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests a specific channel configuration from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param index The channel index.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getChannel(destNum: Int, index: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests the current ringtone from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getRingtone(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests the current canned messages from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getCannedMessages(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Requests the hardware connection status from a remote node.
|
||||
*
|
||||
* @param destNum The remote node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int)
|
||||
|
||||
// Admin operations
|
||||
/**
|
||||
* Commands a node to reboot.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun reboot(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Commands a node to reboot into DFU (Device Firmware Update) mode.
|
||||
*
|
||||
* @param nodeNum The target node number.
|
||||
*/
|
||||
suspend fun rebootToDfu(nodeNum: Int)
|
||||
|
||||
/**
|
||||
* Initiates an Over-The-Air (OTA) reboot request.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The target node number.
|
||||
* @param mode The OTA mode.
|
||||
* @param hash Optional hash for verification.
|
||||
*/
|
||||
suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?)
|
||||
|
||||
/**
|
||||
* Commands a node to shut down.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun shutdown(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Performs a factory reset on a node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
*/
|
||||
suspend fun factoryReset(destNum: Int, packetId: Int)
|
||||
|
||||
/**
|
||||
* Resets the NodeDB on a node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param packetId The request packet ID.
|
||||
* @param preserveFavorites Whether to keep favorite nodes in the database.
|
||||
*/
|
||||
suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean)
|
||||
|
||||
/**
|
||||
* Removes a node from the mesh by its node number.
|
||||
*
|
||||
* @param packetId The request packet ID.
|
||||
* @param nodeNum The node number to remove.
|
||||
*/
|
||||
suspend fun removeByNodenum(packetId: Int, nodeNum: Int)
|
||||
|
||||
// Batch editing
|
||||
/**
|
||||
* Requests the current GPS position from a remote node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
* @param currentPosition Our current position to provide in the request.
|
||||
*/
|
||||
suspend fun requestPosition(destNum: Int, currentPosition: Position)
|
||||
|
||||
/**
|
||||
* Requests detailed user info from a remote node.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
*/
|
||||
suspend fun requestUserInfo(destNum: Int)
|
||||
|
||||
/**
|
||||
* Initiates a traceroute request to a remote node.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The destination node number.
|
||||
*/
|
||||
suspend fun requestTraceroute(requestId: Int, destNum: Int)
|
||||
|
||||
/**
|
||||
* Requests telemetry data from a remote node.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The destination node number.
|
||||
* @param typeValue The numeric type of telemetry requested.
|
||||
*/
|
||||
suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int)
|
||||
|
||||
/**
|
||||
* Requests neighbor information (detected nodes) from a remote node.
|
||||
*
|
||||
* @param requestId The request ID.
|
||||
* @param destNum The destination node number.
|
||||
*/
|
||||
suspend fun requestNeighborInfo(requestId: Int, destNum: Int)
|
||||
|
||||
/**
|
||||
* Signals the start of a batch configuration session.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
*/
|
||||
suspend fun beginEditSettings(destNum: Int)
|
||||
|
||||
/**
|
||||
* Commits all pending configuration changes in a batch session.
|
||||
*
|
||||
* @param destNum The target node number.
|
||||
*/
|
||||
suspend fun commitEditSettings(destNum: Int)
|
||||
|
||||
// Helpers
|
||||
/**
|
||||
* Generates a unique packet ID for a new request.
|
||||
*
|
||||
* @return A unique 32-bit integer.
|
||||
*/
|
||||
fun getPacketId(): Int
|
||||
|
||||
/** Starts providing the phone's location to the mesh. */
|
||||
|
|
@ -87,4 +319,11 @@ interface RadioController {
|
|||
|
||||
/** Stops providing the phone's location to the mesh. */
|
||||
fun stopProvideLocation()
|
||||
|
||||
/**
|
||||
* Changes the device address (e.g., BLE MAC, IP address) we are communicating with.
|
||||
*
|
||||
* @param address The new device identifier.
|
||||
*/
|
||||
fun setDeviceAddress(address: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/** Exception thrown when an operation is attempted while not connected to a mesh radio. */
|
||||
open class RadioNotConnectedException(message: String = "Not connected to radio") : Exception(message)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 okio.ByteString
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
data class Reaction(
|
||||
val replyId: Int,
|
||||
val user: User,
|
||||
val emoji: String,
|
||||
val timestamp: Long,
|
||||
val snr: Float,
|
||||
val rssi: Int,
|
||||
val hopsAway: Int,
|
||||
val packetId: Int = 0,
|
||||
val status: MessageStatus = MessageStatus.UNKNOWN,
|
||||
val routingError: Int = 0,
|
||||
val relays: Int = 0,
|
||||
val relayNode: Int? = null,
|
||||
val to: String? = null,
|
||||
val channel: Int = 0,
|
||||
val sfppHash: ByteString? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.tak_role_forwardobserver
|
||||
import org.meshtastic.core.resources.tak_role_hq
|
||||
import org.meshtastic.core.resources.tak_role_k9
|
||||
import org.meshtastic.core.resources.tak_role_medic
|
||||
import org.meshtastic.core.resources.tak_role_rto
|
||||
import org.meshtastic.core.resources.tak_role_sniper
|
||||
import org.meshtastic.core.resources.tak_role_teamlead
|
||||
import org.meshtastic.core.resources.tak_role_teammember
|
||||
import org.meshtastic.core.resources.tak_role_unspecified
|
||||
import org.meshtastic.core.resources.tak_team_blue
|
||||
import org.meshtastic.core.resources.tak_team_brown
|
||||
import org.meshtastic.core.resources.tak_team_cyan
|
||||
import org.meshtastic.core.resources.tak_team_dark_blue
|
||||
import org.meshtastic.core.resources.tak_team_dark_green
|
||||
import org.meshtastic.core.resources.tak_team_green
|
||||
import org.meshtastic.core.resources.tak_team_magenta
|
||||
import org.meshtastic.core.resources.tak_team_maroon
|
||||
import org.meshtastic.core.resources.tak_team_orange
|
||||
import org.meshtastic.core.resources.tak_team_purple
|
||||
import org.meshtastic.core.resources.tak_team_red
|
||||
import org.meshtastic.core.resources.tak_team_teal
|
||||
import org.meshtastic.core.resources.tak_team_unspecified_color
|
||||
import org.meshtastic.core.resources.tak_team_white
|
||||
import org.meshtastic.core.resources.tak_team_yellow
|
||||
import org.meshtastic.proto.MemberRole
|
||||
import org.meshtastic.proto.Team
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun getStringResFrom(team: Team): StringResource = when (team) {
|
||||
Team.Unspecifed_Color -> Res.string.tak_team_unspecified_color
|
||||
Team.White -> Res.string.tak_team_white
|
||||
Team.Yellow -> Res.string.tak_team_yellow
|
||||
Team.Orange -> Res.string.tak_team_orange
|
||||
Team.Magenta -> Res.string.tak_team_magenta
|
||||
Team.Red -> Res.string.tak_team_red
|
||||
Team.Maroon -> Res.string.tak_team_maroon
|
||||
Team.Purple -> Res.string.tak_team_purple
|
||||
Team.Dark_Blue -> Res.string.tak_team_dark_blue
|
||||
Team.Blue -> Res.string.tak_team_blue
|
||||
Team.Cyan -> Res.string.tak_team_cyan
|
||||
Team.Teal -> Res.string.tak_team_teal
|
||||
Team.Green -> Res.string.tak_team_green
|
||||
Team.Dark_Green -> Res.string.tak_team_dark_green
|
||||
Team.Brown -> Res.string.tak_team_brown
|
||||
}
|
||||
|
||||
fun getStringResFrom(role: MemberRole): StringResource = when (role) {
|
||||
MemberRole.Unspecifed -> Res.string.tak_role_unspecified
|
||||
MemberRole.TeamMember -> Res.string.tak_role_teammember
|
||||
MemberRole.TeamLead -> Res.string.tak_role_teamlead
|
||||
MemberRole.HQ -> Res.string.tak_role_hq
|
||||
MemberRole.Sniper -> Res.string.tak_role_sniper
|
||||
MemberRole.Medic -> Res.string.tak_role_medic
|
||||
MemberRole.ForwardObserver -> Res.string.tak_role_forwardobserver
|
||||
MemberRole.RTO -> Res.string.tak_role_rto
|
||||
MemberRole.K9 -> Res.string.tak_role_k9
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "MagicNumber")
|
||||
fun getColorFrom(team: Team): Long = when (team) {
|
||||
Team.Unspecifed_Color -> 0xFF00FFFF // Default to Cyan
|
||||
Team.White -> 0xFFFFFFFF
|
||||
Team.Yellow -> 0xFFFFFF00
|
||||
Team.Orange -> 0xFFFFA500
|
||||
Team.Magenta -> 0xFFFF00FF
|
||||
Team.Red -> 0xFFFF0000
|
||||
Team.Maroon -> 0xFF800000
|
||||
Team.Purple -> 0xFF800080
|
||||
Team.Dark_Blue -> 0xFF00008B
|
||||
Team.Blue -> 0xFF0000FF
|
||||
Team.Cyan -> 0xFF00FFFF
|
||||
Team.Teal -> 0xFF008080
|
||||
Team.Green -> 0xFF00FF00
|
||||
Team.Dark_Green -> 0xFF006400
|
||||
Team.Brown -> 0xFFA52A2A
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.service
|
||||
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.proto.SharedContact
|
||||
|
||||
sealed class ServiceAction {
|
||||
data class GetDeviceMetadata(val destNum: Int) : ServiceAction()
|
||||
|
||||
data class Favorite(val node: Node) : ServiceAction()
|
||||
|
||||
data class Ignore(val node: Node) : ServiceAction()
|
||||
|
||||
data class Mute(val node: Node) : ServiceAction()
|
||||
|
||||
data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
|
||||
|
||||
data class ImportContact(val contact: SharedContact) : ServiceAction()
|
||||
|
||||
data class SendContact(val contact: SharedContact) : ServiceAction()
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.service
|
||||
|
||||
data class TracerouteResponse(
|
||||
val message: String,
|
||||
val destinationNodeNum: Int,
|
||||
val requestId: Int,
|
||||
val forwardRoute: List<Int> = emptyList(),
|
||||
val returnRoute: List<Int> = emptyList(),
|
||||
val logUuid: String? = null,
|
||||
) {
|
||||
val hasOverlay: Boolean
|
||||
get() = forwardRoute.isNotEmpty() || returnRoute.isNotEmpty()
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ fun ChannelSet.hasLoraConfig(): Boolean = lora_config != null
|
|||
*/
|
||||
fun ChannelSet.getChannelUrl(upperCasePrefix: Boolean = false, shouldAdd: Boolean = false): CommonUri {
|
||||
val channelBytes = ChannelSet.ADAPTER.encode(this)
|
||||
val enc = channelBytes.toByteString().base64Url()
|
||||
val enc = channelBytes.toByteString().base64Url().replace("=", "")
|
||||
val p = if (upperCasePrefix) CHANNEL_URL_PREFIX.uppercase() else CHANNEL_URL_PREFIX
|
||||
val query = if (shouldAdd) "?add=true" else ""
|
||||
return CommonUri.parse("$p$query#$enc")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue