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
|
|
@ -46,14 +46,14 @@ import co.touchlab.kermit.Logger
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.google.protobuf.ByteString
|
||||
import com.google.protobuf.Descriptors
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.WriterException
|
||||
import com.journeyapps.barcodescanner.BarcodeEncoder
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.strings.Res
|
||||
|
|
@ -62,8 +62,8 @@ import org.meshtastic.core.strings.scan_qr_code
|
|||
import org.meshtastic.core.strings.share_contact
|
||||
import org.meshtastic.core.ui.R
|
||||
import org.meshtastic.core.ui.share.SharedContactDialog
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import org.meshtastic.proto.User
|
||||
import java.net.MalformedURLException
|
||||
|
||||
/**
|
||||
|
|
@ -76,9 +76,9 @@ import java.net.MalformedURLException
|
|||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun AddContactFAB(
|
||||
sharedContact: AdminProtos.SharedContact?,
|
||||
sharedContact: SharedContact?,
|
||||
modifier: Modifier = Modifier,
|
||||
onSharedContactRequested: (AdminProtos.SharedContact?) -> Unit,
|
||||
onSharedContactRequested: (SharedContact?) -> Unit,
|
||||
) {
|
||||
val barcodeLauncher =
|
||||
rememberLauncherForActivityResult(ScanContract()) { result ->
|
||||
|
|
@ -161,13 +161,13 @@ private fun SharedContact(contactUri: Uri) {
|
|||
@Composable
|
||||
fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit) {
|
||||
if (contact == null) return
|
||||
val sharedContact = AdminProtos.SharedContact.newBuilder().setUser(contact.user).setNodeNum(contact.num).build()
|
||||
val sharedContact = SharedContact(user = contact.user, node_num = contact.num)
|
||||
val uri = sharedContact.getSharedContactUrl()
|
||||
SimpleAlertDialog(
|
||||
title = Res.string.share_contact,
|
||||
text = {
|
||||
Column {
|
||||
Text(contact.user.longName)
|
||||
Text(contact.user.long_name)
|
||||
SharedContact(contactUri = uri)
|
||||
}
|
||||
},
|
||||
|
|
@ -204,43 +204,41 @@ internal const val URL_PREFIX = "https://$MESHTASTIC_HOST$CONTACT_SHARE_PATH#"
|
|||
private const val BASE64FLAGS = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING
|
||||
private const val CAMERA_ID = 0
|
||||
|
||||
/**
|
||||
* Converts a URI to a [AdminProtos.SharedContact].
|
||||
*
|
||||
* @throws MalformedURLException if the URI is not a valid Meshtastic contact sharing URL.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@Throws(MalformedURLException::class)
|
||||
fun Uri.toSharedContact(): AdminProtos.SharedContact {
|
||||
fun Uri.toSharedContact(): SharedContact {
|
||||
if (fragment.isNullOrBlank() || !host.equals(MESHTASTIC_HOST, true) || !path.equals(CONTACT_SHARE_PATH, true)) {
|
||||
throw MalformedURLException("Not a valid Meshtastic URL: ${toString().take(40)}")
|
||||
}
|
||||
val url = AdminProtos.SharedContact.parseFrom(Base64.decode(fragment!!, BASE64FLAGS))
|
||||
return url.toBuilder().build()
|
||||
return SharedContact.ADAPTER.decode(Base64.decode(fragment!!, BASE64FLAGS).toByteString())
|
||||
}
|
||||
|
||||
/** Converts a [AdminProtos.SharedContact] to its corresponding URI representation. */
|
||||
fun AdminProtos.SharedContact.getSharedContactUrl(): Uri {
|
||||
val bytes = this.toByteArray() ?: ByteArray(0)
|
||||
/** Converts a [SharedContact] to its corresponding URI representation. */
|
||||
fun SharedContact.getSharedContactUrl(): Uri {
|
||||
val bytes = SharedContact.ADAPTER.encode(this)
|
||||
val enc = Base64.encodeToString(bytes, BASE64FLAGS)
|
||||
return "$URL_PREFIX$enc".toUri()
|
||||
}
|
||||
|
||||
/** Compares two [MeshProtos.User] objects and returns a string detailing the differences. */
|
||||
fun compareUsers(oldUser: MeshProtos.User, newUser: MeshProtos.User): String {
|
||||
/** Compares two [User] objects and returns a string detailing the differences. */
|
||||
fun compareUsers(oldUser: User, newUser: User): String {
|
||||
val changes = mutableListOf<String>()
|
||||
|
||||
// Iterate over all fields in the User message descriptor
|
||||
for (fieldDescriptor: Descriptors.FieldDescriptor in MeshProtos.User.getDescriptor().fields) {
|
||||
val fieldName = fieldDescriptor.name
|
||||
val oldValue = if (oldUser.hasField(fieldDescriptor)) oldUser.getField(fieldDescriptor) else null
|
||||
val newValue = if (newUser.hasField(fieldDescriptor)) newUser.getField(fieldDescriptor) else null
|
||||
|
||||
if (oldValue != newValue) {
|
||||
val oldValueString = valueToString(oldValue, fieldDescriptor)
|
||||
val newValueString = valueToString(newValue, fieldDescriptor)
|
||||
changes.add("$fieldName: $oldValueString -> $newValueString")
|
||||
}
|
||||
if (oldUser.id != newUser.id) changes.add("id: ${oldUser.id} -> ${newUser.id}")
|
||||
if (oldUser.long_name != newUser.long_name) changes.add("long_name: ${oldUser.long_name} -> ${newUser.long_name}")
|
||||
if (oldUser.short_name != newUser.short_name) {
|
||||
changes.add("short_name: ${oldUser.short_name} -> ${newUser.short_name}")
|
||||
}
|
||||
if (oldUser.macaddr != newUser.macaddr) {
|
||||
changes.add("macaddr: ${oldUser.macaddr?.base64()} -> ${newUser.macaddr?.base64()}")
|
||||
}
|
||||
if (oldUser.hw_model != newUser.hw_model) changes.add("hw_model: ${oldUser.hw_model} -> ${newUser.hw_model}")
|
||||
if (oldUser.is_licensed != newUser.is_licensed) {
|
||||
changes.add("is_licensed: ${oldUser.is_licensed} -> ${newUser.is_licensed}")
|
||||
}
|
||||
if (oldUser.role != newUser.role) changes.add("role: ${oldUser.role} -> ${newUser.role}")
|
||||
if (oldUser.public_key != newUser.public_key) {
|
||||
changes.add("public_key: ${oldUser.public_key?.base64()} -> ${newUser.public_key?.base64()}")
|
||||
}
|
||||
|
||||
return if (changes.isEmpty()) {
|
||||
|
|
@ -250,52 +248,20 @@ fun compareUsers(oldUser: MeshProtos.User, newUser: MeshProtos.User): String {
|
|||
}
|
||||
}
|
||||
|
||||
/** Converts a [MeshProtos.User] object to a string representation of its fields and values. */
|
||||
fun userFieldsToString(user: MeshProtos.User): String {
|
||||
/** Converts a [User] object to a string representation of its fields and values. */
|
||||
fun userFieldsToString(user: User): String {
|
||||
val fieldLines = mutableListOf<String>()
|
||||
|
||||
for (fieldDescriptor: Descriptors.FieldDescriptor in MeshProtos.User.getDescriptor().fields) {
|
||||
val fieldName = fieldDescriptor.name
|
||||
if (user.hasField(fieldDescriptor)) {
|
||||
val value = user.getField(fieldDescriptor)
|
||||
val valueString = valueToString(value, fieldDescriptor) // Using the helper from previous example
|
||||
fieldLines.add("$fieldName: $valueString")
|
||||
} else if (fieldDescriptor.isRepeated || fieldDescriptor.hasDefaultValue() || fieldDescriptor.hasPresence()) {
|
||||
val defaultValue = fieldDescriptor.defaultValue
|
||||
val valueString =
|
||||
if (fieldDescriptor.isRepeated) {
|
||||
"[]" // Empty list
|
||||
} else if (user.hasField(fieldDescriptor)) {
|
||||
valueToString(user.getField(fieldDescriptor), fieldDescriptor)
|
||||
} else {
|
||||
valueToString(defaultValue, fieldDescriptor)
|
||||
}
|
||||
fieldLines.add("id: ${user.id}")
|
||||
fieldLines.add("long_name: ${user.long_name}")
|
||||
fieldLines.add("short_name: ${user.short_name}")
|
||||
fieldLines.add("macaddr: ${user.macaddr?.base64()}")
|
||||
fieldLines.add("hw_model: ${user.hw_model}")
|
||||
fieldLines.add("is_licensed: ${user.is_licensed}")
|
||||
fieldLines.add("role: ${user.role}")
|
||||
fieldLines.add("public_key: ${user.public_key?.base64()}")
|
||||
|
||||
fieldLines.add("$fieldName: $valueString")
|
||||
}
|
||||
}
|
||||
return if (fieldLines.isEmpty()) {
|
||||
"User object has no fields set."
|
||||
} else {
|
||||
fieldLines.joinToString("\n")
|
||||
}
|
||||
return fieldLines.joinToString("\n")
|
||||
}
|
||||
|
||||
private fun valueToString(value: Any?, fieldDescriptor: Descriptors.FieldDescriptor): String {
|
||||
if (value == null) {
|
||||
return "null"
|
||||
}
|
||||
return when (fieldDescriptor.type) {
|
||||
Descriptors.FieldDescriptor.Type.BYTES -> {
|
||||
// For ByteString, you might want to display it as hex or Base64
|
||||
// For simplicity, here we'll just show its size.
|
||||
if (value is ByteString) {
|
||||
Base64.encodeToString(value.toByteArray(), Base64.DEFAULT).trim()
|
||||
} else {
|
||||
value.toString().trim()
|
||||
}
|
||||
}
|
||||
// Add more custom formatting for other types if needed
|
||||
else -> value.toString().trim()
|
||||
}
|
||||
}
|
||||
private fun ByteString.base64(): String = Base64.encodeToString(this.toByteArray(), Base64.DEFAULT).trim()
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.protobuf.ProtocolMessageEnum
|
||||
|
||||
@Composable
|
||||
fun <T : Enum<T>> DropDownPreference(
|
||||
|
|
@ -71,21 +70,7 @@ fun <T> DropDownPreference(
|
|||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val deprecatedItems: List<T> = remember {
|
||||
if (selectedItem is ProtocolMessageEnum) {
|
||||
val enum = (selectedItem as? Enum<*>)?.declaringJavaClass?.enumConstants
|
||||
val descriptor = (selectedItem as ProtocolMessageEnum).descriptorForType
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(
|
||||
enum?.filter { entries -> descriptor.values.any { it.name == entries.name && it.options.deprecated } }
|
||||
?: emptyList()
|
||||
)
|
||||
as List<T>
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
val deprecatedItems: List<T> = emptyList() // Protobuf-Java specific deprecation check removed
|
||||
Column(modifier = modifier.fillMaxWidth().padding(8.dp)) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
|
|
|
|||
|
|
@ -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 org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -43,11 +42,11 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.protobuf.ByteString
|
||||
import okio.ByteString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Channel
|
||||
import org.meshtastic.core.model.util.base64ToByteString
|
||||
import org.meshtastic.core.model.util.encodeToString
|
||||
import org.meshtastic.core.model.util.toByteString
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.error
|
||||
import org.meshtastic.core.strings.reset
|
||||
|
|
@ -88,7 +87,7 @@ fun EditBase64Preference(
|
|||
value = valueState,
|
||||
onValueChange = {
|
||||
valueState = it
|
||||
runCatching { it.toByteString() }.onSuccess(onValueChange)
|
||||
runCatching { it.base64ToByteString() }.onSuccess(onValueChange)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().onFocusChanged { focusState -> isFocused = focusState.isFocused },
|
||||
enabled = enabled,
|
||||
|
|
@ -147,7 +146,7 @@ private fun EditBase64PreferencePreview() {
|
|||
value = Channel.getRandomKey(),
|
||||
enabled = true,
|
||||
keyboardActions = KeyboardActions {},
|
||||
onValueChange = {},
|
||||
onValueChange = { _ -> },
|
||||
onGenerateKey = {},
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -39,7 +38,6 @@ import androidx.compose.ui.text.input.ImeAction
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.protobuf.ByteString
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.add
|
||||
|
|
@ -48,10 +46,8 @@ import org.meshtastic.core.strings.gpio_pin
|
|||
import org.meshtastic.core.strings.ignore_incoming
|
||||
import org.meshtastic.core.strings.name
|
||||
import org.meshtastic.core.strings.type
|
||||
import org.meshtastic.proto.ModuleConfigProtos.RemoteHardwarePin
|
||||
import org.meshtastic.proto.ModuleConfigProtos.RemoteHardwarePinType
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.remoteHardwarePin
|
||||
import org.meshtastic.proto.RemoteHardwarePin
|
||||
import org.meshtastic.proto.RemoteHardwarePinType
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
|
@ -110,7 +106,7 @@ inline fun <reified T> EditListPreference(
|
|||
trailingIcon = trailingIcon,
|
||||
)
|
||||
}
|
||||
is ByteString -> {
|
||||
is okio.ByteString -> {
|
||||
EditBase64Preference(
|
||||
title = "${index + 1}/$maxCount",
|
||||
value = value,
|
||||
|
|
@ -126,12 +122,13 @@ inline fun <reified T> EditListPreference(
|
|||
is RemoteHardwarePin -> {
|
||||
EditTextPreference(
|
||||
title = stringResource(Res.string.gpio_pin),
|
||||
value = value.gpioPin,
|
||||
value = value.gpio_pin,
|
||||
enabled = enabled,
|
||||
keyboardActions = keyboardActions,
|
||||
onValueChanged = {
|
||||
onValueChanged = { newValue ->
|
||||
val it = newValue as Int
|
||||
if (it in 0..255) {
|
||||
listState[index] = value.copy { gpioPin = it } as T
|
||||
listState[index] = value.copy(gpio_pin = it) as T
|
||||
onValuesChanged(listState)
|
||||
}
|
||||
},
|
||||
|
|
@ -145,8 +142,9 @@ inline fun <reified T> EditListPreference(
|
|||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done),
|
||||
keyboardActions = keyboardActions,
|
||||
onValueChanged = {
|
||||
listState[index] = value.copy { name = it } as T
|
||||
onValueChanged = { newValue ->
|
||||
val it = newValue as String
|
||||
listState[index] = value.copy(name = it) as T
|
||||
onValuesChanged(listState)
|
||||
},
|
||||
trailingIcon = trailingIcon,
|
||||
|
|
@ -156,11 +154,11 @@ inline fun <reified T> EditListPreference(
|
|||
enabled = enabled,
|
||||
items =
|
||||
RemoteHardwarePinType.entries
|
||||
.filter { it != RemoteHardwarePinType.UNRECOGNIZED }
|
||||
.filter { it != RemoteHardwarePinType.UNKNOWN }
|
||||
.map { it to it.name },
|
||||
selectedItem = value.type,
|
||||
onItemSelected = {
|
||||
listState[index] = value.copy { type = it } as T
|
||||
listState[index] = value.copy(type = it) as T
|
||||
onValuesChanged(listState)
|
||||
},
|
||||
)
|
||||
|
|
@ -174,8 +172,8 @@ inline fun <reified T> EditListPreference(
|
|||
val newElement =
|
||||
when (T::class) {
|
||||
Int::class -> 0 as T
|
||||
ByteString::class -> ByteString.EMPTY as T
|
||||
RemoteHardwarePin::class -> remoteHardwarePin {} as T
|
||||
okio.ByteString::class -> okio.ByteString.EMPTY as T
|
||||
RemoteHardwarePin::class -> RemoteHardwarePin() as T
|
||||
else -> throw IllegalArgumentException("Unsupported type: ${T::class}")
|
||||
}
|
||||
listState.add(listState.size, newElement)
|
||||
|
|
@ -204,11 +202,7 @@ private fun EditListPreferencePreview() {
|
|||
title = "Available pins",
|
||||
list =
|
||||
listOf(
|
||||
remoteHardwarePin {
|
||||
gpioPin = 12
|
||||
name = "Front door"
|
||||
type = RemoteHardwarePinType.DIGITAL_READ
|
||||
},
|
||||
RemoteHardwarePin(gpio_pin = 12, name = "Front door", type = RemoteHardwarePinType.DIGITAL_READ),
|
||||
),
|
||||
maxCount = 4,
|
||||
enabled = true,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import org.meshtastic.core.strings.altitude
|
|||
import org.meshtastic.core.strings.elevation_suffix
|
||||
import org.meshtastic.core.ui.icon.Elevation
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.proto.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import org.meshtastic.proto.Config.DisplayConfig.DisplayUnits
|
||||
|
||||
@Composable
|
||||
fun ElevationInfo(
|
||||
|
|
|
|||
|
|
@ -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 org.meshtastic.core.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -38,9 +37,9 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.meshtastic.core.database.model.Node
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit)? = null) {
|
||||
|
|
@ -53,12 +52,12 @@ fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit
|
|||
Modifier.width(IntrinsicSize.Min)
|
||||
.defaultMinSize(minWidth = 72.dp, minHeight = 32.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
.semantics { contentDescription = node.user.shortName.ifEmpty { "Node" } },
|
||||
.semantics { contentDescription = node.user.short_name.ifEmpty { "Node" } },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = node.user.shortName.ifEmpty { "???" },
|
||||
text = node.user.short_name.ifEmpty { "???" },
|
||||
fontSize = MaterialTheme.typography.labelLarge.fontSize,
|
||||
textDecoration = TextDecoration.LineThrough.takeIf { node.isIgnored },
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -80,15 +79,14 @@ fun NodeChip(modifier: Modifier = Modifier, node: Node, onClick: ((Node) -> Unit
|
|||
@Preview
|
||||
@Composable
|
||||
private fun NodeChipPreview() {
|
||||
val user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build()
|
||||
val user = User(short_name = "\uD83E\uDEE0", long_name = "John Doe")
|
||||
val node =
|
||||
Node(
|
||||
num = 13444,
|
||||
user = user,
|
||||
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),
|
||||
)
|
||||
NodeChip(node = node)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.protobuf.ByteString
|
||||
import okio.ByteString
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.model.Channel
|
||||
|
|
|
|||
|
|
@ -80,9 +80,9 @@ import org.meshtastic.core.ui.icon.Warning
|
|||
import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
|
||||
import org.meshtastic.core.ui.theme.StatusColors.StatusYellow
|
||||
import org.meshtastic.proto.AppOnlyProtos
|
||||
import org.meshtastic.proto.ChannelProtos.ChannelSettings
|
||||
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config.LoRaConfig
|
||||
|
||||
private const val PRECISE_POSITION_BITS = 32
|
||||
|
||||
|
|
@ -279,15 +279,15 @@ fun SecurityIcon(
|
|||
|
||||
/** Extension property to check if the channel uses a low entropy PSK (not securely encrypted). */
|
||||
val Channel.isLowEntropyKey: Boolean
|
||||
get() = settings.psk.size() <= 1
|
||||
get() = settings.psk.size <= 1
|
||||
|
||||
/** Extension property to check if the channel has precise location enabled. */
|
||||
val Channel.isPreciseLocation: Boolean
|
||||
get() = settings.moduleSettings.positionPrecision == PRECISE_POSITION_BITS
|
||||
get() = settings.module_settings?.position_precision == PRECISE_POSITION_BITS
|
||||
|
||||
/** Extension property to check if MQTT is enabled for the channel. */
|
||||
val Channel.isMqttEnabled: Boolean
|
||||
get() = settings.uplinkEnabled
|
||||
get() = settings.uplink_enabled ?: false
|
||||
|
||||
/**
|
||||
* Overload for [SecurityIcon] that takes a [Channel] object to determine its security state.
|
||||
|
|
@ -343,7 +343,7 @@ fun SecurityIcon(
|
|||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
channelSet: AppOnlyProtos.ChannelSet,
|
||||
channelSet: ChannelSet,
|
||||
channelIndex: Int,
|
||||
baseContentDescription: String = stringResource(Res.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
|
|
@ -369,17 +369,19 @@ fun SecurityIcon(
|
|||
*/
|
||||
@Composable
|
||||
fun SecurityIcon(
|
||||
channelSet: AppOnlyProtos.ChannelSet,
|
||||
channelSet: ChannelSet,
|
||||
channelName: String,
|
||||
baseContentDescription: String = stringResource(Res.string.security_icon_description),
|
||||
externalOnClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val channelByNameMap =
|
||||
remember(channelSet) { channelSet.settingsList.associateBy { Channel(it, channelSet.loraConfig).name } }
|
||||
remember(channelSet) {
|
||||
channelSet.settings.associateBy { Channel(it, channelSet.lora_config ?: Channel.default.loraConfig).name }
|
||||
}
|
||||
|
||||
channelByNameMap[channelName]?.let { channelSetting ->
|
||||
SecurityIcon(
|
||||
channel = Channel(channelSetting, channelSet.loraConfig),
|
||||
channel = Channel(channelSetting, channelSet.lora_config ?: Channel.default.loraConfig),
|
||||
baseContentDescription = baseContentDescription,
|
||||
externalOnClick = externalOnClick,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -62,13 +62,13 @@ fun SignalInfo(
|
|||
IconInfo(
|
||||
icon = MeshtasticIcons.ChannelUtilization,
|
||||
contentDescription = stringResource(Res.string.channel_utilization),
|
||||
text = "%.1f%%".format(node.deviceMetrics.channelUtilization),
|
||||
text = "%.1f%%".format(node.deviceMetrics.channel_utilization),
|
||||
contentColor = contentColor,
|
||||
)
|
||||
IconInfo(
|
||||
icon = MeshtasticIcons.AirUtilization,
|
||||
contentDescription = stringResource(Res.string.air_utilization),
|
||||
text = "%.1f%%".format(node.deviceMetrics.airUtilTx),
|
||||
text = "%.1f%%".format(node.deviceMetrics.air_util_tx),
|
||||
contentColor = contentColor,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,16 @@
|
|||
package org.meshtastic.core.ui.component.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.google.protobuf.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import org.meshtastic.core.model.DeviceMetrics.Companion.currentTime
|
||||
import org.meshtastic.proto.ConfigProtos
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.deviceMetrics
|
||||
import org.meshtastic.proto.environmentMetrics
|
||||
import org.meshtastic.proto.paxcount
|
||||
import org.meshtastic.proto.position
|
||||
import org.meshtastic.proto.user
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceMetrics
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.HardwareModel
|
||||
import org.meshtastic.proto.Paxcount
|
||||
import org.meshtastic.proto.Position
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.random.Random
|
||||
|
||||
class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
||||
|
|
@ -34,32 +34,26 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
|||
Node(
|
||||
num = 1955,
|
||||
user =
|
||||
user {
|
||||
id = "mickeyMouseId"
|
||||
longName = "Mickey Mouse"
|
||||
shortName = "MM"
|
||||
hwModel = MeshProtos.HardwareModel.TBEAM
|
||||
role = ConfigProtos.Config.DeviceConfig.Role.ROUTER
|
||||
},
|
||||
position =
|
||||
position {
|
||||
latitudeI = 338125110
|
||||
longitudeI = -1179189760
|
||||
altitude = 138
|
||||
satsInView = 4
|
||||
},
|
||||
User(
|
||||
id = "mickeyMouseId",
|
||||
long_name = "Mickey Mouse",
|
||||
short_name = "MM",
|
||||
hw_model = HardwareModel.TBEAM,
|
||||
role = Config.DeviceConfig.Role.ROUTER,
|
||||
),
|
||||
position = Position(latitude_i = 338125110, longitude_i = -1179189760, altitude = 138, sats_in_view = 4),
|
||||
lastHeard = currentTime(),
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics =
|
||||
deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
DeviceMetrics(
|
||||
channel_utilization = 2.4F,
|
||||
air_util_tx = 3.5F,
|
||||
battery_level = 85,
|
||||
voltage = 3.7F,
|
||||
uptime_seconds = 3600,
|
||||
),
|
||||
isFavorite = true,
|
||||
hopsAway = 0,
|
||||
)
|
||||
|
|
@ -68,67 +62,55 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
|||
mickeyMouse.copy(
|
||||
num = Random.nextInt(),
|
||||
user =
|
||||
user {
|
||||
longName = "Minnie Mouse"
|
||||
shortName = "MiMo"
|
||||
id = "minnieMouseId"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
},
|
||||
User(
|
||||
long_name = "Minnie Mouse",
|
||||
short_name = "MiMo",
|
||||
id = "minnieMouseId",
|
||||
hw_model = HardwareModel.HELTEC_V3,
|
||||
),
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
position = position {},
|
||||
position = Position(),
|
||||
hopsAway = 1,
|
||||
)
|
||||
|
||||
private val donaldDuck =
|
||||
Node(
|
||||
num = Random.nextInt(),
|
||||
position =
|
||||
position {
|
||||
latitudeI = 338052347
|
||||
longitudeI = -1179208460
|
||||
altitude = 121
|
||||
satsInView = 66
|
||||
},
|
||||
position = Position(latitude_i = 338052347, longitude_i = -1179208460, altitude = 121, sats_in_view = 66),
|
||||
lastHeard = currentTime() - 300,
|
||||
channel = 0,
|
||||
snr = 12.5F,
|
||||
rssi = -42,
|
||||
deviceMetrics =
|
||||
deviceMetrics {
|
||||
channelUtilization = 2.4F
|
||||
airUtilTx = 3.5F
|
||||
batteryLevel = 85
|
||||
voltage = 3.7F
|
||||
uptimeSeconds = 3600
|
||||
},
|
||||
DeviceMetrics(
|
||||
channel_utilization = 2.4F,
|
||||
air_util_tx = 3.5F,
|
||||
battery_level = 85,
|
||||
voltage = 3.7F,
|
||||
uptime_seconds = 3600,
|
||||
),
|
||||
user =
|
||||
user {
|
||||
id = "donaldDuckId"
|
||||
longName = "Donald Duck, the Grand Duck of the Ducks"
|
||||
shortName = "DoDu"
|
||||
hwModel = MeshProtos.HardwareModel.HELTEC_V3
|
||||
publicKey = ByteString.copyFrom(ByteArray(32) { 1 })
|
||||
},
|
||||
User(
|
||||
id = "donaldDuckId",
|
||||
long_name = "Donald Duck, the Grand Duck of the Ducks",
|
||||
short_name = "DoDu",
|
||||
hw_model = HardwareModel.HELTEC_V3,
|
||||
public_key = ByteArray(32) { 1 }.toByteString(),
|
||||
),
|
||||
environmentMetrics =
|
||||
environmentMetrics {
|
||||
temperature = 28.0F
|
||||
relativeHumidity = 50.0F
|
||||
barometricPressure = 1013.25F
|
||||
gasResistance = 0.0F
|
||||
voltage = 3.7F
|
||||
current = 0.0F
|
||||
iaq = 100
|
||||
barometricPressure = 1013.25F
|
||||
soilTemperature = 28.0F
|
||||
soilMoisture = 50
|
||||
},
|
||||
paxcounter =
|
||||
paxcount {
|
||||
wifi = 30
|
||||
ble = 39
|
||||
uptime = 420
|
||||
},
|
||||
EnvironmentMetrics(
|
||||
temperature = 28.0F,
|
||||
relative_humidity = 50.0F,
|
||||
barometric_pressure = 1013.25F,
|
||||
gas_resistance = 0.0F,
|
||||
voltage = 3.7F,
|
||||
current = 0.0F,
|
||||
iaq = 100,
|
||||
soil_temperature = 28.0F,
|
||||
soil_moisture = 50,
|
||||
),
|
||||
paxcounter = Paxcount(wifi = 30, ble = 39, uptime = 420),
|
||||
isFavorite = true,
|
||||
hopsAway = 2,
|
||||
)
|
||||
|
|
@ -136,14 +118,9 @@ class NodePreviewParameterProvider : PreviewParameterProvider<Node> {
|
|||
private val unknown =
|
||||
donaldDuck.copy(
|
||||
user =
|
||||
user {
|
||||
id = "myId"
|
||||
longName = "Meshtastic myId"
|
||||
shortName = "myId"
|
||||
hwModel = MeshProtos.HardwareModel.UNSET
|
||||
},
|
||||
environmentMetrics = environmentMetrics {},
|
||||
paxcounter = paxcount {},
|
||||
User(id = "myId", long_name = "Meshtastic myId", short_name = "myId", hw_model = HardwareModel.UNSET),
|
||||
environmentMetrics = EnvironmentMetrics(),
|
||||
paxcounter = Paxcount(),
|
||||
)
|
||||
|
||||
private val almostNothing = Node(num = Random.nextInt())
|
||||
|
|
|
|||
|
|
@ -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,29 +14,27 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package org.meshtastic.core.ui.component.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import org.meshtastic.core.database.model.Node
|
||||
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
|
||||
|
||||
/** Simple [PreviewParameterProvider] that provides true and false values. */
|
||||
class BooleanProvider : PreviewParameterProvider<Boolean> {
|
||||
override val values: Sequence<Boolean> = sequenceOf(false, true)
|
||||
}
|
||||
|
||||
private val user = MeshProtos.User.newBuilder().setShortName("\uD83E\uDEE0").setLongName("John Doe").build()
|
||||
private val user = User(short_name = "\uD83E\uDEE0", long_name = "John Doe")
|
||||
val previewNode =
|
||||
Node(
|
||||
num = 13444,
|
||||
user = user,
|
||||
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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -60,10 +60,7 @@ import org.meshtastic.core.strings.new_channel_rcvd
|
|||
import org.meshtastic.core.strings.replace
|
||||
import org.meshtastic.core.strings.replace_channels_and_settings_description
|
||||
import org.meshtastic.core.ui.component.ChannelSelection
|
||||
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
|
||||
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset
|
||||
import org.meshtastic.proto.channelSet
|
||||
import org.meshtastic.proto.copy
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
|
||||
@Composable
|
||||
fun ScannedQrCodeDialog(
|
||||
|
|
@ -91,7 +88,7 @@ fun ScannedQrCodeDialog(
|
|||
onDismiss: () -> Unit,
|
||||
onConfirm: (ChannelSet) -> Unit,
|
||||
) {
|
||||
var shouldReplace by remember { mutableStateOf(incoming.hasLoraConfig()) }
|
||||
var shouldReplace by remember { mutableStateOf(incoming.lora_config != null) }
|
||||
|
||||
val channelSet =
|
||||
remember(shouldReplace) {
|
||||
|
|
@ -99,67 +96,65 @@ fun ScannedQrCodeDialog(
|
|||
// When replacing, apply the incoming LoRa configuration but preserve certain
|
||||
// locally safe fields such as MQTT flags and TX power. This prevents QR codes
|
||||
// from unintentionally overriding device-specific power limits (e.g. E22 caps).
|
||||
incoming.copy {
|
||||
loraConfig =
|
||||
loraConfig.copy {
|
||||
configOkToMqtt = channels.loraConfig.configOkToMqtt
|
||||
txPower = channels.loraConfig.txPower
|
||||
}
|
||||
}
|
||||
incoming.copy(
|
||||
lora_config =
|
||||
incoming.lora_config?.copy(
|
||||
config_ok_to_mqtt = channels.lora_config?.config_ok_to_mqtt ?: false,
|
||||
tx_power = channels.lora_config?.tx_power ?: 0,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
channels.copy {
|
||||
// To guarantee consistent ordering, using a LinkedHashSet which iterates through
|
||||
// its entries according to the order an item was *first* inserted.
|
||||
// https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-linked-hash-set/
|
||||
val result = LinkedHashSet(settings + incoming.settingsList)
|
||||
settings.clear()
|
||||
settings.addAll(result)
|
||||
}
|
||||
// To guarantee consistent ordering, using a LinkedHashSet which iterates through
|
||||
// its entries according to the order an item was *first* inserted.
|
||||
val result = (channels.settings + incoming.settings).distinct()
|
||||
channels.copy(settings = result)
|
||||
}
|
||||
}
|
||||
|
||||
val modemPresetName = Channel(loraConfig = channelSet.loraConfig).name
|
||||
val modemPresetName = Channel(loraConfig = channelSet.lora_config ?: Channel.default.loraConfig).name
|
||||
|
||||
/* Holds selections made by the user */
|
||||
val channelSelections =
|
||||
remember(channelSet) { mutableStateListOf(elements = Array(size = channelSet.settingsCount, init = { true })) }
|
||||
remember(channelSet) { mutableStateListOf(elements = Array(size = channelSet.settings.size, init = { true })) }
|
||||
|
||||
val selectedChannelSet =
|
||||
channelSet.copy {
|
||||
// When adding (not replacing), include all previous channels + selected new channels.
|
||||
// Since 'channelSet.settings' already contains the merged distinct list, we just filter it.
|
||||
val result =
|
||||
settings.filterIndexed { i, _ ->
|
||||
val isExisting = !shouldReplace && i < channels.settingsCount
|
||||
if (shouldReplace) {
|
||||
channelSet.copy(
|
||||
settings = channelSet.settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true },
|
||||
)
|
||||
} else {
|
||||
channelSet.copy(
|
||||
settings =
|
||||
channelSet.settings.filterIndexed { i, _ ->
|
||||
val isExisting = i < channels.settings.size
|
||||
isExisting || channelSelections.getOrNull(i) == true
|
||||
}
|
||||
settings.clear()
|
||||
settings.addAll(result)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Compute LoRa configuration changes when in replace mode
|
||||
val loraChanges =
|
||||
remember(shouldReplace, channels, incoming) {
|
||||
if (shouldReplace && incoming.hasLoraConfig()) {
|
||||
val current = channels.loraConfig
|
||||
val new = incoming.loraConfig
|
||||
if (shouldReplace && incoming.lora_config != null) {
|
||||
val current = channels.lora_config
|
||||
val new = incoming.lora_config
|
||||
val changes = mutableListOf<String>()
|
||||
|
||||
if (current.hopLimit != new.hopLimit) {
|
||||
changes.add("Hop Limit: ${current.hopLimit} -> ${new.hopLimit}")
|
||||
if (current?.hop_limit != new?.hop_limit) {
|
||||
changes.add("Hop Limit: ${current?.hop_limit} -> ${new?.hop_limit}")
|
||||
}
|
||||
if (current.getRegion() != new.getRegion()) {
|
||||
val currentRegionDesc = current.getRegion()?.name ?: "Unknown"
|
||||
val newRegionDesc = new.getRegion()?.name ?: "Unknown"
|
||||
if (current?.region != new?.region) {
|
||||
val currentRegionDesc = current?.region?.name ?: "Unknown"
|
||||
val newRegionDesc = new?.region?.name ?: "Unknown"
|
||||
changes.add("Region: $currentRegionDesc -> $newRegionDesc")
|
||||
}
|
||||
if (current.modemPreset != new.modemPreset) {
|
||||
val currentPresetDesc = ModemPreset.forNumber(current.modemPreset.number)?.name ?: "Unknown"
|
||||
val newPresetDesc = ModemPreset.forNumber(new.modemPreset.number)?.name ?: "Unknown"
|
||||
if (current?.modem_preset != new?.modem_preset) {
|
||||
val currentPresetDesc = current?.modem_preset?.name ?: "Unknown"
|
||||
val newPresetDesc = new?.modem_preset?.name ?: "Unknown"
|
||||
changes.add("Modem Preset: $currentPresetDesc -> $newPresetDesc")
|
||||
}
|
||||
if (current.usePreset != new.usePreset) {
|
||||
changes.add("Use Preset: ${current.usePreset} -> ${new.usePreset}")
|
||||
if (current?.use_preset != new?.use_preset) {
|
||||
changes.add("Use Preset: ${current?.use_preset} -> ${new?.use_preset}")
|
||||
}
|
||||
|
||||
changes
|
||||
|
|
@ -204,16 +199,16 @@ fun ScannedQrCodeDialog(
|
|||
)
|
||||
}
|
||||
|
||||
itemsIndexed(channelSet.settingsList) { index, channel ->
|
||||
val isExisting = !shouldReplace && index < channels.settingsCount
|
||||
val channelObj = Channel(channel, channelSet.loraConfig)
|
||||
itemsIndexed(channelSet.settings) { index, channel ->
|
||||
val isExisting = !shouldReplace && index < channels.settings.size
|
||||
val channelObj = Channel(channel, channelSet.lora_config ?: Channel.default.loraConfig)
|
||||
ChannelSelection(
|
||||
index = index,
|
||||
title = channel.name.ifEmpty { modemPresetName },
|
||||
enabled = !isExisting,
|
||||
isSelected = if (isExisting) true else channelSelections[index],
|
||||
onSelected = {
|
||||
if (it || selectedChannelSet.settingsCount > 1) {
|
||||
if (it || selectedChannelSet.settings.size > 1) {
|
||||
channelSelections[index] = it
|
||||
}
|
||||
},
|
||||
|
|
@ -256,7 +251,7 @@ fun ScannedQrCodeDialog(
|
|||
OutlinedButton(
|
||||
onClick = { shouldReplace = true },
|
||||
modifier = Modifier.height(48.dp).weight(1f),
|
||||
enabled = incoming.hasLoraConfig(),
|
||||
enabled = incoming.lora_config != null,
|
||||
colors = if (shouldReplace) selectedColors else unselectedColors,
|
||||
) {
|
||||
Text(text = stringResource(Res.string.replace))
|
||||
|
|
@ -285,7 +280,7 @@ fun ScannedQrCodeDialog(
|
|||
onDismiss()
|
||||
onConfirm(selectedChannelSet)
|
||||
},
|
||||
enabled = selectedChannelSet.settingsCount in 1..8,
|
||||
enabled = selectedChannelSet.settings.size in 1..8,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.accept),
|
||||
|
|
@ -306,16 +301,8 @@ fun ScannedQrCodeDialog(
|
|||
@Composable
|
||||
private fun ScannedQrCodeDialogPreview() {
|
||||
ScannedQrCodeDialog(
|
||||
channels =
|
||||
channelSet {
|
||||
settings.add(Channel.default.settings)
|
||||
loraConfig = Channel.default.loraConfig
|
||||
},
|
||||
incoming =
|
||||
channelSet {
|
||||
settings.add(Channel.default.settings)
|
||||
loraConfig = Channel.default.loraConfig
|
||||
},
|
||||
channels = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
|
||||
incoming = ChannelSet(settings = listOf(Channel.default.settings), lora_config = Channel.default.loraConfig),
|
||||
onDismiss = {},
|
||||
onConfirm = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 org.meshtastic.core.ui.qr
|
||||
|
||||
import android.os.RemoteException
|
||||
|
|
@ -27,12 +26,10 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
|
|||
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.Channel
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -43,23 +40,24 @@ constructor(
|
|||
private val serviceRepository: ServiceRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = channelSet {})
|
||||
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(initialValue = ChannelSet())
|
||||
|
||||
private val localConfig =
|
||||
radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig.getDefaultInstance())
|
||||
private val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
|
||||
|
||||
/** 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 loraConfig = channelSet.lora_config
|
||||
if (loraConfig != null && localConfig.value.lora != loraConfig) {
|
||||
setConfig(Config(lora = loraConfig))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setChannel(channel: ChannelProtos.Channel) {
|
||||
private fun setChannel(channel: Channel) {
|
||||
try {
|
||||
serviceRepository.meshService?.setChannel(channel.toByteArray())
|
||||
serviceRepository.meshService?.setChannel(Channel.ADAPTER.encode(channel))
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Set channel error" }
|
||||
}
|
||||
|
|
@ -68,7 +66,7 @@ constructor(
|
|||
// Set the radio config (also updates our saved copy in preferences)
|
||||
private fun setConfig(config: Config) {
|
||||
try {
|
||||
serviceRepository.meshService?.setConfig(config.toByteArray())
|
||||
serviceRepository.meshService?.setConfig(Config.ADAPTER.encode(config))
|
||||
} catch (ex: RemoteException) {
|
||||
Logger.e(ex) { "Set config error" }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 org.meshtastic.core.ui.share
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -35,18 +34,19 @@ import org.meshtastic.core.strings.public_key_changed
|
|||
import org.meshtastic.core.ui.component.SimpleAlertDialog
|
||||
import org.meshtastic.core.ui.component.compareUsers
|
||||
import org.meshtastic.core.ui.component.userFieldsToString
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
/** A dialog for importing a shared contact that was scanned from a QR code. */
|
||||
@Composable
|
||||
fun SharedContactDialog(
|
||||
sharedContact: AdminProtos.SharedContact,
|
||||
sharedContact: SharedContact,
|
||||
onDismiss: () -> Unit,
|
||||
viewModel: SharedContactViewModel = hiltViewModel(),
|
||||
) {
|
||||
val unfilteredNodes by viewModel.unfilteredNodes.collectAsStateWithLifecycle()
|
||||
|
||||
val nodeNum = sharedContact.nodeNum
|
||||
val nodeNum = sharedContact.node_num
|
||||
val node = unfilteredNodes.find { it.num == nodeNum }
|
||||
|
||||
SimpleAlertDialog(
|
||||
|
|
@ -55,16 +55,18 @@ fun SharedContactDialog(
|
|||
Column {
|
||||
if (node != null) {
|
||||
Text(text = stringResource(Res.string.import_known_shared_contact_text))
|
||||
if (node.user.publicKey.size() > 0 && node.user.publicKey != sharedContact.user?.publicKey) {
|
||||
if (
|
||||
(node.user.public_key?.size ?: 0) > 0 && node.user.public_key != sharedContact.user?.public_key
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.public_key_changed),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
Text(text = compareUsers(node.user, sharedContact.user))
|
||||
Text(text = compareUsers(node.user, sharedContact.user ?: User()))
|
||||
} else {
|
||||
Text(text = userFieldsToString(sharedContact.user))
|
||||
Text(text = userFieldsToString(sharedContact.user ?: User()))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 org.meshtastic.core.ui.share
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -27,7 +26,7 @@ import org.meshtastic.core.database.model.Node
|
|||
import org.meshtastic.core.service.ServiceAction
|
||||
import org.meshtastic.core.service.ServiceRepository
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.AdminProtos
|
||||
import org.meshtastic.proto.SharedContact
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -41,6 +40,6 @@ constructor(
|
|||
val unfilteredNodes: StateFlow<List<Node>> =
|
||||
nodeRepository.getNodes().stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
fun addSharedContact(sharedContact: AdminProtos.SharedContact) =
|
||||
fun addSharedContact(sharedContact: SharedContact) =
|
||||
viewModelScope.launch { serviceRepository.onServiceAction(ServiceAction.ImportContact(sharedContact)) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,39 +22,39 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.strings.Res
|
||||
import org.meshtastic.core.strings.unknown_age
|
||||
import org.meshtastic.proto.ChannelProtos
|
||||
import org.meshtastic.proto.ChannelProtos.ChannelSettings
|
||||
import org.meshtastic.proto.MeshProtos
|
||||
import org.meshtastic.proto.MeshProtos.MeshPacket
|
||||
import org.meshtastic.proto.MeshProtos.Position
|
||||
import org.meshtastic.proto.channel
|
||||
import org.meshtastic.proto.channelSettings
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.Position
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
private const val SECONDS_TO_MILLIS = 1000L
|
||||
|
||||
@Composable
|
||||
fun MeshProtos.Position.formatPositionTime(): String {
|
||||
fun Position.formatPositionTime(): String {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds
|
||||
val isOlderThanSixMonths = time * SECONDS_TO_MILLIS < sixMonthsAgo
|
||||
val isOlderThanSixMonths = (time ?: 0) * SECONDS_TO_MILLIS < sixMonthsAgo
|
||||
val timeText =
|
||||
if (isOlderThanSixMonths) {
|
||||
stringResource(Res.string.unknown_age)
|
||||
} else {
|
||||
DateUtils.formatDateTime(
|
||||
LocalContext.current,
|
||||
time * SECONDS_TO_MILLIS,
|
||||
(time ?: 0) * SECONDS_TO_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_ABBREV_ALL,
|
||||
)
|
||||
}
|
||||
return timeText
|
||||
}
|
||||
|
||||
fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) {
|
||||
runCatching { Position.parseFrom(decoded.payload) }.getOrNull()
|
||||
} else {
|
||||
null
|
||||
fun MeshPacket.toPosition(): Position? {
|
||||
val decoded = decoded ?: return null
|
||||
return if (decoded.want_response != true) {
|
||||
decoded.payload.let { runCatching { Position.ADAPTER.decode(it) }.getOrNull() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,20 +65,20 @@ fun MeshPacket.toPosition(): Position? = if (!decoded.wantResponse) {
|
|||
* @param old The current [ChannelSettings] list (required when disabling unused channels).
|
||||
* @return A [Channel] list containing only the modified channels.
|
||||
*/
|
||||
fun getChannelList(new: List<ChannelSettings>, old: List<ChannelSettings>): List<ChannelProtos.Channel> = buildList {
|
||||
fun getChannelList(new: List<ChannelSettings>, old: List<ChannelSettings>): List<Channel> = buildList {
|
||||
for (i in 0..maxOf(old.lastIndex, new.lastIndex)) {
|
||||
if (old.getOrNull(i) != new.getOrNull(i)) {
|
||||
add(
|
||||
channel {
|
||||
Channel(
|
||||
role =
|
||||
when (i) {
|
||||
0 -> ChannelProtos.Channel.Role.PRIMARY
|
||||
in 1..new.lastIndex -> ChannelProtos.Channel.Role.SECONDARY
|
||||
else -> ChannelProtos.Channel.Role.DISABLED
|
||||
}
|
||||
index = i
|
||||
settings = new.getOrNull(i) ?: channelSettings {}
|
||||
},
|
||||
when (i) {
|
||||
0 -> Channel.Role.PRIMARY
|
||||
in 1..new.lastIndex -> Channel.Role.SECONDARY
|
||||
else -> Channel.Role.DISABLED
|
||||
},
|
||||
index = i,
|
||||
settings = new.getOrNull(i) ?: ChannelSettings(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue