mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(wire): migrate from protobuf -> wire (#4401)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
9dbc8b7fbf
commit
25657e8f8f
239 changed files with 7149 additions and 6144 deletions
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 com.geeksville.mesh.model
|
||||
|
||||
import android.app.Application
|
||||
|
|
@ -65,9 +64,9 @@ import org.meshtastic.core.strings.client_notification
|
|||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.core.ui.component.toSharedContact
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
|
||||
// Given a human name, strip out the first letter of the first three words and return that as the
|
||||
|
|
@ -119,11 +118,11 @@ constructor(
|
|||
|
||||
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
|
||||
|
||||
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmwareEdition }
|
||||
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmware_edition }
|
||||
|
||||
val clientNotification: StateFlow<MeshProtos.ClientNotification?> = serviceRepository.clientNotification
|
||||
val clientNotification: StateFlow<ClientNotification?> = serviceRepository.clientNotification
|
||||
|
||||
fun clearClientNotification(notification: MeshProtos.ClientNotification) {
|
||||
fun clearClientNotification(notification: ClientNotification) {
|
||||
serviceRepository.clearClientNotification()
|
||||
meshServiceNotifications.clearClientNotification(notification)
|
||||
}
|
||||
|
|
@ -215,8 +214,8 @@ constructor(
|
|||
Logger.d { "ViewModel created" }
|
||||
}
|
||||
|
||||
private val _sharedContactRequested: MutableStateFlow<AdminProtos.SharedContact?> = MutableStateFlow(null)
|
||||
val sharedContactRequested: StateFlow<AdminProtos.SharedContact?>
|
||||
private val _sharedContactRequested: MutableStateFlow<SharedContact?> = MutableStateFlow(null)
|
||||
val sharedContactRequested: StateFlow<SharedContact?>
|
||||
get() = _sharedContactRequested.asStateFlow()
|
||||
|
||||
fun setSharedContactRequested(url: Uri, onFailure: () -> Unit) {
|
||||
|
|
@ -236,8 +235,8 @@ constructor(
|
|||
val connectionState
|
||||
get() = serviceRepository.connectionState
|
||||
|
||||
private val _requestChannelSet = MutableStateFlow<AppOnlyProtos.ChannelSet?>(null)
|
||||
val requestChannelSet: StateFlow<AppOnlyProtos.ChannelSet?>
|
||||
private val _requestChannelSet = MutableStateFlow<ChannelSet?>(null)
|
||||
val requestChannelSet: StateFlow<ChannelSet?>
|
||||
get() = _requestChannelSet
|
||||
|
||||
fun requestChannelUrl(url: Uri, onFailure: () -> Unit) =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,16 +14,15 @@
|
|||
* 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 com.geeksville.mesh.repository.network
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.geeksville.mesh.util.ignoreException
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.eclipse.paho.client.mqttv3.DisconnectedBufferOptions
|
||||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
|
||||
import org.eclipse.paho.client.mqttv3.MqttAsyncClient
|
||||
|
|
@ -35,8 +34,7 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
|
|||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.util.subscribeList
|
||||
import org.meshtastic.proto.MeshProtos.MqttClientProxyMessage
|
||||
import org.meshtastic.proto.mqttClientProxyMessage
|
||||
import org.meshtastic.proto.MqttClientProxyMessage
|
||||
import java.net.URI
|
||||
import java.security.SecureRandom
|
||||
import javax.inject.Inject
|
||||
|
|
@ -87,14 +85,14 @@ constructor(
|
|||
// Create a custom SSLContext that trusts all certificates
|
||||
sslContext.init(null, arrayOf<TrustManager>(TrustAllX509TrustManager()), SecureRandom())
|
||||
|
||||
val rootTopic = mqttConfig.root.ifEmpty { DEFAULT_TOPIC_ROOT }
|
||||
val rootTopic = mqttConfig?.root?.ifEmpty { DEFAULT_TOPIC_ROOT }
|
||||
|
||||
val connectOptions =
|
||||
MqttConnectOptions().apply {
|
||||
userName = mqttConfig.username
|
||||
password = mqttConfig.password.toCharArray()
|
||||
userName = mqttConfig?.username
|
||||
password = mqttConfig?.password?.toCharArray()
|
||||
isAutomaticReconnect = true
|
||||
if (mqttConfig.tlsEnabled) {
|
||||
if (mqttConfig?.tls_enabled == true) {
|
||||
socketFactory = sslContext.socketFactory
|
||||
}
|
||||
}
|
||||
|
|
@ -117,7 +115,7 @@ constructor(
|
|||
}
|
||||
.forEach { globalId ->
|
||||
subscribe("$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+")
|
||||
if (mqttConfig.jsonEnabled) subscribe("$rootTopic$JSON_TOPIC_LEVEL$globalId/+")
|
||||
if (mqttConfig?.json_enabled == true) subscribe("$rootTopic$JSON_TOPIC_LEVEL$globalId/+")
|
||||
}
|
||||
subscribe("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+")
|
||||
}
|
||||
|
|
@ -129,11 +127,11 @@ constructor(
|
|||
|
||||
override fun messageArrived(topic: String, message: MqttMessage) {
|
||||
trySend(
|
||||
mqttClientProxyMessage {
|
||||
this.topic = topic
|
||||
data = ByteString.copyFrom(message.payload)
|
||||
retained = message.isRetained
|
||||
},
|
||||
MqttClientProxyMessage(
|
||||
topic = topic,
|
||||
data_ = message.payload.toByteString(),
|
||||
retained = message.isRetained,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -142,12 +140,11 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val scheme = if (mqttConfig.tlsEnabled) "ssl" else "tcp"
|
||||
val scheme = if (mqttConfig?.tls_enabled == true) "ssl" else "tcp"
|
||||
val (host, port) =
|
||||
mqttConfig.address
|
||||
.ifEmpty { DEFAULT_SERVER_ADDRESS }
|
||||
.split(":", limit = 2)
|
||||
.let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1) }
|
||||
(mqttConfig?.address ?: DEFAULT_SERVER_ADDRESS).split(":", limit = 2).let {
|
||||
it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1)
|
||||
}
|
||||
|
||||
mqttClient =
|
||||
MqttAsyncClient(URI(scheme, null, host, port, "", "", "").toString(), ownerId, MemoryPersistence()).apply {
|
||||
|
|
|
|||
|
|
@ -19,39 +19,40 @@ package com.geeksville.mesh.repository.radio
|
|||
import co.touchlab.kermit.Logger
|
||||
import com.geeksville.mesh.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.model.getInitials
|
||||
import com.google.protobuf.ByteString
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.delay
|
||||
import okio.ByteString.Companion.encodeUtf8
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.ConfigKt
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.ModuleConfigProtos
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.channel
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.deviceMetadata
|
||||
import org.meshtastic.proto.fromRadio
|
||||
import org.meshtastic.proto.moduleConfig
|
||||
import org.meshtastic.proto.queueStatus
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.Neighbor
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.NodeInfo
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.QueueStatus
|
||||
import org.meshtastic.proto.Routing
|
||||
import org.meshtastic.proto.StatusMessage
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.random.Random
|
||||
import org.meshtastic.proto.Channel as ProtoChannel
|
||||
import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
private val defaultLoRaConfig =
|
||||
ConfigKt.loRaConfig {
|
||||
usePreset = true
|
||||
region = ConfigProtos.Config.LoRaConfig.RegionCode.TW
|
||||
}
|
||||
private val defaultLoRaConfig = Config.LoRaConfig(use_preset = true, region = Config.LoRaConfig.RegionCode.TW)
|
||||
|
||||
private val defaultChannel = channel {
|
||||
settings = Channel.default.settings
|
||||
role = ChannelProtos.Channel.Role.PRIMARY
|
||||
}
|
||||
private val defaultChannel = ProtoChannel(settings = Channel.default.settings, role = ProtoChannel.Role.PRIMARY)
|
||||
|
||||
/** A simulated interface that is used for testing in the simulator */
|
||||
@Suppress("detekt:TooManyFunctions", "detekt:MagicNumber")
|
||||
|
|
@ -77,46 +78,57 @@ constructor(
|
|||
}
|
||||
|
||||
override fun handleSendToRadio(p: ByteArray) {
|
||||
val pr = MeshProtos.ToRadio.parseFrom(p)
|
||||
sendQueueStatus(pr.packet.id)
|
||||
val pr = ToRadio.ADAPTER.decode(p)
|
||||
val packet = pr.packet
|
||||
if (packet != null) {
|
||||
sendQueueStatus(packet.id)
|
||||
}
|
||||
|
||||
val data = if (pr.hasPacket()) pr.packet.decoded else null
|
||||
val data = packet?.decoded
|
||||
|
||||
when {
|
||||
pr.wantConfigId != 0 -> sendConfigResponse(pr.wantConfigId)
|
||||
data != null && data.portnum == Portnums.PortNum.ADMIN_APP ->
|
||||
handleAdminPacket(pr, AdminProtos.AdminMessage.parseFrom(data.payload))
|
||||
pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr)
|
||||
(pr.want_config_id ?: 0) != 0 -> sendConfigResponse(pr.want_config_id ?: 0)
|
||||
data != null && data.portnum == PortNum.ADMIN_APP ->
|
||||
handleAdminPacket(pr, AdminMessage.ADAPTER.decode(data.payload))
|
||||
packet != null && packet.want_ack == true -> sendFakeAck(pr)
|
||||
else -> Logger.i { "Ignoring data sent to mock interface $pr" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAdminPacket(pr: MeshProtos.ToRadio, d: AdminProtos.AdminMessage) {
|
||||
private fun handleAdminPacket(pr: ToRadio, d: AdminMessage) {
|
||||
val packet = pr.packet ?: return
|
||||
when {
|
||||
d.getConfigRequest == AdminProtos.AdminMessage.ConfigType.LORA_CONFIG ->
|
||||
sendAdmin(pr.packet.to, pr.packet.from, pr.packet.id) {
|
||||
getConfigResponse = config { lora = defaultLoRaConfig }
|
||||
d.get_config_request == AdminMessage.ConfigType.LORA_CONFIG ->
|
||||
sendAdmin(packet.to, packet.from, packet.id) {
|
||||
copy(get_config_response = Config(lora = defaultLoRaConfig))
|
||||
}
|
||||
|
||||
d.getChannelRequest != 0 ->
|
||||
sendAdmin(pr.packet.to, pr.packet.from, pr.packet.id) {
|
||||
getChannelResponse = channel {
|
||||
index = d.getChannelRequest - 1 // 0 based on the response
|
||||
if (d.getChannelRequest == 1) {
|
||||
settings = Channel.default.settings
|
||||
role = ChannelProtos.Channel.Role.PRIMARY
|
||||
}
|
||||
}
|
||||
(d.get_channel_request ?: 0) != 0 ->
|
||||
sendAdmin(packet.to, packet.from, packet.id) {
|
||||
copy(
|
||||
get_channel_response =
|
||||
ProtoChannel(
|
||||
index = (d.get_channel_request ?: 0) - 1, // 0 based on the response
|
||||
settings = if (d.get_channel_request == 1) Channel.default.settings else null,
|
||||
role =
|
||||
if (d.get_channel_request == 1) {
|
||||
ProtoChannel.Role.PRIMARY
|
||||
} else {
|
||||
ProtoChannel.Role.DISABLED
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
d.getModuleConfigRequest == AdminProtos.AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG ->
|
||||
sendAdmin(pr.packet.to, pr.packet.from, pr.packet.id) {
|
||||
getModuleConfigResponse = moduleConfig {
|
||||
statusmessage =
|
||||
ModuleConfigProtos.ModuleConfig.StatusMessageConfig.newBuilder()
|
||||
.setNodeStatus("Going to the farm.. to grow wheat.")
|
||||
.build()
|
||||
}
|
||||
d.get_module_config_request == AdminMessage.ModuleConfigType.STATUSMESSAGE_CONFIG ->
|
||||
sendAdmin(packet.to, packet.from, packet.id) {
|
||||
copy(
|
||||
get_module_config_response =
|
||||
ModuleConfig(
|
||||
statusmessage =
|
||||
ModuleConfig.StatusMessageConfig(node_status = "Going to the farm.. to grow wheat."),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
else -> Logger.i { "Ignoring admin sent to mock interface $d" }
|
||||
|
|
@ -128,207 +140,169 @@ constructor(
|
|||
}
|
||||
|
||||
// / Generate a fake text message from a node
|
||||
private fun makeTextMessage(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply {
|
||||
private fun makeTextMessage(numIn: Int) = FromRadio(
|
||||
packet =
|
||||
MeshProtos.MeshPacket.newBuilder()
|
||||
.apply {
|
||||
id = packetIdSequence.next()
|
||||
from = numIn
|
||||
to = 0xffffffff.toInt() // ugly way of saying broadcast
|
||||
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||
rxSnr = 1.5f
|
||||
decoded =
|
||||
MeshProtos.Data.newBuilder()
|
||||
.apply {
|
||||
portnum = Portnums.PortNum.TEXT_MESSAGE_APP
|
||||
payload = ByteString.copyFromUtf8("This simulated node sends Hi!")
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
MeshPacket(
|
||||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.TEXT_MESSAGE_APP,
|
||||
payload = "This simulated node sends Hi!".encodeUtf8(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun makeNeighborInfo(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply {
|
||||
private fun makeNeighborInfo(numIn: Int) = FromRadio(
|
||||
packet =
|
||||
MeshProtos.MeshPacket.newBuilder()
|
||||
.apply {
|
||||
id = packetIdSequence.next()
|
||||
from = numIn
|
||||
to = 0xffffffff.toInt() // broadcast
|
||||
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||
rxSnr = 1.5f
|
||||
decoded =
|
||||
MeshProtos.Data.newBuilder()
|
||||
.apply {
|
||||
portnum = Portnums.PortNum.NEIGHBORINFO_APP
|
||||
payload =
|
||||
MeshProtos.NeighborInfo.newBuilder()
|
||||
.setNodeId(numIn)
|
||||
.setLastSentById(numIn)
|
||||
.setNodeBroadcastIntervalSecs(60)
|
||||
.addNeighbors(
|
||||
MeshProtos.Neighbor.newBuilder()
|
||||
.setNodeId(numIn + 1)
|
||||
.setSnr(10.0f)
|
||||
.setLastRxTime((System.currentTimeMillis() / 1000).toInt())
|
||||
.setNodeBroadcastIntervalSecs(60)
|
||||
.build(),
|
||||
)
|
||||
.addNeighbors(
|
||||
MeshProtos.Neighbor.newBuilder()
|
||||
.setNodeId(numIn + 2)
|
||||
.setSnr(12.0f)
|
||||
.setLastRxTime((System.currentTimeMillis() / 1000).toInt())
|
||||
.setNodeBroadcastIntervalSecs(60)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
.toByteString()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
MeshPacket(
|
||||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.NEIGHBORINFO_APP,
|
||||
payload =
|
||||
NeighborInfo(
|
||||
node_id = numIn,
|
||||
last_sent_by_id = numIn,
|
||||
node_broadcast_interval_secs = 60,
|
||||
neighbors =
|
||||
listOf(
|
||||
Neighbor(
|
||||
node_id = numIn + 1,
|
||||
snr = 10.0f,
|
||||
last_rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
node_broadcast_interval_secs = 60,
|
||||
),
|
||||
Neighbor(
|
||||
node_id = numIn + 2,
|
||||
snr = 12.0f,
|
||||
last_rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
node_broadcast_interval_secs = 60,
|
||||
),
|
||||
),
|
||||
)
|
||||
.encode()
|
||||
.toByteString(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun makePosition(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply {
|
||||
private fun makePosition(numIn: Int) = FromRadio(
|
||||
packet =
|
||||
MeshProtos.MeshPacket.newBuilder()
|
||||
.apply {
|
||||
id = packetIdSequence.next()
|
||||
from = numIn
|
||||
to = 0xffffffff.toInt() // ugly way of saying broadcast
|
||||
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||
rxSnr = 1.5f
|
||||
decoded =
|
||||
MeshProtos.Data.newBuilder()
|
||||
.apply {
|
||||
portnum = Portnums.PortNum.POSITION_APP
|
||||
payload =
|
||||
MeshProtos.Position.newBuilder()
|
||||
.setLatitudeI(Position.degI(32.776665))
|
||||
.setLongitudeI(Position.degI(-96.796989))
|
||||
.setAltitude(150)
|
||||
.setTime((System.currentTimeMillis() / 1000).toInt())
|
||||
.setPrecisionBits(15)
|
||||
.build()
|
||||
.toByteString()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
MeshPacket(
|
||||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.POSITION_APP,
|
||||
payload =
|
||||
ProtoPosition(
|
||||
latitude_i = org.meshtastic.core.model.Position.degI(32.776665),
|
||||
longitude_i = org.meshtastic.core.model.Position.degI(-96.796989),
|
||||
altitude = 150,
|
||||
time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
precision_bits = 15,
|
||||
)
|
||||
.encode()
|
||||
.toByteString(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun makeTelemetry(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply {
|
||||
private fun makeTelemetry(numIn: Int) = FromRadio(
|
||||
packet =
|
||||
MeshProtos.MeshPacket.newBuilder()
|
||||
.apply {
|
||||
id = packetIdSequence.next()
|
||||
from = numIn
|
||||
to = 0xffffffff.toInt() // broadcast
|
||||
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||
rxSnr = 1.5f
|
||||
decoded =
|
||||
MeshProtos.Data.newBuilder()
|
||||
.apply {
|
||||
portnum = Portnums.PortNum.TELEMETRY_APP
|
||||
payload =
|
||||
TelemetryProtos.Telemetry.newBuilder()
|
||||
.setTime((System.currentTimeMillis() / 1000).toInt())
|
||||
.setDeviceMetrics(
|
||||
TelemetryProtos.DeviceMetrics.newBuilder()
|
||||
.setBatteryLevel(85)
|
||||
.setVoltage(4.1f)
|
||||
.setChannelUtilization(0.12f)
|
||||
.setAirUtilTx(0.05f)
|
||||
.setUptimeSeconds(123456)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
.toByteString()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
MeshPacket(
|
||||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.TELEMETRY_APP,
|
||||
payload =
|
||||
Telemetry(
|
||||
time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
device_metrics =
|
||||
DeviceMetrics(
|
||||
battery_level = 85,
|
||||
voltage = 4.1f,
|
||||
channel_utilization = 0.12f,
|
||||
air_util_tx = 0.05f,
|
||||
uptime_seconds = 123456,
|
||||
),
|
||||
)
|
||||
.encode()
|
||||
.toByteString(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun makeNodeStatus(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply {
|
||||
private fun makeNodeStatus(numIn: Int) = FromRadio(
|
||||
packet =
|
||||
MeshProtos.MeshPacket.newBuilder()
|
||||
.apply {
|
||||
id = packetIdSequence.next()
|
||||
from = numIn
|
||||
to = 0xffffffff.toInt() // broadcast
|
||||
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||
rxSnr = 1.5f
|
||||
decoded =
|
||||
MeshProtos.Data.newBuilder()
|
||||
.apply {
|
||||
portnum = Portnums.PortNum.NODE_STATUS_APP
|
||||
payload =
|
||||
MeshProtos.StatusMessage.newBuilder()
|
||||
.setStatus("Going to the farm.. to grow wheat.")
|
||||
.build()
|
||||
.toByteString()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
MeshPacket(
|
||||
id = packetIdSequence.next(),
|
||||
from = numIn,
|
||||
to = 0xffffffff.toInt(), // broadcast
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.NODE_STATUS_APP,
|
||||
payload =
|
||||
StatusMessage(status = "Going to the farm.. to grow wheat.").encode().toByteString(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun makeDataPacket(fromIn: Int, toIn: Int, data: MeshProtos.Data.Builder) =
|
||||
MeshProtos.FromRadio.newBuilder().apply {
|
||||
packet =
|
||||
MeshProtos.MeshPacket.newBuilder()
|
||||
.apply {
|
||||
id = packetIdSequence.next()
|
||||
from = fromIn
|
||||
to = toIn
|
||||
rxTime = (System.currentTimeMillis() / 1000).toInt()
|
||||
rxSnr = 1.5f
|
||||
decoded = data.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
private fun makeDataPacket(fromIn: Int, toIn: Int, data: Data) = FromRadio(
|
||||
packet =
|
||||
MeshPacket(
|
||||
id = packetIdSequence.next(),
|
||||
from = fromIn,
|
||||
to = toIn,
|
||||
rx_time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
rx_snr = 1.5f,
|
||||
decoded = data,
|
||||
),
|
||||
)
|
||||
|
||||
private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) = makeDataPacket(
|
||||
fromIn,
|
||||
toIn,
|
||||
MeshProtos.Data.newBuilder().apply {
|
||||
portnum = Portnums.PortNum.ROUTING_APP
|
||||
payload = MeshProtos.Routing.newBuilder().apply {}.build().toByteString()
|
||||
requestId = msgId
|
||||
},
|
||||
Data(portnum = PortNum.ROUTING_APP, payload = Routing().encode().toByteString(), request_id = msgId),
|
||||
)
|
||||
|
||||
private fun sendQueueStatus(msgId: Int) = service.handleFromRadio(
|
||||
fromRadio {
|
||||
queueStatus = queueStatus {
|
||||
res = 0
|
||||
free = 16
|
||||
meshPacketId = msgId
|
||||
}
|
||||
}
|
||||
.toByteArray(),
|
||||
FromRadio(queueStatus = QueueStatus(res = 0, free = 16, mesh_packet_id = msgId)).encode(),
|
||||
)
|
||||
|
||||
private fun sendAdmin(fromIn: Int, toIn: Int, reqId: Int, initFn: AdminProtos.AdminMessage.Builder.() -> Unit) {
|
||||
private fun sendAdmin(fromIn: Int, toIn: Int, reqId: Int, initFn: AdminMessage.() -> AdminMessage) {
|
||||
val adminMsg = AdminMessage().initFn()
|
||||
val p =
|
||||
makeDataPacket(
|
||||
fromIn,
|
||||
toIn,
|
||||
MeshProtos.Data.newBuilder().apply {
|
||||
portnum = Portnums.PortNum.ADMIN_APP
|
||||
payload = AdminProtos.AdminMessage.newBuilder().also { initFn(it) }.build().toByteString()
|
||||
requestId = reqId
|
||||
},
|
||||
Data(portnum = PortNum.ADMIN_APP, payload = adminMsg.encode().toByteString(), request_id = reqId),
|
||||
)
|
||||
service.handleFromRadio(p.build().toByteArray())
|
||||
service.handleFromRadio(p.encode())
|
||||
}
|
||||
|
||||
// / Send a fake ack packet back if the sender asked for want_ack
|
||||
private fun sendFakeAck(pr: MeshProtos.ToRadio) = service.serviceScope.handledLaunch {
|
||||
private fun sendFakeAck(pr: ToRadio) = service.serviceScope.handledLaunch {
|
||||
val packet = pr.packet ?: return@handledLaunch
|
||||
delay(2000)
|
||||
service.handleFromRadio(makeAck(MY_NODE + 1, pr.packet.from, pr.packet.id).build().toByteArray())
|
||||
service.handleFromRadio(makeAck(MY_NODE + 1, packet.from ?: 0, packet.id).encode())
|
||||
}
|
||||
|
||||
private fun sendConfigResponse(configId: Int) {
|
||||
|
|
@ -336,57 +310,45 @@ constructor(
|
|||
|
||||
// / Generate a fake node info entry
|
||||
@Suppress("MagicNumber")
|
||||
fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = MeshProtos.FromRadio.newBuilder().apply {
|
||||
nodeInfo =
|
||||
MeshProtos.NodeInfo.newBuilder()
|
||||
.apply {
|
||||
num = numIn
|
||||
user =
|
||||
MeshProtos.User.newBuilder()
|
||||
.apply {
|
||||
id = DataPacket.nodeNumToDefaultId(numIn)
|
||||
longName = "Sim " + Integer.toHexString(num)
|
||||
shortName = getInitials(longName)
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
}
|
||||
.build()
|
||||
position =
|
||||
MeshProtos.Position.newBuilder()
|
||||
.apply {
|
||||
latitudeI = Position.degI(lat)
|
||||
longitudeI = Position.degI(lon)
|
||||
altitude = 35
|
||||
time = (System.currentTimeMillis() / 1000).toInt()
|
||||
precisionBits = Random.nextInt(10, 19)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = FromRadio(
|
||||
node_info =
|
||||
NodeInfo(
|
||||
num = numIn,
|
||||
user =
|
||||
User(
|
||||
id = DataPacket.nodeNumToDefaultId(numIn),
|
||||
long_name = "Sim " + Integer.toHexString(numIn),
|
||||
short_name = getInitials("Sim " + Integer.toHexString(numIn)),
|
||||
hw_model = HardwareModel.ANDROID_SIM,
|
||||
),
|
||||
position =
|
||||
ProtoPosition(
|
||||
latitude_i = org.meshtastic.core.model.Position.degI(lat),
|
||||
longitude_i = org.meshtastic.core.model.Position.degI(lon),
|
||||
altitude = 35,
|
||||
time = (System.currentTimeMillis() / 1000).toInt(),
|
||||
precision_bits = Random.nextInt(10, 19),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Simulated network data to feed to our app
|
||||
val packets =
|
||||
arrayOf(
|
||||
// MyNodeInfo
|
||||
MeshProtos.FromRadio.newBuilder().apply {
|
||||
myInfo = MeshProtos.MyNodeInfo.newBuilder().apply { myNodeNum = MY_NODE }.build()
|
||||
},
|
||||
MeshProtos.FromRadio.newBuilder().apply {
|
||||
metadata = deviceMetadata {
|
||||
firmwareVersion = "9.9.9.abcdefg"
|
||||
hwModel = MeshProtos.HardwareModel.ANDROID_SIM
|
||||
}
|
||||
},
|
||||
FromRadio(my_info = ProtoMyNodeInfo(my_node_num = MY_NODE)),
|
||||
FromRadio(
|
||||
metadata = DeviceMetadata(firmware_version = "9.9.9.abcdefg", hw_model = HardwareModel.ANDROID_SIM),
|
||||
),
|
||||
|
||||
// Fake NodeDB
|
||||
makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas
|
||||
makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson
|
||||
MeshProtos.FromRadio.newBuilder().apply { config = config { lora = defaultLoRaConfig } },
|
||||
MeshProtos.FromRadio.newBuilder().apply { channel = defaultChannel },
|
||||
MeshProtos.FromRadio.newBuilder().apply { configCompleteId = configId },
|
||||
FromRadio(config = Config(lora = defaultLoRaConfig)),
|
||||
FromRadio(channel = defaultChannel),
|
||||
FromRadio(config_complete_id = configId),
|
||||
|
||||
// Done with config response, now pretend to receive some text messages
|
||||
|
||||
makeTextMessage(MY_NODE + 1),
|
||||
makeNeighborInfo(MY_NODE + 1),
|
||||
makePosition(MY_NODE + 1),
|
||||
|
|
@ -394,6 +356,6 @@ constructor(
|
|||
makeNodeStatus(MY_NODE + 1),
|
||||
)
|
||||
|
||||
packets.forEach { p -> service.handleFromRadio(p.build().toByteArray()) }
|
||||
packets.forEach { p -> service.handleFromRadio(p.encode()) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ import org.meshtastic.core.di.ProcessLifecycle
|
|||
import org.meshtastic.core.model.util.anonymize
|
||||
import org.meshtastic.core.prefs.radio.RadioPrefs
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -149,9 +150,8 @@ constructor(
|
|||
if (now - lastHeartbeatMillis > HEARTBEAT_INTERVAL_MILLIS) {
|
||||
if (radioIf is SerialInterface) {
|
||||
Logger.i { "Sending ToRadio heartbeat" }
|
||||
val heartbeat =
|
||||
MeshProtos.ToRadio.newBuilder().setHeartbeat(MeshProtos.Heartbeat.getDefaultInstance()).build()
|
||||
handleSendToRadio(heartbeat.toByteArray())
|
||||
val heartbeat = ToRadio(heartbeat = Heartbeat())
|
||||
handleSendToRadio(heartbeat.encode())
|
||||
} else {
|
||||
// For BLE and TCP this will check if the connection is still alive
|
||||
radioIf.keepAlive()
|
||||
|
|
@ -234,8 +234,6 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// ignoreException { Logger.d { "FromRadio: ${MeshProtos.FromRadio.parseFrom(p }}" } }
|
||||
|
||||
try {
|
||||
processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) }
|
||||
emitReceiveActivity()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import dagger.assisted.AssistedInject
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.IOException
|
||||
|
|
@ -166,11 +168,8 @@ constructor(
|
|||
|
||||
override fun keepAlive() {
|
||||
Logger.d { "[$address] TCP keepAlive" }
|
||||
val heartbeat =
|
||||
org.meshtastic.proto.MeshProtos.ToRadio.newBuilder()
|
||||
.setHeartbeat(org.meshtastic.proto.MeshProtos.Heartbeat.getDefaultInstance())
|
||||
.build()
|
||||
handleSendToRadio(heartbeat.toByteArray())
|
||||
val heartbeat = ToRadio(heartbeat = Heartbeat())
|
||||
handleSendToRadio(heartbeat.encode())
|
||||
}
|
||||
|
||||
// Create a socket to make the connection with the server
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ package com.geeksville.mesh.service
|
|||
import co.touchlab.kermit.Logger
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Dispatches non-packet [MeshProtos.FromRadio] variants to their respective handlers. This class is stateless and
|
||||
* handles routing for config, metadata, and specialized system messages.
|
||||
* Dispatches non-packet [FromRadio] variants to their respective handlers. This class is stateless and handles routing
|
||||
* for config, metadata, and specialized system messages.
|
||||
*/
|
||||
@Singleton
|
||||
class FromRadioPacketHandler
|
||||
|
|
@ -38,41 +38,47 @@ constructor(
|
|||
private val serviceNotifications: MeshServiceNotifications,
|
||||
) {
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun handleFromRadio(proto: MeshProtos.FromRadio) {
|
||||
when (proto.payloadVariantCase) {
|
||||
MeshProtos.FromRadio.PayloadVariantCase.MY_INFO -> router.configFlowManager.handleMyInfo(proto.myInfo)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.METADATA ->
|
||||
router.configFlowManager.handleLocalMetadata(proto.metadata)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.NODE_INFO -> {
|
||||
router.configFlowManager.handleNodeInfo(proto.nodeInfo)
|
||||
fun handleFromRadio(proto: FromRadio) {
|
||||
val myInfo = proto.my_info
|
||||
val metadata = proto.metadata
|
||||
val nodeInfo = proto.node_info
|
||||
val configCompleteId = proto.config_complete_id
|
||||
val mqttProxyMessage = proto.mqttClientProxyMessage
|
||||
val queueStatus = proto.queueStatus
|
||||
val config = proto.config
|
||||
val moduleConfig = proto.moduleConfig
|
||||
val channel = proto.channel
|
||||
val clientNotification = proto.clientNotification
|
||||
|
||||
when {
|
||||
myInfo != null -> router.configFlowManager.handleMyInfo(myInfo)
|
||||
metadata != null -> router.configFlowManager.handleLocalMetadata(metadata)
|
||||
nodeInfo != null -> {
|
||||
router.configFlowManager.handleNodeInfo(nodeInfo)
|
||||
serviceRepository.setStatusMessage("Nodes (${router.configFlowManager.newNodeCount})")
|
||||
}
|
||||
MeshProtos.FromRadio.PayloadVariantCase.CONFIG_COMPLETE_ID ->
|
||||
router.configFlowManager.handleConfigComplete(proto.configCompleteId)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.MQTTCLIENTPROXYMESSAGE ->
|
||||
mqttManager.handleMqttProxyMessage(proto.mqttClientProxyMessage)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.QUEUESTATUS -> packetHandler.handleQueueStatus(proto.queueStatus)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.CONFIG -> router.configHandler.handleDeviceConfig(proto.config)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.MODULECONFIG ->
|
||||
router.configHandler.handleModuleConfig(proto.moduleConfig)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.CHANNEL -> router.configHandler.handleChannel(proto.channel)
|
||||
MeshProtos.FromRadio.PayloadVariantCase.CLIENTNOTIFICATION -> {
|
||||
serviceRepository.setClientNotification(proto.clientNotification)
|
||||
serviceNotifications.showClientNotification(proto.clientNotification)
|
||||
packetHandler.removeResponse(proto.clientNotification.replyId, complete = false)
|
||||
configCompleteId != null -> router.configFlowManager.handleConfigComplete(configCompleteId)
|
||||
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
|
||||
queueStatus != null -> packetHandler.handleQueueStatus(queueStatus)
|
||||
config != null -> router.configHandler.handleDeviceConfig(config)
|
||||
moduleConfig != null -> router.configHandler.handleModuleConfig(moduleConfig)
|
||||
channel != null -> router.configHandler.handleChannel(channel)
|
||||
clientNotification != null -> {
|
||||
serviceRepository.setClientNotification(clientNotification)
|
||||
serviceNotifications.showClientNotification(clientNotification)
|
||||
packetHandler.removeResponse(clientNotification.reply_id ?: 0, complete = false)
|
||||
}
|
||||
// Logging-only variants are handled by MeshMessageProcessor before dispatching here
|
||||
MeshProtos.FromRadio.PayloadVariantCase.PACKET,
|
||||
MeshProtos.FromRadio.PayloadVariantCase.LOG_RECORD,
|
||||
MeshProtos.FromRadio.PayloadVariantCase.REBOOTED,
|
||||
MeshProtos.FromRadio.PayloadVariantCase.XMODEMPACKET,
|
||||
MeshProtos.FromRadio.PayloadVariantCase.DEVICEUICONFIG,
|
||||
MeshProtos.FromRadio.PayloadVariantCase.FILEINFO,
|
||||
-> {
|
||||
proto.packet != null ||
|
||||
proto.log_record != null ||
|
||||
proto.rebooted != null ||
|
||||
proto.xmodemPacket != null ||
|
||||
proto.deviceuiConfig != null ||
|
||||
proto.fileInfo != null -> {
|
||||
/* No specialized routing needed here */
|
||||
}
|
||||
|
||||
else -> Logger.d { "Dispatcher ignoring ${proto.payloadVariantCase}" }
|
||||
else -> Logger.d { "Dispatcher ignoring FromRadio variant" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ package com.geeksville.mesh.service
|
|||
|
||||
import com.geeksville.mesh.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.util.ignoreException
|
||||
import com.google.protobuf.ByteString
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.analytics.DataPair
|
||||
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
|
|
@ -34,17 +34,17 @@ import org.meshtastic.core.model.Position
|
|||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.ModuleConfigProtos
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.user
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.OTAMode
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.User
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
|
||||
@Singleton
|
||||
class MeshActionHandler
|
||||
@Inject
|
||||
|
|
@ -80,10 +80,12 @@ constructor(
|
|||
is ServiceAction.Reaction -> handleReaction(action, myNodeNum)
|
||||
is ServiceAction.ImportContact -> handleImportContact(action, myNodeNum)
|
||||
is ServiceAction.SendContact -> {
|
||||
commandSender.sendAdmin(myNodeNum) { addContact = action.contact }
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(add_contact = action.contact) }
|
||||
}
|
||||
is ServiceAction.GetDeviceMetadata -> {
|
||||
commandSender.sendAdmin(action.destNum, wantResponse = true) { getDeviceMetadataRequest = true }
|
||||
commandSender.sendAdmin(action.destNum, wantResponse = true) {
|
||||
AdminMessage(get_device_metadata_request = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,7 +94,11 @@ constructor(
|
|||
private fun handleFavorite(action: ServiceAction.Favorite, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (node.isFavorite) removeFavoriteNode = node.num else setFavoriteNode = node.num
|
||||
if (node.isFavorite) {
|
||||
AdminMessage(remove_favorite_node = node.num)
|
||||
} else {
|
||||
AdminMessage(set_favorite_node = node.num)
|
||||
}
|
||||
}
|
||||
nodeManager.updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite }
|
||||
}
|
||||
|
|
@ -100,14 +106,18 @@ constructor(
|
|||
private fun handleIgnore(action: ServiceAction.Ignore, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
if (node.isIgnored) removeIgnoredNode = node.num else setIgnoredNode = node.num
|
||||
if (node.isIgnored) {
|
||||
AdminMessage(remove_ignored_node = node.num)
|
||||
} else {
|
||||
AdminMessage(set_ignored_node = node.num)
|
||||
}
|
||||
}
|
||||
nodeManager.updateNodeInfo(node.num) { it.isIgnored = !node.isIgnored }
|
||||
}
|
||||
|
||||
private fun handleMute(action: ServiceAction.Mute, myNodeNum: Int) {
|
||||
val node = action.node
|
||||
commandSender.sendAdmin(myNodeNum) { toggleMutedNode = node.num }
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(toggle_muted_node = node.num) }
|
||||
nodeManager.updateNodeInfo(node.num) { it.isMuted = !node.isMuted }
|
||||
}
|
||||
|
||||
|
|
@ -118,8 +128,8 @@ constructor(
|
|||
org.meshtastic.core.model
|
||||
.DataPacket(
|
||||
to = destId,
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = action.emoji.encodeToByteArray(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
bytes = action.emoji.encodeToByteArray().toByteString(),
|
||||
channel = channel,
|
||||
replyId = action.replyId,
|
||||
wantAck = true,
|
||||
|
|
@ -131,9 +141,13 @@ constructor(
|
|||
}
|
||||
|
||||
private fun handleImportContact(action: ServiceAction.ImportContact, myNodeNum: Int) {
|
||||
val verifiedContact = action.contact.toBuilder().setManuallyVerified(true).build()
|
||||
commandSender.sendAdmin(myNodeNum) { addContact = verifiedContact }
|
||||
nodeManager.handleReceivedUser(verifiedContact.nodeNum, verifiedContact.user, manuallyVerified = true)
|
||||
val verifiedContact = action.contact.copy(manually_verified = true)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(add_contact = verifiedContact) }
|
||||
nodeManager.handleReceivedUser(
|
||||
verifiedContact.node_num ?: 0,
|
||||
verifiedContact.user ?: User(),
|
||||
manuallyVerified = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun rememberReaction(action: ServiceAction.Reaction, packetId: Int, myNodeNum: Int) {
|
||||
|
|
@ -158,30 +172,16 @@ constructor(
|
|||
}
|
||||
|
||||
fun handleSetOwner(u: org.meshtastic.core.model.MeshUser, myNodeNum: Int) {
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
setOwner = user {
|
||||
id = u.id
|
||||
longName = u.longName
|
||||
shortName = u.shortName
|
||||
isLicensed = u.isLicensed
|
||||
}
|
||||
}
|
||||
nodeManager.handleReceivedUser(
|
||||
myNodeNum,
|
||||
user {
|
||||
id = u.id
|
||||
longName = u.longName
|
||||
shortName = u.shortName
|
||||
isLicensed = u.isLicensed
|
||||
},
|
||||
)
|
||||
val newUser = User(id = u.id, long_name = u.longName, short_name = u.shortName, is_licensed = u.isLicensed)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_owner = newUser) }
|
||||
nodeManager.handleReceivedUser(myNodeNum, newUser)
|
||||
}
|
||||
|
||||
fun handleSend(p: DataPacket, myNodeNum: Int) {
|
||||
commandSender.sendData(p)
|
||||
serviceBroadcasts.broadcastMessageStatus(p)
|
||||
dataHandler.rememberDataPacket(p, myNodeNum, false)
|
||||
val bytes = p.bytes ?: ByteArray(0)
|
||||
val bytes = p.bytes ?: okio.ByteString.EMPTY
|
||||
analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType))
|
||||
}
|
||||
|
||||
|
|
@ -200,79 +200,83 @@ constructor(
|
|||
|
||||
fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int) {
|
||||
nodeManager.removeByNodenum(nodeNum)
|
||||
commandSender.sendAdmin(myNodeNum, requestId) { removeByNodenum = nodeNum }
|
||||
commandSender.sendAdmin(myNodeNum, requestId) { AdminMessage(remove_by_nodenum = nodeNum) }
|
||||
}
|
||||
|
||||
fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val u = MeshProtos.User.parseFrom(payload)
|
||||
commandSender.sendAdmin(destNum, id) { setOwner = u }
|
||||
val u = User.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_owner = u) }
|
||||
}
|
||||
|
||||
fun handleGetRemoteOwner(id: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { getOwnerRequest = true }
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_owner_request = true) }
|
||||
}
|
||||
|
||||
fun handleSetConfig(payload: ByteArray, myNodeNum: Int) {
|
||||
val c = ConfigProtos.Config.parseFrom(payload)
|
||||
commandSender.sendAdmin(myNodeNum) { setConfig = c }
|
||||
val c = Config.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_config = c) }
|
||||
}
|
||||
|
||||
fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val c = ConfigProtos.Config.parseFrom(payload)
|
||||
commandSender.sendAdmin(destNum, id) { setConfig = c }
|
||||
val c = Config.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_config = c) }
|
||||
}
|
||||
|
||||
fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) {
|
||||
if (config == AdminProtos.AdminMessage.ConfigType.SESSIONKEY_CONFIG_VALUE) {
|
||||
getDeviceMetadataRequest = true
|
||||
if (config == AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) {
|
||||
AdminMessage(get_device_metadata_request = true)
|
||||
} else {
|
||||
getConfigRequestValue = config
|
||||
AdminMessage(get_config_request = AdminMessage.ConfigType.fromValue(config))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) {
|
||||
val c = ModuleConfigProtos.ModuleConfig.parseFrom(payload)
|
||||
commandSender.sendAdmin(destNum, id) { setModuleConfig = c }
|
||||
val c = ModuleConfig.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_module_config = c) }
|
||||
}
|
||||
|
||||
fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { getModuleConfigRequestValue = config }
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) {
|
||||
AdminMessage(get_module_config_request = AdminMessage.ModuleConfigType.fromValue(config))
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSetRingtone(destNum: Int, ringtone: String) {
|
||||
commandSender.sendAdmin(destNum) { setRingtoneMessage = ringtone }
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(set_ringtone_message = ringtone) }
|
||||
}
|
||||
|
||||
fun handleGetRingtone(id: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { getRingtoneRequest = true }
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_ringtone_request = true) }
|
||||
}
|
||||
|
||||
fun handleSetCannedMessages(destNum: Int, messages: String) {
|
||||
commandSender.sendAdmin(destNum) { setCannedMessageModuleMessages = messages }
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(set_canned_message_module_messages = messages) }
|
||||
}
|
||||
|
||||
fun handleGetCannedMessages(id: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { getCannedMessageModuleMessagesRequest = true }
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) {
|
||||
AdminMessage(get_canned_message_module_messages_request = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSetChannel(payload: ByteArray?, myNodeNum: Int) {
|
||||
if (payload != null) {
|
||||
val c = ChannelProtos.Channel.parseFrom(payload)
|
||||
commandSender.sendAdmin(myNodeNum) { setChannel = c }
|
||||
val c = Channel.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_channel = c) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?) {
|
||||
if (payload != null) {
|
||||
val c = ChannelProtos.Channel.parseFrom(payload)
|
||||
commandSender.sendAdmin(destNum, id) { setChannel = c }
|
||||
val c = Channel.ADAPTER.decode(payload)
|
||||
commandSender.sendAdmin(destNum, id) { AdminMessage(set_channel = c) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int) {
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { getChannelRequest = index + 1 }
|
||||
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_channel_request = index + 1) }
|
||||
}
|
||||
|
||||
fun handleRequestNeighborInfo(requestId: Int, destNum: Int) {
|
||||
|
|
@ -280,15 +284,15 @@ constructor(
|
|||
}
|
||||
|
||||
fun handleBeginEditSettings(destNum: Int) {
|
||||
commandSender.sendAdmin(destNum) { beginEditSettings = true }
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(begin_edit_settings = true) }
|
||||
}
|
||||
|
||||
fun handleCommitEditSettings(destNum: Int) {
|
||||
commandSender.sendAdmin(destNum) { commitEditSettings = true }
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(commit_edit_settings = true) }
|
||||
}
|
||||
|
||||
fun handleRebootToDfu(destNum: Int) {
|
||||
commandSender.sendAdmin(destNum) { enterDfuModeRequest = true }
|
||||
commandSender.sendAdmin(destNum) { AdminMessage(enter_dfu_mode_request = true) }
|
||||
}
|
||||
|
||||
fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int) {
|
||||
|
|
@ -296,33 +300,32 @@ constructor(
|
|||
}
|
||||
|
||||
fun handleRequestShutdown(requestId: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, requestId) { shutdownSeconds = DEFAULT_REBOOT_DELAY }
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(shutdown_seconds = DEFAULT_REBOOT_DELAY) }
|
||||
}
|
||||
|
||||
fun handleRequestReboot(requestId: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, requestId) { rebootSeconds = DEFAULT_REBOOT_DELAY }
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(reboot_seconds = DEFAULT_REBOOT_DELAY) }
|
||||
}
|
||||
|
||||
fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {
|
||||
val otaMode = AdminProtos.OTAMode.forNumber(mode) ?: AdminProtos.OTAMode.NO_REBOOT_OTA
|
||||
val otaEventBuilder = AdminProtos.AdminMessage.OTAEvent.newBuilder()
|
||||
otaEventBuilder.rebootOtaMode = otaMode
|
||||
if (hash != null) {
|
||||
otaEventBuilder.otaHash = ByteString.copyFrom(hash)
|
||||
}
|
||||
commandSender.sendAdmin(destNum, requestId) { otaRequest = otaEventBuilder.build() }
|
||||
val otaMode = OTAMode.fromValue(mode) ?: OTAMode.NO_REBOOT_OTA
|
||||
val otaEvent =
|
||||
AdminMessage.OTAEvent(reboot_ota_mode = otaMode, ota_hash = hash?.toByteString() ?: okio.ByteString.EMPTY)
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(ota_request = otaEvent) }
|
||||
}
|
||||
|
||||
fun handleRequestFactoryReset(requestId: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, requestId) { factoryResetDevice = 1 }
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(factory_reset_device = 1) }
|
||||
}
|
||||
|
||||
fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) {
|
||||
commandSender.sendAdmin(destNum, requestId) { nodedbReset = preserveFavorites }
|
||||
commandSender.sendAdmin(destNum, requestId) { AdminMessage(nodedb_reset = preserveFavorites) }
|
||||
}
|
||||
|
||||
fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int) {
|
||||
commandSender.sendAdmin(destNum, requestId, wantResponse = true) { getDeviceConnectionStatusRequest = true }
|
||||
commandSender.sendAdmin(destNum, requestId, wantResponse = true) {
|
||||
AdminMessage(get_device_connection_status_request = true)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleUpdateLastAddress(deviceAddr: String?) {
|
||||
|
|
|
|||
|
|
@ -19,29 +19,31 @@ package com.geeksville.mesh.service
|
|||
import android.os.RemoteException
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.protobuf.ByteString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.model.TelemetryType
|
||||
import org.meshtastic.core.model.util.isWithinSizeLimit
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
|
||||
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.paxcount
|
||||
import org.meshtastic.proto.position
|
||||
import org.meshtastic.proto.telemetry
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Constants
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Neighbor
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
|
@ -68,17 +70,13 @@ constructor(
|
|||
val tracerouteStartTimes = ConcurrentHashMap<Int, Long>()
|
||||
val neighborInfoStartTimes = ConcurrentHashMap<Int, Long>()
|
||||
|
||||
private val localConfig = MutableStateFlow(LocalConfig.getDefaultInstance())
|
||||
private val channelSet = MutableStateFlow(ChannelSet.getDefaultInstance())
|
||||
private val localConfig = MutableStateFlow(LocalConfig())
|
||||
private val channelSet = MutableStateFlow(ChannelSet())
|
||||
|
||||
@Volatile var lastNeighborInfo: MeshProtos.NeighborInfo? = null
|
||||
@Volatile var lastNeighborInfo: NeighborInfo? = null
|
||||
|
||||
private val rememberDataType =
|
||||
setOf(
|
||||
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
Portnums.PortNum.ALERT_APP_VALUE,
|
||||
Portnums.PortNum.WAYPOINT_APP_VALUE,
|
||||
)
|
||||
setOf(PortNum.TEXT_MESSAGE_APP.value, PortNum.ALERT_APP.value, PortNum.WAYPOINT_APP.value)
|
||||
|
||||
fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
|
|
@ -100,7 +98,7 @@ constructor(
|
|||
sessionPasskey.set(key)
|
||||
}
|
||||
|
||||
private fun computeHopLimit(): Int = localConfig.value.lora.hopLimit.takeIf { it > 0 } ?: DEFAULT_HOP_LIMIT
|
||||
private fun computeHopLimit(): Int = (localConfig.value.lora?.hop_limit ?: 0).takeIf { it > 0 } ?: DEFAULT_HOP_LIMIT
|
||||
|
||||
private fun getAdminChannelIndex(toNum: Int): Int {
|
||||
val myNum = nodeManager?.myNodeNum ?: return 0
|
||||
|
|
@ -112,7 +110,7 @@ constructor(
|
|||
myNum == toNum -> 0
|
||||
myNode?.hasPKC == true && destNode?.hasPKC == true -> DataPacket.PKC_CHANNEL_INDEX
|
||||
else ->
|
||||
channelSet.value.settingsList
|
||||
channelSet.value.settings
|
||||
.indexOfFirst { it.name.equals(ADMIN_CHANNEL_NAME, ignoreCase = true) }
|
||||
.coerceAtLeast(0)
|
||||
}
|
||||
|
|
@ -121,11 +119,22 @@ constructor(
|
|||
|
||||
fun sendData(p: DataPacket) {
|
||||
if (p.id == 0) p.id = generatePacketId()
|
||||
val bytes = p.bytes ?: ByteArray(0)
|
||||
val bytes = p.bytes ?: ByteString.EMPTY
|
||||
require(p.dataType != 0) { "Port numbers must be non-zero!" }
|
||||
if (bytes.size >= MeshProtos.Constants.DATA_PAYLOAD_LEN_VALUE) {
|
||||
|
||||
// Use Wire extension for accurate size validation
|
||||
val data =
|
||||
Data(
|
||||
portnum = PortNum.fromValue(p.dataType) ?: PortNum.UNKNOWN_APP,
|
||||
payload = bytes,
|
||||
reply_id = p.replyId ?: 0,
|
||||
emoji = p.emoji,
|
||||
)
|
||||
|
||||
if (!Data.ADAPTER.isWithinSizeLimit(data, Constants.DATA_PAYLOAD_LEN.value)) {
|
||||
val actualSize = Data.ADAPTER.encodedSize(data)
|
||||
p.status = MessageStatus.ERROR
|
||||
throw RemoteException("Message too long")
|
||||
throw RemoteException("Message too long: $actualSize bytes (max ${Constants.DATA_PAYLOAD_LEN.value})")
|
||||
} else {
|
||||
p.status = MessageStatus.QUEUED
|
||||
}
|
||||
|
|
@ -144,17 +153,20 @@ constructor(
|
|||
|
||||
private fun sendNow(p: DataPacket) {
|
||||
val meshPacket =
|
||||
newMeshPacketTo(p.to ?: DataPacket.ID_BROADCAST).buildMeshPacket(
|
||||
buildMeshPacket(
|
||||
to = resolveNodeNum(p.to ?: DataPacket.ID_BROADCAST),
|
||||
id = p.id,
|
||||
wantAck = p.wantAck,
|
||||
hopLimit = if (p.hopLimit > 0) p.hopLimit else computeHopLimit(),
|
||||
channel = p.channel,
|
||||
) {
|
||||
portnumValue = p.dataType
|
||||
payload = ByteString.copyFrom(p.bytes ?: ByteArray(0))
|
||||
p.replyId?.let { if (it != 0) replyId = it }
|
||||
if (p.emoji != 0) emoji = p.emoji
|
||||
}
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.fromValue(p.dataType) ?: PortNum.UNKNOWN_APP,
|
||||
payload = p.bytes ?: ByteString.EMPTY,
|
||||
reply_id = p.replyId ?: 0,
|
||||
emoji = p.emoji,
|
||||
),
|
||||
)
|
||||
p.time = System.currentTimeMillis()
|
||||
packetHandler?.sendToRadio(meshPacket)
|
||||
}
|
||||
|
|
@ -182,64 +194,73 @@ constructor(
|
|||
destNum: Int,
|
||||
requestId: Int = generatePacketId(),
|
||||
wantResponse: Boolean = false,
|
||||
initFn: AdminProtos.AdminMessage.Builder.() -> Unit,
|
||||
initFn: () -> AdminMessage,
|
||||
) {
|
||||
val adminMsg = initFn().copy(session_passkey = sessionPasskey.get())
|
||||
val packet =
|
||||
newMeshPacketTo(destNum).buildAdminPacket(id = requestId, wantResponse = wantResponse, initFn = initFn)
|
||||
buildAdminPacket(to = destNum, id = requestId, wantResponse = wantResponse, adminMessage = adminMsg)
|
||||
packetHandler?.sendToRadio(packet)
|
||||
}
|
||||
|
||||
fun sendPosition(pos: MeshProtos.Position, destNum: Int? = null, wantResponse: Boolean = false) {
|
||||
fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int? = null, wantResponse: Boolean = false) {
|
||||
val myNum = nodeManager?.myNodeNum ?: return
|
||||
val idNum = destNum ?: myNum
|
||||
Logger.d { "Sending our position/time to=$idNum ${Position(pos)}" }
|
||||
Logger.d { "Sending our position/time to=$idNum $pos" }
|
||||
|
||||
if (!localConfig.value.position.fixedPosition) {
|
||||
if (localConfig.value.position?.fixed_position != true) {
|
||||
nodeManager.handleReceivedPosition(myNum, myNum, pos)
|
||||
}
|
||||
|
||||
packetHandler?.sendToRadio(
|
||||
newMeshPacketTo(idNum).buildMeshPacket(
|
||||
buildMeshPacket(
|
||||
to = idNum,
|
||||
channel = if (destNum == null) 0 else nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
priority = MeshPacket.Priority.BACKGROUND,
|
||||
) {
|
||||
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
||||
payload = pos.toByteString()
|
||||
this.wantResponse = wantResponse
|
||||
},
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.POSITION_APP,
|
||||
payload = pos.encode().toByteString(),
|
||||
want_response = wantResponse,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestPosition(destNum: Int, currentPosition: Position) {
|
||||
val meshPosition = position {
|
||||
latitudeI = Position.degI(currentPosition.latitude)
|
||||
longitudeI = Position.degI(currentPosition.longitude)
|
||||
altitude = currentPosition.altitude
|
||||
time = (System.currentTimeMillis() / TIME_MS_TO_S).toInt()
|
||||
}
|
||||
val meshPosition =
|
||||
org.meshtastic.proto.Position(
|
||||
latitude_i = Position.degI(currentPosition.latitude),
|
||||
longitude_i = Position.degI(currentPosition.longitude),
|
||||
altitude = currentPosition.altitude,
|
||||
time = (System.currentTimeMillis() / TIME_MS_TO_S).toInt(),
|
||||
)
|
||||
packetHandler?.sendToRadio(
|
||||
newMeshPacketTo(destNum).buildMeshPacket(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
|
||||
priority = MeshPacket.Priority.BACKGROUND,
|
||||
) {
|
||||
portnumValue = Portnums.PortNum.POSITION_APP_VALUE
|
||||
payload = meshPosition.toByteString()
|
||||
wantResponse = true
|
||||
},
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.POSITION_APP,
|
||||
payload = meshPosition.encode().toByteString(),
|
||||
want_response = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun setFixedPosition(destNum: Int, pos: Position) {
|
||||
val meshPos = position {
|
||||
latitudeI = Position.degI(pos.latitude)
|
||||
longitudeI = Position.degI(pos.longitude)
|
||||
altitude = pos.altitude
|
||||
}
|
||||
val meshPos =
|
||||
org.meshtastic.proto.Position(
|
||||
latitude_i = Position.degI(pos.latitude),
|
||||
longitude_i = Position.degI(pos.longitude),
|
||||
altitude = pos.altitude,
|
||||
)
|
||||
sendAdmin(destNum) {
|
||||
if (pos != Position(0.0, 0.0, 0)) {
|
||||
setFixedPosition = meshPos
|
||||
AdminMessage(set_fixed_position = meshPos)
|
||||
} else {
|
||||
removeFixedPosition = true
|
||||
AdminMessage(remove_fixed_position = true)
|
||||
}
|
||||
}
|
||||
nodeManager?.handleReceivedPosition(destNum, nodeManager.myNodeNum ?: 0, meshPos)
|
||||
|
|
@ -249,64 +270,67 @@ constructor(
|
|||
val myNum = nodeManager?.myNodeNum ?: return
|
||||
val myNode = nodeManager.getOrCreateNodeInfo(myNum)
|
||||
packetHandler?.sendToRadio(
|
||||
newMeshPacketTo(destNum).buildMeshPacket(channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0) {
|
||||
portnumValue = Portnums.PortNum.NODEINFO_APP_VALUE
|
||||
wantResponse = true
|
||||
payload = myNode.user.toByteString()
|
||||
},
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.NODEINFO_APP,
|
||||
want_response = true,
|
||||
payload = myNode.user.encode().toByteString(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestTraceroute(requestId: Int, destNum: Int) {
|
||||
tracerouteStartTimes[requestId] = System.currentTimeMillis()
|
||||
packetHandler?.sendToRadio(
|
||||
newMeshPacketTo(destNum).buildMeshPacket(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
wantAck = true,
|
||||
id = requestId,
|
||||
channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
|
||||
) {
|
||||
portnumValue = Portnums.PortNum.TRACEROUTE_APP_VALUE
|
||||
wantResponse = true
|
||||
},
|
||||
decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) {
|
||||
val type = TelemetryType.entries.getOrNull(typeValue) ?: TelemetryType.DEVICE
|
||||
|
||||
val portNum: Portnums.PortNum
|
||||
val portNum: PortNum
|
||||
val payloadBytes: ByteString
|
||||
|
||||
if (type == TelemetryType.PAX) {
|
||||
portNum = Portnums.PortNum.PAXCOUNTER_APP
|
||||
payloadBytes = paxcount {}.toByteString()
|
||||
portNum = PortNum.PAXCOUNTER_APP
|
||||
payloadBytes = org.meshtastic.proto.Paxcount().encode().toByteString()
|
||||
} else {
|
||||
portNum = Portnums.PortNum.TELEMETRY_APP
|
||||
portNum = PortNum.TELEMETRY_APP
|
||||
payloadBytes =
|
||||
telemetry {
|
||||
when (type) {
|
||||
TelemetryType.ENVIRONMENT ->
|
||||
environmentMetrics = TelemetryProtos.EnvironmentMetrics.getDefaultInstance()
|
||||
TelemetryType.AIR_QUALITY ->
|
||||
airQualityMetrics = TelemetryProtos.AirQualityMetrics.getDefaultInstance()
|
||||
TelemetryType.POWER -> powerMetrics = TelemetryProtos.PowerMetrics.getDefaultInstance()
|
||||
TelemetryType.LOCAL_STATS -> localStats = TelemetryProtos.LocalStats.getDefaultInstance()
|
||||
TelemetryType.DEVICE -> deviceMetrics = TelemetryProtos.DeviceMetrics.getDefaultInstance()
|
||||
TelemetryType.HOST -> hostMetrics = TelemetryProtos.HostMetrics.getDefaultInstance()
|
||||
}
|
||||
}
|
||||
Telemetry(
|
||||
device_metrics =
|
||||
if (type == TelemetryType.DEVICE) org.meshtastic.proto.DeviceMetrics() else null,
|
||||
environment_metrics =
|
||||
if (type == TelemetryType.ENVIRONMENT) org.meshtastic.proto.EnvironmentMetrics() else null,
|
||||
air_quality_metrics =
|
||||
if (type == TelemetryType.AIR_QUALITY) org.meshtastic.proto.AirQualityMetrics() else null,
|
||||
power_metrics = if (type == TelemetryType.POWER) org.meshtastic.proto.PowerMetrics() else null,
|
||||
local_stats =
|
||||
if (type == TelemetryType.LOCAL_STATS) org.meshtastic.proto.LocalStats() else null,
|
||||
host_metrics = if (type == TelemetryType.HOST) org.meshtastic.proto.HostMetrics() else null,
|
||||
)
|
||||
.encode()
|
||||
.toByteString()
|
||||
}
|
||||
|
||||
packetHandler?.sendToRadio(
|
||||
newMeshPacketTo(destNum).buildMeshPacket(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
id = requestId,
|
||||
channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
|
||||
) {
|
||||
portnumValue = portNum.number
|
||||
payload = payloadBytes
|
||||
wantResponse = true
|
||||
},
|
||||
decoded = Data(portnum = portNum, payload = payloadBytes, want_response = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -319,44 +343,47 @@ constructor(
|
|||
?: run {
|
||||
val oneHour = 1.hours.inWholeMinutes.toInt()
|
||||
Logger.d { "No stored neighbor info from connected radio, sending dummy data" }
|
||||
MeshProtos.NeighborInfo.newBuilder()
|
||||
.setNodeId(myNum)
|
||||
.setLastSentById(myNum)
|
||||
.setNodeBroadcastIntervalSecs(oneHour)
|
||||
.addNeighbors(
|
||||
MeshProtos.Neighbor.newBuilder()
|
||||
.setNodeId(0) // Dummy node ID that can be intercepted
|
||||
.setSnr(0f)
|
||||
.setLastRxTime((System.currentTimeMillis() / TIME_MS_TO_S).toInt())
|
||||
.setNodeBroadcastIntervalSecs(oneHour)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
NeighborInfo(
|
||||
node_id = myNum,
|
||||
last_sent_by_id = myNum,
|
||||
node_broadcast_interval_secs = oneHour,
|
||||
neighbors =
|
||||
listOf(
|
||||
Neighbor(
|
||||
node_id = 0, // Dummy node ID that can be intercepted
|
||||
snr = 0f,
|
||||
last_rx_time = (System.currentTimeMillis() / TIME_MS_TO_S).toInt(),
|
||||
node_broadcast_interval_secs = oneHour,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Send the neighbor info from our connected radio to ourselves (simulated)
|
||||
packetHandler?.sendToRadio(
|
||||
newMeshPacketTo(destNum).buildMeshPacket(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
wantAck = true,
|
||||
id = requestId,
|
||||
channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
|
||||
) {
|
||||
portnumValue = Portnums.PortNum.NEIGHBORINFO_APP_VALUE
|
||||
payload = neighborInfoToSend.toByteString()
|
||||
wantResponse = true
|
||||
},
|
||||
decoded =
|
||||
Data(
|
||||
portnum = PortNum.NEIGHBORINFO_APP,
|
||||
payload = neighborInfoToSend.encode().toByteString(),
|
||||
want_response = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// Send request to remote
|
||||
packetHandler?.sendToRadio(
|
||||
newMeshPacketTo(destNum).buildMeshPacket(
|
||||
buildMeshPacket(
|
||||
to = destNum,
|
||||
wantAck = true,
|
||||
id = requestId,
|
||||
channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
|
||||
) {
|
||||
portnumValue = Portnums.PortNum.NEIGHBORINFO_APP_VALUE
|
||||
wantResponse = true
|
||||
},
|
||||
decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, want_response = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -377,59 +404,60 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun newMeshPacketTo(toId: String): MeshPacket.Builder {
|
||||
val destNum = resolveNodeNum(toId)
|
||||
return newMeshPacketTo(destNum)
|
||||
}
|
||||
|
||||
private fun newMeshPacketTo(destNum: Int): MeshPacket.Builder = MeshPacket.newBuilder().apply { to = destNum }
|
||||
|
||||
private fun MeshPacket.Builder.buildMeshPacket(
|
||||
private fun buildMeshPacket(
|
||||
to: Int,
|
||||
wantAck: Boolean = false,
|
||||
id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one
|
||||
hopLimit: Int = 0,
|
||||
channel: Int = 0,
|
||||
priority: MeshPacket.Priority = MeshPacket.Priority.UNSET,
|
||||
initFn: MeshProtos.Data.Builder.() -> Unit,
|
||||
decoded: Data,
|
||||
): MeshPacket {
|
||||
this.id = id
|
||||
this.wantAck = wantAck
|
||||
val actualHopLimit = if (hopLimit > 0) hopLimit else computeHopLimit()
|
||||
this.hopLimit = actualHopLimit
|
||||
this.hopStart = actualHopLimit
|
||||
this.priority = priority
|
||||
|
||||
var pkiEncrypted = false
|
||||
var publicKey: ByteString = ByteString.EMPTY
|
||||
var actualChannel = channel
|
||||
|
||||
if (channel == DataPacket.PKC_CHANNEL_INDEX) {
|
||||
pkiEncrypted = true
|
||||
nodeManager?.nodeDBbyNodeNum?.get(to)?.user?.publicKey?.let { publicKey = it }
|
||||
} else {
|
||||
this.channel = channel
|
||||
publicKey = nodeManager?.nodeDBbyNodeNum?.get(to)?.user?.public_key ?: ByteString.EMPTY
|
||||
actualChannel = 0
|
||||
}
|
||||
|
||||
this.decoded = MeshProtos.Data.newBuilder().apply(initFn).build()
|
||||
return build()
|
||||
return MeshPacket(
|
||||
to = to,
|
||||
id = id,
|
||||
want_ack = wantAck,
|
||||
hop_limit = actualHopLimit,
|
||||
hop_start = actualHopLimit,
|
||||
priority = priority,
|
||||
pki_encrypted = pkiEncrypted,
|
||||
public_key = publicKey,
|
||||
channel = actualChannel,
|
||||
decoded = decoded,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MeshPacket.Builder.buildAdminPacket(
|
||||
private fun buildAdminPacket(
|
||||
to: Int,
|
||||
id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one
|
||||
wantResponse: Boolean = false,
|
||||
initFn: AdminProtos.AdminMessage.Builder.() -> Unit,
|
||||
adminMessage: AdminMessage,
|
||||
): MeshPacket =
|
||||
buildMeshPacket(
|
||||
to = to,
|
||||
id = id,
|
||||
wantAck = true,
|
||||
channel = getAdminChannelIndex(to),
|
||||
priority = MeshPacket.Priority.RELIABLE,
|
||||
) {
|
||||
this.wantResponse = wantResponse
|
||||
portnumValue = Portnums.PortNum.ADMIN_APP_VALUE
|
||||
payload =
|
||||
AdminProtos.AdminMessage.newBuilder()
|
||||
.apply(initFn)
|
||||
.setSessionPasskey(sessionPasskey.get())
|
||||
.build()
|
||||
.toByteString()
|
||||
}
|
||||
decoded =
|
||||
Data(
|
||||
want_response = wantResponse,
|
||||
portnum = PortNum.ADMIN_APP,
|
||||
payload = adminMessage.encode().toByteString(),
|
||||
),
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val PACKET_ID_MASK = 0xffffffffL
|
||||
|
|
|
|||
|
|
@ -28,7 +28,12 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
|
|||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
import org.meshtastic.proto.MyNodeInfo
|
||||
import org.meshtastic.proto.NodeInfo
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -57,11 +62,11 @@ constructor(
|
|||
this.scope = scope
|
||||
}
|
||||
|
||||
private val newNodes = mutableListOf<MeshProtos.NodeInfo>()
|
||||
private val newNodes = mutableListOf<NodeInfo>()
|
||||
val newNodeCount: Int
|
||||
get() = newNodes.size
|
||||
|
||||
private var rawMyNodeInfo: MeshProtos.MyNodeInfo? = null
|
||||
private var rawMyNodeInfo: MyNodeInfo? = null
|
||||
private var newMyNodeInfo: MyNodeEntity? = null
|
||||
private var myNodeInfo: MyNodeEntity? = null
|
||||
|
||||
|
|
@ -92,9 +97,7 @@ constructor(
|
|||
|
||||
private fun sendHeartbeat() {
|
||||
try {
|
||||
packetHandler.sendToRadio(
|
||||
MeshProtos.ToRadio.newBuilder().apply { heartbeat = MeshProtos.Heartbeat.getDefaultInstance() },
|
||||
)
|
||||
packetHandler.sendToRadio(ToRadio(heartbeat = Heartbeat()))
|
||||
Logger.d { "Heartbeat sent between nonce stages" }
|
||||
} catch (ex: IOException) {
|
||||
Logger.w(ex) { "Failed to send heartbeat; proceeding with node-info stage" }
|
||||
|
|
@ -127,10 +130,10 @@ constructor(
|
|||
analytics.setDeviceAttributes(mi.firmwareVersion ?: "unknown", mi.model ?: "unknown")
|
||||
}
|
||||
|
||||
fun handleMyInfo(myInfo: MeshProtos.MyNodeInfo) {
|
||||
Logger.i { "MyNodeInfo received: ${myInfo.myNodeNum}" }
|
||||
fun handleMyInfo(myInfo: MyNodeInfo) {
|
||||
Logger.i { "MyNodeInfo received: ${myInfo.my_node_num}" }
|
||||
rawMyNodeInfo = myInfo
|
||||
nodeManager.myNodeNum = myInfo.myNodeNum
|
||||
nodeManager.myNodeNum = myInfo.my_node_num ?: 0
|
||||
regenMyNodeInfo()
|
||||
|
||||
scope.handledLaunch {
|
||||
|
|
@ -140,42 +143,42 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleLocalMetadata(metadata: MeshProtos.DeviceMetadata) {
|
||||
fun handleLocalMetadata(metadata: DeviceMetadata) {
|
||||
Logger.i { "Local Metadata received" }
|
||||
regenMyNodeInfo(metadata)
|
||||
}
|
||||
|
||||
fun handleNodeInfo(info: MeshProtos.NodeInfo) {
|
||||
fun handleNodeInfo(info: NodeInfo) {
|
||||
newNodes.add(info)
|
||||
}
|
||||
|
||||
private fun regenMyNodeInfo(metadata: MeshProtos.DeviceMetadata? = MeshProtos.DeviceMetadata.getDefaultInstance()) {
|
||||
private fun regenMyNodeInfo(metadata: DeviceMetadata? = DeviceMetadata()) {
|
||||
val myInfo = rawMyNodeInfo
|
||||
if (myInfo != null) {
|
||||
val mi =
|
||||
with(myInfo) {
|
||||
MyNodeEntity(
|
||||
myNodeNum = myNodeNum,
|
||||
myNodeNum = my_node_num ?: 0,
|
||||
model =
|
||||
when (val hwModel = metadata?.hwModel) {
|
||||
when (val hwModel = metadata?.hw_model) {
|
||||
null,
|
||||
MeshProtos.HardwareModel.UNSET,
|
||||
HardwareModel.UNSET,
|
||||
-> null
|
||||
else -> hwModel.name.replace('_', '-').replace('p', '.').lowercase()
|
||||
},
|
||||
firmwareVersion = metadata?.firmwareVersion,
|
||||
firmwareVersion = metadata?.firmware_version,
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = commandSender.getCurrentPacketId() and 0xffffffffL,
|
||||
messageTimeoutMsec = 300000,
|
||||
minAppVersion = minAppVersion,
|
||||
minAppVersion = min_app_version ?: 0,
|
||||
maxChannels = 8,
|
||||
hasWifi = metadata?.hasWifi == true,
|
||||
deviceId = deviceId.toStringUtf8(),
|
||||
pioEnv = if (myInfo.pioEnv.isNullOrEmpty()) null else myInfo.pioEnv,
|
||||
deviceId = device_id?.utf8() ?: "",
|
||||
pioEnv = if (myInfo.pio_env.isNullOrEmpty()) null else myInfo.pio_env,
|
||||
)
|
||||
}
|
||||
if (metadata != null && metadata != MeshProtos.DeviceMetadata.getDefaultInstance()) {
|
||||
if (metadata != null && metadata != DeviceMetadata()) {
|
||||
scope.handledLaunch { nodeRepository.insertMetadata(MetadataEntity(mi.myNodeNum, metadata)) }
|
||||
}
|
||||
newMyNodeInfo = mi
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ import kotlinx.coroutines.flow.launchIn
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import org.meshtastic.core.data.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
|
||||
import org.meshtastic.proto.LocalOnlyProtos.LocalModuleConfig
|
||||
import org.meshtastic.proto.ModuleConfigProtos
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -44,15 +44,12 @@ constructor(
|
|||
) {
|
||||
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
private val _localConfig = MutableStateFlow(LocalConfig.getDefaultInstance())
|
||||
private val _localConfig = MutableStateFlow(LocalConfig())
|
||||
val localConfig = _localConfig.asStateFlow()
|
||||
|
||||
private val _moduleConfig = MutableStateFlow(LocalModuleConfig.getDefaultInstance())
|
||||
private val _moduleConfig = MutableStateFlow(LocalModuleConfig())
|
||||
val moduleConfig = _moduleConfig.asStateFlow()
|
||||
|
||||
private val configTotal = ConfigProtos.Config.getDescriptor().fields.size
|
||||
private val moduleTotal = ModuleConfigProtos.ModuleConfig.getDescriptor().fields.size
|
||||
|
||||
fun start(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
radioConfigRepository.localConfigFlow.onEach { _localConfig.value = it }.launchIn(scope)
|
||||
|
|
@ -60,28 +57,27 @@ constructor(
|
|||
radioConfigRepository.moduleConfigFlow.onEach { _moduleConfig.value = it }.launchIn(scope)
|
||||
}
|
||||
|
||||
fun handleDeviceConfig(config: ConfigProtos.Config) {
|
||||
fun handleDeviceConfig(config: Config) {
|
||||
scope.handledLaunch { radioConfigRepository.setLocalConfig(config) }
|
||||
val configCount = _localConfig.value.allFields.size
|
||||
serviceRepository.setStatusMessage("Device config ($configCount / $configTotal)")
|
||||
serviceRepository.setStatusMessage("Device config received")
|
||||
}
|
||||
|
||||
fun handleModuleConfig(config: ModuleConfigProtos.ModuleConfig) {
|
||||
fun handleModuleConfig(config: ModuleConfig) {
|
||||
scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) }
|
||||
val moduleCount = _moduleConfig.value.allFields.size
|
||||
serviceRepository.setStatusMessage("Module config ($moduleCount / $moduleTotal)")
|
||||
serviceRepository.setStatusMessage("Module config received")
|
||||
}
|
||||
|
||||
fun handleChannel(ch: ChannelProtos.Channel) {
|
||||
fun handleChannel(ch: Channel) {
|
||||
// We always want to save channel settings we receive from the radio
|
||||
scope.handledLaunch { radioConfigRepository.updateChannelSettings(ch) }
|
||||
|
||||
// Update status message if we have node info, otherwise use a generic one
|
||||
val mi = nodeManager.getMyNodeInfo()
|
||||
val index = ch.index ?: 0
|
||||
if (mi != null) {
|
||||
serviceRepository.setStatusMessage("Channels (${ch.index + 1} / ${mi.maxChannels})")
|
||||
serviceRepository.setStatusMessage("Channels (${index + 1} / ${mi.maxChannels})")
|
||||
} else {
|
||||
serviceRepository.setStatusMessage("Channels (${ch.index + 1})")
|
||||
serviceRepository.setStatusMessage("Channels (${index + 1})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,9 +42,10 @@ import org.meshtastic.core.strings.connected_count
|
|||
import org.meshtastic.core.strings.connecting
|
||||
import org.meshtastic.core.strings.device_sleeping
|
||||
import org.meshtastic.core.strings.disconnected
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.MeshProtos.ToRadio
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
@ -101,8 +102,8 @@ constructor(
|
|||
private fun onRadioConnectionState(newState: ConnectionState) {
|
||||
scope.handledLaunch {
|
||||
val localConfig = radioConfigRepository.localConfigFlow.first()
|
||||
val isRouter = localConfig.device.role == ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
||||
val lsEnabled = localConfig.power.isPowerSaving || isRouter
|
||||
val isRouter = localConfig.device?.role == Config.DeviceConfig.Role.ROUTER
|
||||
val lsEnabled = localConfig.power?.is_power_saving == true || isRouter
|
||||
|
||||
val effectiveState =
|
||||
when (newState) {
|
||||
|
|
@ -161,7 +162,7 @@ constructor(
|
|||
scope.handledLaunch {
|
||||
try {
|
||||
val localConfig = radioConfigRepository.localConfigFlow.first()
|
||||
val timeout = (localConfig.power?.lsSecs ?: 0) + DEVICE_SLEEP_TIMEOUT_SECONDS
|
||||
val timeout = (localConfig.power?.ls_secs ?: 0) + DEVICE_SLEEP_TIMEOUT_SECONDS
|
||||
Logger.d { "Waiting for sleeping device, timeout=$timeout secs" }
|
||||
delay(timeout.seconds)
|
||||
Logger.w { "Device timeout out, setting disconnected" }
|
||||
|
|
@ -191,11 +192,11 @@ constructor(
|
|||
}
|
||||
|
||||
fun startConfigOnly() {
|
||||
packetHandler.sendToRadio(ToRadio.newBuilder().apply { wantConfigId = CONFIG_ONLY_NONCE })
|
||||
packetHandler.sendToRadio(ToRadio(want_config_id = CONFIG_ONLY_NONCE))
|
||||
}
|
||||
|
||||
fun startNodeInfoOnly() {
|
||||
packetHandler.sendToRadio(ToRadio.newBuilder().apply { wantConfigId = NODE_INFO_NONCE })
|
||||
packetHandler.sendToRadio(ToRadio(want_config_id = NODE_INFO_NONCE))
|
||||
}
|
||||
|
||||
fun onHasSettings() {
|
||||
|
|
@ -204,7 +205,11 @@ constructor(
|
|||
// Start MQTT if enabled
|
||||
scope.handledLaunch {
|
||||
val moduleConfig = radioConfigRepository.moduleConfigFlow.first()
|
||||
mqttManager.start(scope, moduleConfig.mqtt.enabled, moduleConfig.mqtt.proxyToClientEnabled)
|
||||
mqttManager.start(
|
||||
scope,
|
||||
moduleConfig.mqtt?.enabled == true,
|
||||
moduleConfig.mqtt?.proxy_to_client_enabled == true,
|
||||
)
|
||||
}
|
||||
|
||||
reportConnection()
|
||||
|
|
@ -213,12 +218,14 @@ constructor(
|
|||
// Request history
|
||||
scope.handledLaunch {
|
||||
val moduleConfig = radioConfigRepository.moduleConfigFlow.first()
|
||||
historyManager.requestHistoryReplay("onHasSettings", myNodeNum, moduleConfig.storeForward, "Unknown")
|
||||
moduleConfig.store_forward?.let {
|
||||
historyManager.requestHistoryReplay("onHasSettings", myNodeNum, it, "Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// Set time
|
||||
commandSender.sendAdmin(myNodeNum) {
|
||||
setTimeOnly = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt()
|
||||
AdminMessage(set_time_only = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt())
|
||||
}
|
||||
updateStatusNotification()
|
||||
}
|
||||
|
|
@ -234,11 +241,11 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun updateTelemetry(telemetry: TelemetryProtos.Telemetry) {
|
||||
fun updateTelemetry(telemetry: Telemetry) {
|
||||
updateStatusNotification(telemetry)
|
||||
}
|
||||
|
||||
fun updateStatusNotification(telemetry: TelemetryProtos.Telemetry? = null): Notification {
|
||||
fun updateStatusNotification(telemetry: Telemetry? = null): Notification {
|
||||
val summary =
|
||||
when (connectionStateHolder.connectionState.value) {
|
||||
is ConnectionState.Connected ->
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import co.touchlab.kermit.Logger
|
|||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.repository.radio.InterfaceId
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import com.meshtastic.core.strings.getString
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
|
@ -29,6 +28,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.first
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.analytics.DataPair
|
||||
import org.meshtastic.core.analytics.platform.PlatformAnalytics
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
|
|
@ -38,6 +38,8 @@ import org.meshtastic.core.database.entity.ReactionEntity
|
|||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.util.SfppHasher
|
||||
import org.meshtastic.core.model.util.decodeOrNull
|
||||
import org.meshtastic.core.model.util.toOneLiner
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.RetryEvent
|
||||
|
|
@ -48,20 +50,25 @@ import org.meshtastic.core.strings.critical_alert
|
|||
import org.meshtastic.core.strings.error_duty_cycle
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.core.strings.waypoint_received
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.PaxcountProtos
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.StoreAndForwardProtos
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.AdminMessage
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.Routing
|
||||
import org.meshtastic.proto.StatusMessage
|
||||
import org.meshtastic.proto.StoreAndForward
|
||||
import org.meshtastic.proto.StoreForwardPlusPlus
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import org.meshtastic.proto.Waypoint
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass")
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "CyclomaticComplexMethod")
|
||||
@Singleton
|
||||
class MeshDataHandler
|
||||
@Inject
|
||||
|
|
@ -93,10 +100,10 @@ constructor(
|
|||
|
||||
private val rememberDataType =
|
||||
setOf(
|
||||
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
Portnums.PortNum.ALERT_APP_VALUE,
|
||||
Portnums.PortNum.WAYPOINT_APP_VALUE,
|
||||
Portnums.PortNum.NODE_STATUS_APP_VALUE,
|
||||
PortNum.TEXT_MESSAGE_APP.value,
|
||||
PortNum.ALERT_APP.value,
|
||||
PortNum.WAYPOINT_APP.value,
|
||||
PortNum.NODE_STATUS_APP.value,
|
||||
)
|
||||
|
||||
fun handleReceivedData(packet: MeshPacket, myNodeNum: Int, logUuid: String? = null, logInsertJob: Job? = null) {
|
||||
|
|
@ -121,14 +128,15 @@ constructor(
|
|||
logInsertJob: Job?,
|
||||
): Boolean {
|
||||
var shouldBroadcast = !fromUs
|
||||
when (packet.decoded.portnumValue) {
|
||||
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> handleTextMessage(packet, dataPacket, myNodeNum)
|
||||
Portnums.PortNum.NODE_STATUS_APP_VALUE -> handleNodeStatus(packet, dataPacket, myNodeNum)
|
||||
Portnums.PortNum.ALERT_APP_VALUE -> rememberDataPacket(dataPacket, myNodeNum)
|
||||
Portnums.PortNum.WAYPOINT_APP_VALUE -> handleWaypoint(packet, dataPacket, myNodeNum)
|
||||
Portnums.PortNum.POSITION_APP_VALUE -> handlePosition(packet, dataPacket, myNodeNum)
|
||||
Portnums.PortNum.NODEINFO_APP_VALUE -> if (!fromUs) handleNodeInfo(packet)
|
||||
Portnums.PortNum.TELEMETRY_APP_VALUE -> handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
val decoded = packet.decoded ?: return shouldBroadcast
|
||||
when (decoded.portnum) {
|
||||
PortNum.TEXT_MESSAGE_APP -> handleTextMessage(packet, dataPacket, myNodeNum)
|
||||
PortNum.NODE_STATUS_APP -> handleNodeStatus(packet, dataPacket, myNodeNum)
|
||||
PortNum.ALERT_APP -> rememberDataPacket(dataPacket, myNodeNum)
|
||||
PortNum.WAYPOINT_APP -> handleWaypoint(packet, dataPacket, myNodeNum)
|
||||
PortNum.POSITION_APP -> handlePosition(packet, dataPacket, myNodeNum)
|
||||
PortNum.NODEINFO_APP -> if (!fromUs) handleNodeInfo(packet)
|
||||
PortNum.TELEMETRY_APP -> handleTelemetry(packet, dataPacket, myNodeNum)
|
||||
else -> shouldBroadcast = handleSpecializedDataPacket(packet, dataPacket, myNodeNum, logUuid, logInsertJob)
|
||||
}
|
||||
return shouldBroadcast
|
||||
|
|
@ -142,189 +150,192 @@ constructor(
|
|||
logInsertJob: Job?,
|
||||
): Boolean {
|
||||
var shouldBroadcast = false
|
||||
when (packet.decoded.portnumValue) {
|
||||
Portnums.PortNum.TRACEROUTE_APP_VALUE -> {
|
||||
val decoded = packet.decoded ?: return shouldBroadcast
|
||||
when (decoded.portnum) {
|
||||
PortNum.TRACEROUTE_APP -> {
|
||||
tracerouteHandler.handleTraceroute(packet, logUuid, logInsertJob)
|
||||
shouldBroadcast = false
|
||||
}
|
||||
Portnums.PortNum.ROUTING_APP_VALUE -> {
|
||||
PortNum.ROUTING_APP -> {
|
||||
handleRouting(packet, dataPacket)
|
||||
shouldBroadcast = true
|
||||
}
|
||||
|
||||
Portnums.PortNum.PAXCOUNTER_APP_VALUE -> {
|
||||
PortNum.PAXCOUNTER_APP -> {
|
||||
handlePaxCounter(packet)
|
||||
shouldBroadcast = false
|
||||
}
|
||||
|
||||
Portnums.PortNum.STORE_FORWARD_APP_VALUE -> {
|
||||
PortNum.STORE_FORWARD_APP -> {
|
||||
handleStoreAndForward(packet, dataPacket, myNodeNum)
|
||||
shouldBroadcast = false
|
||||
}
|
||||
|
||||
Portnums.PortNum.STORE_FORWARD_PLUSPLUS_APP_VALUE -> {
|
||||
PortNum.STORE_FORWARD_PLUSPLUS_APP -> {
|
||||
handleStoreForwardPlusPlus(packet)
|
||||
shouldBroadcast = false
|
||||
}
|
||||
|
||||
Portnums.PortNum.ADMIN_APP_VALUE -> {
|
||||
PortNum.ADMIN_APP -> {
|
||||
handleAdminMessage(packet, myNodeNum)
|
||||
shouldBroadcast = false
|
||||
}
|
||||
|
||||
Portnums.PortNum.NEIGHBORINFO_APP_VALUE -> {
|
||||
PortNum.NEIGHBORINFO_APP -> {
|
||||
neighborInfoHandler.handleNeighborInfo(packet)
|
||||
shouldBroadcast = true
|
||||
}
|
||||
|
||||
Portnums.PortNum.RANGE_TEST_APP_VALUE,
|
||||
Portnums.PortNum.DETECTION_SENSOR_APP_VALUE,
|
||||
PortNum.RANGE_TEST_APP,
|
||||
PortNum.DETECTION_SENSOR_APP,
|
||||
-> {
|
||||
handleRangeTest(dataPacket, myNodeNum)
|
||||
shouldBroadcast = false
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return shouldBroadcast
|
||||
}
|
||||
|
||||
private fun handleRangeTest(dataPacket: DataPacket, myNodeNum: Int) {
|
||||
val u = dataPacket.copy(dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
|
||||
val u = dataPacket.copy(dataType = PortNum.TEXT_MESSAGE_APP.value)
|
||||
rememberDataPacket(u, myNodeNum)
|
||||
}
|
||||
|
||||
private fun handleStoreAndForward(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
|
||||
val u = StoreAndForwardProtos.StoreAndForward.parseFrom(packet.decoded.payload)
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val u = StoreAndForward.ADAPTER.decode(payload)
|
||||
handleReceivedStoreAndForward(dataPacket, u, myNodeNum)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun handleStoreForwardPlusPlus(packet: MeshPacket) {
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val sfpp =
|
||||
try {
|
||||
MeshProtos.StoreForwardPlusPlus.parseFrom(packet.decoded.payload)
|
||||
} catch (e: InvalidProtocolBufferException) {
|
||||
StoreForwardPlusPlus.ADAPTER.decode(payload)
|
||||
} catch (e: IOException) {
|
||||
Logger.e(e) { "Failed to parse StoreForwardPlusPlus packet" }
|
||||
return
|
||||
}
|
||||
Logger.d { "Received StoreForwardPlusPlus packet: $sfpp" }
|
||||
|
||||
when (sfpp.sfppMessageType) {
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE,
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_FIRSTHALF,
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_SECONDHALF,
|
||||
when (sfpp.sfpp_message_type) {
|
||||
StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE,
|
||||
StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_FIRSTHALF,
|
||||
StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE_SECONDHALF,
|
||||
-> {
|
||||
val isFragment = sfpp.sfppMessageType != MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE
|
||||
val isFragment = sfpp.sfpp_message_type != StoreForwardPlusPlus.SFPP_message_type.LINK_PROVIDE
|
||||
|
||||
// If it has a commit hash, it's already on the chain (Confirmed)
|
||||
// Otherwise it's still being routed via SF++ (Routing)
|
||||
val status = if (sfpp.commitHash.isEmpty) MessageStatus.SFPP_ROUTING else MessageStatus.SFPP_CONFIRMED
|
||||
val status =
|
||||
if (sfpp.commit_hash.size == 0) MessageStatus.SFPP_ROUTING else MessageStatus.SFPP_CONFIRMED
|
||||
|
||||
// Prefer a full 16-byte hash calculated from the message bytes if available
|
||||
// But only if it's NOT a fragment, otherwise the calculated hash would be wrong
|
||||
val hash =
|
||||
when {
|
||||
!sfpp.messageHash.isEmpty -> sfpp.messageHash.toByteArray()
|
||||
!isFragment && !sfpp.message.isEmpty -> {
|
||||
sfpp.message_hash.size != 0 -> sfpp.message_hash.toByteArray()
|
||||
!isFragment && sfpp.message.size != 0 -> {
|
||||
SfppHasher.computeMessageHash(
|
||||
encryptedPayload = sfpp.message.toByteArray(),
|
||||
// Map 0 back to NODENUM_BROADCAST to match firmware hash calculation
|
||||
to =
|
||||
if (sfpp.encapsulatedTo == 0) DataPacket.NODENUM_BROADCAST else sfpp.encapsulatedTo,
|
||||
from = sfpp.encapsulatedFrom,
|
||||
id = sfpp.encapsulatedId,
|
||||
if (sfpp.encapsulated_to == 0) {
|
||||
DataPacket.NODENUM_BROADCAST
|
||||
} else {
|
||||
sfpp.encapsulated_to
|
||||
},
|
||||
from = sfpp.encapsulated_from,
|
||||
id = sfpp.encapsulated_id,
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
} ?: return
|
||||
|
||||
Logger.d {
|
||||
"SFPP updateStatus: packetId=${sfpp.encapsulatedId} from=${sfpp.encapsulatedFrom} " +
|
||||
"to=${sfpp.encapsulatedTo} myNodeNum=${nodeManager.myNodeNum} status=$status"
|
||||
"SFPP updateStatus: packetId=${sfpp.encapsulated_id} from=${sfpp.encapsulated_from} " +
|
||||
"to=${sfpp.encapsulated_to} myNodeNum=${nodeManager.myNodeNum} status=$status"
|
||||
}
|
||||
scope.handledLaunch {
|
||||
packetRepository
|
||||
.get()
|
||||
.updateSFPPStatus(
|
||||
packetId = sfpp.encapsulatedId,
|
||||
from = sfpp.encapsulatedFrom,
|
||||
to = sfpp.encapsulatedTo,
|
||||
packetId = sfpp.encapsulated_id,
|
||||
from = sfpp.encapsulated_from,
|
||||
to = sfpp.encapsulated_to,
|
||||
hash = hash,
|
||||
status = status,
|
||||
rxTime = sfpp.encapsulatedRxtime.toLong() and 0xFFFFFFFFL,
|
||||
myNodeNum = nodeManager.myNodeNum,
|
||||
rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
|
||||
myNodeNum = nodeManager.myNodeNum ?: 0,
|
||||
)
|
||||
serviceBroadcasts.broadcastMessageStatus(sfpp.encapsulatedId, status)
|
||||
serviceBroadcasts.broadcastMessageStatus(sfpp.encapsulated_id, status)
|
||||
}
|
||||
}
|
||||
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.CANON_ANNOUNCE -> {
|
||||
StoreForwardPlusPlus.SFPP_message_type.CANON_ANNOUNCE -> {
|
||||
scope.handledLaunch {
|
||||
packetRepository
|
||||
.get()
|
||||
.updateSFPPStatusByHash(
|
||||
hash = sfpp.messageHash.toByteArray(),
|
||||
status = MessageStatus.SFPP_CONFIRMED,
|
||||
rxTime = sfpp.encapsulatedRxtime.toLong() and 0xFFFFFFFFL,
|
||||
)
|
||||
sfpp.message_hash.let {
|
||||
packetRepository
|
||||
.get()
|
||||
.updateSFPPStatusByHash(
|
||||
hash = it.toByteArray(),
|
||||
status = MessageStatus.SFPP_CONFIRMED,
|
||||
rxTime = sfpp.encapsulated_rxtime.toLong() and 0xFFFFFFFFL,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.CHAIN_QUERY -> {
|
||||
StoreForwardPlusPlus.SFPP_message_type.CHAIN_QUERY -> {
|
||||
Logger.i { "SF++: Node ${packet.from} is querying chain status" }
|
||||
}
|
||||
|
||||
MeshProtos.StoreForwardPlusPlus.SFPP_message_type.LINK_REQUEST -> {
|
||||
StoreForwardPlusPlus.SFPP_message_type.LINK_REQUEST -> {
|
||||
Logger.i { "SF++: Node ${packet.from} is requesting links" }
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePaxCounter(packet: MeshPacket) {
|
||||
val p = PaxcountProtos.Paxcount.parseFrom(packet.decoded.payload)
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val p = Paxcount.ADAPTER.decodeOrNull(payload, Logger) ?: return
|
||||
nodeManager.handleReceivedPaxcounter(packet.from, p)
|
||||
}
|
||||
|
||||
private fun handlePosition(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
|
||||
val p = MeshProtos.Position.parseFrom(packet.decoded.payload)
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val p = Position.ADAPTER.decodeOrNull(payload, Logger) ?: return
|
||||
Logger.d { "Position from ${packet.from}: ${Position.ADAPTER.toOneLiner(p)}" }
|
||||
nodeManager.handleReceivedPosition(packet.from, myNodeNum, p, dataPacket.time)
|
||||
}
|
||||
|
||||
private fun handleWaypoint(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
|
||||
val u = MeshProtos.Waypoint.parseFrom(packet.decoded.payload)
|
||||
if (u.lockedTo != 0 && u.lockedTo != packet.from) return
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val u = Waypoint.ADAPTER.decode(payload)
|
||||
if (u.locked_to != 0 && u.locked_to != packet.from) return
|
||||
val currentSecond = (System.currentTimeMillis() / MILLISECONDS_IN_SECOND).toInt()
|
||||
rememberDataPacket(dataPacket, myNodeNum, updateNotification = u.expire > currentSecond)
|
||||
}
|
||||
|
||||
private fun handleAdminMessage(packet: MeshPacket, myNodeNum: Int) {
|
||||
val u = AdminProtos.AdminMessage.parseFrom(packet.decoded.payload)
|
||||
commandSender.setSessionPasskey(u.sessionPasskey)
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val u = AdminMessage.ADAPTER.decode(payload)
|
||||
u.session_passkey.let { commandSender.setSessionPasskey(it) }
|
||||
|
||||
if (packet.from == myNodeNum) {
|
||||
when (u.payloadVariantCase) {
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE ->
|
||||
configHandler.handleDeviceConfig(u.getConfigResponse)
|
||||
|
||||
AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE ->
|
||||
configHandler.handleChannel(u.getChannelResponse)
|
||||
|
||||
else -> {}
|
||||
}
|
||||
val fromNum = packet.from
|
||||
if (fromNum == myNodeNum) {
|
||||
u.get_config_response?.let { configHandler.handleDeviceConfig(it) }
|
||||
u.get_channel_response?.let { configHandler.handleChannel(it) }
|
||||
}
|
||||
|
||||
if (u.payloadVariantCase == AdminProtos.AdminMessage.PayloadVariantCase.GET_DEVICE_METADATA_RESPONSE) {
|
||||
if (packet.from == myNodeNum) {
|
||||
configFlowManager.handleLocalMetadata(u.getDeviceMetadataResponse)
|
||||
u.get_device_metadata_response?.let { metadata ->
|
||||
if (fromNum == myNodeNum) {
|
||||
configFlowManager.handleLocalMetadata(metadata)
|
||||
} else {
|
||||
nodeManager.insertMetadata(packet.from, u.getDeviceMetadataResponse)
|
||||
nodeManager.insertMetadata(fromNum, metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTextMessage(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
|
||||
if (packet.decoded.replyId != 0 && packet.decoded.emoji != 0) {
|
||||
val decoded = packet.decoded ?: return
|
||||
if (decoded.reply_id != 0 && decoded.emoji != 0) {
|
||||
rememberReaction(packet)
|
||||
} else {
|
||||
rememberDataPacket(dataPacket, myNodeNum)
|
||||
|
|
@ -332,25 +343,28 @@ constructor(
|
|||
}
|
||||
|
||||
private fun handleNodeInfo(packet: MeshPacket) {
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val u =
|
||||
MeshProtos.User.parseFrom(packet.decoded.payload).copy {
|
||||
if (isLicensed) clearPublicKey()
|
||||
if (packet.viaMqtt) longName = "$longName (MQTT)"
|
||||
}
|
||||
User.ADAPTER.decode(payload)
|
||||
.let { if (it.is_licensed == true) it.copy(public_key = okio.ByteString.EMPTY) else it }
|
||||
.let { if (packet.via_mqtt == true) it.copy(long_name = "${it.long_name} (MQTT)") else it }
|
||||
nodeManager.handleReceivedUser(packet.from, u, packet.channel)
|
||||
}
|
||||
|
||||
private fun handleNodeStatus(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
|
||||
val s = MeshProtos.StatusMessage.parseFrom(packet.decoded.payload)
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val s = StatusMessage.ADAPTER.decodeOrNull(payload, Logger) ?: return
|
||||
nodeManager.handleReceivedNodeStatus(packet.from, s)
|
||||
rememberDataPacket(dataPacket, myNodeNum)
|
||||
}
|
||||
|
||||
private fun handleTelemetry(packet: MeshPacket, dataPacket: DataPacket, myNodeNum: Int) {
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val t =
|
||||
TelemetryProtos.Telemetry.parseFrom(packet.decoded.payload).copy {
|
||||
if (time == 0) time = (dataPacket.time.milliseconds.inWholeSeconds).toInt()
|
||||
(Telemetry.ADAPTER.decodeOrNull(payload, Logger) ?: return).let {
|
||||
if (it.time == 0) it.copy(time = (dataPacket.time.milliseconds.inWholeSeconds).toInt()) else it
|
||||
}
|
||||
Logger.d { "Telemetry from ${packet.from}: ${Telemetry.ADAPTER.toOneLiner(t)}" }
|
||||
val fromNum = packet.from
|
||||
val isRemote = (fromNum != myNodeNum)
|
||||
if (!isRemote) {
|
||||
|
|
@ -358,13 +372,16 @@ constructor(
|
|||
}
|
||||
|
||||
nodeManager.updateNodeInfo(fromNum) { nodeEntity ->
|
||||
val metrics = t.device_metrics
|
||||
val environment = t.environment_metrics
|
||||
val power = t.power_metrics
|
||||
when {
|
||||
t.hasDeviceMetrics() -> {
|
||||
metrics != null -> {
|
||||
nodeEntity.deviceTelemetry = t
|
||||
if (fromNum == myNodeNum || (isRemote && nodeEntity.isFavorite)) {
|
||||
if (
|
||||
t.deviceMetrics.voltage > BATTERY_PERCENT_UNSUPPORTED &&
|
||||
t.deviceMetrics.batteryLevel <= BATTERY_PERCENT_LOW_THRESHOLD
|
||||
(metrics.voltage ?: 0f) > BATTERY_PERCENT_UNSUPPORTED &&
|
||||
(metrics.battery_level ?: 0) <= BATTERY_PERCENT_LOW_THRESHOLD
|
||||
) {
|
||||
if (shouldBatteryNotificationShow(fromNum, t, myNodeNum)) {
|
||||
serviceNotifications.showOrUpdateLowBatteryNotification(nodeEntity, isRemote)
|
||||
|
|
@ -378,24 +395,26 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
t.hasEnvironmentMetrics() -> nodeEntity.environmentTelemetry = t
|
||||
t.hasPowerMetrics() -> nodeEntity.powerTelemetry = t
|
||||
environment != null -> nodeEntity.environmentTelemetry = t
|
||||
power != null -> nodeEntity.powerTelemetry = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldBatteryNotificationShow(fromNum: Int, t: TelemetryProtos.Telemetry, myNodeNum: Int): Boolean {
|
||||
private fun shouldBatteryNotificationShow(fromNum: Int, t: Telemetry, myNodeNum: Int): Boolean {
|
||||
val isRemote = (fromNum != myNodeNum)
|
||||
var shouldDisplay = false
|
||||
var forceDisplay = false
|
||||
val metrics = t.device_metrics ?: return false
|
||||
val batteryLevel = metrics.battery_level ?: 0
|
||||
when {
|
||||
t.deviceMetrics.batteryLevel <= BATTERY_PERCENT_CRITICAL_THRESHOLD -> {
|
||||
batteryLevel <= BATTERY_PERCENT_CRITICAL_THRESHOLD -> {
|
||||
shouldDisplay = true
|
||||
forceDisplay = true
|
||||
}
|
||||
|
||||
t.deviceMetrics.batteryLevel == BATTERY_PERCENT_LOW_THRESHOLD -> shouldDisplay = true
|
||||
t.deviceMetrics.batteryLevel.mod(BATTERY_PERCENT_LOW_DIVISOR) == 0 && !isRemote -> shouldDisplay = true
|
||||
batteryLevel == BATTERY_PERCENT_LOW_THRESHOLD -> shouldDisplay = true
|
||||
batteryLevel.mod(BATTERY_PERCENT_LOW_DIVISOR) == 0 && !isRemote -> shouldDisplay = true
|
||||
|
||||
isRemote -> shouldDisplay = true
|
||||
}
|
||||
|
|
@ -411,31 +430,32 @@ constructor(
|
|||
}
|
||||
|
||||
private fun handleRouting(packet: MeshPacket, dataPacket: DataPacket) {
|
||||
val r = MeshProtos.Routing.parseFrom(packet.decoded.payload)
|
||||
if (r.errorReason == MeshProtos.Routing.Error.DUTY_CYCLE_LIMIT) {
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val r = Routing.ADAPTER.decodeOrNull(payload, Logger) ?: return
|
||||
if (r.error_reason == Routing.Error.DUTY_CYCLE_LIMIT) {
|
||||
serviceRepository.setErrorMessage(getString(Res.string.error_duty_cycle))
|
||||
}
|
||||
handleAckNak(
|
||||
packet.decoded.requestId,
|
||||
packet.decoded?.request_id ?: 0,
|
||||
dataMapper.toNodeID(packet.from),
|
||||
r.errorReasonValue,
|
||||
r.error_reason?.value ?: 0,
|
||||
dataPacket.relayNode,
|
||||
)
|
||||
packetHandler.removeResponse(packet.decoded.requestId, complete = true)
|
||||
packet.decoded?.request_id?.let { packetHandler.removeResponse(it, complete = true) }
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
private fun handleAckNak(requestId: Int, fromId: String, routingError: Int, relayNode: Int?) {
|
||||
scope.handledLaunch {
|
||||
val isAck = routingError == MeshProtos.Routing.Error.NONE_VALUE
|
||||
val isAck = routingError == Routing.Error.NONE.value
|
||||
val p = packetRepository.get().getPacketById(requestId)
|
||||
val reaction = packetRepository.get().getReactionByPacketId(requestId)
|
||||
|
||||
val isMaxRetransmit = routingError == MeshProtos.Routing.Error.MAX_RETRANSMIT_VALUE
|
||||
val isMaxRetransmit = routingError == Routing.Error.MAX_RETRANSMIT.value
|
||||
val shouldRetry =
|
||||
isMaxRetransmit &&
|
||||
p != null &&
|
||||
p.port_num == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE &&
|
||||
p.port_num == PortNum.TEXT_MESSAGE_APP.value &&
|
||||
(p.data.from == DataPacket.ID_LOCAL || p.data.from == nodeManager.getMyId()) &&
|
||||
p.data.retryCount < MAX_RETRY_ATTEMPTS
|
||||
|
||||
|
|
@ -482,7 +502,7 @@ constructor(
|
|||
relayNode = null,
|
||||
)
|
||||
val updatedPacket =
|
||||
p.copy(packetId = newId, data = updatedData, routingError = MeshProtos.Routing.Error.NONE_VALUE)
|
||||
p.copy(packetId = newId, data = updatedData, routingError = Routing.Error.NONE.value)
|
||||
packetRepository.get().update(updatedPacket)
|
||||
|
||||
Logger.w { "[ackNak] retrying req=$requestId newId=$newId retry=$newRetryCount" }
|
||||
|
|
@ -496,7 +516,7 @@ constructor(
|
|||
return@handledLaunch
|
||||
}
|
||||
|
||||
if (shouldRetryReaction && reaction != null) {
|
||||
if (shouldRetryReaction) {
|
||||
val newRetryCount = reaction.retryCount + 1
|
||||
|
||||
// Emit retry event to UI and wait for user response
|
||||
|
|
@ -519,8 +539,8 @@ constructor(
|
|||
DataPacket(
|
||||
to = reaction.to,
|
||||
channel = reaction.channel,
|
||||
bytes = reaction.emoji.toByteArray(Charsets.UTF_8),
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = reaction.emoji.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
replyId = reaction.replyId,
|
||||
wantAck = true,
|
||||
emoji = reaction.emoji.codePointAt(0),
|
||||
|
|
@ -534,7 +554,7 @@ constructor(
|
|||
status = MessageStatus.QUEUED,
|
||||
retryCount = newRetryCount,
|
||||
relayNode = null,
|
||||
routingError = MeshProtos.Routing.Error.NONE_VALUE,
|
||||
routingError = Routing.Error.NONE.value,
|
||||
)
|
||||
packetRepository.get().updateReaction(updatedReaction)
|
||||
|
||||
|
|
@ -579,59 +599,53 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleReceivedStoreAndForward(
|
||||
dataPacket: DataPacket,
|
||||
s: StoreAndForwardProtos.StoreAndForward,
|
||||
myNodeNum: Int,
|
||||
) {
|
||||
Logger.d { "StoreAndForward: ${s.variantCase} ${s.rr} from ${dataPacket.from}" }
|
||||
private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForward, myNodeNum: Int) {
|
||||
Logger.d { "StoreAndForward: variant from ${dataPacket.from}" }
|
||||
val transport = currentTransport()
|
||||
val isHistory = s.variantCase == StoreAndForwardProtos.StoreAndForward.VariantCase.HISTORY
|
||||
val lastRequest = if (isHistory) s.history.lastRequest else 0
|
||||
val h = s.history
|
||||
val lastRequest = h?.last_request ?: 0
|
||||
val baseContext = "transport=$transport from=${dataPacket.from}"
|
||||
historyLog { "rxStoreForward $baseContext variant=${s.variantCase} rr=${s.rr} lastRequest=$lastRequest" }
|
||||
when (s.variantCase) {
|
||||
StoreAndForwardProtos.StoreAndForward.VariantCase.STATS -> {
|
||||
historyLog { "rxStoreForward $baseContext lastRequest=$lastRequest" }
|
||||
when {
|
||||
s.stats != null -> {
|
||||
val text = s.stats.toString()
|
||||
val u =
|
||||
dataPacket.copy(
|
||||
bytes = text.encodeToByteArray(),
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = text.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
)
|
||||
rememberDataPacket(u, myNodeNum)
|
||||
}
|
||||
StoreAndForwardProtos.StoreAndForward.VariantCase.HISTORY -> {
|
||||
val h = s.history
|
||||
h != null -> {
|
||||
@Suppress("MaxLineLength")
|
||||
historyLog(Log.DEBUG) {
|
||||
"routerHistory $baseContext messages=${h.historyMessages} window=${h.window} lastReq=${h.lastRequest}"
|
||||
"routerHistory $baseContext messages=${h.history_messages} window=${h.window} lastReq=${h.last_request}"
|
||||
}
|
||||
val text =
|
||||
"Total messages: ${h.historyMessages}\n" +
|
||||
"Total messages: ${h.history_messages}\n" +
|
||||
"History window: ${h.window.milliseconds.inWholeMinutes} min\n" +
|
||||
"Last request: ${h.lastRequest}"
|
||||
"Last request: ${h.last_request}"
|
||||
val u =
|
||||
dataPacket.copy(
|
||||
bytes = text.encodeToByteArray(),
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = text.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
)
|
||||
rememberDataPacket(u, myNodeNum)
|
||||
historyManager.updateStoreForwardLastRequest("router_history", h.lastRequest, transport)
|
||||
historyManager.updateStoreForwardLastRequest("router_history", h.last_request, transport)
|
||||
}
|
||||
StoreAndForwardProtos.StoreAndForward.VariantCase.HEARTBEAT -> {
|
||||
val hb = s.heartbeat
|
||||
s.heartbeat != null -> {
|
||||
val hb = s.heartbeat!!
|
||||
historyLog { "rxHeartbeat $baseContext period=${hb.period} secondary=${hb.secondary}" }
|
||||
}
|
||||
StoreAndForwardProtos.StoreAndForward.VariantCase.TEXT -> {
|
||||
if (s.rr == StoreAndForwardProtos.StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) {
|
||||
s.text != null -> {
|
||||
if (s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) {
|
||||
dataPacket.to = DataPacket.ID_BROADCAST
|
||||
}
|
||||
@Suppress("MaxLineLength")
|
||||
historyLog(Log.DEBUG) {
|
||||
"rxText $baseContext id=${dataPacket.id} ts=${dataPacket.time} to=${dataPacket.to} decision=remember"
|
||||
}
|
||||
val u =
|
||||
dataPacket.copy(bytes = s.text.toByteArray(), dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE)
|
||||
val u = dataPacket.copy(bytes = s.text, dataType = PortNum.TEXT_MESSAGE_APP.value)
|
||||
rememberDataPacket(u, myNodeNum)
|
||||
}
|
||||
else -> {}
|
||||
|
|
@ -689,7 +703,7 @@ constructor(
|
|||
}
|
||||
|
||||
private suspend fun PacketRepository.shouldFilterMessage(dataPacket: DataPacket, contactKey: String): Boolean {
|
||||
if (dataPacket.dataType != Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) return false
|
||||
if (dataPacket.dataType != PortNum.TEXT_MESSAGE_APP.value) return false
|
||||
val isFilteringDisabled = getContactSettings(contactKey).filteringDisabled
|
||||
return messageFilterService.shouldFilter(dataPacket.text.orEmpty(), isFilteringDisabled)
|
||||
}
|
||||
|
|
@ -703,7 +717,7 @@ constructor(
|
|||
val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted
|
||||
val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true
|
||||
val isSilent = conversationMuted || nodeMuted
|
||||
if (packet.port_num == Portnums.PortNum.ALERT_APP_VALUE && !isSilent) {
|
||||
if (packet.port_num == PortNum.ALERT_APP.value && !isSilent) {
|
||||
serviceNotifications.showAlertNotification(
|
||||
contactKey,
|
||||
getSenderName(dataPacket),
|
||||
|
|
@ -717,18 +731,18 @@ constructor(
|
|||
private fun getSenderName(packet: DataPacket): String {
|
||||
if (packet.from == DataPacket.ID_LOCAL) {
|
||||
val myId = nodeManager.getMyId()
|
||||
return nodeManager.nodeDBbyID[myId]?.user?.longName ?: getString(Res.string.unknown_username)
|
||||
return nodeManager.nodeDBbyID[myId]?.user?.long_name ?: getString(Res.string.unknown_username)
|
||||
}
|
||||
return nodeManager.nodeDBbyID[packet.from]?.user?.longName ?: getString(Res.string.unknown_username)
|
||||
return nodeManager.nodeDBbyID[packet.from]?.user?.long_name ?: getString(Res.string.unknown_username)
|
||||
}
|
||||
|
||||
private suspend fun updateNotification(contactKey: String, dataPacket: DataPacket, isSilent: Boolean) {
|
||||
when (dataPacket.dataType) {
|
||||
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> {
|
||||
PortNum.TEXT_MESSAGE_APP.value -> {
|
||||
val message = dataPacket.text!!
|
||||
val channelName =
|
||||
if (dataPacket.to == DataPacket.ID_BROADCAST) {
|
||||
radioConfigRepository.channelSetFlow.first().settingsList.getOrNull(dataPacket.channel)?.name
|
||||
radioConfigRepository.channelSetFlow.first().settings.getOrNull(dataPacket.channel)?.name
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -742,7 +756,7 @@ constructor(
|
|||
)
|
||||
}
|
||||
|
||||
Portnums.PortNum.WAYPOINT_APP_VALUE -> {
|
||||
PortNum.WAYPOINT_APP.value -> {
|
||||
val message = getString(Res.string.waypoint_received, dataPacket.waypoint!!.name)
|
||||
serviceNotifications.updateWaypointNotification(
|
||||
contactKey,
|
||||
|
|
@ -759,24 +773,25 @@ constructor(
|
|||
|
||||
@Suppress("LongMethod", "KotlinConstantConditions")
|
||||
private fun rememberReaction(packet: MeshPacket) = scope.handledLaunch {
|
||||
val emoji = packet.decoded.payload.toByteArray().decodeToString()
|
||||
val decoded = packet.decoded ?: return@handledLaunch
|
||||
val emoji = decoded.payload.toByteArray().decodeToString()
|
||||
val fromId = dataMapper.toNodeID(packet.from)
|
||||
val toId = dataMapper.toNodeID(packet.to)
|
||||
|
||||
val reaction =
|
||||
ReactionEntity(
|
||||
myNodeNum = nodeManager.myNodeNum ?: 0,
|
||||
replyId = packet.decoded.replyId,
|
||||
replyId = decoded.reply_id,
|
||||
userId = fromId,
|
||||
emoji = emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
snr = packet.rxSnr,
|
||||
rssi = packet.rxRssi,
|
||||
snr = packet.rx_snr,
|
||||
rssi = packet.rx_rssi,
|
||||
hopsAway =
|
||||
if (packet.hopStart == 0 || packet.hopLimit > packet.hopStart) {
|
||||
if (packet.hop_start == 0 || packet.hop_limit > packet.hop_start) {
|
||||
HOPS_AWAY_UNAVAILABLE
|
||||
} else {
|
||||
packet.hopStart - packet.hopLimit
|
||||
packet.hop_start - packet.hop_limit
|
||||
},
|
||||
packetId = packet.id,
|
||||
status = MessageStatus.RECEIVED,
|
||||
|
|
@ -788,7 +803,7 @@ constructor(
|
|||
val existingReactions = packetRepository.get().findReactionsWithId(packet.id)
|
||||
if (existingReactions.isNotEmpty()) {
|
||||
Logger.d {
|
||||
"Skipping duplicate reaction: packetId=${packet.id} replyId=${packet.decoded.replyId} " +
|
||||
"Skipping duplicate reaction: packetId=${packet.id} replyId=${decoded.reply_id} " +
|
||||
"from=$fromId emoji=$emoji (already have ${existingReactions.size} reaction(s))"
|
||||
}
|
||||
return@handledLaunch
|
||||
|
|
@ -797,7 +812,7 @@ constructor(
|
|||
packetRepository.get().insertReaction(reaction)
|
||||
|
||||
// Find the original packet to get the contactKey
|
||||
packetRepository.get().getPacketByPacketId(packet.decoded.replyId)?.let { original ->
|
||||
packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { original ->
|
||||
// Skip notification if the original message was filtered
|
||||
if (original.packet.filtered) return@let
|
||||
|
||||
|
|
@ -811,7 +826,7 @@ constructor(
|
|||
if (original.packet.data.to == DataPacket.ID_BROADCAST) {
|
||||
radioConfigRepository.channelSetFlow
|
||||
.first()
|
||||
.settingsList
|
||||
.settings
|
||||
.getOrNull(original.packet.data.channel)
|
||||
?.name
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@
|
|||
*/
|
||||
package com.geeksville.mesh.service
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -29,27 +30,25 @@ class MeshDataMapper @Inject constructor(private val nodeManager: MeshNodeManage
|
|||
nodeManager.nodeDBbyNodeNum[n]?.user?.id ?: DataPacket.nodeNumToDefaultId(n)
|
||||
}
|
||||
|
||||
fun toDataPacket(packet: MeshPacket): DataPacket? = if (!packet.hasDecoded()) {
|
||||
null
|
||||
} else {
|
||||
val data = packet.decoded
|
||||
DataPacket(
|
||||
fun toDataPacket(packet: MeshPacket): DataPacket? {
|
||||
val decoded = packet.decoded ?: return null
|
||||
return DataPacket(
|
||||
from = toNodeID(packet.from),
|
||||
to = toNodeID(packet.to),
|
||||
time = packet.rxTime * 1000L,
|
||||
time = packet.rx_time * 1000L,
|
||||
id = packet.id,
|
||||
dataType = data.portnumValue,
|
||||
bytes = data.payload.toByteArray(),
|
||||
hopLimit = packet.hopLimit,
|
||||
channel = if (packet.pkiEncrypted) DataPacket.PKC_CHANNEL_INDEX else packet.channel,
|
||||
wantAck = packet.wantAck,
|
||||
hopStart = packet.hopStart,
|
||||
snr = packet.rxSnr,
|
||||
rssi = packet.rxRssi,
|
||||
replyId = data.replyId,
|
||||
relayNode = packet.relayNode,
|
||||
viaMqtt = packet.viaMqtt,
|
||||
emoji = data.emoji,
|
||||
dataType = decoded.portnum.value,
|
||||
bytes = decoded.payload.toByteArray().toByteString(),
|
||||
hopLimit = packet.hop_limit,
|
||||
channel = if (packet.pki_encrypted == true) DataPacket.PKC_CHANNEL_INDEX else packet.channel,
|
||||
wantAck = packet.want_ack == true,
|
||||
hopStart = packet.hop_start,
|
||||
snr = packet.rx_snr,
|
||||
rssi = packet.rx_rssi,
|
||||
replyId = decoded.reply_id,
|
||||
relayNode = packet.relay_node,
|
||||
viaMqtt = packet.via_mqtt == true,
|
||||
emoji = decoded.emoji,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@ import androidx.annotation.VisibleForTesting
|
|||
import co.touchlab.kermit.Logger
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
|
||||
import com.google.protobuf.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.prefs.mesh.MeshPrefs
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.ModuleConfigProtos
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.StoreAndForwardProtos
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.StoreAndForward
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -47,15 +48,14 @@ constructor(
|
|||
lastRequest: Int,
|
||||
historyReturnWindow: Int,
|
||||
historyReturnMax: Int,
|
||||
): StoreAndForwardProtos.StoreAndForward {
|
||||
val historyBuilder = StoreAndForwardProtos.StoreAndForward.History.newBuilder()
|
||||
if (lastRequest > 0) historyBuilder.lastRequest = lastRequest
|
||||
if (historyReturnWindow > 0) historyBuilder.window = historyReturnWindow
|
||||
if (historyReturnMax > 0) historyBuilder.historyMessages = historyReturnMax
|
||||
return StoreAndForwardProtos.StoreAndForward.newBuilder()
|
||||
.setRr(StoreAndForwardProtos.StoreAndForward.RequestResponse.CLIENT_HISTORY)
|
||||
.setHistory(historyBuilder)
|
||||
.build()
|
||||
): StoreAndForward {
|
||||
val history =
|
||||
StoreAndForward.History(
|
||||
last_request = lastRequest.coerceAtLeast(0),
|
||||
window = historyReturnWindow.coerceAtLeast(0),
|
||||
history_messages = historyReturnMax.coerceAtLeast(0),
|
||||
)
|
||||
return StoreAndForward(rr = StoreAndForward.RequestResponse.CLIENT_HISTORY, history = history)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
@ -86,7 +86,7 @@ constructor(
|
|||
fun requestHistoryReplay(
|
||||
trigger: String,
|
||||
myNodeNum: Int?,
|
||||
storeForwardConfig: ModuleConfigProtos.ModuleConfig.StoreForwardConfig?,
|
||||
storeForwardConfig: ModuleConfig.StoreForwardConfig?,
|
||||
transport: String,
|
||||
) {
|
||||
val address = activeDeviceAddress()
|
||||
|
|
@ -99,8 +99,8 @@ constructor(
|
|||
val lastRequest = meshPrefs.getStoreForwardLastRequest(address)
|
||||
val (window, max) =
|
||||
resolveHistoryRequestParameters(
|
||||
storeForwardConfig?.historyReturnWindow ?: 0,
|
||||
storeForwardConfig?.historyReturnMax ?: 0,
|
||||
storeForwardConfig?.history_return_window ?: 0,
|
||||
storeForwardConfig?.history_return_max ?: 0,
|
||||
)
|
||||
|
||||
val request = buildStoreForwardHistoryRequest(lastRequest, window, max)
|
||||
|
|
@ -112,19 +112,11 @@ constructor(
|
|||
|
||||
runCatching {
|
||||
packetHandler.sendToRadio(
|
||||
MeshPacket.newBuilder()
|
||||
.apply {
|
||||
to = myNodeNum
|
||||
decoded =
|
||||
org.meshtastic.proto.MeshProtos.Data.newBuilder()
|
||||
.apply {
|
||||
portnumValue = Portnums.PortNum.STORE_FORWARD_APP_VALUE
|
||||
payload = ByteString.copyFrom(request.toByteArray())
|
||||
}
|
||||
.build()
|
||||
priority = MeshPacket.Priority.BACKGROUND
|
||||
}
|
||||
.build(),
|
||||
MeshPacket(
|
||||
to = myNodeNum,
|
||||
decoded = Data(portnum = PortNum.STORE_FORWARD_APP, payload = request.encode().toByteString()),
|
||||
priority = MeshPacket.Priority.BACKGROUND,
|
||||
),
|
||||
)
|
||||
}
|
||||
.onFailure { ex -> historyLog(Log.WARN, ex) { "requestHistory failed" } }
|
||||
|
|
|
|||
|
|
@ -29,11 +29,10 @@ import kotlinx.coroutines.flow.onEach
|
|||
import org.meshtastic.core.common.hasLocationPermission
|
||||
import org.meshtastic.core.data.repository.LocationRepository
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.position
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
@Singleton
|
||||
class MeshLocationManager
|
||||
|
|
@ -46,7 +45,7 @@ constructor(
|
|||
private var locationFlow: Job? = null
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun start(scope: CoroutineScope, sendPositionFn: (MeshProtos.Position) -> Unit) {
|
||||
fun start(scope: CoroutineScope, sendPositionFn: (ProtoPosition) -> Unit) {
|
||||
this.scope = scope
|
||||
if (locationFlow?.isActive == true) return
|
||||
|
||||
|
|
@ -56,18 +55,21 @@ constructor(
|
|||
.getLocations()
|
||||
.onEach { location ->
|
||||
sendPositionFn(
|
||||
position {
|
||||
latitudeI = Position.degI(location.latitude)
|
||||
longitudeI = Position.degI(location.longitude)
|
||||
ProtoPosition(
|
||||
latitude_i = Position.degI(location.latitude),
|
||||
longitude_i = Position.degI(location.longitude),
|
||||
altitude =
|
||||
if (LocationCompat.hasMslAltitude(location)) {
|
||||
altitude = LocationCompat.getMslAltitudeMeters(location).toInt()
|
||||
}
|
||||
altitudeHae = location.altitude.toInt()
|
||||
time = (location.time.milliseconds.inWholeSeconds).toInt()
|
||||
groundSpeed = location.speed.toInt()
|
||||
groundTrack = location.bearing.toInt()
|
||||
locationSource = MeshProtos.Position.LocSource.LOC_EXTERNAL
|
||||
},
|
||||
LocationCompat.getMslAltitudeMeters(location).toInt()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
altitude_hae = location.altitude.toInt(),
|
||||
time = (location.time.milliseconds.inWholeSeconds).toInt(),
|
||||
ground_speed = location.speed.toInt(),
|
||||
ground_track = location.bearing.toInt(),
|
||||
location_source = ProtoPosition.LocSource.LOC_EXTERNAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(scope)
|
||||
|
|
|
|||
|
|
@ -30,11 +30,10 @@ import kotlinx.coroutines.flow.onEach
|
|||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.FromRadio.PayloadVariantCase
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.fromRadio
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.LogRecord
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import java.util.ArrayDeque
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
|
@ -77,17 +76,12 @@ constructor(
|
|||
}
|
||||
|
||||
fun handleFromRadio(bytes: ByteArray, myNodeNum: Int?) {
|
||||
runCatching { MeshProtos.FromRadio.parseFrom(bytes) }
|
||||
.onSuccess { proto ->
|
||||
if (proto.payloadVariantCase == PayloadVariantCase.PAYLOADVARIANT_NOT_SET) {
|
||||
Logger.w { "Received FromRadio with PAYLOADVARIANT_NOT_SET. rawBytes=${bytes.toHexString()}" }
|
||||
}
|
||||
processFromRadio(proto, myNodeNum)
|
||||
}
|
||||
runCatching { FromRadio.ADAPTER.decode(bytes) }
|
||||
.onSuccess { proto -> processFromRadio(proto, myNodeNum) }
|
||||
.onFailure { primaryException ->
|
||||
runCatching {
|
||||
val logRecord = MeshProtos.LogRecord.parseFrom(bytes)
|
||||
processFromRadio(fromRadio { this.logRecord = logRecord }, myNodeNum)
|
||||
val logRecord = LogRecord.ADAPTER.decode(bytes)
|
||||
processFromRadio(FromRadio(log_record = logRecord), myNodeNum)
|
||||
}
|
||||
.onFailure { _ ->
|
||||
Logger.e(primaryException) {
|
||||
|
|
@ -98,31 +92,32 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun processFromRadio(proto: MeshProtos.FromRadio, myNodeNum: Int?) {
|
||||
private fun processFromRadio(proto: FromRadio, myNodeNum: Int?) {
|
||||
// Audit log every incoming variant
|
||||
logVariant(proto)
|
||||
|
||||
if (proto.payloadVariantCase == PayloadVariantCase.PACKET) {
|
||||
handleReceivedMeshPacket(proto.packet, myNodeNum)
|
||||
val packet = proto.packet
|
||||
if (packet != null) {
|
||||
handleReceivedMeshPacket(packet, myNodeNum)
|
||||
} else {
|
||||
fromRadioDispatcher.handleFromRadio(proto)
|
||||
}
|
||||
}
|
||||
|
||||
private fun logVariant(proto: MeshProtos.FromRadio) {
|
||||
private fun logVariant(proto: FromRadio) {
|
||||
val (type, message) =
|
||||
when (proto.payloadVariantCase) {
|
||||
PayloadVariantCase.LOG_RECORD -> "LogRecord" to proto.logRecord.toString()
|
||||
PayloadVariantCase.REBOOTED -> "Rebooted" to proto.rebooted.toString()
|
||||
PayloadVariantCase.XMODEMPACKET -> "XmodemPacket" to proto.xmodemPacket.toString()
|
||||
PayloadVariantCase.DEVICEUICONFIG -> "DeviceUIConfig" to proto.deviceuiConfig.toString()
|
||||
PayloadVariantCase.FILEINFO -> "FileInfo" to proto.fileInfo.toString()
|
||||
PayloadVariantCase.MY_INFO -> "MyInfo" to proto.myInfo.toString()
|
||||
PayloadVariantCase.NODE_INFO -> "NodeInfo" to proto.nodeInfo.toString()
|
||||
PayloadVariantCase.CONFIG -> "Config" to proto.config.toString()
|
||||
PayloadVariantCase.MODULECONFIG -> "ModuleConfig" to proto.moduleConfig.toString()
|
||||
PayloadVariantCase.CHANNEL -> "Channel" to proto.channel.toString()
|
||||
PayloadVariantCase.CLIENTNOTIFICATION -> "ClientNotification" to proto.clientNotification.toString()
|
||||
when {
|
||||
proto.log_record != null -> "LogRecord" to proto.log_record.toString()
|
||||
proto.rebooted != null -> "Rebooted" to proto.rebooted.toString()
|
||||
proto.xmodemPacket != null -> "XmodemPacket" to proto.xmodemPacket.toString()
|
||||
proto.deviceuiConfig != null -> "DeviceUIConfig" to proto.deviceuiConfig.toString()
|
||||
proto.fileInfo != null -> "FileInfo" to proto.fileInfo.toString()
|
||||
proto.my_info != null -> "MyInfo" to proto.my_info.toString()
|
||||
proto.node_info != null -> "NodeInfo" to proto.node_info.toString()
|
||||
proto.config != null -> "Config" to proto.config.toString()
|
||||
proto.moduleConfig != null -> "ModuleConfig" to proto.moduleConfig.toString()
|
||||
proto.channel != null -> "Channel" to proto.channel.toString()
|
||||
proto.clientNotification != null -> "ClientNotification" to proto.clientNotification.toString()
|
||||
else -> return
|
||||
}
|
||||
|
||||
|
|
@ -139,8 +134,12 @@ constructor(
|
|||
|
||||
fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) {
|
||||
val rxTime =
|
||||
if (packet.rxTime == 0) (System.currentTimeMillis().milliseconds.inWholeSeconds).toInt() else packet.rxTime
|
||||
val preparedPacket = packet.toBuilder().setRxTime(rxTime).build()
|
||||
if (packet.rx_time == 0) {
|
||||
(System.currentTimeMillis().milliseconds.inWholeSeconds).toInt()
|
||||
} else {
|
||||
packet.rx_time
|
||||
}
|
||||
val preparedPacket = packet.copy(rx_time = rxTime)
|
||||
|
||||
if (nodeManager.isNodeDbReady.value) {
|
||||
processReceivedMeshPacket(preparedPacket, myNodeNum)
|
||||
|
|
@ -151,23 +150,15 @@ constructor(
|
|||
val dropped = earlyReceivedPackets.removeFirst()
|
||||
historyLog(Log.WARN) {
|
||||
val portLabel =
|
||||
if (dropped.hasDecoded()) {
|
||||
Portnums.PortNum.forNumber(dropped.decoded.portnumValue)?.name
|
||||
?: dropped.decoded.portnumValue.toString()
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
dropped.decoded?.portnum?.name ?: dropped.decoded?.portnum?.value?.toString() ?: "unknown"
|
||||
"dropEarlyPacket bufferFull size=$queueSize id=${dropped.id} port=$portLabel"
|
||||
}
|
||||
}
|
||||
earlyReceivedPackets.addLast(preparedPacket)
|
||||
val portLabel =
|
||||
if (preparedPacket.hasDecoded()) {
|
||||
Portnums.PortNum.forNumber(preparedPacket.decoded.portnumValue)?.name
|
||||
?: preparedPacket.decoded.portnumValue.toString()
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
preparedPacket.decoded?.portnum?.name
|
||||
?: preparedPacket.decoded?.portnum?.value?.toString()
|
||||
?: "unknown"
|
||||
historyLog {
|
||||
"queueEarlyPacket size=${earlyReceivedPackets.size} id=${preparedPacket.id} port=$portLabel"
|
||||
}
|
||||
|
|
@ -189,7 +180,7 @@ constructor(
|
|||
}
|
||||
|
||||
private fun processReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) {
|
||||
if (!packet.hasDecoded()) return
|
||||
val decoded = packet.decoded ?: return
|
||||
val log =
|
||||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
|
|
@ -197,8 +188,8 @@ constructor(
|
|||
received_date = System.currentTimeMillis(),
|
||||
raw_message = packet.toString(),
|
||||
fromNum = packet.from,
|
||||
portNum = packet.decoded.portnumValue,
|
||||
fromRadio = fromRadio { this.packet = packet },
|
||||
portNum = decoded.portnum.value,
|
||||
fromRadio = FromRadio(packet = packet),
|
||||
)
|
||||
val logJob = insertMeshLog(log)
|
||||
logInsertJobByPacketId[packet.id] = logJob
|
||||
|
|
@ -207,23 +198,24 @@ constructor(
|
|||
scope.handledLaunch { serviceRepository.emitMeshPacket(packet) }
|
||||
|
||||
myNodeNum?.let { myNum ->
|
||||
val isOtherNode = myNum != packet.from
|
||||
val from = packet.from
|
||||
val isOtherNode = myNum != from
|
||||
nodeManager.updateNodeInfo(myNum, withBroadcast = isOtherNode) {
|
||||
it.lastHeard = (System.currentTimeMillis().milliseconds.inWholeSeconds).toInt()
|
||||
}
|
||||
nodeManager.updateNodeInfo(packet.from, withBroadcast = false, channel = packet.channel) {
|
||||
it.lastHeard = packet.rxTime
|
||||
it.snr = packet.rxSnr
|
||||
it.rssi = packet.rxRssi
|
||||
nodeManager.updateNodeInfo(from, withBroadcast = false, channel = packet.channel) {
|
||||
it.lastHeard = packet.rx_time
|
||||
it.snr = packet.rx_snr
|
||||
it.rssi = packet.rx_rssi
|
||||
it.hopsAway =
|
||||
if (packet.decoded.portnumValue == Portnums.PortNum.RANGE_TEST_APP_VALUE) {
|
||||
if (decoded.portnum == PortNum.RANGE_TEST_APP) {
|
||||
0
|
||||
} else if (packet.hopStart == 0 && !packet.decoded.hasBitfield()) {
|
||||
} else if (packet.hop_start == 0 && (decoded.bitfield ?: 0) == 0) {
|
||||
-1
|
||||
} else if (packet.hopLimit > packet.hopStart) {
|
||||
} else if (packet.hop_limit > packet.hop_start) {
|
||||
-1
|
||||
} else {
|
||||
packet.hopStart - packet.hopLimit
|
||||
packet.hop_start - packet.hop_limit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ import kotlinx.coroutines.flow.catch
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.ToRadio
|
||||
import org.meshtastic.proto.MqttClientProxyMessage
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -48,9 +48,7 @@ constructor(
|
|||
if (enabled && proxyToClientEnabled) {
|
||||
mqttMessageFlow =
|
||||
mqttRepository.proxyMessageFlow
|
||||
.onEach { message ->
|
||||
packetHandler.sendToRadio(ToRadio.newBuilder().apply { mqttClientProxyMessage = message })
|
||||
}
|
||||
.onEach { message -> packetHandler.sendToRadio(ToRadio(mqttClientProxyMessage = message)) }
|
||||
.catch { throwable -> serviceRepository.setErrorMessage("MqttClientProxy failed: $throwable") }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
|
@ -64,18 +62,18 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleMqttProxyMessage(message: MeshProtos.MqttClientProxyMessage) {
|
||||
Logger.d { "[mqttClientProxyMessage] ${message.topic}" }
|
||||
with(message) {
|
||||
when (payloadVariantCase) {
|
||||
MeshProtos.MqttClientProxyMessage.PayloadVariantCase.TEXT -> {
|
||||
mqttRepository.publish(topic, text.encodeToByteArray(), retained)
|
||||
}
|
||||
MeshProtos.MqttClientProxyMessage.PayloadVariantCase.DATA -> {
|
||||
mqttRepository.publish(topic, data.toByteArray(), retained)
|
||||
}
|
||||
else -> {}
|
||||
fun handleMqttProxyMessage(message: MqttClientProxyMessage) {
|
||||
val topic = message.topic ?: ""
|
||||
Logger.d { "[mqttClientProxyMessage] $topic" }
|
||||
val retained = message.retained == true
|
||||
when {
|
||||
message.text != null -> {
|
||||
mqttRepository.publish(topic, message.text!!.encodeToByteArray(), retained)
|
||||
}
|
||||
message.data_ != null -> {
|
||||
mqttRepository.publish(topic, message.data_!!.toByteArray(), retained)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.NeighborInfo
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -46,29 +46,31 @@ constructor(
|
|||
}
|
||||
|
||||
fun handleNeighborInfo(packet: MeshPacket) {
|
||||
val ni = MeshProtos.NeighborInfo.parseFrom(packet.decoded.payload)
|
||||
val payload = packet.decoded?.payload ?: return
|
||||
val ni = NeighborInfo.ADAPTER.decode(payload)
|
||||
|
||||
// Store the last neighbor info from our connected radio
|
||||
if (packet.from == nodeManager.myNodeNum) {
|
||||
val from = packet.from ?: 0
|
||||
if (from == nodeManager.myNodeNum) {
|
||||
commandSender.lastNeighborInfo = ni
|
||||
Logger.d { "Stored last neighbor info from connected radio" }
|
||||
}
|
||||
|
||||
// Update Node DB
|
||||
nodeManager.nodeDBbyNodeNum[packet.from]?.let { serviceBroadcasts.broadcastNodeChange(it.toNodeInfo()) }
|
||||
nodeManager.nodeDBbyNodeNum[from]?.let { serviceBroadcasts.broadcastNodeChange(it.toNodeInfo()) }
|
||||
|
||||
// Format for UI response
|
||||
val requestId = packet.decoded.requestId
|
||||
val requestId = packet.decoded?.request_id ?: 0
|
||||
val start = commandSender.neighborInfoStartTimes.remove(requestId)
|
||||
|
||||
val neighbors =
|
||||
ni.neighborsList.joinToString("\n") { n ->
|
||||
val node = nodeManager.nodeDBbyNodeNum[n.nodeId]
|
||||
ni.neighbors.joinToString("\n") { n ->
|
||||
val node = nodeManager.nodeDBbyNodeNum[n.node_id]
|
||||
val name = node?.let { "${it.longName} (${it.shortName})" } ?: getString(Res.string.unknown_username)
|
||||
"• $name (SNR: ${n.snr})"
|
||||
}
|
||||
|
||||
val formatted = "Neighbors of ${nodeManager.nodeDBbyNodeNum[packet.from]?.longName ?: "Unknown"}:\n$neighbors"
|
||||
val formatted = "Neighbors of ${nodeManager.nodeDBbyNodeNum[from]?.longName ?: "Unknown"}:\n$neighbors"
|
||||
|
||||
val responseText =
|
||||
if (start != null) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import okio.ByteString
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.entity.MetadataEntity
|
||||
import org.meshtastic.core.database.entity.NodeEntity
|
||||
|
|
@ -32,17 +33,19 @@ import org.meshtastic.core.model.MyNodeInfo
|
|||
import org.meshtastic.core.model.NodeInfo
|
||||
import org.meshtastic.core.model.Position
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.PaxcountProtos
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.telemetry
|
||||
import org.meshtastic.proto.user
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.StatusMessage
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import org.meshtastic.proto.User
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
|
||||
import org.meshtastic.proto.Position as ProtoPosition
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
|
||||
@Singleton
|
||||
class MeshNodeManager
|
||||
@Inject
|
||||
|
|
@ -93,8 +96,8 @@ constructor(
|
|||
val myNode = nodeDBbyNodeNum[mi.myNodeNum]
|
||||
return MyNodeInfo(
|
||||
myNodeNum = mi.myNodeNum,
|
||||
hasGPS = myNode?.position?.latitudeI != 0,
|
||||
model = mi.model ?: myNode?.user?.hwModel?.name,
|
||||
hasGPS = (myNode?.position?.latitude_i ?: 0) != 0,
|
||||
model = mi.model ?: myNode?.user?.hw_model?.name,
|
||||
firmwareVersion = mi.firmwareVersion,
|
||||
couldUpdate = mi.couldUpdate,
|
||||
shouldUpdate = mi.shouldUpdate,
|
||||
|
|
@ -122,18 +125,19 @@ constructor(
|
|||
|
||||
fun getOrCreateNodeInfo(n: Int, channel: Int = 0): NodeEntity = nodeDBbyNodeNum.getOrPut(n) {
|
||||
val userId = DataPacket.nodeNumToDefaultId(n)
|
||||
val defaultUser = user {
|
||||
id = userId
|
||||
longName = "Meshtastic ${userId.takeLast(n = 4)}"
|
||||
shortName = userId.takeLast(n = 4)
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
}
|
||||
val defaultUser =
|
||||
User(
|
||||
id = userId,
|
||||
long_name = "Meshtastic ${userId.takeLast(n = 4)}",
|
||||
short_name = userId.takeLast(n = 4),
|
||||
hw_model = HardwareModel.UNSET,
|
||||
)
|
||||
|
||||
NodeEntity(
|
||||
num = n,
|
||||
user = defaultUser,
|
||||
longName = defaultUser.longName,
|
||||
shortName = defaultUser.shortName,
|
||||
longName = defaultUser.long_name,
|
||||
shortName = defaultUser.short_name,
|
||||
channel = channel,
|
||||
)
|
||||
}
|
||||
|
|
@ -154,25 +158,25 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun insertMetadata(nodeNum: Int, metadata: MeshProtos.DeviceMetadata) {
|
||||
fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) {
|
||||
scope.handledLaunch { nodeRepository?.insertMetadata(MetadataEntity(nodeNum, metadata)) }
|
||||
}
|
||||
|
||||
fun handleReceivedUser(fromNum: Int, p: MeshProtos.User, channel: Int = 0, manuallyVerified: Boolean = false) {
|
||||
fun handleReceivedUser(fromNum: Int, p: User, channel: Int = 0, manuallyVerified: Boolean = false) {
|
||||
updateNodeInfo(fromNum) {
|
||||
val newNode = (it.isUnknownUser && p.hwModel != MeshProtos.HardwareModel.UNSET)
|
||||
val newNode = (it.isUnknownUser && p.hw_model != HardwareModel.UNSET)
|
||||
val shouldPreserve = shouldPreserveExistingUser(it.user, p)
|
||||
|
||||
if (shouldPreserve) {
|
||||
it.longName = it.user.longName
|
||||
it.shortName = it.user.shortName
|
||||
it.longName = it.user.long_name
|
||||
it.shortName = it.user.short_name
|
||||
it.channel = channel
|
||||
it.manuallyVerified = manuallyVerified
|
||||
} else {
|
||||
val keyMatch = !it.hasPKC || it.user.publicKey == p.publicKey
|
||||
it.user = if (keyMatch) p else p.copy { publicKey = NodeEntity.ERROR_BYTE_STRING }
|
||||
it.longName = p.longName
|
||||
it.shortName = p.shortName
|
||||
val keyMatch = !it.hasPKC || it.user.public_key == p.public_key
|
||||
it.user = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY)
|
||||
it.longName = p.long_name
|
||||
it.shortName = p.short_name
|
||||
it.channel = channel
|
||||
it.manuallyVerified = manuallyVerified
|
||||
if (newNode) {
|
||||
|
|
@ -185,72 +189,74 @@ constructor(
|
|||
fun handleReceivedPosition(
|
||||
fromNum: Int,
|
||||
myNodeNum: Int,
|
||||
p: MeshProtos.Position,
|
||||
p: ProtoPosition,
|
||||
defaultTime: Long = System.currentTimeMillis(),
|
||||
) {
|
||||
if (myNodeNum == fromNum && p.latitudeI == 0 && p.longitudeI == 0) {
|
||||
if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) {
|
||||
Logger.d { "Ignoring nop position update for the local node" }
|
||||
} else {
|
||||
updateNodeInfo(fromNum) { it.setPosition(p, (defaultTime / TIME_MS_TO_S).toInt()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun handleReceivedTelemetry(fromNum: Int, telemetry: TelemetryProtos.Telemetry) {
|
||||
fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) {
|
||||
updateNodeInfo(fromNum) { nodeEntity ->
|
||||
when {
|
||||
telemetry.hasDeviceMetrics() -> nodeEntity.deviceTelemetry = telemetry
|
||||
telemetry.hasEnvironmentMetrics() -> nodeEntity.environmentTelemetry = telemetry
|
||||
telemetry.hasPowerMetrics() -> nodeEntity.powerTelemetry = telemetry
|
||||
telemetry.device_metrics != null -> nodeEntity.deviceTelemetry = telemetry
|
||||
telemetry.environment_metrics != null -> nodeEntity.environmentTelemetry = telemetry
|
||||
telemetry.power_metrics != null -> nodeEntity.powerTelemetry = telemetry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleReceivedPaxcounter(fromNum: Int, p: PaxcountProtos.Paxcount) {
|
||||
fun handleReceivedPaxcounter(fromNum: Int, p: Paxcount) {
|
||||
updateNodeInfo(fromNum) { it.paxcounter = p }
|
||||
}
|
||||
|
||||
fun handleReceivedNodeStatus(fromNum: Int, s: MeshProtos.StatusMessage) {
|
||||
fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) {
|
||||
updateNodeInfo(fromNum) { it.nodeStatus = s.status }
|
||||
}
|
||||
|
||||
fun installNodeInfo(info: MeshProtos.NodeInfo, withBroadcast: Boolean = true) {
|
||||
fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true) {
|
||||
updateNodeInfo(info.num, withBroadcast = withBroadcast) { entity ->
|
||||
if (info.hasUser()) {
|
||||
if (shouldPreserveExistingUser(entity.user, info.user)) {
|
||||
entity.longName = entity.user.longName
|
||||
entity.shortName = entity.user.shortName
|
||||
val user = info.user
|
||||
if (user != null) {
|
||||
if (shouldPreserveExistingUser(entity.user, user)) {
|
||||
entity.longName = entity.user.long_name
|
||||
entity.shortName = entity.user.short_name
|
||||
} else {
|
||||
entity.user =
|
||||
info.user.copy {
|
||||
if (isLicensed) clearPublicKey()
|
||||
if (info.viaMqtt) longName = "$longName (MQTT)"
|
||||
}
|
||||
entity.longName = entity.user.longName
|
||||
entity.shortName = entity.user.shortName
|
||||
var newUser = user.let { if (it.is_licensed) it.copy(public_key = ByteString.EMPTY) else it }
|
||||
if (info.via_mqtt) {
|
||||
newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)")
|
||||
}
|
||||
entity.user = newUser
|
||||
entity.longName = newUser.long_name
|
||||
entity.shortName = newUser.short_name
|
||||
}
|
||||
}
|
||||
if (info.hasPosition()) {
|
||||
entity.position = info.position
|
||||
entity.latitude = Position.degD(info.position.latitudeI)
|
||||
entity.longitude = Position.degD(info.position.longitudeI)
|
||||
val position = info.position
|
||||
if (position != null) {
|
||||
entity.position = position
|
||||
entity.latitude = Position.degD(position.latitude_i ?: 0)
|
||||
entity.longitude = Position.degD(position.longitude_i ?: 0)
|
||||
}
|
||||
entity.lastHeard = info.lastHeard
|
||||
if (info.hasDeviceMetrics()) {
|
||||
entity.deviceTelemetry = telemetry { deviceMetrics = info.deviceMetrics }
|
||||
entity.lastHeard = info.last_heard
|
||||
if (info.device_metrics != null) {
|
||||
entity.deviceTelemetry = Telemetry(device_metrics = info.device_metrics)
|
||||
}
|
||||
entity.channel = info.channel
|
||||
entity.viaMqtt = info.viaMqtt
|
||||
entity.hopsAway = if (info.hasHopsAway()) info.hopsAway else -1
|
||||
entity.isFavorite = info.isFavorite
|
||||
entity.isIgnored = info.isIgnored
|
||||
entity.isMuted = info.isMuted
|
||||
entity.viaMqtt = info.via_mqtt
|
||||
entity.hopsAway = info.hops_away ?: -1
|
||||
entity.isFavorite = info.is_favorite
|
||||
entity.isIgnored = info.is_ignored
|
||||
entity.isMuted = info.is_muted
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldPreserveExistingUser(existing: MeshProtos.User, incoming: MeshProtos.User): Boolean {
|
||||
val isDefaultName = incoming.longName.matches(Regex("^Meshtastic [0-9a-fA-F]{4}$"))
|
||||
val isDefaultHwModel = incoming.hwModel == MeshProtos.HardwareModel.UNSET
|
||||
val hasExistingUser = existing.id.isNotEmpty() && existing.hwModel != MeshProtos.HardwareModel.UNSET
|
||||
private fun shouldPreserveExistingUser(existing: User, incoming: User): Boolean {
|
||||
val isDefaultName = (incoming.long_name).matches(Regex("^Meshtastic [0-9a-fA-F]{4}$"))
|
||||
val isDefaultHwModel = incoming.hw_model == HardwareModel.UNSET
|
||||
val hasExistingUser = (existing.id).isNotEmpty() && existing.hw_model != HardwareModel.UNSET
|
||||
return hasExistingUser && isDefaultName && isDefaultHwModel
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import org.meshtastic.core.service.IMeshService
|
|||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.core.service.SERVICE_NOTIFY_ID
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.proto.PortNum
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
|
@ -89,7 +90,7 @@ class MeshService : Service() {
|
|||
|
||||
companion object {
|
||||
fun actionReceived(portNum: Int): String {
|
||||
val portType = org.meshtastic.proto.Portnums.PortNum.forNumber(portNum)
|
||||
val portType = PortNum.fromValue(portNum)
|
||||
val portStr = portType?.toString() ?: portNum.toString()
|
||||
return com.geeksville.mesh.service.actionReceived(portStr)
|
||||
}
|
||||
|
|
@ -217,9 +218,7 @@ class MeshService : Service() {
|
|||
override fun send(p: DataPacket) = toRemoteExceptions { router.actionHandler.handleSend(p, myNodeNum) }
|
||||
|
||||
override fun getConfig(): ByteArray = toRemoteExceptions {
|
||||
runBlocking {
|
||||
radioConfigRepository.localConfigFlow.first().toByteArray() ?: throw NoDeviceConfigException()
|
||||
}
|
||||
runBlocking { radioConfigRepository.localConfigFlow.first().encode() }
|
||||
}
|
||||
|
||||
override fun setConfig(payload: ByteArray) = toRemoteExceptions {
|
||||
|
|
@ -279,7 +278,7 @@ class MeshService : Service() {
|
|||
}
|
||||
|
||||
override fun getChannelSet(): ByteArray = toRemoteExceptions {
|
||||
runBlocking { radioConfigRepository.channelSetFlow.first().toByteArray() }
|
||||
runBlocking { radioConfigRepository.channelSetFlow.first().encode() }
|
||||
}
|
||||
|
||||
override fun getNodes(): List<NodeInfo> = nodeManager.getNodes()
|
||||
|
|
|
|||
|
|
@ -73,9 +73,10 @@ import org.meshtastic.core.strings.new_node_seen
|
|||
import org.meshtastic.core.strings.no_local_stats
|
||||
import org.meshtastic.core.strings.reply
|
||||
import org.meshtastic.core.strings.you
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.TelemetryProtos.LocalStats
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.LocalStats
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
|
@ -264,35 +265,32 @@ constructor(
|
|||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
var cachedTelemetry: TelemetryProtos.Telemetry? = null
|
||||
var cachedTelemetry: Telemetry? = null
|
||||
var cachedLocalStats: LocalStats? = null
|
||||
var nextStatsUpdateMillis: Long = 0
|
||||
var cachedMessage: String? = null
|
||||
|
||||
// region Public Notification Methods
|
||||
override fun updateServiceStateNotification(
|
||||
summaryString: String?,
|
||||
telemetry: TelemetryProtos.Telemetry?,
|
||||
): Notification {
|
||||
val hasLocalStats = telemetry?.hasLocalStats() == true
|
||||
val hasDeviceMetrics = telemetry?.hasDeviceMetrics() == true
|
||||
override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Notification {
|
||||
val hasLocalStats = telemetry?.local_stats != null
|
||||
val hasDeviceMetrics = telemetry?.device_metrics != null
|
||||
val message =
|
||||
if (hasLocalStats) {
|
||||
val localStats = telemetry.localStats
|
||||
val localStatsMessage = localStats?.formatToString()
|
||||
cachedTelemetry = telemetry
|
||||
nextStatsUpdateMillis = System.currentTimeMillis() + FIFTEEN_MINUTES_IN_MILLIS
|
||||
localStatsMessage
|
||||
} else if (cachedTelemetry == null && hasDeviceMetrics) {
|
||||
val deviceMetrics = telemetry.deviceMetrics
|
||||
val deviceMetricsMessage = deviceMetrics.formatToString()
|
||||
if (cachedLocalStats == null) {
|
||||
when {
|
||||
hasLocalStats -> {
|
||||
val localStatsMessage = telemetry?.local_stats?.formatToString()
|
||||
cachedTelemetry = telemetry
|
||||
nextStatsUpdateMillis = System.currentTimeMillis() + FIFTEEN_MINUTES_IN_MILLIS
|
||||
localStatsMessage
|
||||
}
|
||||
nextStatsUpdateMillis = System.currentTimeMillis()
|
||||
deviceMetricsMessage
|
||||
} else {
|
||||
null
|
||||
cachedTelemetry == null && hasDeviceMetrics -> {
|
||||
val deviceMetricsMessage = telemetry?.device_metrics?.formatToString()
|
||||
if (cachedLocalStats == null) {
|
||||
cachedTelemetry = telemetry
|
||||
}
|
||||
nextStatsUpdateMillis = System.currentTimeMillis()
|
||||
deviceMetricsMessage
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
cachedMessage = message ?: cachedMessage ?: getString(Res.string.no_local_stats)
|
||||
|
|
@ -388,7 +386,7 @@ constructor(
|
|||
}
|
||||
|
||||
val ourNode = nodeRepository.get().ourNodeInfo.value
|
||||
val meName = ourNode?.user?.longName ?: getString(Res.string.you)
|
||||
val meName = ourNode?.user?.long_name ?: getString(Res.string.you)
|
||||
val me =
|
||||
Person.Builder()
|
||||
.setName(meName)
|
||||
|
|
@ -433,7 +431,7 @@ constructor(
|
|||
}
|
||||
|
||||
override fun showNewNodeSeenNotification(node: NodeEntity) {
|
||||
val notification = createNewNodeSeenNotification(node.user.shortName, node.user.longName)
|
||||
val notification = createNewNodeSeenNotification(node.user.short_name, node.user.long_name)
|
||||
notificationManager.notify(node.num, notification)
|
||||
}
|
||||
|
||||
|
|
@ -442,7 +440,7 @@ constructor(
|
|||
notificationManager.notify(node.num, notification)
|
||||
}
|
||||
|
||||
override fun showClientNotification(clientNotification: MeshProtos.ClientNotification) {
|
||||
override fun showClientNotification(clientNotification: ClientNotification) {
|
||||
val notification =
|
||||
createClientNotification(getString(Res.string.client_notification), clientNotification.message)
|
||||
notificationManager.notify(clientNotification.toString().hashCode(), notification)
|
||||
|
|
@ -452,7 +450,7 @@ constructor(
|
|||
|
||||
override fun cancelLowBatteryNotification(node: NodeEntity) = notificationManager.cancel(node.num)
|
||||
|
||||
override fun clearClientNotification(notification: MeshProtos.ClientNotification) =
|
||||
override fun clearClientNotification(notification: ClientNotification) =
|
||||
notificationManager.cancel(notification.toString().hashCode())
|
||||
|
||||
// endregion
|
||||
|
|
@ -499,7 +497,7 @@ constructor(
|
|||
}
|
||||
|
||||
val ourNode = nodeRepository.get().ourNodeInfo.value
|
||||
val meName = ourNode?.user?.longName ?: getString(Res.string.you)
|
||||
val meName = ourNode?.user?.long_name ?: getString(Res.string.you)
|
||||
val me =
|
||||
Person.Builder()
|
||||
.setName(meName)
|
||||
|
|
@ -516,14 +514,14 @@ constructor(
|
|||
// Use the node attached to the message directly to ensure correct identification
|
||||
val person =
|
||||
Person.Builder()
|
||||
.setName(msg.node.user.longName)
|
||||
.setName(msg.node.user.long_name)
|
||||
.setKey(msg.node.user.id)
|
||||
.setIcon(createPersonIcon(msg.node.user.shortName, msg.node.colors.second, msg.node.colors.first))
|
||||
.setIcon(createPersonIcon(msg.node.user.short_name, msg.node.colors.second, msg.node.colors.first))
|
||||
.build()
|
||||
|
||||
val text =
|
||||
msg.originalMessage?.let { original ->
|
||||
"↩️ \"${original.node.user.shortName}: ${original.text.take(SNIPPET_LENGTH)}...\": ${msg.text}"
|
||||
"↩️ \"${original.node.user.short_name}: ${original.text.take(SNIPPET_LENGTH)}...\": ${msg.text}"
|
||||
} ?: msg.text
|
||||
|
||||
style.addMessage(text, msg.receivedTime, person)
|
||||
|
|
@ -533,11 +531,11 @@ constructor(
|
|||
val reactorNode = nodeRepository.get().getNode(reaction.user.id)
|
||||
val reactor =
|
||||
Person.Builder()
|
||||
.setName(reaction.user.longName)
|
||||
.setName(reaction.user.long_name)
|
||||
.setKey(reaction.user.id)
|
||||
.setIcon(
|
||||
createPersonIcon(
|
||||
reaction.user.shortName,
|
||||
reaction.user.short_name,
|
||||
reactorNode.colors.second,
|
||||
reactorNode.colors.first,
|
||||
),
|
||||
|
|
@ -612,7 +610,7 @@ constructor(
|
|||
.build()
|
||||
}
|
||||
|
||||
private fun createNewNodeSeenNotification(name: String, message: String?): Notification {
|
||||
private fun createNewNodeSeenNotification(name: String, message: String): Notification {
|
||||
val title = getString(Res.string.new_node_seen).format(name)
|
||||
val builder =
|
||||
commonBuilder(NotificationType.NewNode)
|
||||
|
|
@ -621,24 +619,23 @@ constructor(
|
|||
.setContentTitle(title)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setShowWhen(true)
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
|
||||
message?.let {
|
||||
builder.setContentText(it)
|
||||
builder.setStyle(NotificationCompat.BigTextStyle().bigText(it))
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun createLowBatteryNotification(node: NodeEntity, isRemote: Boolean): Notification {
|
||||
val type = if (isRemote) NotificationType.LowBatteryRemote else NotificationType.LowBatteryLocal
|
||||
val title = getString(Res.string.low_battery_title).format(node.shortName)
|
||||
val message = getString(Res.string.low_battery_message).format(node.longName, node.deviceMetrics.batteryLevel)
|
||||
val batteryLevel = node.deviceTelemetry?.device_metrics?.battery_level ?: 0
|
||||
val message = getString(Res.string.low_battery_message).format(node.longName, batteryLevel)
|
||||
|
||||
return commonBuilder(type)
|
||||
.setCategory(Notification.CATEGORY_STATUS)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setProgress(MAX_BATTERY_LEVEL, node.deviceMetrics.batteryLevel, false)
|
||||
.setProgress(MAX_BATTERY_LEVEL, batteryLevel, false)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
|
|
@ -647,17 +644,13 @@ constructor(
|
|||
.build()
|
||||
}
|
||||
|
||||
private fun createClientNotification(name: String, message: String?): Notification =
|
||||
private fun createClientNotification(name: String, message: String): Notification =
|
||||
commonBuilder(NotificationType.Client)
|
||||
.setCategory(Notification.CATEGORY_ERROR)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(name)
|
||||
.apply {
|
||||
message?.let {
|
||||
setContentText(it)
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(it))
|
||||
}
|
||||
}
|
||||
.setContentText(message)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(message))
|
||||
.build()
|
||||
|
||||
// endregion
|
||||
|
|
@ -804,34 +797,19 @@ constructor(
|
|||
}
|
||||
|
||||
// Extension function to format LocalStats into a readable string.
|
||||
private fun LocalStats?.formatToString(): String? = this?.allFields
|
||||
?.mapNotNull { (k, v) ->
|
||||
when (k.name) {
|
||||
"num_online_nodes",
|
||||
"num_total_nodes",
|
||||
-> null // Exclude these fields
|
||||
"uptime_seconds" -> "Uptime: ${formatUptime(v as Int)}"
|
||||
"channel_utilization" -> "ChUtil: %.2f%%".format(v)
|
||||
"air_util_tx" -> "AirUtilTX: %.2f%%".format(v)
|
||||
else -> {
|
||||
val formattedKey = k.name.replace('_', ' ').replaceFirstChar { it.titlecase() }
|
||||
"$formattedKey: $v"
|
||||
}
|
||||
}
|
||||
}
|
||||
?.joinToString("\n")
|
||||
private fun LocalStats.formatToString(): String {
|
||||
val parts = mutableListOf<String>()
|
||||
parts.add("Uptime: ${formatUptime(uptime_seconds)}")
|
||||
parts.add("ChUtil: %.2f%%".format(channel_utilization))
|
||||
parts.add("AirUtilTX: %.2f%%".format(air_util_tx))
|
||||
return parts.joinToString("\n")
|
||||
}
|
||||
|
||||
private fun TelemetryProtos.DeviceMetrics?.formatToString(): String? = this?.allFields
|
||||
?.mapNotNull { (k, v) ->
|
||||
when (k.name) {
|
||||
"battery_level" -> "Battery Level: $v"
|
||||
"uptime_seconds" -> "Uptime: ${formatUptime(v as Int)}"
|
||||
"channel_utilization" -> "ChUtil: %.2f%%".format(v)
|
||||
"air_util_tx" -> "AirUtilTX: %.2f%%".format(v)
|
||||
else -> {
|
||||
val formattedKey = k.name.replace('_', ' ').replaceFirstChar { it.titlecase() }
|
||||
"$formattedKey: $v"
|
||||
}
|
||||
}
|
||||
}
|
||||
?.joinToString("\n")
|
||||
private fun DeviceMetrics.formatToString(): String {
|
||||
val parts = mutableListOf<String>()
|
||||
battery_level?.let { parts.add("Battery Level: $it") }
|
||||
uptime_seconds?.let { parts.add("Uptime: ${formatUptime(it)}") }
|
||||
channel_utilization?.let { parts.add("ChUtil: %.2f%%".format(it)) }
|
||||
air_util_tx?.let { parts.add("AirUtilTX: %.2f%%".format(it)) }
|
||||
return parts.joinToString("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import org.meshtastic.core.strings.traceroute_duration
|
|||
import org.meshtastic.core.strings.traceroute_route_back_to_us
|
||||
import org.meshtastic.core.strings.traceroute_route_towards_dest
|
||||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -65,13 +65,13 @@ constructor(
|
|||
headerBack = getString(Res.string.traceroute_route_back_to_us),
|
||||
) ?: return
|
||||
|
||||
val requestId = packet.decoded.requestId
|
||||
val requestId = packet.decoded?.request_id ?: 0
|
||||
if (logUuid != null) {
|
||||
scope.handledLaunch {
|
||||
logInsertJob?.join()
|
||||
val routeDiscovery = packet.fullRouteDiscovery
|
||||
val forwardRoute = routeDiscovery?.routeList.orEmpty()
|
||||
val returnRoute = routeDiscovery?.routeBackList.orEmpty()
|
||||
val forwardRoute = routeDiscovery?.route.orEmpty()
|
||||
val returnRoute = routeDiscovery?.route_back.orEmpty()
|
||||
val routeNodeNums = (forwardRoute + returnRoute).distinct()
|
||||
val nodeDbByNum = nodeRepository.nodeDBbyNum.value
|
||||
val snapshotPositions =
|
||||
|
|
@ -93,15 +93,15 @@ constructor(
|
|||
}
|
||||
|
||||
val routeDiscovery = packet.fullRouteDiscovery
|
||||
val destination = routeDiscovery?.routeList?.firstOrNull() ?: routeDiscovery?.routeBackList?.lastOrNull() ?: 0
|
||||
val destination = routeDiscovery?.route?.firstOrNull() ?: routeDiscovery?.route_back?.lastOrNull() ?: 0
|
||||
|
||||
serviceRepository.setTracerouteResponse(
|
||||
TracerouteResponse(
|
||||
message = responseText,
|
||||
destinationNodeNum = destination,
|
||||
requestId = requestId,
|
||||
forwardRoute = routeDiscovery?.routeList.orEmpty(),
|
||||
returnRoute = routeDiscovery?.routeBackList.orEmpty(),
|
||||
forwardRoute = routeDiscovery?.route.orEmpty(),
|
||||
returnRoute = routeDiscovery?.route_back.orEmpty(),
|
||||
logUuid = logUuid,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ import org.meshtastic.core.model.MessageStatus
|
|||
import org.meshtastic.core.model.util.toOneLineString
|
||||
import org.meshtastic.core.model.util.toPIIString
|
||||
import org.meshtastic.core.service.ConnectionState
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.MeshProtos.ToRadio
|
||||
import org.meshtastic.proto.fromRadio
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.QueueStatus
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
|
@ -76,24 +76,24 @@ constructor(
|
|||
* Send a command/packet to our radio. But cope with the possibility that we might start up before we are fully
|
||||
* bound to the RadioInterfaceService
|
||||
*/
|
||||
fun sendToRadio(p: ToRadio.Builder) {
|
||||
val built = p.build()
|
||||
Logger.d { "Sending to radio ${built.toPIIString()}" }
|
||||
val b = built.toByteArray()
|
||||
fun sendToRadio(p: ToRadio) {
|
||||
Logger.d { "Sending to radio ${p.toPIIString()}" }
|
||||
val b = p.encode()
|
||||
|
||||
radioInterfaceService.sendToRadio(b)
|
||||
changeStatus(p.packet.id, MessageStatus.ENROUTE)
|
||||
p.packet?.id?.let { changeStatus(it, MessageStatus.ENROUTE) }
|
||||
|
||||
if (p.packet.hasDecoded()) {
|
||||
val packet = p.packet
|
||||
if (packet?.decoded != null) {
|
||||
val packetToSave =
|
||||
MeshLog(
|
||||
uuid = UUID.randomUUID().toString(),
|
||||
message_type = "Packet",
|
||||
received_date = System.currentTimeMillis(),
|
||||
raw_message = p.packet.toString(),
|
||||
fromNum = p.packet.from,
|
||||
portNum = p.packet.decoded.portnumValue,
|
||||
fromRadio = fromRadio { packet = p.packet },
|
||||
raw_message = packet.toString(),
|
||||
fromNum = packet.from ?: 0,
|
||||
portNum = packet.decoded?.portnum?.value ?: 0,
|
||||
fromRadio = FromRadio(packet = packet),
|
||||
)
|
||||
insertMeshLog(packetToSave)
|
||||
}
|
||||
|
|
@ -119,9 +119,9 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun handleQueueStatus(queueStatus: MeshProtos.QueueStatus) {
|
||||
fun handleQueueStatus(queueStatus: QueueStatus) {
|
||||
Logger.d { "[queueStatus] ${queueStatus.toOneLineString()}" }
|
||||
val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, meshPacketId) }
|
||||
val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, mesh_packet_id) }
|
||||
if (success && isFull) return // Queue is full, wait for free != 0
|
||||
if (requestId != 0) {
|
||||
queueResponse.remove(requestId)?.complete(success)
|
||||
|
|
@ -192,7 +192,7 @@ constructor(
|
|||
if (connectionStateHolder.connectionState.value != ConnectionState.Connected) {
|
||||
throw RadioNotConnectedException()
|
||||
}
|
||||
sendToRadio(ToRadio.newBuilder().apply { this.packet = packet })
|
||||
sendToRadio(ToRadio(packet = packet))
|
||||
} catch (ex: Exception) {
|
||||
Logger.e(ex) { "sendToRadio error: ${ex.message}" }
|
||||
deferred.complete(false)
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
import org.meshtastic.core.database.entity.ReactionEntity
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.service.MeshServiceNotifications
|
||||
import org.meshtastic.proto.Portnums
|
||||
import org.meshtastic.proto.PortNum
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
|
@ -74,8 +76,8 @@ class ReactionReceiver : BroadcastReceiver() {
|
|||
DataPacket(
|
||||
to = toId,
|
||||
channel = channelIndex,
|
||||
bytes = emoji.toByteArray(Charsets.UTF_8),
|
||||
dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
|
||||
bytes = emoji.encodeToByteArray().toByteString(),
|
||||
dataType = PortNum.TEXT_MESSAGE_APP.value,
|
||||
replyId = packetId,
|
||||
wantAck = true,
|
||||
emoji = emoji.codePointAt(0),
|
||||
|
|
@ -90,7 +92,7 @@ class ReactionReceiver : BroadcastReceiver() {
|
|||
emoji = emoji,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
packetId = reactionPacket.id,
|
||||
status = org.meshtastic.core.model.MessageStatus.QUEUED,
|
||||
status = MessageStatus.QUEUED,
|
||||
to = toId,
|
||||
channel = channelIndex,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -150,7 +150,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusBlue
|
|||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.util.toMessageRes
|
||||
import org.meshtastic.feature.node.metrics.annotateTraceroute
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
|
||||
enum class TopLevelDestination(val label: StringResource, val icon: ImageVector, val route: Route) {
|
||||
Conversations(Res.string.conversations, MeshtasticIcons.Conversations, ContactsRoutes.ContactsGraph),
|
||||
|
|
@ -222,7 +221,7 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
clientNotification?.let { notification ->
|
||||
var message = notification.message
|
||||
val compromisedKeys =
|
||||
if (notification.hasLowEntropyKey() || notification.hasDuplicatedPublicKey()) {
|
||||
if (notification.low_entropy_key != null || notification.duplicated_public_key != null) {
|
||||
message = stringResource(Res.string.compromised_keys)
|
||||
true
|
||||
} else {
|
||||
|
|
@ -291,101 +290,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: BTScanMode
|
|||
onDismiss = { tracerouteMapError = null },
|
||||
)
|
||||
}
|
||||
// FIXME: uncomment and update Capabilities.kt when working better
|
||||
//
|
||||
// val neighborInfoResponse by uIViewModel.neighborInfoResponse.observeAsState()
|
||||
// neighborInfoResponse?.let { response ->
|
||||
// SimpleAlertDialog(
|
||||
// title = Res.string.neighbor_info,
|
||||
// text = {
|
||||
// Column(modifier = Modifier.fillMaxWidth()) {
|
||||
// fun tryParseNeighborInfo(input: String): MeshProtos.NeighborInfo? {
|
||||
// // First, try parsing directly from raw bytes of the string
|
||||
// var neighborInfo: MeshProtos.NeighborInfo? =
|
||||
// runCatching { MeshProtos.NeighborInfo.parseFrom(input.toByteArray()) }.getOrNull()
|
||||
//
|
||||
// if (neighborInfo == null) {
|
||||
// // Next, try to decode a hex dump embedded as text (e.g., "AA BB CC ...")
|
||||
// val hexPairs = Regex("""\b[0-9A-Fa-f]{2}\b""").findAll(input).map { it.value
|
||||
// }.toList()
|
||||
// @Suppress("detekt:MagicNumber") // byte offsets
|
||||
// if (hexPairs.size >= 4) {
|
||||
// val bytes = hexPairs.map { it.toInt(16).toByte() }.toByteArray()
|
||||
// neighborInfo = runCatching { MeshProtos.NeighborInfo.parseFrom(bytes)
|
||||
// }.getOrNull()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return neighborInfo
|
||||
// }
|
||||
//
|
||||
// val parsed = tryParseNeighborInfo(response)
|
||||
// if (parsed != null) {
|
||||
// fun fmtNode(nodeNum: Int): String = "!%08x".format(nodeNum)
|
||||
// Text(text = "NeighborInfo:", style = MaterialTheme.typography.bodyMedium)
|
||||
// Text(
|
||||
// text = "node_id: ${fmtNode(parsed.nodeId)}",
|
||||
// style = MaterialTheme.typography.bodySmall,
|
||||
// modifier = Modifier.padding(top = 8.dp),
|
||||
// )
|
||||
// Text(
|
||||
// text = "last_sent_by_id: ${fmtNode(parsed.lastSentById)}",
|
||||
// style = MaterialTheme.typography.bodySmall,
|
||||
// modifier = Modifier.padding(top = 2.dp),
|
||||
// )
|
||||
// Text(
|
||||
// text = "node_broadcast_interval_secs: ${parsed.nodeBroadcastIntervalSecs}",
|
||||
// style = MaterialTheme.typography.bodySmall,
|
||||
// modifier = Modifier.padding(top = 2.dp),
|
||||
// )
|
||||
// if (parsed.neighborsCount > 0) {
|
||||
// Text(
|
||||
// text = "neighbors:",
|
||||
// style = MaterialTheme.typography.bodySmall,
|
||||
// modifier = Modifier.padding(top = 4.dp),
|
||||
// )
|
||||
// parsed.neighborsList.forEach { n ->
|
||||
// Text(
|
||||
// text = " - node_id: ${fmtNode(n.nodeId)} snr: ${n.snr}",
|
||||
// style = MaterialTheme.typography.bodySmall,
|
||||
// modifier = Modifier.padding(start = 8.dp),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// val rawBytes = response.toByteArray()
|
||||
//
|
||||
// @Suppress("detekt:MagicNumber") // byte offsets
|
||||
// val isBinary = response.any { it.code < 32 && it != '\n' && it != '\r' && it != '\t' }
|
||||
// if (isBinary) {
|
||||
// val hexString = rawBytes.joinToString(" ") { "%02X".format(it) }
|
||||
// Text(
|
||||
// text = "Binary data (hex view):",
|
||||
// style = MaterialTheme.typography.bodyMedium,
|
||||
// modifier = Modifier.padding(bottom = 4.dp),
|
||||
// )
|
||||
// Text(
|
||||
// text = hexString,
|
||||
// style =
|
||||
// MaterialTheme.typography.bodyMedium.copy(
|
||||
// fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
// ),
|
||||
// modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
|
||||
// )
|
||||
// } else {
|
||||
// Text(
|
||||
// text = response,
|
||||
// style = MaterialTheme.typography.bodyMedium,
|
||||
// modifier = Modifier.padding(bottom = 8.dp).fillMaxWidth(),
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// dismissText = stringResource(Res.string.okay),
|
||||
// onDismiss = { uIViewModel.clearNeighborInfoResponse() },
|
||||
// )
|
||||
// }
|
||||
val navSuiteType = NavigationSuiteScaffoldDefaults.navigationSuiteType(currentWindowAdaptiveInfo())
|
||||
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)
|
||||
|
|
@ -605,18 +509,7 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
viewModel.latestStableFirmwareRelease.collectAsStateWithLifecycle(DeviceVersion("2.6.4"))
|
||||
LaunchedEffect(connectionState, firmwareEdition) {
|
||||
if (connectionState == ConnectionState.Connected) {
|
||||
firmwareEdition?.let { edition ->
|
||||
Logger.d { "FirmwareEdition: ${edition.name}" }
|
||||
when (edition) {
|
||||
MeshProtos.FirmwareEdition.VANILLA -> {
|
||||
// Handle any specific logic for VANILLA firmware edition if needed
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Handle other firmware editions if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
firmwareEdition?.let { edition -> Logger.d { "FirmwareEdition: ${edition.name}" } }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ import org.meshtastic.feature.settings.navigation.ConfigRoute
|
|||
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
fun String?.isValidAddress(): Boolean = if (this.isNullOrBlank()) {
|
||||
false
|
||||
|
|
@ -120,13 +120,12 @@ fun ConnectionsScreen(
|
|||
val config by connectionsViewModel.localConfig.collectAsStateWithLifecycle()
|
||||
val scrollState = rememberScrollState()
|
||||
val scanStatusText by scanModel.errorText.observeAsState("")
|
||||
val connectionState by
|
||||
connectionsViewModel.connectionState.collectAsStateWithLifecycle(ConnectionState.Disconnected)
|
||||
val connectionState by connectionsViewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val scanning by scanModel.spinner.collectAsStateWithLifecycle(false)
|
||||
val ourNode by connectionsViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val selectedDevice by scanModel.selectedNotNullFlow.collectAsStateWithLifecycle()
|
||||
val bluetoothState by connectionsViewModel.bluetoothState.collectAsStateWithLifecycle()
|
||||
val regionUnset = config.lora.region == ConfigProtos.Config.LoRaConfig.RegionCode.UNSET
|
||||
val regionUnset = config.lora?.region == Config.LoRaConfig.RegionCode.UNSET
|
||||
|
||||
val bleDevices by scanModel.bleDevicesForUi.collectAsStateWithLifecycle()
|
||||
val discoveredTcpDevices by scanModel.discoveredTcpDevicesForUi.collectAsStateWithLifecycle()
|
||||
|
|
@ -219,7 +218,7 @@ fun ConnectionsScreen(
|
|||
CurrentlyConnectedInfo(
|
||||
node = node,
|
||||
bleDevice =
|
||||
bleDevices.firstOrNull { it.fullAddress == selectedDevice }
|
||||
bleDevices.find { it.fullAddress == selectedDevice }
|
||||
as DeviceListEntry.Ble?,
|
||||
onNavigateToNodeDetails = onNavigateToNodeDetails,
|
||||
onClickDisconnect = { scanModel.disconnect() },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 com.geeksville.mesh.ui.connections
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -30,7 +29,7 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.prefs.ui.UiPrefs
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -45,7 +44,7 @@ constructor(
|
|||
) : ViewModel() {
|
||||
|
||||
val localConfig: StateFlow<LocalConfig> =
|
||||
radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance())
|
||||
radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
|
||||
|
||||
val connectionState = serviceRepository.connectionState
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 com.geeksville.mesh.ui.connections.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
|
@ -56,9 +55,9 @@ import org.meshtastic.core.ui.component.MaterialBluetoothSignalInfo
|
|||
import org.meshtastic.core.ui.component.NodeChip
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.PaxcountProtos
|
||||
import org.meshtastic.proto.TelemetryProtos
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private const val RSSI_DELAY = 10
|
||||
|
|
@ -113,9 +112,9 @@ fun CurrentlyConnectedInfo(
|
|||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f, fill = true)) {
|
||||
Text(text = node.user.longName, style = MaterialTheme.typography.titleMedium)
|
||||
Text(text = node.user.long_name ?: "", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
node.metadata?.firmwareVersion?.let { firmwareVersion ->
|
||||
node.metadata?.firmware_version?.let { firmwareVersion ->
|
||||
Text(
|
||||
text = stringResource(Res.string.firmware_version, firmwareVersion),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
|
|
@ -150,14 +149,10 @@ private fun CurrentlyConnectedInfoPreview() {
|
|||
node =
|
||||
Node(
|
||||
num = 13444,
|
||||
user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build(),
|
||||
user = User(short_name = "\uD83E\uDEE0", long_name = "John Doe"),
|
||||
isIgnored = false,
|
||||
paxcounter = PaxcountProtos.Paxcount.newBuilder().setBle(10).setWifi(5).build(),
|
||||
environmentMetrics =
|
||||
TelemetryProtos.EnvironmentMetrics.newBuilder()
|
||||
.setTemperature(25f)
|
||||
.setRelativeHumidity(60f)
|
||||
.build(),
|
||||
paxcounter = Paxcount(ble = 10, wifi = 5),
|
||||
environmentMetrics = EnvironmentMetrics(temperature = 25f, relative_humidity = 60f),
|
||||
),
|
||||
onNavigateToNodeDetails = {},
|
||||
onClickDisconnect = {},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 com.geeksville.mesh.ui.contact
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
|
@ -60,7 +59,7 @@ import org.meshtastic.core.strings.some_username
|
|||
import org.meshtastic.core.strings.unknown_username
|
||||
import org.meshtastic.core.ui.component.SecurityIcon
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
|
@ -72,7 +71,7 @@ fun ContactItem(
|
|||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
onNodeChipClick: () -> Unit = {},
|
||||
channels: AppOnlyProtos.ChannelSet? = null,
|
||||
channels: ChannelSet? = null,
|
||||
) = with(contact) {
|
||||
val isOutlined = !selected && !isActive
|
||||
|
||||
|
|
@ -113,7 +112,7 @@ fun ContactItem(
|
|||
@Composable
|
||||
private fun ContactHeader(
|
||||
contact: Contact,
|
||||
channels: AppOnlyProtos.ChannelSet?,
|
||||
channels: ChannelSet?,
|
||||
modifier: Modifier = Modifier,
|
||||
onNodeChipClick: () -> Unit = {},
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ import org.meshtastic.core.ui.icon.QrCode2
|
|||
import org.meshtastic.core.ui.icon.SelectAll
|
||||
import org.meshtastic.core.ui.icon.VolumeMuteTwoTone
|
||||
import org.meshtastic.core.ui.icon.VolumeUpTwoTone
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
|
@ -129,8 +129,8 @@ fun ContactsScreen(
|
|||
// Create channel placeholders (always show broadcast contacts, even when empty)
|
||||
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
||||
val channelPlaceholders =
|
||||
remember(channels.settingsList.size) {
|
||||
(0 until channels.settingsList.size).map { ch ->
|
||||
remember(channels.settings.size) {
|
||||
(0 until channels.settings.size).map { ch ->
|
||||
Contact(
|
||||
contactKey = "$ch^all",
|
||||
shortName = "$ch",
|
||||
|
|
@ -485,7 +485,7 @@ private fun ContactListViewPaged(
|
|||
onNodeChipClick: (Contact) -> Unit,
|
||||
listState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
channels: AppOnlyProtos.ChannelSet? = null,
|
||||
channels: ChannelSet? = null,
|
||||
) {
|
||||
val haptics = LocalHapticFeedback.current
|
||||
|
||||
|
|
@ -521,7 +521,7 @@ private fun ContactListContentInternal(
|
|||
onLongClick: (Contact) -> Unit,
|
||||
onNodeChipClick: (Contact) -> Unit,
|
||||
listState: LazyListState,
|
||||
channels: AppOnlyProtos.ChannelSet?,
|
||||
channels: ChannelSet?,
|
||||
haptics: HapticFeedback,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -559,7 +559,7 @@ private fun LazyListScope.contactListPlaceholdersItems(
|
|||
onClick: (Contact) -> Unit,
|
||||
onLongClick: (Contact) -> Unit,
|
||||
onNodeChipClick: (Contact) -> Unit,
|
||||
channels: AppOnlyProtos.ChannelSet?,
|
||||
channels: ChannelSet?,
|
||||
haptics: HapticFeedback,
|
||||
) {
|
||||
items(
|
||||
|
|
@ -592,7 +592,7 @@ private fun LazyListScope.contactListPagedItems(
|
|||
onClick: (Contact) -> Unit,
|
||||
onLongClick: (Contact) -> Unit,
|
||||
onNodeChipClick: (Contact) -> Unit,
|
||||
channels: AppOnlyProtos.ChannelSet?,
|
||||
channels: ChannelSet?,
|
||||
haptics: HapticFeedback,
|
||||
) {
|
||||
items(
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ import org.meshtastic.core.service.ServiceRepository
|
|||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.channel_name
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import org.meshtastic.proto.channelSet
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import javax.inject.Inject
|
||||
import kotlin.collections.map as collectionsMap
|
||||
|
||||
|
|
@ -61,7 +60,7 @@ constructor(
|
|||
|
||||
val connectionState = serviceRepository.connectionState
|
||||
|
||||
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = channelSet {})
|
||||
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet())
|
||||
|
||||
// Combine node info and myId to reduce argument count in subsequent combines
|
||||
private val identityFlow: Flow<Pair<MyNodeEntity?, String?>> =
|
||||
|
|
@ -86,7 +85,7 @@ constructor(
|
|||
val myNodeNum = myNodeInfo?.myNodeNum ?: return@combine emptyList()
|
||||
// Add empty channel placeholders (always show Broadcast contacts, even when empty)
|
||||
val placeholder =
|
||||
(0 until channelSet.settingsCount).associate { ch ->
|
||||
(0 until channelSet.settings.size).associate { ch ->
|
||||
val contactKey = "$ch${DataPacket.ID_BROADCAST}"
|
||||
val data = DataPacket(bytes = null, dataType = 1, time = 0L, channel = ch)
|
||||
contactKey to Packet(0L, myNodeNum, 1, contactKey, 0L, true, data)
|
||||
|
|
@ -104,12 +103,12 @@ constructor(
|
|||
val user = getUser(if (fromLocal) data.to else data.from)
|
||||
val node = getNode(if (fromLocal) data.to else data.from)
|
||||
|
||||
val shortName = user.shortName
|
||||
val shortName = user.short_name ?: ""
|
||||
val longName =
|
||||
if (toBroadcast) {
|
||||
channelSet.getChannel(data.channel)?.name ?: getString(Res.string.channel_name)
|
||||
} else {
|
||||
user.longName
|
||||
user.long_name ?: ""
|
||||
}
|
||||
|
||||
Contact(
|
||||
|
|
@ -121,7 +120,7 @@ constructor(
|
|||
unreadCount = packetRepository.getUnreadCount(contactKey),
|
||||
messageCount = packetRepository.getMessageCount(contactKey),
|
||||
isMuted = settings[contactKey]?.isMuted == true,
|
||||
isUnmessageable = user.isUnmessagable,
|
||||
isUnmessageable = user.is_unmessagable ?: false,
|
||||
nodeColors =
|
||||
if (!toBroadcast) {
|
||||
node.colors
|
||||
|
|
@ -157,12 +156,12 @@ constructor(
|
|||
val user = getUser(if (fromLocal) data.to else data.from)
|
||||
val node = getNode(if (fromLocal) data.to else data.from)
|
||||
|
||||
val shortName = user.shortName
|
||||
val shortName = user.short_name ?: ""
|
||||
val longName =
|
||||
if (toBroadcast) {
|
||||
channelSet.getChannel(data.channel)?.name ?: getString(Res.string.channel_name)
|
||||
} else {
|
||||
user.longName
|
||||
user.long_name ?: ""
|
||||
}
|
||||
|
||||
Contact(
|
||||
|
|
@ -174,7 +173,7 @@ constructor(
|
|||
unreadCount = packetRepository.getUnreadCount(contactKey),
|
||||
messageCount = packetRepository.getMessageCount(contactKey),
|
||||
isMuted = settings[contactKey]?.isMuted == true,
|
||||
isUnmessageable = user.isUnmessagable,
|
||||
isUnmessageable = user.is_unmessagable ?: false,
|
||||
nodeColors =
|
||||
if (!toBroadcast) {
|
||||
node.colors
|
||||
|
|
@ -215,7 +214,7 @@ constructor(
|
|||
|
||||
private data class ContactsPagedParams(
|
||||
val myNodeNum: Int?,
|
||||
val channelSet: AppOnlyProtos.ChannelSet,
|
||||
val channelSet: ChannelSet,
|
||||
val settings: Map<String, ContactSettings>,
|
||||
val myId: String?,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -128,11 +128,9 @@ import org.meshtastic.feature.settings.navigation.ConfigRoute
|
|||
import org.meshtastic.feature.settings.navigation.getNavRouteFrom
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigViewModel
|
||||
import org.meshtastic.feature.settings.radio.component.PacketResponseStateDialog
|
||||
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.channelSet
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config
|
||||
|
||||
/**
|
||||
* Composable screen for managing and sharing Meshtastic channels. Allows users to view, edit, and share channel
|
||||
|
|
@ -156,7 +154,8 @@ fun ChannelScreen(
|
|||
|
||||
val channels by viewModel.channels.collectAsStateWithLifecycle()
|
||||
var channelSet by remember(channels) { mutableStateOf(channels) }
|
||||
val modemPresetName by remember(channels) { mutableStateOf(Channel(loraConfig = channels.loraConfig).name) }
|
||||
val modemPresetName by
|
||||
remember(channels) { mutableStateOf(Channel(loraConfig = channels.lora_config ?: Config.LoRaConfig()).name) }
|
||||
|
||||
var showResetDialog by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -185,16 +184,18 @@ fun ChannelScreen(
|
|||
|
||||
/* Holds selections made by the user for QR generation. */
|
||||
val channelSelections =
|
||||
rememberSaveable(saver = listSaver(save = { it.toList() }, restore = { it.toMutableStateList() })) {
|
||||
mutableStateListOf(elements = Array(size = 8, init = { true }))
|
||||
rememberSaveable(
|
||||
saver =
|
||||
listSaver<SnapshotStateList<Boolean>, Boolean>(
|
||||
save = { it.toList() },
|
||||
restore = { it.toMutableStateList() },
|
||||
),
|
||||
) {
|
||||
mutableStateListOf(true, true, true, true, true, true, true, true)
|
||||
}
|
||||
|
||||
val selectedChannelSet =
|
||||
channelSet.copy {
|
||||
val result = settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
|
||||
settings.clear()
|
||||
settings.addAll(result)
|
||||
}
|
||||
channelSet.copy(settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true })
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
|
@ -244,11 +245,8 @@ fun ChannelScreen(
|
|||
}
|
||||
}
|
||||
|
||||
fun installSettings(newChannel: ChannelProtos.ChannelSettings, newLoRaConfig: ConfigProtos.Config.LoRaConfig) {
|
||||
val newSet = channelSet {
|
||||
settings.add(newChannel)
|
||||
loraConfig = newLoRaConfig
|
||||
}
|
||||
fun installSettings(newChannel: ChannelSettings, newLoRaConfig: Config.LoRaConfig) {
|
||||
val newSet = ChannelSet(settings = listOf(newChannel), lora_config = newLoRaConfig)
|
||||
installSettings(newSet)
|
||||
}
|
||||
|
||||
|
|
@ -264,13 +262,12 @@ fun ChannelScreen(
|
|||
TextButton(
|
||||
onClick = {
|
||||
Logger.d { "Switching back to default channel" }
|
||||
installSettings(
|
||||
Channel.default.settings,
|
||||
Channel.default.loraConfig.copy {
|
||||
region = viewModel.region
|
||||
txEnabled = viewModel.txEnabled
|
||||
},
|
||||
)
|
||||
val lora =
|
||||
(Channel.default.loraConfig).copy(
|
||||
region = viewModel.region,
|
||||
tx_enabled = viewModel.txEnabled,
|
||||
)
|
||||
installSettings(Channel.default.settings, lora)
|
||||
showResetDialog = false
|
||||
},
|
||||
) {
|
||||
|
|
@ -504,17 +501,13 @@ private fun ChannelListView(
|
|||
onClick: () -> Unit = {},
|
||||
) {
|
||||
val selectedChannelSet =
|
||||
channelSet.copy {
|
||||
val result = settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
|
||||
settings.clear()
|
||||
settings.addAll(result)
|
||||
}
|
||||
channelSet.copy(settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true })
|
||||
|
||||
AdaptiveTwoPane(
|
||||
first = {
|
||||
channelSet.settingsList.forEachIndexed { index, channel ->
|
||||
val channelObj = Channel(channel, channelSet.loraConfig)
|
||||
val displayTitle = channel.name.ifEmpty { modemPresetName }
|
||||
channelSet.settings.forEachIndexed { index, channel ->
|
||||
val channelObj = Channel(channel, channelSet.lora_config ?: Config.LoRaConfig())
|
||||
val displayTitle = if (channel.name.isEmpty()) modemPresetName else channel.name
|
||||
|
||||
ChannelSelection(
|
||||
index = index,
|
||||
|
|
@ -522,7 +515,7 @@ private fun ChannelListView(
|
|||
enabled = enabled,
|
||||
isSelected = channelSelections[index],
|
||||
onSelected = {
|
||||
if (it || selectedChannelSet.settingsCount > 1) {
|
||||
if (it || selectedChannelSet.settings.size > 1) {
|
||||
channelSelections[index] = it
|
||||
}
|
||||
},
|
||||
|
|
@ -583,11 +576,7 @@ fun ModemPresetInfoPreview() {
|
|||
private fun ChannelScreenPreview() {
|
||||
ChannelListView(
|
||||
enabled = true,
|
||||
channelSet =
|
||||
channelSet {
|
||||
settings.add(Channel.default.settings)
|
||||
loraConfig = Channel.default.loraConfig
|
||||
},
|
||||
channelSet = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
|
||||
modemPresetName = Channel.default.name,
|
||||
channelSelections = listOf(true).toMutableStateList(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
* 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
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
* 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 com.geeksville.mesh.ui.sharing
|
||||
|
||||
import android.net.Uri
|
||||
|
|
@ -33,13 +32,10 @@ import org.meshtastic.core.model.util.toChannelSet
|
|||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.util.getChannelList
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.ConfigProtos.Config
|
||||
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
|
||||
import org.meshtastic.proto.channelSet
|
||||
import org.meshtastic.proto.config
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -53,29 +49,28 @@ constructor(
|
|||
|
||||
val connectionState = serviceRepository.connectionState
|
||||
|
||||
val localConfig =
|
||||
radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance())
|
||||
val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
|
||||
|
||||
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = channelSet {})
|
||||
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet())
|
||||
|
||||
// managed mode disables all access to configuration
|
||||
val isManaged: Boolean
|
||||
get() = localConfig.value.security.isManaged
|
||||
get() = localConfig.value.security?.is_managed == true
|
||||
|
||||
var txEnabled: Boolean
|
||||
get() = localConfig.value.lora.txEnabled
|
||||
get() = localConfig.value.lora?.tx_enabled == true
|
||||
set(value) {
|
||||
updateLoraConfig { it.copy { txEnabled = value } }
|
||||
updateLoraConfig { it.copy(tx_enabled = value) }
|
||||
}
|
||||
|
||||
var region: Config.LoRaConfig.RegionCode
|
||||
get() = localConfig.value.lora.region
|
||||
get() = localConfig.value.lora?.region ?: Config.LoRaConfig.RegionCode.UNSET
|
||||
set(value) {
|
||||
updateLoraConfig { it.copy { region = value } }
|
||||
updateLoraConfig { it.copy(region = value) }
|
||||
}
|
||||
|
||||
private val _requestChannelSet = MutableStateFlow<AppOnlyProtos.ChannelSet?>(null)
|
||||
val requestChannelSet: StateFlow<AppOnlyProtos.ChannelSet?>
|
||||
private val _requestChannelSet = MutableStateFlow<ChannelSet?>(null)
|
||||
val requestChannelSet: StateFlow<ChannelSet?>
|
||||
get() = _requestChannelSet
|
||||
|
||||
fun requestChannelUrl(url: Uri, onError: () -> Unit) = runCatching { _requestChannelSet.value = url.toChannelSet() }
|
||||
|
|
@ -89,17 +84,19 @@ constructor(
|
|||
}
|
||||
|
||||
/** Set the radio config (also updates our saved copy in preferences). */
|
||||
fun setChannels(channelSet: AppOnlyProtos.ChannelSet) = viewModelScope.launch {
|
||||
getChannelList(channelSet.settingsList, channels.value.settingsList).forEach(::setChannel)
|
||||
radioConfigRepository.replaceAllSettings(channelSet.settingsList)
|
||||
fun setChannels(channelSet: ChannelSet) = viewModelScope.launch {
|
||||
getChannelList(channelSet.settings, channels.value.settings).forEach(::setChannel)
|
||||
radioConfigRepository.replaceAllSettings(channelSet.settings)
|
||||
|
||||
val newConfig = config { lora = channelSet.loraConfig }
|
||||
if (localConfig.value.lora != newConfig.lora) setConfig(newConfig)
|
||||
val newLoraConfig = channelSet.lora_config
|
||||
if (localConfig.value.lora != newLoraConfig) {
|
||||
setConfig(Config(lora = newLoraConfig))
|
||||
}
|
||||
}
|
||||
|
||||
fun setChannel(channel: ChannelProtos.Channel) {
|
||||
fun setChannel(channel: Channel) {
|
||||
try {
|
||||
serviceRepository.meshService?.setChannel(channel.toByteArray())
|
||||
serviceRepository.meshService?.setChannel(channel.encode())
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Set channel error" }
|
||||
}
|
||||
|
|
@ -108,7 +105,7 @@ constructor(
|
|||
// Set the radio config (also updates our saved copy in preferences)
|
||||
fun setConfig(config: Config) {
|
||||
try {
|
||||
serviceRepository.meshService?.setConfig(config.toByteArray())
|
||||
serviceRepository.meshService?.setConfig(config.encode())
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Set config error" }
|
||||
}
|
||||
|
|
@ -119,7 +116,7 @@ constructor(
|
|||
}
|
||||
|
||||
private inline fun updateLoraConfig(crossinline body: (Config.LoRaConfig) -> Config.LoRaConfig) {
|
||||
val data = body(localConfig.value.lora)
|
||||
setConfig(config { lora = data })
|
||||
val data = body(localConfig.value.lora ?: Config.LoRaConfig())
|
||||
setConfig(Config(lora = data))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue