From 8fb41aab741441b914e09b4c5bfdd7b1e71567d4 Mon Sep 17 00:00:00 2001
From: Phil Oliver <3497406+poliver@users.noreply.github.com>
Date: Fri, 19 Sep 2025 15:53:43 -0400
Subject: [PATCH] Modularize some model classes (#3153)
---
.../java/com/geeksville/mesh/ChannelTest.kt | 4 +-
.../mesh/compose/ScannedQrCodeDialogTest.kt | 55 +++----
.../mesh/android/GeeksvilleApplication.kt | 2 +-
.../mesh/android/GeeksvilleApplication.kt | 2 +-
.../database/entity/DeviceHardwareEntity.kt | 2 +-
.../java/com/geeksville/mesh/model/Channel.kt | 120 ---------------
.../com/geeksville/mesh/model/ChannelSet.kt | 1 +
.../geeksville/mesh/model/MetricsViewModel.kt | 1 +
.../java/com/geeksville/mesh/model/UIState.kt | 2 +-
.../api/DeviceHardwareRepository.kt | 2 +-
.../mesh/repository/radio/MockInterface.kt | 2 +-
.../geeksville/mesh/service/MeshService.kt | 2 +-
.../common/components/EditBase64Preference.kt | 30 ++--
.../common/components/ScannedQrCodeDialog.kt | 2 +-
.../mesh/ui/common/components/SecurityIcon.kt | 2 +-
.../mesh/ui/metrics/TracerouteLog.kt | 4 +-
.../com/geeksville/mesh/ui/node/NodeDetail.kt | 2 +-
.../ui/node/components/NodeKeyStatusIcon.kt | 2 +-
.../components/ChannelSettingsItemList.kt | 2 +-
.../radio/components/EditChannelDialog.kt | 2 +-
.../radio/components/LoRaConfigItemList.kt | 6 +-
.../com/geeksville/mesh/ui/sharing/Channel.kt | 2 +-
core/model/build.gradle.kts | 5 +-
.../org/meshtastic/core/model/Channel.kt | 143 ++++++++++++++++++
.../meshtastic/core}/model/ChannelOption.kt | 2 +-
.../meshtastic/core}/model/DeviceHardware.kt | 4 +-
.../meshtastic/core}/model/RouteDiscovery.kt | 2 +-
27 files changed, 215 insertions(+), 190 deletions(-)
delete mode 100644 app/src/main/java/com/geeksville/mesh/model/Channel.kt
create mode 100644 core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt
rename {app/src/main/java/com/geeksville/mesh => core/model/src/main/kotlin/org/meshtastic/core}/model/ChannelOption.kt (99%)
rename {app/src/main/java/com/geeksville/mesh => core/model/src/main/kotlin/org/meshtastic/core}/model/DeviceHardware.kt (94%)
rename {app/src/main/java/com/geeksville/mesh => core/model/src/main/kotlin/org/meshtastic/core}/model/RouteDiscovery.kt (99%)
diff --git a/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt b/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt
index bc92c5a82..a8d5cbd86 100644
--- a/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt
+++ b/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt
@@ -18,14 +18,14 @@
package com.geeksville.mesh
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.URL_PREFIX
import com.geeksville.mesh.model.getChannelUrl
-import com.geeksville.mesh.model.numChannels
import com.geeksville.mesh.model.toChannelSet
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
+import org.meshtastic.core.model.Channel
+import org.meshtastic.core.model.numChannels
@RunWith(AndroidJUnit4::class)
class ChannelTest {
diff --git a/app/src/androidTest/java/com/geeksville/mesh/compose/ScannedQrCodeDialogTest.kt b/app/src/androidTest/java/com/geeksville/mesh/compose/ScannedQrCodeDialogTest.kt
index d9ce55676..96bf082d9 100644
--- a/app/src/androidTest/java/com/geeksville/mesh/compose/ScannedQrCodeDialogTest.kt
+++ b/app/src/androidTest/java/com/geeksville/mesh/compose/ScannedQrCodeDialogTest.kt
@@ -29,21 +29,19 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.copy
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.meshtastic.core.model.Channel
@RunWith(AndroidJUnit4::class)
class ScannedQrCodeDialogTest {
- @get:Rule
- val composeTestRule = createComposeRule()
+ @get:Rule val composeTestRule = createComposeRule()
- private fun getString(id: Int): String =
- InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
+ private fun getString(id: Int): String = InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
private fun getRandomKey() = Channel.getRandomKey()
@@ -56,26 +54,28 @@ class ScannedQrCodeDialogTest {
settings.addAll(
listOf(
Channel.default.settings,
- channelSettings { name = "2"; psk = getRandomKey() },
- channelSettings { name = "3"; psk = getRandomKey() },
- channelSettings { name = "admin"; psk = getRandomKey() },
- )
+ channelSettings {
+ name = "2"
+ psk = getRandomKey()
+ },
+ channelSettings {
+ name = "3"
+ psk = getRandomKey()
+ },
+ channelSettings {
+ name = "admin"
+ psk = getRandomKey()
+ },
+ ),
)
- loraConfig = Channel.default.loraConfig
- .copy { modemPreset = ConfigProtos.Config.LoRaConfig.ModemPreset.SHORT_FAST }
+ loraConfig =
+ Channel.default.loraConfig.copy { modemPreset = ConfigProtos.Config.LoRaConfig.ModemPreset.SHORT_FAST }
}
- private fun testScannedQrCodeDialog(
- onDismiss: () -> Unit = {},
- onConfirm: (ChannelSet) -> Unit = {},
- ) = composeTestRule.setContent {
- ScannedQrCodeDialog(
- channels = channels,
- incoming = incoming,
- onDismiss = onDismiss,
- onConfirm = onConfirm,
- )
- }
+ private fun testScannedQrCodeDialog(onDismiss: () -> Unit = {}, onConfirm: (ChannelSet) -> Unit = {}) =
+ composeTestRule.setContent {
+ ScannedQrCodeDialog(channels = channels, incoming = incoming, onDismiss = onDismiss, onConfirm = onConfirm)
+ }
@Test
fun testScannedQrCodeDialog_showsDialogTitle() {
@@ -149,11 +149,12 @@ class ScannedQrCodeDialogTest {
}
// Verify onConfirm is called with the correct ChannelSet
- val expectedChannelSet = channels.copy {
- val list = LinkedHashSet(settings + incoming.settingsList)
- settings.clear()
- settings.addAll(list)
- }
+ val expectedChannelSet =
+ channels.copy {
+ val list = LinkedHashSet(settings + incoming.settingsList)
+ settings.clear()
+ settings.addAll(list)
+ }
Assert.assertEquals(expectedChannelSet, actualChannelSet)
}
}
diff --git a/app/src/fdroid/java/com/geeksville/mesh/android/GeeksvilleApplication.kt b/app/src/fdroid/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
index 936f1d379..8117c46c0 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
+++ b/app/src/fdroid/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
@@ -29,7 +29,7 @@ import com.geeksville.mesh.analytics.NopAnalytics
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.info
import com.geeksville.mesh.android.prefs.AnalyticsPrefs
-import com.geeksville.mesh.model.DeviceHardware
+import org.meshtastic.core.model.DeviceHardware
import timber.log.Timber
abstract class GeeksvilleApplication :
diff --git a/app/src/google/java/com/geeksville/mesh/android/GeeksvilleApplication.kt b/app/src/google/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
index d66d03597..110be10b6 100644
--- a/app/src/google/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
+++ b/app/src/google/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
@@ -49,7 +49,6 @@ import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.analytics.AnalyticsProvider
import com.geeksville.mesh.analytics.FirebaseAnalytics
import com.geeksville.mesh.android.prefs.AnalyticsPrefs
-import com.geeksville.mesh.model.DeviceHardware
import com.geeksville.mesh.util.exceptionReporter
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailabilityLight
@@ -60,6 +59,7 @@ import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.initialize
import com.suddenh4x.ratingdialog.AppRating
import io.opentelemetry.api.GlobalOpenTelemetry
+import org.meshtastic.core.model.DeviceHardware
import timber.log.Timber
abstract class GeeksvilleApplication :
diff --git a/app/src/main/java/com/geeksville/mesh/database/entity/DeviceHardwareEntity.kt b/app/src/main/java/com/geeksville/mesh/database/entity/DeviceHardwareEntity.kt
index ae717888f..ccaaff4a8 100644
--- a/app/src/main/java/com/geeksville/mesh/database/entity/DeviceHardwareEntity.kt
+++ b/app/src/main/java/com/geeksville/mesh/database/entity/DeviceHardwareEntity.kt
@@ -20,8 +20,8 @@ package com.geeksville.mesh.database.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
-import com.geeksville.mesh.model.DeviceHardware
import kotlinx.serialization.Serializable
+import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.network.model.NetworkDeviceHardware
@Serializable
diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt
deleted file mode 100644
index ae4235ec0..000000000
--- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (c) 2025 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.geeksville.mesh.model
-
-import com.geeksville.mesh.ChannelProtos
-import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset
-import com.geeksville.mesh.ConfigKt.loRaConfig
-import com.geeksville.mesh.ConfigProtos
-import com.geeksville.mesh.channelSettings
-import com.google.protobuf.ByteString
-import java.security.SecureRandom
-
-/** Utility function to make it easy to declare byte arrays - FIXME move someplace better */
-fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
-fun xorHash(b: ByteArray) = b.fold(0) { acc, x -> acc xor (x.toInt() and 0xff) }
-
-data class Channel(
- val settings: ChannelProtos.ChannelSettings = default.settings,
- val loraConfig: ConfigProtos.Config.LoRaConfig = default.loraConfig,
-) {
- companion object {
- // These bytes must match the well known and not secret bytes used the default channel AES128 key device code
- private val channelDefaultKey = byteArrayOfInts(
- 0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
- 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01
- )
-
- private val cleartextPSK = ByteString.EMPTY
- private val defaultPSK =
- byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK
-
- // The default channel that devices ship with
- val default = Channel(
- channelSettings { psk = ByteString.copyFrom(defaultPSK) },
- // references: NodeDB::installDefaultConfig / Channels::initDefaultChannel
- loRaConfig {
- usePreset = true
- modemPreset = ModemPreset.LONG_FAST
- hopLimit = 3
- txEnabled = true
- }
- )
-
- fun getRandomKey(size: Int = 32): ByteString {
- val bytes = ByteArray(size)
- val random = SecureRandom()
- random.nextBytes(bytes)
- return ByteString.copyFrom(bytes)
- }
- }
-
- // Return the name of our channel as a human readable string. If empty string, assume "Default" per mesh.proto spec
- val name: String
- get() = settings.name.ifEmpty {
- // We have a new style 'empty' channel name. Use the same logic from the device to convert that to a human readable name
- if (loraConfig.usePreset) when (loraConfig.modemPreset) {
- ModemPreset.SHORT_TURBO -> "ShortTurbo"
- ModemPreset.SHORT_FAST -> "ShortFast"
- ModemPreset.SHORT_SLOW -> "ShortSlow"
- ModemPreset.MEDIUM_FAST -> "MediumFast"
- ModemPreset.MEDIUM_SLOW -> "MediumSlow"
- ModemPreset.LONG_FAST -> "LongFast"
- ModemPreset.LONG_SLOW -> "LongSlow"
- ModemPreset.LONG_MODERATE -> "LongMod"
- ModemPreset.VERY_LONG_SLOW -> "VLongSlow"
- else -> "Invalid"
- } else "Custom"
- }
-
- val psk: ByteString
- get() = if (settings.psk.size() != 1) {
- settings.psk // A standard PSK
- } else {
- // One of our special 1 byte PSKs, see mesh.proto for docs.
- val pskIndex = settings.psk.byteAt(0).toInt()
-
- if (pskIndex == 0) {
- cleartextPSK
- } else {
- // Treat an index of 1 as the old channelDefaultKey and work up from there
- val bytes = channelDefaultKey.clone()
- bytes[bytes.size - 1] = (0xff and (bytes[bytes.size - 1] + pskIndex - 1)).toByte()
- ByteString.copyFrom(bytes)
- }
- }
-
- /**
- * Given a channel name and psk, return the (0 to 255) hash for that channel
- */
- val hash: Int get() = xorHash(name.toByteArray()) xor xorHash(psk.toByteArray())
-
- val channelNum: Int get() = loraConfig.channelNum(name)
-
- val radioFreq: Float get() = loraConfig.radioFreq(channelNum)
-
- override fun equals(other: Any?): Boolean = (other is Channel)
- && psk.toByteArray() contentEquals other.psk.toByteArray()
- && name == other.name
-
- override fun hashCode(): Int {
- var result = settings.hashCode()
- result = 31 * result + loraConfig.hashCode()
- return result
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
index 06d769199..56e964624 100644
--- a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt
@@ -25,6 +25,7 @@ import com.geeksville.mesh.android.BuildUtils.errormsg
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
import com.journeyapps.barcodescanner.BarcodeEncoder
+import org.meshtastic.core.model.Channel
import java.net.MalformedURLException
import kotlin.jvm.Throws
diff --git a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
index 4dd963354..03db586a0 100644
--- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
@@ -62,6 +62,7 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.navigation.NodesRoutes
import java.io.BufferedWriter
import java.io.FileNotFoundException
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
index 3d11b4246..b5c908b9d 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -74,7 +74,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@@ -84,6 +83,7 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.meshtastic.core.model.DeviceHardware
import javax.inject.Inject
// Given a human name, strip out the first letter of the first three words and return that as the
diff --git a/app/src/main/java/com/geeksville/mesh/repository/api/DeviceHardwareRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/api/DeviceHardwareRepository.kt
index c64964dfc..f8104f565 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/api/DeviceHardwareRepository.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/api/DeviceHardwareRepository.kt
@@ -21,9 +21,9 @@ import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.warn
import com.geeksville.mesh.database.entity.DeviceHardwareEntity
import com.geeksville.mesh.database.entity.asExternalModel
-import com.geeksville.mesh.model.DeviceHardware
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
import java.util.concurrent.TimeUnit
import javax.inject.Inject
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt
index 29c05a1df..a4b19fcf2 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt
@@ -32,13 +32,13 @@ import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.config
import com.geeksville.mesh.deviceMetadata
import com.geeksville.mesh.fromRadio
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.queueStatus
import com.google.protobuf.ByteString
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
+import org.meshtastic.core.model.Channel
import kotlin.random.Random
private val defaultLoRaConfig =
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index b5bcb3f40..1e1e7b5d8 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -74,7 +74,6 @@ import com.geeksville.mesh.fromRadio
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.NO_DEVICE_SELECTED
import com.geeksville.mesh.model.Node
-import com.geeksville.mesh.model.getFullTracerouteResponse
import com.geeksville.mesh.position
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
import com.geeksville.mesh.repository.location.LocationRepository
@@ -104,6 +103,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import org.meshtastic.core.model.getFullTracerouteResponse
import java.util.Random
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt
index 64e3f27a9..8ae1da409 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/EditBase64Preference.kt
@@ -44,10 +44,10 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.R
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.util.encodeToString
import com.geeksville.mesh.util.toByteString
import com.google.protobuf.ByteString
+import org.meshtastic.core.model.Channel
@Suppress("LongMethod")
@Composable
@@ -62,7 +62,6 @@ fun EditBase64Preference(
onGenerateKey: (() -> Unit)? = null,
trailingIcon: (@Composable () -> Unit)? = null,
) {
-
var valueState by remember { mutableStateOf(value.encodeToString()) }
val isError = value.encodeToString() != valueState
@@ -74,11 +73,12 @@ fun EditBase64Preference(
}
}
- val (icon, description) = when {
- isError -> Icons.TwoTone.Close to stringResource(R.string.error)
- onGenerateKey != null && !isFocused -> Icons.TwoTone.Refresh to stringResource(R.string.reset)
- else -> null to null
- }
+ val (icon, description) =
+ when {
+ isError -> Icons.TwoTone.Close to stringResource(R.string.error)
+ onGenerateKey != null && !isFocused -> Icons.TwoTone.Refresh to stringResource(R.string.reset)
+ else -> null to null
+ }
OutlinedTextField(
value = valueState,
@@ -86,16 +86,13 @@ fun EditBase64Preference(
valueState = it
runCatching { it.toByteString() }.onSuccess(onValueChange)
},
- modifier = modifier
- .fillMaxWidth()
- .onFocusChanged { focusState -> isFocused = focusState.isFocused },
+ modifier = modifier.fillMaxWidth().onFocusChanged { focusState -> isFocused = focusState.isFocused },
enabled = enabled,
readOnly = readOnly,
label = { Text(text = title) },
isError = isError,
- keyboardOptions = KeyboardOptions.Default.copy(
- keyboardType = KeyboardType.Password, imeAction = ImeAction.Done
- ),
+ keyboardOptions =
+ KeyboardOptions.Default.copy(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = keyboardActions,
trailingIcon = {
if (icon != null) {
@@ -113,11 +110,12 @@ fun EditBase64Preference(
Icon(
imageVector = icon,
contentDescription = description,
- tint = if (isError) {
+ tint =
+ if (isError) {
MaterialTheme.colorScheme.error
} else {
LocalContentColor.current
- }
+ },
)
}
} else if (trailingIcon != null) {
@@ -137,6 +135,6 @@ private fun EditBase64PreferencePreview() {
keyboardActions = KeyboardActions {},
onValueChange = {},
onGenerateKey = {},
- modifier = Modifier.padding(16.dp)
+ modifier = Modifier.padding(16.dp),
)
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt
index 679dcdc58..940c750d4 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/ScannedQrCodeDialog.kt
@@ -55,9 +55,9 @@ import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset
import com.geeksville.mesh.R
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
+import org.meshtastic.core.model.Channel
@Composable
fun ScannedQrCodeDialog(viewModel: UIViewModel, incoming: ChannelSet) {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt
index e94588f12..adee808bf 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/common/components/SecurityIcon.kt
@@ -64,11 +64,11 @@ import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.R
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.getChannel
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
+import org.meshtastic.core.model.Channel
private const val PRECISE_POSITION_BITS = 32
diff --git a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
index 99a149c81..cb9f1dbc8 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/metrics/TracerouteLog.kt
@@ -59,11 +59,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.model.MetricsViewModel
-import com.geeksville.mesh.model.fullRouteDiscovery
-import com.geeksville.mesh.model.getTracerouteResponse
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.metrics.CommonCharts.MS_PER_SEC
+import org.meshtastic.core.model.fullRouteDiscovery
+import org.meshtastic.core.model.getTracerouteResponse
import java.text.DateFormat
@OptIn(ExperimentalFoundationApi::class)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
index a4e23dcac..558cb9fdd 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetail.kt
@@ -127,7 +127,6 @@ import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.FirmwareRelease
import com.geeksville.mesh.database.entity.asDeviceVersion
-import com.geeksville.mesh.model.DeviceHardware
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.MetricsState
import com.geeksville.mesh.model.MetricsViewModel
@@ -159,6 +158,7 @@ import com.geeksville.mesh.util.toDistanceString
import com.geeksville.mesh.util.toSmallDistanceString
import com.geeksville.mesh.util.toSpeedString
import com.mikepenz.markdown.m3.Markdown
+import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt
index 3f8e2435c..557cc4c6f 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/components/NodeKeyStatusIcon.kt
@@ -55,13 +55,13 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.geeksville.mesh.R
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.common.components.CopyIconButton
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusGreen
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusYellow
import com.google.protobuf.ByteString
+import org.meshtastic.core.model.Channel
@Composable
private fun KeyStatusDialog(@StringRes title: Int, @StringRes text: Int, key: ByteString?, onDismiss: () -> Unit = {}) =
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt
index 09c3b4824..e82faed7b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/ChannelSettingsItemList.kt
@@ -72,7 +72,6 @@ import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.channelSettings
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.components.PreferenceFooter
@@ -81,6 +80,7 @@ import com.geeksville.mesh.ui.common.components.dragContainer
import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.common.components.rememberDragDropState
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
+import org.meshtastic.core.model.Channel
@Composable
private fun ChannelItem(
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/EditChannelDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/EditChannelDialog.kt
index 85df04701..2eb0941af 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/EditChannelDialog.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/EditChannelDialog.kt
@@ -46,11 +46,11 @@ import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.copy
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.ui.common.components.EditBase64Preference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PositionPrecisionPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
+import org.meshtastic.core.model.Channel
@Suppress("LongMethod")
@Composable
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
index ccc52aa38..e2d4b80ac 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/components/LoRaConfigItemList.kt
@@ -38,9 +38,6 @@ import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.R
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
-import com.geeksville.mesh.model.Channel
-import com.geeksville.mesh.model.RegionInfo
-import com.geeksville.mesh.model.numChannels
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.EditTextPreference
import com.geeksville.mesh.ui.common.components.PreferenceCategory
@@ -48,6 +45,9 @@ import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.SignedIntegerEditTextPreference
import com.geeksville.mesh.ui.common.components.SwitchPreference
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
+import org.meshtastic.core.model.Channel
+import org.meshtastic.core.model.RegionInfo
+import org.meshtastic.core.model.numChannels
@Composable
fun LoRaConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel()) {
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
index 86cbd0287..7be388b08 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/Channel.kt
@@ -96,7 +96,6 @@ import com.geeksville.mesh.android.BuildUtils.errormsg
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy
-import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getChannelUrl
import com.geeksville.mesh.model.qrCode
@@ -115,6 +114,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions
import kotlinx.coroutines.launch
+import org.meshtastic.core.model.Channel
import org.meshtastic.core.navigation.Route
/**
diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts
index 8047f460d..45c560556 100644
--- a/core/model/build.gradle.kts
+++ b/core/model/build.gradle.kts
@@ -16,10 +16,11 @@
*/
plugins {
- alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.kover)
+ alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.kotlinx.serialization)
}
android { namespace = "org.meshtastic.core.model" }
-dependencies {}
+dependencies { implementation(projects.core.proto) }
diff --git a/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt
new file mode 100644
index 000000000..31c976173
--- /dev/null
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/Channel.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2025 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.meshtastic.core.model
+
+import com.geeksville.mesh.ChannelProtos
+import com.geeksville.mesh.ConfigKt.loRaConfig
+import com.geeksville.mesh.ConfigProtos
+import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset
+import com.geeksville.mesh.channelSettings
+import com.google.protobuf.ByteString
+import java.security.SecureRandom
+
+/** Utility function to make it easy to declare byte arrays - FIXME move someplace better */
+fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
+
+fun xorHash(b: ByteArray) = b.fold(0) { acc, x -> acc xor (x.toInt() and 0xff) }
+
+data class Channel(
+ val settings: ChannelProtos.ChannelSettings = default.settings,
+ val loraConfig: ConfigProtos.Config.LoRaConfig = default.loraConfig,
+) {
+ companion object {
+ // These bytes must match the well known and not secret bytes used the default channel AES128 key device code
+ private val channelDefaultKey =
+ byteArrayOfInts(
+ 0xd4,
+ 0xf1,
+ 0xbb,
+ 0x3a,
+ 0x20,
+ 0x29,
+ 0x07,
+ 0x59,
+ 0xf0,
+ 0xbc,
+ 0xff,
+ 0xab,
+ 0xcf,
+ 0x4e,
+ 0x69,
+ 0x01,
+ )
+
+ private val cleartextPSK = ByteString.EMPTY
+ private val defaultPSK = byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK
+
+ // The default channel that devices ship with
+ val default =
+ Channel(
+ channelSettings { psk = ByteString.copyFrom(defaultPSK) },
+ // references: NodeDB::installDefaultConfig / Channels::initDefaultChannel
+ loRaConfig {
+ usePreset = true
+ modemPreset = ModemPreset.LONG_FAST
+ hopLimit = 3
+ txEnabled = true
+ },
+ )
+
+ fun getRandomKey(size: Int = 32): ByteString {
+ val bytes = ByteArray(size)
+ val random = SecureRandom()
+ random.nextBytes(bytes)
+ return ByteString.copyFrom(bytes)
+ }
+ }
+
+ // Return the name of our channel as a human readable string. If empty string, assume "Default" per mesh.proto spec
+ val name: String
+ get() =
+ settings.name.ifEmpty {
+ // We have a new style 'empty' channel name. Use the same logic from the device to convert that to a
+ // human readable name
+ if (loraConfig.usePreset) {
+ when (loraConfig.modemPreset) {
+ ModemPreset.SHORT_TURBO -> "ShortTurbo"
+ ModemPreset.SHORT_FAST -> "ShortFast"
+ ModemPreset.SHORT_SLOW -> "ShortSlow"
+ ModemPreset.MEDIUM_FAST -> "MediumFast"
+ ModemPreset.MEDIUM_SLOW -> "MediumSlow"
+ ModemPreset.LONG_FAST -> "LongFast"
+ ModemPreset.LONG_SLOW -> "LongSlow"
+ ModemPreset.LONG_MODERATE -> "LongMod"
+ ModemPreset.VERY_LONG_SLOW -> "VLongSlow"
+ else -> "Invalid"
+ }
+ } else {
+ "Custom"
+ }
+ }
+
+ val psk: ByteString
+ get() =
+ if (settings.psk.size() != 1) {
+ settings.psk // A standard PSK
+ } else {
+ // One of our special 1 byte PSKs, see mesh.proto for docs.
+ val pskIndex = settings.psk.byteAt(0).toInt()
+
+ if (pskIndex == 0) {
+ cleartextPSK
+ } else {
+ // Treat an index of 1 as the old channelDefaultKey and work up from there
+ val bytes = channelDefaultKey.clone()
+ bytes[bytes.size - 1] = (0xff and (bytes[bytes.size - 1] + pskIndex - 1)).toByte()
+ ByteString.copyFrom(bytes)
+ }
+ }
+
+ /** Given a channel name and psk, return the (0 to 255) hash for that channel */
+ val hash: Int
+ get() = xorHash(name.toByteArray()) xor xorHash(psk.toByteArray())
+
+ val channelNum: Int
+ get() = loraConfig.channelNum(name)
+
+ val radioFreq: Float
+ get() = loraConfig.radioFreq(channelNum)
+
+ override fun equals(other: Any?): Boolean =
+ (other is Channel) && psk.toByteArray() contentEquals other.psk.toByteArray() && name == other.name
+
+ override fun hashCode(): Int {
+ var result = settings.hashCode()
+ result = 31 * result + loraConfig.hashCode()
+ return result
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
similarity index 99%
rename from app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
rename to core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
index ffccb9dd8..10e345f17 100644
--- a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/ChannelOption.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.model
+package org.meshtastic.core.model
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset
diff --git a/app/src/main/java/com/geeksville/mesh/model/DeviceHardware.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceHardware.kt
similarity index 94%
rename from app/src/main/java/com/geeksville/mesh/model/DeviceHardware.kt
rename to core/model/src/main/kotlin/org/meshtastic/core/model/DeviceHardware.kt
index b0524e280..31dec4fa5 100644
--- a/app/src/main/java/com/geeksville/mesh/model/DeviceHardware.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/DeviceHardware.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.model
+package org.meshtastic.core.model
import kotlinx.serialization.Serializable
@@ -33,5 +33,5 @@ data class DeviceHardware(
val platformioTarget: String = "",
val requiresDfu: Boolean? = null,
val supportLevel: Int? = null,
- val tags: List? = null
+ val tags: List? = null,
)
diff --git a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt b/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt
similarity index 99%
rename from app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt
rename to core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt
index 59a74881a..7a56fecbd 100644
--- a/app/src/main/java/com/geeksville/mesh/model/RouteDiscovery.kt
+++ b/core/model/src/main/kotlin/org/meshtastic/core/model/RouteDiscovery.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.model
+package org.meshtastic.core.model
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.RouteDiscovery