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