From db2ef75e0821e399247b07e04aa16a9f2ec0330e Mon Sep 17 00:00:00 2001
From: Phil Oliver <3497406+poliver@users.noreply.github.com>
Date: Tue, 30 Sep 2025 16:55:56 -0400
Subject: [PATCH] Add `:core:service` (#3253)
---
app/build.gradle.kts | 2 +-
.../geeksville/mesh/ui/map/MapViewModel.kt | 2 +-
.../geeksville/mesh/ui/map/MapViewModel.kt | 2 +-
.../com/geeksville/mesh/MeshServiceClient.kt | 3 +-
.../com/geeksville/mesh/model/BTScanModel.kt | 2 +-
.../geeksville/mesh/model/MetricsViewModel.kt | 4 +-
.../java/com/geeksville/mesh/model/UIState.kt | 4 +-
.../repository/radio/RadioInterfaceService.kt | 2 +-
.../geeksville/mesh/service/MeshService.kt | 17 +-
.../mesh/service/MeshServiceBroadcasts.kt | 1 +
.../MeshServiceConnectionStateHolder.kt | 1 +
.../geeksville/mesh/service/PacketHandler.kt | 1 +
.../geeksville/mesh/service/ReplyReceiver.kt | 1 +
.../main/java/com/geeksville/mesh/ui/Main.kt | 2 +-
.../mesh/ui/connections/ConnectionsScreen.kt | 2 +-
.../ui/connections/ConnectionsViewModel.kt | 2 +-
.../ui/connections/components/BLEDevices.kt | 2 +-
.../connections/components/NetworkDevices.kt | 2 +-
.../connections/components/TopLevelNavIcon.kt | 2 +-
.../ui/connections/components/UsbDevices.kt | 2 +-
.../mesh/ui/contact/ContactsViewModel.kt | 2 +-
.../mesh/ui/map/BaseMapViewModel.kt | 2 +-
.../mesh/ui/message/MessageViewModel.kt | 4 +-
.../com/geeksville/mesh/ui/node/NodeDetail.kt | 2 +-
.../mesh/ui/node/NodeDetailViewModel.kt | 4 +-
.../com/geeksville/mesh/ui/node/NodeScreen.kt | 2 +-
.../geeksville/mesh/ui/node/NodesViewModel.kt | 4 +-
.../mesh/ui/settings/SettingsViewModel.kt | 4 +-
.../radio/CleanNodeDatabaseViewModel.kt | 2 +-
.../ui/settings/radio/RadioConfigViewModel.kt | 6 +-
.../com/geeksville/mesh/ui/sharing/Channel.kt | 2 +-
core/service/build.gradle.kts | 34 +++
.../core/service}/IMeshService.aidl | 3 +-
.../core}/service/ConnectionState.kt | 2 +-
.../meshtastic/core/service/ServiceAction.kt | 33 ++
.../core}/service/ServiceRepository.kt | 11 +-
mesh_service_example/README.md | 2 +-
mesh_service_example/build.gradle.kts | 3 +-
.../com/geeksville/mesh/IMeshService.aidl | 171 -----------
.../kotlin/com/geeksville/mesh/DataPacket.kt | 209 -------------
.../kotlin/com/geeksville/mesh/MyNodeInfo.kt | 44 ---
.../kotlin/com/geeksville/mesh/NodeInfo.kt | 232 --------------
.../com/geeksville/mesh/util/Extensions.kt | 70 -----
.../com/geeksville/mesh/util/LocationUtils.kt | 289 ------------------
.../meshserviceexample/MainActivity.kt | 8 +-
settings.gradle.kts | 1 +
46 files changed, 125 insertions(+), 1077 deletions(-)
create mode 100644 core/service/build.gradle.kts
rename {app/src/main/aidl/com/geeksville/mesh => core/service/src/main/aidl/org/meshtastic/core/service}/IMeshService.aidl (99%)
rename {app/src/main/java/com/geeksville/mesh => core/service/src/main/kotlin/org/meshtastic/core}/service/ConnectionState.kt (96%)
create mode 100644 core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt
rename {app/src/main/java/com/geeksville/mesh => core/service/src/main/kotlin/org/meshtastic/core}/service/ServiceRepository.kt (93%)
delete mode 100644 mesh_service_example/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
delete mode 100644 mesh_service_example/src/main/kotlin/com/geeksville/mesh/DataPacket.kt
delete mode 100644 mesh_service_example/src/main/kotlin/com/geeksville/mesh/MyNodeInfo.kt
delete mode 100644 mesh_service_example/src/main/kotlin/com/geeksville/mesh/NodeInfo.kt
delete mode 100644 mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/Extensions.kt
delete mode 100644 mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/LocationUtils.kt
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e6d175d84..282939bd3 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -155,7 +155,6 @@ android {
}
}
bundle { language { enableSplit = false } }
- buildFeatures { aidl = true }
}
secrets {
@@ -189,6 +188,7 @@ dependencies {
implementation(projects.core.network)
implementation(projects.core.prefs)
implementation(projects.core.proto)
+ implementation(projects.core.service)
implementation(projects.core.strings)
implementation(projects.core.ui)
implementation(projects.feature.map)
diff --git a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt
index 1d9bcfdc2..0120ddf54 100644
--- a/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt
+++ b/app/src/fdroid/java/com/geeksville/mesh/ui/map/MapViewModel.kt
@@ -19,7 +19,6 @@ package com.geeksville.mesh.ui.map
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
-import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
@@ -28,6 +27,7 @@ import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.prefs.map.MapPrefs
+import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
@HiltViewModel
diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
index a77723b48..d9be78a2b 100644
--- a/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
+++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapViewModel.kt
@@ -23,7 +23,6 @@ import androidx.core.net.toFile
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.android.BuildUtils.debug
-import com.geeksville.mesh.service.ServiceRepository
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.TileProvider
import com.google.android.gms.maps.model.UrlTileProvider
@@ -55,6 +54,7 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.prefs.map.GoogleMapsPrefs
import org.meshtastic.core.prefs.map.MapPrefs
+import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
diff --git a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
index 88e68729c..e444b1c1a 100644
--- a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
+++ b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
@@ -27,10 +27,11 @@ import com.geeksville.mesh.android.BindFailedException
import com.geeksville.mesh.android.ServiceClient
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.service.MeshService
-import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.service.startService
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.Job
+import org.meshtastic.core.service.IMeshService
+import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt
index 6f0a3ef83..0c937263b 100644
--- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt
@@ -34,7 +34,6 @@ import com.geeksville.mesh.repository.radio.InterfaceId
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.service.MeshService
-import com.geeksville.mesh.service.ServiceRepository
import com.hoho.android.usbserial.driver.UsbSerialDriver
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
@@ -52,6 +51,7 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
import org.meshtastic.core.model.util.anonymize
+import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import javax.inject.Inject
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 21a39e21b..f31196933 100644
--- a/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/MetricsViewModel.kt
@@ -35,8 +35,6 @@ import com.geeksville.mesh.Portnums
import com.geeksville.mesh.Portnums.PortNum
import com.geeksville.mesh.TelemetryProtos.Telemetry
import com.geeksville.mesh.android.Logging
-import com.geeksville.mesh.service.ServiceAction
-import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.util.safeNumber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -65,6 +63,8 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.prefs.map.MapPrefs
+import org.meshtastic.core.service.ServiceAction
+import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import org.meshtastic.feature.map.model.CustomTileSource
import java.io.BufferedWriter
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 b28a5af32..baec8f3ae 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt
@@ -32,7 +32,6 @@ import com.geeksville.mesh.AppOnlyProtos
import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ChannelProtos.ChannelSettings
import com.geeksville.mesh.ConfigProtos.Config
-import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos
@@ -45,7 +44,6 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshServiceNotifications
-import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.util.safeNumber
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -75,6 +73,8 @@ import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.util.toChannelSet
+import org.meshtastic.core.service.IMeshService
+import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import javax.inject.Inject
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
index 34b622cd1..428f2e05e 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
@@ -30,7 +30,6 @@ import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.network.NetworkRepository
-import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.util.ignoreException
import com.geeksville.mesh.util.toRemoteExceptions
import kotlinx.coroutines.CoroutineScope
@@ -49,6 +48,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.prefs.radio.RadioPrefs
+import org.meshtastic.core.service.ConnectionState
import javax.inject.Inject
import javax.inject.Singleton
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 046a38fe1..b94d4afa5 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -34,7 +34,6 @@ import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.DeviceUIProtos
-import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos
@@ -103,6 +102,10 @@ import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
import org.meshtastic.core.prefs.mesh.MeshPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
+import org.meshtastic.core.service.ConnectionState
+import org.meshtastic.core.service.IMeshService
+import org.meshtastic.core.service.ServiceAction
+import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import timber.log.Timber
import java.util.Random
@@ -111,18 +114,6 @@ import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
import kotlin.math.absoluteValue
-sealed class ServiceAction {
- data class GetDeviceMetadata(val destNum: Int) : ServiceAction()
-
- data class Favorite(val node: Node) : ServiceAction()
-
- data class Ignore(val node: Node) : ServiceAction()
-
- data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
-
- data class AddSharedContact(val contact: AdminProtos.SharedContact) : ServiceAction()
-}
-
/**
* Handles all the communication with android apps. Also keeps an internal model of the network state.
*
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
index d36a6f9ce..94fcca450 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
@@ -24,6 +24,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.NodeInfo
+import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
import javax.inject.Singleton
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceConnectionStateHolder.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceConnectionStateHolder.kt
index 40c6f99de..3be8f7594 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceConnectionStateHolder.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceConnectionStateHolder.kt
@@ -17,6 +17,7 @@
package com.geeksville.mesh.service
+import org.meshtastic.core.service.ConnectionState
import javax.inject.Inject
import javax.inject.Singleton
diff --git a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
index f29b674b5..7b3338601 100644
--- a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
@@ -40,6 +40,7 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
+import org.meshtastic.core.service.ConnectionState
import java.util.UUID
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit
diff --git a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
index 7b8d4f1d5..257405509 100644
--- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
@@ -22,6 +22,7 @@ import androidx.core.app.RemoteInput
import dagger.hilt.android.AndroidEntryPoint
import jakarta.inject.Inject
import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.service.ServiceRepository
/**
* A [BroadcastReceiver] that handles inline replies from notifications.
diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
index eb466ea77..92ccbf6b8 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -88,7 +88,6 @@ import com.geeksville.mesh.navigation.mapGraph
import com.geeksville.mesh.navigation.nodesGraph
import com.geeksville.mesh.navigation.settingsGraph
import com.geeksville.mesh.repository.radio.MeshActivity
-import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
@@ -108,6 +107,7 @@ import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.MultipleChoiceAlertDialog
import org.meshtastic.core.ui.component.SimpleAlertDialog
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
index 6f011199a..6d14e3b2b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsScreen.kt
@@ -64,7 +64,6 @@ import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.getNavRouteFrom
-import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.connections.components.BLEDevices
import com.geeksville.mesh.ui.connections.components.ConnectionsSegmentedBar
@@ -78,6 +77,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import kotlinx.coroutines.delay
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt
index b1866546d..c748a68ed 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt
@@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
-import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -33,6 +32,7 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.prefs.ui.UiPrefs
+import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
@HiltViewModel
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt
index a7a921b69..38092eed9 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/BLEDevices.kt
@@ -48,10 +48,10 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
-import com.geeksville.mesh.service.ConnectionState
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
import com.google.accompanist.permissions.rememberMultiplePermissionsState
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt
index 394613e72..6575038c2 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/NetworkDevices.kt
@@ -54,9 +54,9 @@ import androidx.compose.ui.unit.dp
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.repository.network.NetworkRepository
-import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.connections.isIPAddress
import kotlinx.coroutines.launch
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/TopLevelNavIcon.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/TopLevelNavIcon.kt
index a7f17cf5e..3d298a991 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/TopLevelNavIcon.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/TopLevelNavIcon.kt
@@ -36,9 +36,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.TopLevelDestination
import com.geeksville.mesh.ui.connections.DeviceType
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.ui.icon.Device
import org.meshtastic.core.ui.icon.MeshtasticIcons
import org.meshtastic.core.ui.icon.NoDevice
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt
index 2a1ba8d24..d95768cf2 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/UsbDevices.kt
@@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.geeksville.mesh.model.BTScanModel
import com.geeksville.mesh.model.DeviceListEntry
-import com.geeksville.mesh.service.ConnectionState
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt
index abc01c5eb..04f0aa330 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/contact/ContactsViewModel.kt
@@ -22,7 +22,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.model.Contact
-import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
@@ -37,6 +36,7 @@ import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.getChannel
import org.meshtastic.core.model.util.getShortDate
+import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import javax.inject.Inject
import kotlin.collections.map
diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt
index 79884ecc2..c7a37721b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/map/BaseMapViewModel.kt
@@ -22,7 +22,6 @@ import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.service.ServiceRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -38,6 +37,7 @@ import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.prefs.map.MapPrefs
+import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import timber.log.Timber
import java.util.concurrent.TimeUnit
diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/MessageViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/message/MessageViewModel.kt
index a039b18e2..2da36a195 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/message/MessageViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/message/MessageViewModel.kt
@@ -22,8 +22,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.channelSet
import com.geeksville.mesh.service.MeshServiceNotifications
-import com.geeksville.mesh.service.ServiceAction
-import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -43,6 +41,8 @@ import org.meshtastic.core.database.model.Message
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.prefs.ui.UiPrefs
+import org.meshtastic.core.service.ServiceAction
+import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
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 864822a69..39e10bc54 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
@@ -133,7 +133,6 @@ import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.model.MetricsState
import com.geeksville.mesh.model.MetricsViewModel
-import com.geeksville.mesh.service.ServiceAction
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
@@ -162,6 +161,7 @@ import org.meshtastic.core.model.util.toSpeedString
import org.meshtastic.core.navigation.NodeDetailRoutes
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoutes
+import org.meshtastic.core.service.ServiceAction
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.TitledCard
import org.meshtastic.core.ui.theme.AppTheme
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailViewModel.kt
index 420530427..26354439d 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeDetailViewModel.kt
@@ -20,8 +20,6 @@ package com.geeksville.mesh.ui.node
import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.geeksville.mesh.service.ServiceAction
-import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@@ -32,6 +30,8 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.Position
+import org.meshtastic.core.service.ServiceAction
+import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt
index 31278f486..a31b73de3 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt
@@ -60,7 +60,6 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AdminProtos
-import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.node.components.NodeActionDialogs
import com.geeksville.mesh.ui.node.components.NodeFilterTextField
@@ -69,6 +68,7 @@ import com.geeksville.mesh.ui.sharing.AddContactFAB
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.DeviceVersion
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.rememberTimeTickWithLifecycle
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt
index ff0b77fee..5e63de80a 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodesViewModel.kt
@@ -21,8 +21,6 @@ import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.geeksville.mesh.AdminProtos
-import com.geeksville.mesh.service.ServiceAction
-import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -40,6 +38,8 @@ import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.database.model.Node
import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.datastore.UiPreferencesDataSource
+import org.meshtastic.core.service.ServiceAction
+import org.meshtastic.core.service.ServiceRepository
import timber.log.Timber
import javax.inject.Inject
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt
index 5a79ba2e1..46cb9dcf0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/SettingsViewModel.kt
@@ -21,12 +21,10 @@ import android.app.Application
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.Portnums
import com.geeksville.mesh.android.Logging
-import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@@ -50,6 +48,8 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.util.positionToMeter
import org.meshtastic.core.prefs.ui.UiPrefs
+import org.meshtastic.core.service.IMeshService
+import org.meshtastic.core.service.ServiceRepository
import java.io.BufferedWriter
import java.io.FileNotFoundException
import java.io.FileWriter
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseViewModel.kt
index 36ea257a1..54adbef68 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/CleanNodeDatabaseViewModel.kt
@@ -19,13 +19,13 @@ package com.geeksville.mesh.ui.settings.radio
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.geeksville.mesh.service.ServiceRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.entity.NodeEntity
+import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
diff --git a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt
index 243c56e35..f60fff56c 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/settings/radio/RadioConfigViewModel.kt
@@ -37,7 +37,6 @@ import com.geeksville.mesh.ChannelProtos
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.ConfigProtos.Config.SecurityConfig
-import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.Portnums
@@ -51,8 +50,6 @@ import com.geeksville.mesh.moduleConfig
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
import com.geeksville.mesh.repository.location.LocationRepository
-import com.geeksville.mesh.service.ConnectionState
-import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.util.UiText
import com.google.protobuf.MessageLite
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -79,6 +76,9 @@ import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import org.meshtastic.core.prefs.map.MapConsentPrefs
+import org.meshtastic.core.service.ConnectionState
+import org.meshtastic.core.service.IMeshService
+import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.core.strings.R
import java.io.FileOutputStream
import javax.inject.Inject
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 28709d62e..29e3f553b 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
@@ -98,7 +98,6 @@ import com.geeksville.mesh.copy
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.getNavRouteFrom
-import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.ChannelSelection
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
@@ -113,6 +112,7 @@ import org.meshtastic.core.model.util.getChannelUrl
import org.meshtastic.core.model.util.qrCode
import org.meshtastic.core.model.util.toChannelSet
import org.meshtastic.core.navigation.Route
+import org.meshtastic.core.service.ConnectionState
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.AdaptiveTwoPane
import org.meshtastic.core.ui.component.PreferenceFooter
diff --git a/core/service/build.gradle.kts b/core/service/build.gradle.kts
new file mode 100644
index 000000000..df5d77452
--- /dev/null
+++ b/core/service/build.gradle.kts
@@ -0,0 +1,34 @@
+/*
+ * 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 .
+ */
+
+plugins {
+ alias(libs.plugins.meshtastic.android.library)
+ alias(libs.plugins.meshtastic.hilt)
+}
+
+android {
+ buildFeatures { aidl = true }
+ namespace = "org.meshtastic.core.service"
+}
+
+dependencies {
+ implementation(projects.core.database)
+ implementation(projects.core.model)
+ implementation(projects.core.proto)
+ implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.timber)
+}
diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
similarity index 99%
rename from app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
rename to core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
index bf118a33c..13ab9cef7 100644
--- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
+++ b/core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl
@@ -1,5 +1,4 @@
-// com.geeksville.mesh.IMeshService.aidl
-package com.geeksville.mesh;
+package org.meshtastic.core.service;
// Declare any non-default types here with import statements
import org.meshtastic.core.model.DataPacket;
diff --git a/app/src/main/java/com/geeksville/mesh/service/ConnectionState.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ConnectionState.kt
similarity index 96%
rename from app/src/main/java/com/geeksville/mesh/service/ConnectionState.kt
rename to core/service/src/main/kotlin/org/meshtastic/core/service/ConnectionState.kt
index 003f83806..394c760da 100644
--- a/app/src/main/java/com/geeksville/mesh/service/ConnectionState.kt
+++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ConnectionState.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.service
enum class ConnectionState {
/** We are disconnected from the device, and we should be trying to reconnect. */
diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt
new file mode 100644
index 000000000..07031a7f5
--- /dev/null
+++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceAction.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.service
+
+import com.geeksville.mesh.AdminProtos
+import org.meshtastic.core.database.model.Node
+
+sealed class ServiceAction {
+ data class GetDeviceMetadata(val destNum: Int) : ServiceAction()
+
+ data class Favorite(val node: Node) : ServiceAction()
+
+ data class Ignore(val node: Node) : ServiceAction()
+
+ data class Reaction(val emoji: String, val replyId: Int, val contactKey: String) : ServiceAction()
+
+ data class AddSharedContact(val contact: AdminProtos.SharedContact) : ServiceAction()
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/ServiceRepository.kt b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt
similarity index 93%
rename from app/src/main/java/com/geeksville/mesh/service/ServiceRepository.kt
rename to core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt
index 85a72e486..6c4ce6c36 100644
--- a/app/src/main/java/com/geeksville/mesh/service/ServiceRepository.kt
+++ b/core/service/src/main/kotlin/org/meshtastic/core/service/ServiceRepository.kt
@@ -15,25 +15,24 @@
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.service
-import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.MeshProtos.MeshPacket
-import com.geeksville.mesh.android.Logging
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
+import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
/** Repository class for managing the [IMeshService] instance and connection state */
@Suppress("TooManyFunctions")
@Singleton
-class ServiceRepository @Inject constructor() : Logging {
+class ServiceRepository @Inject constructor() {
var meshService: IMeshService? = null
private set
@@ -64,7 +63,7 @@ class ServiceRepository @Inject constructor() : Logging {
get() = _clientNotification
fun setClientNotification(notification: MeshProtos.ClientNotification?) {
- errormsg(notification?.message.orEmpty())
+ Timber.e(notification?.message.orEmpty())
_clientNotification.value = notification
}
@@ -78,7 +77,7 @@ class ServiceRepository @Inject constructor() : Logging {
get() = _errorMessage
fun setErrorMessage(text: String) {
- errormsg(text)
+ Timber.e(text)
_errorMessage.value = text
}
diff --git a/mesh_service_example/README.md b/mesh_service_example/README.md
index 58da40984..3276c769c 100644
--- a/mesh_service_example/README.md
+++ b/mesh_service_example/README.md
@@ -4,7 +4,7 @@ This module provides an example implementation of an app that uses the [AIDL](ht
## Overview
-The [AIDL](../app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl) is defined in the main app module and is used to interact with the mesh network.
+The [AIDL](../core/service/src/main/aidl/org/meshtastic/core/service/IMeshService.aidl) is defined in the main app module and is used to interact with the mesh network.
`mesh_service_example` demonstrates how to build and integrate a custom mesh service within the Meshtastic ecosystem. It is intended as a reference for developers who want to extend or customize mesh-related functionality.
diff --git a/mesh_service_example/build.gradle.kts b/mesh_service_example/build.gradle.kts
index 1642db333..11179b1dc 100644
--- a/mesh_service_example/build.gradle.kts
+++ b/mesh_service_example/build.gradle.kts
@@ -44,7 +44,6 @@ plugins {
android {
namespace = "com.meshtastic.android.meshserviceexample"
- buildFeatures { aidl = true }
defaultConfig {
// Force this app to use the Google variant of any modules it's using that apply AndroidLibraryConventionPlugin
missingDimensionStrategy(FlavorDimension.marketplace.name, MeshtasticFlavor.google.name)
@@ -52,7 +51,9 @@ android {
}
dependencies {
+ implementation(projects.core.model)
implementation(projects.core.proto)
+ implementation(projects.core.service)
implementation(libs.appcompat)
implementation(libs.material)
diff --git a/mesh_service_example/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/mesh_service_example/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
deleted file mode 100644
index a73a9a14c..000000000
--- a/mesh_service_example/src/main/aidl/com/geeksville/mesh/IMeshService.aidl
+++ /dev/null
@@ -1,171 +0,0 @@
-// com.geeksville.mesh.IMeshService.aidl
-package com.geeksville.mesh;
-
-// Declare any non-default types here with import statements
-parcelable DataPacket;
-parcelable NodeInfo;
-parcelable MeshUser;
-parcelable Position;
-parcelable MyNodeInfo;
-
-/**
-This is the public android API for talking to meshtastic radios.
-
-To connect to meshtastic you should bind to it per https://developer.android.com/guide/components/bound-services
-
-The intent you use to reach the service should look like this:
-
- val intent = Intent().apply {
- setClassName(
- "com.geeksville.mesh",
- "com.geeksville.mesh.service.MeshService"
- )
- }
-
-In Android 11+ you *may* need to add the following to the client app's manifest to allow binding of the mesh service:
-
-
-
-For additional information, see https://developer.android.com/guide/topics/manifest/queries-element
-
-
-Once you have bound to the service you should register your broadcast receivers per https://developer.android.com/guide/components/broadcasts#context-registered-receivers
-
- // com.geeksville.mesh.x broadcast intents, where x is:
-
- // RECEIVED. - will **only** deliver packets for the specified port number. If a wellknown portnums.proto name for portnum is known it will be used
- // (i.e. com.geeksville.mesh.RECEIVED.TEXT_MESSAGE_APP) else the numeric portnum will be included as a base 10 integer (com.geeksville.mesh.RECEIVED.4403 etc...)
-
- // NODE_CHANGE for new IDs appearing or disappearing
- // CONNECTION_CHANGED for losing/gaining connection to the packet radio
- // MESSAGE_STATUS_CHANGED for any message status changes (for sent messages only, payload will contain a message ID and a MessageStatus)
-
-Note - these calls might throw RemoteException to indicate mesh error states
-*/
-interface IMeshService {
- /// Tell the service where to send its broadcasts of received packets
- /// This call is only required for manifest declared receivers. If your receiver is context-registered
- /// you don't need this.
- void subscribeReceiver(String packageName, String receiverName);
-
- /**
- * Set the user info for this node
- */
- void setOwner(in MeshUser user);
-
- void setRemoteOwner(in int requestId, in byte []payload);
- void getRemoteOwner(in int requestId, in int destNum);
-
- /// Return my unique user ID string
- String getMyId();
-
- /// Return a unique packet ID
- int getPacketId();
-
- /*
- Send a packet to a specified node name
-
- typ is defined in mesh.proto Data.Type. For now juse use 0 to mean opaque bytes.
-
- destId can be null to indicate "broadcast message"
-
- messageStatus and id of the provided message will be updated by this routine to indicate
- message send status and the ID that can be used to locate the message in the future
- */
- void send(inout DataPacket packet);
-
- /**
- Get the IDs of everyone on the mesh. You should also subscribe for NODE_CHANGE broadcasts.
- */
- List getNodes();
-
- /// This method is only intended for use in our GUI, so the user can set radio options
- /// It returns a DeviceConfig protobuf.
- byte []getConfig();
- /// It sets a Config protobuf via admin packet
- void setConfig(in byte []payload);
-
- /// Set and get a Config protobuf via admin packet
- void setRemoteConfig(in int requestId, in int destNum, in byte []payload);
- void getRemoteConfig(in int requestId, in int destNum, in int configTypeValue);
-
- /// Set and get a ModuleConfig protobuf via admin packet
- void setModuleConfig(in int requestId, in int destNum, in byte []payload);
- void getModuleConfig(in int requestId, in int destNum, in int moduleConfigTypeValue);
-
- /// Set and get the Ext Notification Ringtone string via admin packet
- void setRingtone(in int destNum, in String ringtone);
- void getRingtone(in int requestId, in int destNum);
-
- /// Set and get the Canned Message Messages string via admin packet
- void setCannedMessages(in int destNum, in String messages);
- void getCannedMessages(in int requestId, in int destNum);
-
- /// This method is only intended for use in our GUI, so the user can set radio options
- /// It sets a Channel protobuf via admin packet
- void setChannel(in byte []payload);
-
- /// Set and get a Channel protobuf via admin packet
- void setRemoteChannel(in int requestId, in int destNum, in byte []payload);
- void getRemoteChannel(in int requestId, in int destNum, in int channelIndex);
-
- /// Send beginEditSettings admin packet to nodeNum
- void beginEditSettings();
-
- /// Send commitEditSettings admin packet to nodeNum
- void commitEditSettings();
-
- /// delete a specific nodeNum from nodeDB
- void removeByNodenum(in int requestID, in int nodeNum);
-
- /// Send position packet with wantResponse to nodeNum
- void requestPosition(in int destNum, in Position position);
-
- /// Send setFixedPosition admin packet (or removeFixedPosition if Position is empty)
- void setFixedPosition(in int destNum, in Position position);
-
- /// Send traceroute packet with wantResponse to nodeNum
- void requestTraceroute(in int requestId, in int destNum);
-
- /// Send Shutdown admin packet to nodeNum
- void requestShutdown(in int requestId, in int destNum);
-
- /// Send Reboot admin packet to nodeNum
- void requestReboot(in int requestId, in int destNum);
-
- /// Send FactoryReset admin packet to nodeNum
- void requestFactoryReset(in int requestId, in int destNum);
-
- /// Send NodedbReset admin packet to nodeNum
- void requestNodedbReset(in int requestId, in int destNum);
-
- /// Returns a ChannelSet protobuf
- byte []getChannelSet();
-
- /**
- Is the packet radio currently connected to the phone? Returns a ConnectionState string.
- */
- String connectionState();
-
- /// If a macaddress we will try to talk to our device, if null we will be idle.
- /// Any current connection will be dropped (even if the device address is the same) before reconnecting.
- /// Users should not call this directly, only used internally by the MeshUtil activity
- /// Returns true if the device address actually changed, or false if no change was needed
- boolean setDeviceAddress(String deviceAddr);
-
- /// Get basic device hardware info about our connected radio. Will never return NULL. Will return NULL
- /// if no my node info is available (i.e. it will not throw an exception)
- MyNodeInfo getMyNodeInfo();
-
- /// Start updating the radios firmware
- void startFirmwareUpdate();
-
- /// Return a number 0-100 for firmware update progress. -1 for completed and success, -2 for failure
- int getUpdateStatus();
-
- /// Start providing location (from phone GPS) to mesh
- void startProvideLocation();
-
- /// Stop providing location (from phone GPS) to mesh
- void stopProvideLocation();
-}
diff --git a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/DataPacket.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/DataPacket.kt
deleted file mode 100644
index 9a470cc17..000000000
--- a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/DataPacket.kt
+++ /dev/null
@@ -1,209 +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
-
-import android.os.Parcel
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-import kotlinx.serialization.Serializable
-
-/** Generic [Parcel.readParcelable] Android 13 compatibility extension. */
-private inline fun Parcel.readParcelableCompat(loader: ClassLoader?): T? =
- if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.TIRAMISU) {
- @Suppress("DEPRECATION")
- readParcelable(loader)
- } else {
- readParcelable(loader, T::class.java)
- }
-
-@Parcelize
-enum class MessageStatus : Parcelable {
- UNKNOWN, // Not set for this message
- RECEIVED, // Came in from the mesh
- QUEUED, // Waiting to send to the mesh as soon as we connect to the device
- ENROUTE, // Delivered to the radio, but no ACK or NAK received
- DELIVERED, // We received an ack
- ERROR, // We received back a nak, message not delivered
-}
-
-/** A parcelable version of the protobuf MeshPacket + Data subpacket. */
-@Serializable
-data class DataPacket(
- var to: String? = ID_BROADCAST, // a nodeID string, or ID_BROADCAST for broadcast
- val bytes: ByteArray?,
- // A port number for this packet (formerly called DataType, see portnums.proto for new usage instructions)
- val dataType: Int,
- var from: String? = ID_LOCAL, // a nodeID string, or ID_LOCAL for localhost
- var time: Long = System.currentTimeMillis(), // msecs since 1970
- var id: Int = 0, // 0 means unassigned
- var status: MessageStatus? = MessageStatus.UNKNOWN,
- var hopLimit: Int = 0,
- var channel: Int = 0, // channel index
- var wantAck: Boolean = true, // If true, the receiver should send an ack back
-) : Parcelable {
-
- /** If there was an error with this message, this string describes what was wrong. */
- var errorMessage: String? = null
-
- /** Syntactic sugar to make it easy to create text messages */
- constructor(
- to: String?,
- channel: Int,
- text: String,
- ) : this(
- to = to,
- bytes = text.encodeToByteArray(),
- dataType = Portnums.PortNum.TEXT_MESSAGE_APP_VALUE,
- channel = channel,
- )
-
- /** If this is a text message, return the string, otherwise null */
- val text: String?
- get() =
- if (dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) {
- bytes?.decodeToString()
- } else {
- null
- }
-
- val alert: String?
- get() =
- if (dataType == Portnums.PortNum.ALERT_APP_VALUE) {
- bytes?.decodeToString()
- } else {
- null
- }
-
- constructor(
- to: String?,
- channel: Int,
- waypoint: MeshProtos.Waypoint,
- ) : this(to = to, bytes = waypoint.toByteArray(), dataType = Portnums.PortNum.WAYPOINT_APP_VALUE, channel = channel)
-
- val waypoint: MeshProtos.Waypoint?
- get() =
- if (dataType == Portnums.PortNum.WAYPOINT_APP_VALUE) {
- MeshProtos.Waypoint.parseFrom(bytes)
- } else {
- null
- }
-
- // Autogenerated comparision, because we have a byte array
-
- constructor(
- parcel: Parcel,
- ) : this(
- parcel.readString(),
- parcel.createByteArray(),
- parcel.readInt(),
- parcel.readString(),
- parcel.readLong(),
- parcel.readInt(),
- parcel.readParcelableCompat(MessageStatus::class.java.classLoader),
- parcel.readInt(),
- parcel.readInt(),
- parcel.readInt() == 1,
- )
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as DataPacket
-
- if (from != other.from) return false
- if (to != other.to) return false
- if (channel != other.channel) return false
- if (time != other.time) return false
- if (id != other.id) return false
- if (dataType != other.dataType) return false
- if (!bytes!!.contentEquals(other.bytes!!)) return false
- if (status != other.status) return false
- if (hopLimit != other.hopLimit) return false
- if (wantAck != other.wantAck) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = from.hashCode()
- result = 31 * result + to.hashCode()
- result = 31 * result + time.hashCode()
- result = 31 * result + id
- result = 31 * result + dataType
- result = 31 * result + bytes!!.contentHashCode()
- result = 31 * result + status.hashCode()
- result = 31 * result + hopLimit
- result = 31 * result + channel
- result = 31 * result + wantAck.hashCode()
- return result
- }
-
- override fun writeToParcel(parcel: Parcel, flags: Int) {
- parcel.writeString(to)
- parcel.writeByteArray(bytes)
- parcel.writeInt(dataType)
- parcel.writeString(from)
- parcel.writeLong(time)
- parcel.writeInt(id)
- parcel.writeParcelable(status, flags)
- parcel.writeInt(hopLimit)
- parcel.writeInt(channel)
- parcel.writeInt(if (wantAck) 1 else 0)
- }
-
- override fun describeContents(): Int = 0
-
- // Update our object from our parcel (used for inout parameters
- fun readFromParcel(parcel: Parcel) {
- to = parcel.readString()
- parcel.createByteArray()
- parcel.readInt()
- from = parcel.readString()
- time = parcel.readLong()
- id = parcel.readInt()
- status = parcel.readParcelableCompat(MessageStatus::class.java.classLoader)
- hopLimit = parcel.readInt()
- channel = parcel.readInt()
- wantAck = parcel.readInt() == 1
- }
-
- companion object CREATOR : Parcelable.Creator {
- // Special node IDs that can be used for sending messages
-
- /** the Node ID for broadcast destinations */
- const val ID_BROADCAST = "^all"
-
- /** The Node ID for the local node - used for from when sender doesn't know our local node ID */
- const val ID_LOCAL = "^local"
-
- // special broadcast address
- const val NODENUM_BROADCAST = (0xffffffff).toInt()
-
- // Public-key cryptography (PKC) channel index
- const val PKC_CHANNEL_INDEX = 8
-
- fun nodeNumToDefaultId(n: Int): String = "!%08x".format(n)
-
- fun idToDefaultNodeNum(id: String?): Int? = runCatching { id?.toLong(16)?.toInt() }.getOrNull()
-
- override fun createFromParcel(parcel: Parcel): DataPacket = DataPacket(parcel)
-
- override fun newArray(size: Int): Array = arrayOfNulls(size)
- }
-}
diff --git a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/MyNodeInfo.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/MyNodeInfo.kt
deleted file mode 100644
index fa94cb21a..000000000
--- a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/MyNodeInfo.kt
+++ /dev/null
@@ -1,44 +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
-
-import android.os.Parcelable
-import kotlinx.parcelize.Parcelize
-
-// MyNodeInfo sent via special protobuf from radio
-@Parcelize
-data class MyNodeInfo(
- val myNodeNum: Int,
- val hasGPS: Boolean,
- val model: String?,
- val firmwareVersion: String?,
- val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want
- val shouldUpdate: Boolean, // this device has old firmware
- val currentPacketId: Long,
- val messageTimeoutMsec: Int,
- val minAppVersion: Int,
- val maxChannels: Int,
- val hasWifi: Boolean,
- val channelUtilization: Float,
- val airUtilTx: Float,
- val deviceId: String?,
-) : Parcelable {
- /** A human readable description of the software/hardware version */
- val firmwareString: String
- get() = "$model $firmwareVersion"
-}
diff --git a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/NodeInfo.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/NodeInfo.kt
deleted file mode 100644
index d339a7cfa..000000000
--- a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/NodeInfo.kt
+++ /dev/null
@@ -1,232 +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
-
-import android.graphics.Color
-import android.os.Parcelable
-import com.geeksville.mesh.util.anonymize
-import com.geeksville.mesh.util.bearing
-import com.geeksville.mesh.util.latLongToMeter
-import kotlinx.parcelize.Parcelize
-
-//
-// model objects that directly map to the corresponding protobufs
-//
-
-@Parcelize
-data class MeshUser(
- val id: String,
- val longName: String,
- val shortName: String,
- val hwModel: MeshProtos.HardwareModel,
- val isLicensed: Boolean = false,
- val role: Int = 0,
-) : Parcelable {
-
- override fun toString(): String = "MeshUser(id=${id.anonymize}, " +
- "longName=${longName.anonymize}, " +
- "shortName=${shortName.anonymize}, " +
- "hwModel=$hwModelString, " +
- "isLicensed=$isLicensed, " +
- "role=$role)"
-
- /** Create our model object from a protobuf. */
- constructor(p: MeshProtos.User) : this(p.id, p.longName, p.shortName, p.hwModel, p.isLicensed, p.roleValue)
-
- /**
- * a string version of the hardware model, converted into pretty lowercase and changing _ to -, and p to dot or null
- * if unset
- */
- val hwModelString: String?
- get() =
- if (hwModel == MeshProtos.HardwareModel.UNSET) {
- null
- } else {
- hwModel.name.replace('_', '-').replace('p', '.').lowercase()
- }
-}
-
-@Parcelize
-data class Position(
- val latitude: Double,
- val longitude: Double,
- val altitude: Int,
- val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
- val satellitesInView: Int = 0,
- val groundSpeed: Int = 0,
- val groundTrack: Int = 0, // "heading"
- val precisionBits: Int = 0,
-) : Parcelable {
-
- companion object {
- // / Convert to a double representation of degrees
- fun degD(i: Int) = i * 1e-7
-
- fun degI(d: Double) = (d * 1e7).toInt()
-
- fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
- }
-
- /**
- * Create our model object from a protobuf. If time is unspecified in the protobuf, the provided default time will
- * be used.
- */
- constructor(
- position: MeshProtos.Position,
- defaultTime: Int = currentTime(),
- ) : this(
- // We prefer the int version of lat/lon but if not available use the depreciated legacy version
- degD(position.latitudeI),
- degD(position.longitudeI),
- position.altitude,
- if (position.time != 0) position.time else defaultTime,
- position.satsInView,
- position.groundSpeed,
- position.groundTrack,
- position.precisionBits,
- )
-
- // / @return distance in meters to some other node (or null if unknown)
- fun distance(o: Position) = latLongToMeter(latitude, longitude, o.latitude, o.longitude)
-
- // / @return bearing to the other position in degrees
- fun bearing(o: Position) = bearing(latitude, longitude, o.latitude, o.longitude)
-
- // If GPS gives a crap position don't crash our app
- fun isValid(): Boolean = latitude != 0.0 &&
- longitude != 0.0 &&
- (latitude >= -90 && latitude <= 90.0) &&
- (longitude >= -180 && longitude <= 180)
-
- override fun toString(): String =
- "Position(lat=${latitude.anonymize}, lon=${longitude.anonymize}, alt=${altitude.anonymize}, time=$time)"
-}
-
-@Parcelize
-data class DeviceMetrics(
- val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
- val batteryLevel: Int = 0,
- val voltage: Float,
- val channelUtilization: Float,
- val airUtilTx: Float,
- val uptimeSeconds: Int,
-) : Parcelable {
- companion object {
- fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
- }
-
- /** Create our model object from a protobuf. */
- constructor(
- p: TelemetryProtos.DeviceMetrics,
- telemetryTime: Int = currentTime(),
- ) : this(telemetryTime, p.batteryLevel, p.voltage, p.channelUtilization, p.airUtilTx, p.uptimeSeconds)
-}
-
-@Parcelize
-data class EnvironmentMetrics(
- val time: Int = currentTime(), // default to current time in secs (NOT MILLISECONDS!)
- val temperature: Float,
- val relativeHumidity: Float,
- val barometricPressure: Float,
- val gasResistance: Float,
- val voltage: Float,
- val current: Float,
- val iaq: Int,
-) : Parcelable {
- companion object {
- fun currentTime() = (System.currentTimeMillis() / 1000).toInt()
- }
-}
-
-@Parcelize
-data class NodeInfo(
- val num: Int, // This is immutable, and used as a key
- var user: MeshUser? = null,
- var position: Position? = null,
- var snr: Float = Float.MAX_VALUE,
- var rssi: Int = Int.MAX_VALUE,
- var lastHeard: Int = 0, // the last time we've seen this node in secs since 1970
- var deviceMetrics: DeviceMetrics? = null,
- var channel: Int = 0,
- var environmentMetrics: EnvironmentMetrics? = null,
- var hopsAway: Int = 0,
-) : Parcelable {
-
- val colors: Pair
- get() { // returns foreground and background @ColorInt for each 'num'
- val r = (num and 0xFF0000) shr 16
- val g = (num and 0x00FF00) shr 8
- val b = num and 0x0000FF
- val brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255
- return (if (brightness > 0.5) Color.BLACK else Color.WHITE) to Color.rgb(r, g, b)
- }
-
- val batteryLevel
- get() = deviceMetrics?.batteryLevel
-
- val voltage
- get() = deviceMetrics?.voltage
-
- val batteryStr
- get() = if (batteryLevel in 1..100) String.format("%d%%", batteryLevel) else ""
-
- /** true if the device was heard from recently */
- @Suppress("MagicNumber")
- val isOnline: Boolean
- get() {
- val now = System.currentTimeMillis() / 1000
- val timeout = 15 * 60
- return (now - lastHeard <= timeout)
- }
-
- // / return the position if it is valid, else null
- val validPosition: Position?
- get() {
- return position?.takeIf { it.isValid() }
- }
-
- // / @return distance in meters to some other node (or null if unknown)
- fun distance(o: NodeInfo?): Int? {
- val p = validPosition
- val op = o?.validPosition
- return if (p != null && op != null) p.distance(op).toInt() else null
- }
-
- // / @return bearing to the other position in degrees
- fun bearing(o: NodeInfo?): Int? {
- val p = validPosition
- val op = o?.validPosition
- return if (p != null && op != null) p.bearing(op).toInt() else null
- }
-
- // / @return a nice human readable string for the distance, or null for unknown
- fun distanceStr(o: NodeInfo?, prefUnits: Int = 0) = distance(o)?.let { dist ->
- when {
- dist == 0 -> null // same point
- prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist < 1000 ->
- "%.0f m".format(dist.toDouble())
- prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE && dist >= 1000 ->
- "%.1f km".format(dist / 1000.0)
- prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist < 1609 ->
- "%.0f ft".format(dist.toDouble() * 3.281)
- prefUnits == ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE && dist >= 1609 ->
- "%.1f mi".format(dist / 1609.34)
- else -> null
- }
- }
-}
diff --git a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/Extensions.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/Extensions.kt
deleted file mode 100644
index 03fe16512..000000000
--- a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/Extensions.kt
+++ /dev/null
@@ -1,70 +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.util
-
-import android.widget.EditText
-import com.geeksville.mesh.ConfigProtos
-
-/**
- * When printing strings to logs sometimes we want to print useful debugging information about users or positions. But
- * we don't want to leak things like usernames or locations. So this function if given a string, will return a string
- * which is a maximum of three characters long, taken from the tail of the string. Which should effectively hide real
- * usernames and locations, but still let us see if values were zero, empty or different.
- */
-val Any?.anonymize: String
- get() = this.anonymize()
-
-/** A version of anonymize that allows passing in a custom minimum length */
-fun Any?.anonymize(maxLen: Int = 3) = if (this != null) ("..." + this.toString().takeLast(maxLen)) else "null"
-
-// A toString that makes sure all newlines are removed (for nice logging).
-fun Any.toOneLineString() = this.toString().replace('\n', ' ')
-
-fun ConfigProtos.Config.toOneLineString(): String {
- val redactedFields = """(wifi_psk:|public_key:|private_key:|admin_key:)\s*".*"""
- return this.toString()
- .replace(redactedFields.toRegex()) { "${it.groupValues[1]} \"[REDACTED]\"" }
- .replace('\n', ' ')
-}
-
-// Return a one line string version of an object (but if a release build, just say 'might be PII)
-fun Any.toPIIString() = this.toOneLineString()
-
-fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
-
-fun formatAgo(lastSeenUnix: Int, currentTimeMillis: Long = System.currentTimeMillis()): String {
- val currentTime = (currentTimeMillis / 1000).toInt()
- val diffMin = (currentTime - lastSeenUnix) / 60
- return when {
- diffMin < 1 -> "now"
- diffMin < 60 -> diffMin.toString() + " min"
- diffMin < 2880 -> (diffMin / 60).toString() + " h"
- diffMin < 1440000 -> (diffMin / (60 * 24)).toString() + " d"
- else -> "?"
- }
-}
-
-// Allows usage like email.onEditorAction(EditorInfo.IME_ACTION_NEXT, { confirm() })
-fun EditText.onEditorAction(actionId: Int, func: () -> Unit) {
- setOnEditorActionListener { _, receivedActionId, _ ->
- if (actionId == receivedActionId) {
- func()
- }
- true
- }
-}
diff --git a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/LocationUtils.kt b/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/LocationUtils.kt
deleted file mode 100644
index 3caf2dbb5..000000000
--- a/mesh_service_example/src/main/kotlin/com/geeksville/mesh/util/LocationUtils.kt
+++ /dev/null
@@ -1,289 +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 .
- */
-
-@file:Suppress("TooManyFunctions")
-
-package com.geeksville.mesh.util
-
-import com.geeksville.mesh.MeshProtos
-import com.geeksville.mesh.Position
-import mil.nga.grid.features.Point
-import mil.nga.mgrs.MGRS
-import mil.nga.mgrs.utm.UTM
-import org.osmdroid.util.BoundingBox
-import org.osmdroid.util.GeoPoint
-import java.util.Locale
-import kotlin.math.PI
-import kotlin.math.abs
-import kotlin.math.acos
-import kotlin.math.atan2
-import kotlin.math.cos
-import kotlin.math.log2
-import kotlin.math.pow
-import kotlin.math.sin
-
-/**
- * ****************************************************************************
- * Revive some of my old Gaggle source code...
- *
- * GNU Public License, version 2 All other distribution of Gaggle must conform to the terms of the GNU Public License,
- * version 2. The full text of this license is included in the Gaggle source, see assets/manual/gpl-2.0.txt.
- * ****************************************************************************
- */
-object GPSFormat {
- fun dec(p: Position): String =
- String.format(Locale.getDefault(), "%.5f %.5f", p.latitude, p.longitude).replace(",", ".")
-
- @Suppress("MagicNumber")
- fun dms(p: Position): String {
- val lat = degreesToDMS(p.latitude, true)
- val lon = degreesToDMS(p.longitude, false)
- fun string(a: Array) = String.format(Locale.getDefault(), "%s°%s'%.5s\"%s", a[0], a[1], a[2], a[3])
- return string(lat) + " " + string(lon)
- }
-
- fun utm(p: Position): String {
- val utm = UTM.from(Point.point(p.longitude, p.latitude))
- return String.format(
- Locale.getDefault(),
- "%s%s %.6s %.7s",
- utm.zone,
- utm.toMGRS().band,
- utm.easting,
- utm.northing,
- )
- }
-
- fun mgrs(p: Position): String {
- val mgrs = MGRS.from(Point.point(p.longitude, p.latitude))
- return String.format(
- Locale.getDefault(),
- "%s%s %s%s %05d %05d",
- mgrs.zone,
- mgrs.band,
- mgrs.column,
- mgrs.row,
- mgrs.easting,
- mgrs.northing,
- )
- }
-
- fun toDEC(latitude: Double, longitude: Double): String = "%.5f %.5f".format(latitude, longitude).replace(",", ".")
-
- @Suppress("MagicNumber")
- fun toDMS(latitude: Double, longitude: Double): String {
- val lat = degreesToDMS(latitude, true)
- val lon = degreesToDMS(longitude, false)
- fun string(a: Array) = "%s°%s'%.5s\"%s".format(Locale.getDefault(), a[0], a[1], a[2], a[3])
- return string(lat) + " " + string(lon)
- }
-
- fun toUTM(latitude: Double, longitude: Double): String {
- val utm = UTM.from(Point.point(longitude, latitude))
- return "%s%s %.6s %.7s".format(Locale.getDefault(), utm.zone, utm.toMGRS().band, utm.easting, utm.northing)
- }
-
- fun toMGRS(latitude: Double, longitude: Double): String {
- val mgrs = MGRS.from(Point.point(longitude, latitude))
- return "%s%s %s%s %05d %05d"
- .format(Locale.getDefault(), mgrs.zone, mgrs.band, mgrs.column, mgrs.row, mgrs.easting, mgrs.northing)
- }
-}
-
-/**
- * Format as degrees, minutes, secs
- *
- * @param degIn
- * @param isLatitude
- * @return a string like 120deg
- */
-@Suppress("MagicNumber")
-fun degreesToDMS(degIn: Double, isLatitude: Boolean): Array {
- var degIn = degIn
- val isPos = degIn >= 0
- val dirLetter =
- if (isLatitude) if (isPos) 'N' else 'S'
- else if (isPos) {
- 'E'
- } else {
- 'W'
- }
- degIn = abs(degIn)
- val degOut = degIn.toInt()
- val minutes = 60 * (degIn - degOut)
- val minwhole = minutes.toInt()
- val seconds = (minutes - minwhole) * 60
- return arrayOf(degOut.toString(), minwhole.toString(), seconds.toString(), dirLetter.toString())
-}
-
-@Suppress("MagicNumber")
-fun degreesToDM(degIn: Double, isLatitude: Boolean): Array {
- var degIn = degIn
- val isPos = degIn >= 0
- val dirLetter =
- if (isLatitude) if (isPos) 'N' else 'S'
- else if (isPos) {
- 'E'
- } else {
- 'W'
- }
- degIn = abs(degIn)
- val degOut = degIn.toInt()
- val minutes = 60 * (degIn - degOut)
- val seconds = 0
- return arrayOf(degOut.toString(), minutes.toString(), seconds.toString(), dirLetter.toString())
-}
-
-fun degreesToD(degIn: Double, isLatitude: Boolean): Array {
- var degIn = degIn
- val isPos = degIn >= 0
- val dirLetter =
- if (isLatitude) if (isPos) 'N' else 'S'
- else if (isPos) {
- 'E'
- } else {
- 'W'
- }
- degIn = abs(degIn)
- val degOut = degIn
- val minutes = 0
- val seconds = 0
- return arrayOf(degOut.toString(), minutes.toString(), seconds.toString(), dirLetter.toString())
-}
-
-/**
- * A not super efficent mapping from a starting lat/long + a distance at a certain direction
- *
- * @param lat
- * @param longitude
- * @param distMeters
- * @param theta in radians, 0 == north
- * @return an array with lat and long
- */
-@Suppress("MagicNumber")
-fun addDistance(lat: Double, longitude: Double, distMeters: Double, theta: Double): DoubleArray {
- val dx = distMeters * sin(theta) // theta measured clockwise
- // from due north
- val dy = distMeters * cos(theta) // dx, dy same units as R
- val dLong = dx / (111320 * cos(lat)) // dx, dy in meters
- val dLat = dy / 110540 // result in degrees long/lat
- return doubleArrayOf(lat + dLat, longitude + dLong)
-}
-
-/** @return distance in meters along the surface of the earth (ish) */
-@Suppress("MagicNumber")
-fun latLongToMeter(latA: Double, lngA: Double, latB: Double, lngB: Double): Double {
- val pk = (180 / PI)
- val a1 = latA / pk
- val a2 = lngA / pk
- val b1 = latB / pk
- val b2 = lngB / pk
- val t1 = cos(a1) * cos(a2) * cos(b1) * cos(b2)
- val t2 = cos(a1) * sin(a2) * cos(b1) * sin(b2)
- val t3 = sin(a1) * sin(b1)
- var tt = acos(t1 + t2 + t3)
- if (java.lang.Double.isNaN(tt)) tt = 0.0 // Must have been the same point?
- return 6366000 * tt
-}
-
-// Same as above, but takes Mesh Position proto.
-fun positionToMeter(a: MeshProtos.Position, b: MeshProtos.Position): Double =
- latLongToMeter(a.latitudeI * 1e-7, a.longitudeI * 1e-7, b.latitudeI * 1e-7, b.longitudeI * 1e-7)
-
-/**
- * Convert degrees/mins/secs to a single double
- *
- * @param degrees
- * @param minutes
- * @param seconds
- * @param isPostive
- * @return
- */
-@Suppress("MagicNumber")
-fun dmsToDegrees(degrees: Int, minutes: Int, seconds: Float, isPostive: Boolean): Double =
- (if (isPostive) 1 else -1) * (degrees + minutes / 60.0 + seconds / 3600.0)
-
-@Suppress("MagicNumber")
-fun dmsToDegrees(degrees: Double, minutes: Double, seconds: Double, isPostive: Boolean): Double =
- (if (isPostive) 1 else -1) * (degrees + minutes / 60.0 + seconds / 3600.0)
-
-/**
- * Computes the bearing in degrees between two points on Earth.
- *
- * @param lat1 Latitude of the first point
- * @param lon1 Longitude of the first point
- * @param lat2 Latitude of the second point
- * @param lon2 Longitude of the second point
- * @return Bearing between the two points in degrees. A value of 0 means due north.
- */
-fun bearing(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
- val lat1Rad = Math.toRadians(lat1)
- val lat2Rad = Math.toRadians(lat2)
- val deltaLonRad = Math.toRadians(lon2 - lon1)
- val y = sin(deltaLonRad) * cos(lat2Rad)
- val x = cos(lat1Rad) * sin(lat2Rad) - (sin(lat1Rad) * cos(lat2Rad) * cos(deltaLonRad))
- return radToBearing(atan2(y, x))
-}
-
-/** Converts an angle in radians to degrees */
-@Suppress("MagicNumber")
-fun radToBearing(rad: Double): Double = (Math.toDegrees(rad) + 360) % 360
-
-/**
- * Calculates the zoom level required to fit the entire [BoundingBox] inside the map view.
- *
- * @return The zoom level as a Double value.
- */
-@Suppress("MagicNumber")
-fun BoundingBox.requiredZoomLevel(): Double {
- val topLeft = GeoPoint(this.latNorth, this.lonWest)
- val bottomRight = GeoPoint(this.latSouth, this.lonEast)
- val latLonWidth = topLeft.distanceToAsDouble(GeoPoint(topLeft.latitude, bottomRight.longitude))
- val latLonHeight = topLeft.distanceToAsDouble(GeoPoint(bottomRight.latitude, topLeft.longitude))
- val requiredLatZoom = log2(360.0 / (latLonHeight / 111320))
- val requiredLonZoom = log2(360.0 / (latLonWidth / 111320))
- return maxOf(requiredLatZoom, requiredLonZoom) * 0.8
-}
-
-/**
- * Creates a new bounding box with adjusted dimensions based on the provided [zoomFactor].
- *
- * @return A new [BoundingBox] with added [zoomFactor]. Example:
- * ```
- * // Setting the zoom level directly using setZoom()
- * map.setZoom(14.0)
- * val boundingBoxZoom14 = map.boundingBox
- *
- * // Using zoomIn() results the equivalent BoundingBox with setZoom(15.0)
- * val boundingBoxZoom15 = boundingBoxZoom14.zoomIn(1.0)
- * ```
- */
-fun BoundingBox.zoomIn(zoomFactor: Double): BoundingBox {
- val center = GeoPoint((latNorth + latSouth) / 2, (lonWest + lonEast) / 2)
- val latDiff = latNorth - latSouth
- val lonDiff = lonEast - lonWest
-
- val newLatDiff = latDiff / (2.0.pow(zoomFactor))
- val newLonDiff = lonDiff / (2.0.pow(zoomFactor))
-
- return BoundingBox(
- center.latitude + newLatDiff / 2,
- center.longitude + newLonDiff / 2,
- center.latitude - newLatDiff / 2,
- center.longitude - newLonDiff / 2,
- )
-}
diff --git a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
index 119b74f48..8ef54274f 100644
--- a/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
+++ b/mesh_service_example/src/main/kotlin/com/meshtastic/android/meshserviceexample/MainActivity.kt
@@ -36,11 +36,11 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
-import com.geeksville.mesh.DataPacket
-import com.geeksville.mesh.IMeshService
-import com.geeksville.mesh.MessageStatus
-import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Portnums
+import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.model.MessageStatus
+import org.meshtastic.core.model.NodeInfo
+import org.meshtastic.core.service.IMeshService
private const val TAG: String = "MeshServiceExample"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b091b15e2..0ca6613e7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -28,6 +28,7 @@ include(
":core:network",
":core:prefs",
":core:proto",
+ ":core:service",
":core:strings",
":core:ui",
":feature:map",