diff --git a/.github/workflows/reusable-check.yml b/.github/workflows/reusable-check.yml
index 85b9d46ba9..e480374fd0 100644
--- a/.github/workflows/reusable-check.yml
+++ b/.github/workflows/reusable-check.yml
@@ -89,30 +89,18 @@ jobs:
- name: Determine Tasks
id: tasks
run: |
- TASKS=""
- # Only run Lint and Unit Tests on the first API level and first flavor in the matrix to save time and resources
+ FLAVOR="${{ matrix.flavor }}"
+ FLAVOR_CAP=$(echo $FLAVOR | awk '{print toupper(substr($0,1,1))substr($0,2)}')
IS_FIRST_API=$(echo '${{ inputs.api_levels }}' | jq -r '.[0] == ${{ matrix.api_level }}')
IS_FIRST_FLAVOR=$(echo '${{ inputs.flavors }}' | jq -r '.[0] == "${{ matrix.flavor }}"')
- if [ "$IS_FIRST_API" = "true" ] && [ "$IS_FIRST_FLAVOR" = "true" ]; then
- [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS spotlessCheck detekt "
- [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testDebugUnitTest "
- fi
-
- FLAVOR="${{ matrix.flavor }}"
- if [ "$IS_FIRST_API" = "true" ]; then
- if [ "$FLAVOR" = "google" ]; then
- TASKS="$TASKS assembleGoogleDebug "
- [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testGoogleDebugUnitTest "
- elif [ "$FLAVOR" = "fdroid" ]; then
- TASKS="$TASKS assembleFdroidDebug "
- [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS testFdroidDebugUnitTest "
- fi
- fi
+ # Matrix-specific tasks
+ TASKS="assemble${FLAVOR_CAP}Debug "
+ [ "${{ inputs.run_lint }}" = "true" ] && TASKS="$TASKS lint${FLAVOR_CAP}Debug "
+ [ "${{ inputs.run_unit_tests }}" = "true" ] && TASKS="$TASKS test${FLAVOR_CAP}DebugUnitTest "
# Instrumented Test Tasks
if [ "${{ inputs.run_instrumented_tests }}" = "true" ]; then
- [ "$IS_FIRST_FLAVOR" = "true" ] && TASKS="$TASKS connectedDebugAndroidTest "
if [ "$FLAVOR" = "google" ]; then
TASKS="$TASKS connectedGoogleDebugAndroidTest "
elif [ "$FLAVOR" = "fdroid" ]; then
@@ -120,20 +108,22 @@ jobs:
fi
fi
- # Run coverage report if unit tests were executed
- if [ "${{ inputs.run_unit_tests }}" = "true" ] && [ "$IS_FIRST_API" = "true" ]; then
- if [ "$IS_FIRST_FLAVOR" = "true" ]; then
- TASKS="$TASKS koverXmlReportDebug "
- fi
- if [ "$FLAVOR" = "google" ]; then
- TASKS="$TASKS koverXmlReportGoogleDebug "
- elif [ "$FLAVOR" = "fdroid" ]; then
- TASKS="$TASKS koverXmlReportFdroidDebug "
- fi
+ # Run coverage report for this flavor
+ if [ "${{ inputs.run_unit_tests }}" = "true" ]; then
+ TASKS="$TASKS koverXmlReport${FLAVOR_CAP}Debug "
fi
echo "tasks=$TASKS" >> $GITHUB_OUTPUT
echo "is_first_api=$IS_FIRST_API" >> $GITHUB_OUTPUT
+ echo "is_first_flavor=$IS_FIRST_FLAVOR" >> $GITHUB_OUTPUT
+
+ - name: Code Style & Static Analysis
+ if: steps.tasks.outputs.is_first_api == 'true' && steps.tasks.outputs.is_first_flavor == 'true'
+ run: ./gradlew spotlessCheck detekt -Pci=true
+
+ - name: Shared Unit Tests
+ if: steps.tasks.outputs.is_first_api == 'true' && steps.tasks.outputs.is_first_flavor == 'true' && inputs.run_unit_tests == true
+ run: ./gradlew testDebugUnitTest koverXmlReportDebug -Pci=true --continue
- name: Enable KVM group perms
if: inputs.run_instrumented_tests == true
@@ -142,7 +132,7 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- - name: Run Check (with Emulator)
+ - name: Run Flavor Check (with Emulator)
if: inputs.run_instrumented_tests == true
uses: reactivecircus/android-emulator-runner@v2
env:
@@ -155,7 +145,7 @@ jobs:
disable-animations: true
script: ./gradlew ${{ steps.tasks.outputs.tasks }} -Pci=true -PenableComposeCompilerMetrics=true -PenableComposeCompilerReports=true --continue --scan
- - name: Run Check (no Emulator)
+ - name: Run Flavor Check (no Emulator)
if: inputs.run_instrumented_tests == false
env:
VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }}
diff --git a/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt b/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt
index 6a701aa8c6..2c327a7af7 100644
--- a/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt
+++ b/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt
@@ -26,7 +26,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.prefs.filter.FilterPrefs
-import org.meshtastic.core.service.filter.MessageFilterService
+import org.meshtastic.core.repository.MessageFilter
import javax.inject.Inject
@HiltAndroidTest
@@ -37,7 +37,7 @@ class MessageFilterIntegrationTest {
@Inject lateinit var filterPrefs: FilterPrefs
- @Inject lateinit var filterService: MessageFilterService
+ @Inject lateinit var filterService: MessageFilter
@Before
fun setup() {
diff --git a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt
index 5c546f4763..dd07d74e2a 100644
--- a/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt
+++ b/app/src/main/java/com/geeksville/mesh/ApplicationModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,13 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package com.geeksville.mesh
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
+import com.geeksville.mesh.repository.radio.AndroidRadioInterfaceService
+import com.geeksville.mesh.service.AndroidAppWidgetUpdater
+import com.geeksville.mesh.service.AndroidMeshLocationManager
+import com.geeksville.mesh.service.AndroidMeshWorkerManager
import com.geeksville.mesh.service.MeshServiceNotificationsImpl
+import com.geeksville.mesh.service.ServiceBroadcasts
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -28,7 +32,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.di.ProcessLifecycle
-import org.meshtastic.core.service.MeshServiceNotifications
+import org.meshtastic.core.repository.MeshServiceNotifications
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@@ -37,6 +41,20 @@ interface ApplicationModule {
@Binds fun bindMeshServiceNotifications(impl: MeshServiceNotificationsImpl): MeshServiceNotifications
+ @Binds
+ fun bindMeshLocationManager(impl: AndroidMeshLocationManager): org.meshtastic.core.repository.MeshLocationManager
+
+ @Binds fun bindMeshWorkerManager(impl: AndroidMeshWorkerManager): org.meshtastic.core.repository.MeshWorkerManager
+
+ @Binds fun bindAppWidgetUpdater(impl: AndroidAppWidgetUpdater): org.meshtastic.core.repository.AppWidgetUpdater
+
+ @Binds
+ fun bindRadioInterfaceService(
+ impl: AndroidRadioInterfaceService,
+ ): org.meshtastic.core.repository.RadioInterfaceService
+
+ @Binds fun bindServiceBroadcasts(impl: ServiceBroadcasts): org.meshtastic.core.repository.ServiceBroadcasts
+
companion object {
@Provides @ProcessLifecycle
fun provideProcessLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get()
diff --git a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
index ca4b141a50..74fcea5bf7 100644
--- a/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
+++ b/app/src/main/java/com/geeksville/mesh/MeshServiceClient.kt
@@ -29,10 +29,10 @@ import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.launch
import org.meshtastic.core.common.util.SequentialJob
+import org.meshtastic.core.service.AndroidServiceRepository
import org.meshtastic.core.service.BindFailedException
import org.meshtastic.core.service.IMeshService
import org.meshtastic.core.service.ServiceClient
-import org.meshtastic.core.service.ServiceRepository
import javax.inject.Inject
/** A Activity-lifecycle-aware [ServiceClient] that binds [MeshService] once the Activity is started. */
@@ -41,7 +41,7 @@ class MeshServiceClient
@Inject
constructor(
@ActivityContext private val context: Context,
- private val serviceRepository: ServiceRepository,
+ private val serviceRepository: AndroidServiceRepository,
private val serviceSetupJob: SequentialJob,
) : ServiceClient(IMeshService.Stub::asInterface),
DefaultLifecycleObserver {
diff --git a/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt
index a6759dae6c..4b7a25c501 100644
--- a/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt
+++ b/app/src/main/java/com/geeksville/mesh/domain/usecase/GetDiscoveredDevicesUseCase.kt
@@ -22,18 +22,18 @@ import com.geeksville.mesh.model.DeviceListEntry
import com.geeksville.mesh.model.getMeshtasticShortName
import com.geeksville.mesh.repository.network.NetworkRepository
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.usb.UsbRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.ble.BluetoothRepository
-import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.database.DatabaseManager
-import org.meshtastic.core.database.model.Node
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
+import org.meshtastic.core.model.Node
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.meshtastic
import java.util.Locale
diff --git a/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt b/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt
index 6d2e4c448d..d66d6fff0e 100644
--- a/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/DeviceListEntry.kt
@@ -17,14 +17,14 @@
package com.geeksville.mesh.model
import android.hardware.usb.UsbManager
-import com.geeksville.mesh.repository.radio.InterfaceId
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.hoho.android.usbserial.driver.UsbSerialDriver
import no.nordicsemi.kotlin.ble.client.android.Peripheral
import no.nordicsemi.kotlin.ble.core.BondState
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
-import org.meshtastic.core.database.model.Node
+import org.meshtastic.core.model.InterfaceId
+import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.anonymize
+import org.meshtastic.core.repository.RadioInterfaceService
/**
* A sealed class is used here to represent the different types of devices that can be displayed in the list. This is
diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt
index 52ef78ce5e..a3511ca74a 100644
--- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt
@@ -22,8 +22,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import co.touchlab.kermit.Logger
-import com.geeksville.mesh.repository.radio.MeshActivity
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
@@ -45,21 +43,24 @@ import org.jetbrains.compose.resources.getString
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.data.repository.MeshLogRepository
-import org.meshtastic.core.data.repository.NodeRepository
-import org.meshtastic.core.data.repository.PacketRepository
-import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.datastore.UiPreferencesDataSource
+import org.meshtastic.core.model.MeshActivity
+import org.meshtastic.core.model.MyNodeInfo
+import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.TracerouteMapAvailability
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
+import org.meshtastic.core.model.service.TracerouteResponse
import org.meshtastic.core.model.util.dispatchMeshtasticUri
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.client_notification
import org.meshtastic.core.resources.compromised_keys
+import org.meshtastic.core.service.AndroidServiceRepository
import org.meshtastic.core.service.IMeshService
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.ServiceRepository
-import org.meshtastic.core.service.TracerouteResponse
import org.meshtastic.core.ui.component.ScrollToTopEvent
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.util.ComposableContent
@@ -75,7 +76,8 @@ class UIViewModel
@Inject
constructor(
private val nodeDB: NodeRepository,
- private val serviceRepository: ServiceRepository,
+ private val serviceRepository: AndroidServiceRepository,
+ private val radioController: RadioController,
radioInterfaceService: RadioInterfaceService,
meshLogRepository: MeshLogRepository,
firmwareReleaseRepository: FirmwareReleaseRepository,
@@ -161,6 +163,10 @@ constructor(
val meshService: IMeshService?
get() = serviceRepository.meshService
+ fun setDeviceAddress(address: String) {
+ radioController.setDeviceAddress(address)
+ }
+
val unreadMessageCount =
packetRepository.getUnreadCountTotal().map { it.coerceAtLeast(0) }.stateInWhileSubscribed(initialValue = 0)
@@ -172,7 +178,7 @@ constructor(
}
// hardware info about our local device (can be null)
- val myNodeInfo: StateFlow
+ val myNodeInfo: StateFlow
get() = nodeDB.myNodeInfo
init {
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt
similarity index 87%
rename from app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
rename to app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt
index f7cf8fbd57..cd190ad455 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt
@@ -49,8 +49,11 @@ import org.meshtastic.core.common.util.toRemoteExceptions
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.ConnectionState
+import org.meshtastic.core.model.InterfaceId
+import org.meshtastic.core.model.MeshActivity
import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.prefs.radio.RadioPrefs
+import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.ToRadio
import javax.inject.Inject
@@ -65,9 +68,9 @@ import javax.inject.Singleton
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc... It is designed to be simple so it
* can be stubbed out with a simulated version as needed.
*/
-@Suppress("LongParameterList")
+@Suppress("LongParameterList", "TooManyFunctions")
@Singleton
-open class RadioInterfaceService
+class AndroidRadioInterfaceService
@Inject
constructor(
private val context: Application,
@@ -78,20 +81,20 @@ constructor(
private val radioPrefs: RadioPrefs,
private val interfaceFactory: InterfaceFactory,
private val analytics: PlatformAnalytics,
-) {
+) : RadioInterfaceService {
private val _connectionState = MutableStateFlow(ConnectionState.Disconnected)
- val connectionState: StateFlow = _connectionState.asStateFlow()
+ override val connectionState: StateFlow = _connectionState.asStateFlow()
private val _receivedData = MutableSharedFlow(extraBufferCapacity = 64)
- val receivedData: SharedFlow = _receivedData
+ override val receivedData: SharedFlow = _receivedData
private val _connectionError = MutableSharedFlow(extraBufferCapacity = 64)
val connectionError: SharedFlow = _connectionError.asSharedFlow()
// Thread-safe StateFlow for tracking device address changes
private val _currentDeviceAddressFlow = MutableStateFlow(radioPrefs.devAddr)
- val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow()
+ override val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow()
private val logSends = false
private val logReceives = false
@@ -100,8 +103,11 @@ constructor(
val mockInterfaceAddress: String by lazy { toInterfaceAddress(InterfaceId.MOCK, "") }
+ override val serviceScope: CoroutineScope
+ get() = _serviceScope
+
/** We recreate this scope each time we stop an interface */
- var serviceScope = CoroutineScope(dispatchers.io + SupervisorJob())
+ private var _serviceScope = CoroutineScope(dispatchers.io + SupervisorJob())
private var radioIf: IRadioInterface = NopInterface("")
@@ -165,10 +171,10 @@ constructor(
}
/** Constructs a full radio address for the specific interface type. */
- fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
+ override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String =
interfaceFactory.toInterfaceAddress(interfaceId, rest)
- fun isMockInterface(): Boolean =
+ override fun isMockInterface(): Boolean =
BuildConfig.DEBUG || Settings.System.getString(context.contentResolver, "firebase.test.lab") == "true"
/**
@@ -185,7 +191,7 @@ constructor(
* where a is either x for bluetooth or s for serial and t is an interface specific address (macaddr or a device
* path)
*/
- fun getDeviceAddress(): String? {
+ override fun getDeviceAddress(): String? {
// If the user has unpaired our device, treat things as if we don't have one
var address = radioPrefs.devAddr
@@ -228,10 +234,11 @@ constructor(
}
// Handle an incoming packet from the radio, broadcasts it as an android intent
- open fun handleFromRadio(p: ByteArray) {
+ @Suppress("TooGenericExceptionCaught")
+ override fun handleFromRadio(bytes: ByteArray) {
if (logReceives) {
try {
- receivedPacketsLog.write(p)
+ receivedPacketsLog.write(bytes)
receivedPacketsLog.flush()
} catch (t: Throwable) {
Logger.w(t) { "Failed to write receive log in handleFromRadio" }
@@ -239,29 +246,33 @@ constructor(
}
try {
- processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(p) }
+ processLifecycle.coroutineScope.launch(dispatchers.io) { _receivedData.emit(bytes) }
emitReceiveActivity()
} catch (t: Throwable) {
Logger.e(t) { "RadioInterfaceService.handleFromRadio failed while emitting data" }
}
}
- fun onConnect() {
+ override fun onConnect() {
if (_connectionState.value != ConnectionState.Connected) {
broadcastConnectionChanged(ConnectionState.Connected)
}
}
- fun onDisconnect(isPermanent: Boolean) {
+ override fun onDisconnect(isPermanent: Boolean) {
val newTargetState = if (isPermanent) ConnectionState.Disconnected else ConnectionState.DeviceSleep
if (_connectionState.value != newTargetState) {
broadcastConnectionChanged(newTargetState)
}
}
- fun onDisconnect(error: BleError) {
- processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionError.emit(error) }
- onDisconnect(!error.shouldReconnect)
+ override fun onDisconnect(error: Any) {
+ if (error is BleError) {
+ processLifecycle.coroutineScope.launch(dispatchers.default) { _connectionError.emit(error) }
+ onDisconnect(!error.shouldReconnect)
+ } else {
+ onDisconnect(isPermanent = true)
+ }
}
/** Start our configured interface (if it isn't already running) */
@@ -311,8 +322,8 @@ constructor(
r.close()
// cancel any old jobs and get ready for the new ones
- serviceScope.cancel("stopping interface")
- serviceScope = CoroutineScope(dispatchers.io + SupervisorJob())
+ _serviceScope.cancel("stopping interface")
+ _serviceScope = CoroutineScope(dispatchers.io + SupervisorJob())
if (logSends) {
sentPacketsLog.close()
@@ -356,26 +367,28 @@ constructor(
true
}
- fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions { setBondedDeviceAddress(deviceAddr) }
+ override fun setDeviceAddress(deviceAddr: String?): Boolean = toRemoteExceptions {
+ setBondedDeviceAddress(deviceAddr)
+ }
/**
* If the service is not currently connected to the radio, try to connect now. At boot the radio interface service
* will not connect to a radio until this call is received.
*/
- fun connect() = toRemoteExceptions {
+ override fun connect() = toRemoteExceptions {
// We don't start actually talking to our device until MeshService binds to us - this prevents
// broadcasting connection events before MeshService is ready to receive them
startInterface()
initStateListeners()
}
- fun sendToRadio(a: ByteArray) {
+ override fun sendToRadio(bytes: ByteArray) {
// Do this in the IO thread because it might take a while (and we don't care about the result code)
- serviceScope.handledLaunch { handleSendToRadio(a) }
+ _serviceScope.handledLaunch { handleSendToRadio(bytes) }
}
private val _meshActivity = MutableSharedFlow(extraBufferCapacity = 64)
- val meshActivity: SharedFlow = _meshActivity.asSharedFlow()
+ override val meshActivity: SharedFlow = _meshActivity.asSharedFlow()
private fun emitSendActivity() {
// Use tryEmit for SharedFlow as it's non-blocking
@@ -392,9 +405,3 @@ constructor(
}
}
}
-
-sealed class MeshActivity {
- data object Send : MeshActivity()
-
- data object Receive : MeshActivity()
-}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt
index ffb34c2a8f..f511cb5554 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,41 +14,37 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package com.geeksville.mesh.repository.radio
+import org.meshtastic.core.model.InterfaceId
import javax.inject.Inject
import javax.inject.Provider
/**
* Entry point for create radio backend instances given a specific address.
*
- * This class is responsible for building and dissecting radio addresses based upon
- * their interface type and the "rest" of the address (which varies per implementation).
+ * This class is responsible for building and dissecting radio addresses based upon their interface type and the "rest"
+ * of the address (which varies per implementation).
*/
-class InterfaceFactory @Inject constructor(
+class InterfaceFactory
+@Inject
+constructor(
private val nopInterfaceFactory: NopInterfaceFactory,
- private val specMap: Map>>
+ private val specMap: Map>>,
) {
- internal val nopInterface by lazy {
- nopInterfaceFactory.create("")
- }
+ internal val nopInterface by lazy { nopInterfaceFactory.create("") }
- fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String {
- return "${interfaceId.id}$rest"
- }
+ fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = "${interfaceId.id}$rest"
fun createInterface(address: String): IRadioInterface {
val (spec, rest) = splitAddress(address)
return spec?.createInterface(rest) ?: nopInterface
}
- fun addressValid(address: String?): Boolean {
- return address?.let {
- val (spec, rest) = splitAddress(it)
- spec?.addressValid(rest)
- } ?: false
- }
+ fun addressValid(address: String?): Boolean = address?.let {
+ val (spec, rest) = splitAddress(it)
+ spec?.addressValid(rest)
+ } ?: false
private fun splitAddress(address: String): Pair?, String> {
val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it]?.get() }
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt
index d6d6ae2ea8..fc9170c6af 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,14 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package com.geeksville.mesh.repository.radio
import dagger.MapKey
+import org.meshtastic.core.model.InterfaceId
-/**
- * Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key.
- */
+/** Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key. */
@MapKey
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
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 5b67d694f4..2dc509ed28 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
@@ -27,6 +27,7 @@ import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.util.getInitials
+import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.Data
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt
index 19e0471390..aa72dfdd45 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterface.kt
@@ -18,7 +18,6 @@ package com.geeksville.mesh.repository.radio
import android.annotation.SuppressLint
import co.touchlab.kermit.Logger
-import com.geeksville.mesh.service.RadioNotConnectedException
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CompletableDeferred
@@ -58,6 +57,8 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
import org.meshtastic.core.ble.retryBleOperation
import org.meshtastic.core.common.util.nowMillis
+import org.meshtastic.core.model.RadioNotConnectedException
+import org.meshtastic.core.repository.RadioInterfaceService
import kotlin.time.Duration.Companion.seconds
private const val SCAN_RETRY_COUNT = 3
@@ -95,7 +96,7 @@ constructor(
Logger.w(e) { "[$address] Failed to disconnect in exception handler" }
}
}
- service.onDisconnect(BleError.from(throwable))
+ service.onDisconnect(error = BleError.from(throwable))
}
private val connectionScope: CoroutineScope =
@@ -152,7 +153,7 @@ constructor(
"Packet #$packetsReceived, ${packet.size} bytes (Total: $bytesReceived bytes)"
}
try {
- service.handleFromRadio(p = packet)
+ service.handleFromRadio(packet)
} catch (t: Throwable) {
Logger.e(t) { "[$address] Failed to execute service.handleFromRadio()" }
}
@@ -256,7 +257,7 @@ constructor(
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
"Packets TX: $packetsSent ($bytesSent bytes)"
}
- service.onDisconnect(BleError.Disconnected(reason = state.reason))
+ service.onDisconnect(error = BleError.Disconnected(reason = state.reason))
}
private suspend fun discoverServicesAndSetupCharacteristics() {
@@ -286,12 +287,12 @@ constructor(
service.onConnect()
} else {
Logger.w { "[$address] Discovery failed: missing required characteristics" }
- service.onDisconnect(BleError.DiscoveryFailed("One or more characteristics not found"))
+ service.onDisconnect(error = BleError.DiscoveryFailed("One or more characteristics not found"))
}
} catch (e: Exception) {
Logger.w(e) { "[$address] Service discovery failed" }
bleConnection.disconnect()
- service.onDisconnect(BleError.from(e))
+ service.onDisconnect(error = BleError.from(e))
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt
index 49f989452b..112d38e290 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceSpec.kt
@@ -31,13 +31,10 @@ constructor(
override fun createInterface(rest: String): NordicBleInterface = factory.create(rest)
/** Return true if this address is still acceptable. For BLE that means, still bonded */
- override fun addressValid(rest: String): Boolean {
- val allPaired = bluetoothRepository.state.value.bondedDevices.map { it.address }.toSet()
- return if (!allPaired.contains(rest)) {
- Logger.w { "Ignoring stale bond to ${rest.anonymize}" }
- false
- } else {
- true
- }
+ override fun addressValid(rest: String): Boolean = if (!bluetoothRepository.isBonded(rest)) {
+ Logger.w { "Ignoring stale bond to ${rest.anonymize}" }
+ false
+ } else {
+ true
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt
index 6a1d91f1a4..88d9579170 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2025 Meshtastic LLC
+ * Copyright (c) 2025-2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,7 +14,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-
package com.geeksville.mesh.repository.radio
import dagger.Binds
@@ -23,6 +22,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoMap
import dagger.multibindings.Multibinds
+import org.meshtastic.core.model.InterfaceId
@Suppress("unused") // Used by hilt
@Module
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt
index 4ebaf85d57..04d67b879f 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt
@@ -23,6 +23,7 @@ import com.geeksville.mesh.repository.usb.UsbRepository
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.meshtastic.core.common.util.nowMillis
+import org.meshtastic.core.repository.RadioInterfaceService
import java.util.concurrent.atomic.AtomicReference
/** An interface that assumes we are talking to a meshtastic device via USB serial */
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt
index 538f4088a4..973c388381 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/StreamInterface.kt
@@ -20,6 +20,7 @@ import co.touchlab.kermit.Logger
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import org.meshtastic.core.repository.RadioInterfaceService
/**
* An interface that assumes we are talking to a meshtastic device over some sort of stream connection (serial or TCP
diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt
index e2eeefa4c6..a6a8320a51 100644
--- a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt
+++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt
@@ -26,6 +26,7 @@ import org.meshtastic.core.common.util.Exceptions
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.di.CoroutineDispatchers
+import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.proto.Heartbeat
import org.meshtastic.proto.ToRadio
import java.io.BufferedInputStream
diff --git a/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt
new file mode 100644
index 0000000000..9735b0ab5c
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/service/AndroidAppWidgetUpdater.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.service
+
+import android.content.Context
+import androidx.glance.appwidget.updateAll
+import com.geeksville.mesh.widget.LocalStatsWidget
+import dagger.hilt.android.qualifiers.ApplicationContext
+import org.meshtastic.core.repository.AppWidgetUpdater
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AndroidAppWidgetUpdater @Inject constructor(@ApplicationContext private val context: Context) : AppWidgetUpdater {
+ override suspend fun updateAll() {
+ // Kickstart the widget composition.
+ // The widget internally uses collectAsState() and its own sampled StateFlow
+ // to drive updates automatically without excessive IPC and recreation.
+ @Suppress("TooGenericExceptionCaught")
+ try {
+ LocalStatsWidget().updateAll(context)
+ } catch (e: Exception) {
+ co.touchlab.kermit.Logger.e(e) { "Failed to update widgets" }
+ }
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshLocationManager.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshLocationManager.kt
similarity index 93%
rename from app/src/main/java/com/geeksville/mesh/service/MeshLocationManager.kt
rename to app/src/main/java/com/geeksville/mesh/service/AndroidMeshLocationManager.kt
index 482424a5e6..7ab35c1519 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshLocationManager.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshLocationManager.kt
@@ -29,23 +29,24 @@ import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.data.repository.LocationRepository
import org.meshtastic.core.model.Position
+import org.meshtastic.core.repository.MeshLocationManager
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
import org.meshtastic.proto.Position as ProtoPosition
@Singleton
-class MeshLocationManager
+class AndroidMeshLocationManager
@Inject
constructor(
private val context: Application,
private val locationRepository: LocationRepository,
-) {
+) : MeshLocationManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var locationFlow: Job? = null
@SuppressLint("MissingPermission")
- fun start(scope: CoroutineScope, sendPositionFn: (ProtoPosition) -> Unit) {
+ override fun start(scope: CoroutineScope, sendPositionFn: (ProtoPosition) -> Unit) {
this.scope = scope
if (locationFlow?.isActive == true) return
@@ -76,7 +77,7 @@ constructor(
}
}
- fun stop() {
+ override fun stop() {
if (locationFlow?.isActive == true) {
Logger.i { "Stopping location requests" }
locationFlow?.cancel()
diff --git a/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt
new file mode 100644
index 0000000000..8b235ea5ca
--- /dev/null
+++ b/app/src/main/java/com/geeksville/mesh/service/AndroidMeshWorkerManager.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.service
+
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.workDataOf
+import org.meshtastic.core.repository.MeshWorkerManager
+import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AndroidMeshWorkerManager @Inject constructor(private val workManager: WorkManager) : MeshWorkerManager {
+ override fun enqueueSendMessage(packetId: Int) {
+ val workRequest =
+ OneTimeWorkRequestBuilder()
+ .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId))
+ .build()
+
+ workManager.enqueueUniqueWork(
+ "${SendMessageWorker.WORK_NAME_PREFIX}$packetId",
+ ExistingWorkPolicy.REPLACE,
+ workRequest,
+ )
+ }
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt
index 3f1a85ec35..23f6b17379 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MarkAsReadReceiver.kt
@@ -25,32 +25,34 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.data.repository.PacketRepository
-import org.meshtastic.core.service.MeshServiceNotifications
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.PacketRepository
import javax.inject.Inject
/** A [BroadcastReceiver] that handles "Mark as read" actions from notifications. */
@AndroidEntryPoint
class MarkAsReadReceiver : BroadcastReceiver() {
+
@Inject lateinit var packetRepository: PacketRepository
- @Inject lateinit var meshServiceNotifications: MeshServiceNotifications
+ @Inject lateinit var serviceNotifications: MeshServiceNotifications
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
companion object {
- const val MARK_AS_READ_ACTION = "com.geeksville.mesh.MARK_AS_READ_ACTION"
- const val CONTACT_KEY = "contactKey"
+ const val MARK_AS_READ_ACTION = "com.geeksville.mesh.MARK_AS_READ"
+ const val CONTACT_KEY = "contact_key"
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == MARK_AS_READ_ACTION) {
val contactKey = intent.getStringExtra(CONTACT_KEY) ?: return
val pendingResult = goAsync()
+
scope.launch {
try {
packetRepository.clearUnreadCount(contactKey, nowMillis)
- meshServiceNotifications.cancelMessageNotification(contactKey)
+ serviceNotifications.cancelMessageNotification(contactKey)
} finally {
pendingResult.finish()
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt b/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt
deleted file mode 100644
index 1f284c7a7e..0000000000
--- a/app/src/main/java/com/geeksville/mesh/service/MeshNodeManager.kt
+++ /dev/null
@@ -1,269 +0,0 @@
-/*
- * Copyright (c) 2025-2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * 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.service
-
-import androidx.annotation.VisibleForTesting
-import co.touchlab.kermit.Logger
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.first
-import okio.ByteString
-import org.meshtastic.core.common.util.handledLaunch
-import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.data.repository.NodeRepository
-import org.meshtastic.core.database.entity.MetadataEntity
-import org.meshtastic.core.database.entity.NodeEntity
-import org.meshtastic.core.model.DataPacket
-import org.meshtastic.core.model.MyNodeInfo
-import org.meshtastic.core.model.NodeInfo
-import org.meshtastic.core.model.Position
-import org.meshtastic.core.model.util.NodeIdLookup
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.proto.DeviceMetadata
-import org.meshtastic.proto.HardwareModel
-import org.meshtastic.proto.Paxcount
-import org.meshtastic.proto.StatusMessage
-import org.meshtastic.proto.Telemetry
-import org.meshtastic.proto.User
-import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
-import javax.inject.Singleton
-import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
-import org.meshtastic.proto.Position as ProtoPosition
-
-@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
-@Singleton
-class MeshNodeManager
-@Inject
-constructor(
- private val nodeRepository: NodeRepository?,
- private val serviceBroadcasts: MeshServiceBroadcasts?,
- private val serviceNotifications: MeshServiceNotifications?,
-) : NodeIdLookup {
- private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-
- val nodeDBbyNodeNum = ConcurrentHashMap()
- val nodeDBbyID = ConcurrentHashMap()
-
- fun start(scope: CoroutineScope) {
- this.scope = scope
- }
-
- val isNodeDbReady = MutableStateFlow(false)
- val allowNodeDbWrites = MutableStateFlow(false)
-
- var myNodeNum: Int? = null
-
- companion object {
- private const val TIME_MS_TO_S = 1000L
- }
-
- @VisibleForTesting internal constructor() : this(null, null, null)
-
- fun loadCachedNodeDB() {
- scope.handledLaunch {
- val nodes = nodeRepository?.getNodeEntityDBbyNumFlow()?.first() ?: emptyMap()
- nodeDBbyNodeNum.putAll(nodes)
- nodes.values.forEach { nodeDBbyID[it.user.id] = it }
- myNodeNum = nodeRepository?.myNodeInfo?.value?.myNodeNum
- }
- }
-
- fun clear() {
- nodeDBbyNodeNum.clear()
- nodeDBbyID.clear()
- isNodeDbReady.value = false
- allowNodeDbWrites.value = false
- myNodeNum = null
- }
-
- fun getMyNodeInfo(): MyNodeInfo? {
- val mi = nodeRepository?.myNodeInfo?.value ?: return null
- val myNode = nodeDBbyNodeNum[mi.myNodeNum]
- return MyNodeInfo(
- myNodeNum = mi.myNodeNum,
- hasGPS = (myNode?.position?.latitude_i ?: 0) != 0,
- model = mi.model ?: myNode?.user?.hw_model?.name,
- firmwareVersion = mi.firmwareVersion,
- couldUpdate = mi.couldUpdate,
- shouldUpdate = mi.shouldUpdate,
- currentPacketId = mi.currentPacketId,
- messageTimeoutMsec = mi.messageTimeoutMsec,
- minAppVersion = mi.minAppVersion,
- maxChannels = mi.maxChannels,
- hasWifi = mi.hasWifi,
- channelUtilization = 0f,
- airUtilTx = 0f,
- deviceId = mi.deviceId ?: myNode?.user?.id,
- )
- }
-
- fun getMyId(): String {
- val num = myNodeNum ?: nodeRepository?.myNodeInfo?.value?.myNodeNum ?: return ""
- return nodeDBbyNodeNum[num]?.user?.id ?: ""
- }
-
- fun getNodes(): List = nodeDBbyNodeNum.values.map { it.toNodeInfo() }
-
- fun removeByNodenum(nodeNum: Int) {
- nodeDBbyNodeNum.remove(nodeNum)?.let { nodeDBbyID.remove(it.user.id) }
- }
-
- fun getOrCreateNodeInfo(n: Int, channel: Int = 0): NodeEntity = nodeDBbyNodeNum.getOrPut(n) {
- val userId = DataPacket.nodeNumToDefaultId(n)
- val defaultUser =
- User(
- id = userId,
- long_name = "Meshtastic ${userId.takeLast(n = 4)}",
- short_name = userId.takeLast(n = 4),
- hw_model = HardwareModel.UNSET,
- )
-
- NodeEntity(
- num = n,
- user = defaultUser,
- longName = defaultUser.long_name,
- shortName = defaultUser.short_name,
- channel = channel,
- )
- }
-
- fun updateNodeInfo(nodeNum: Int, withBroadcast: Boolean = true, channel: Int = 0, updateFn: (NodeEntity) -> Unit) {
- val info = getOrCreateNodeInfo(nodeNum, channel)
- updateFn(info)
- if (info.user.id.isNotEmpty()) {
- nodeDBbyID[info.user.id] = info
- }
-
- if (info.user.id.isNotEmpty() && isNodeDbReady.value) {
- scope.handledLaunch { nodeRepository?.upsert(info) }
- }
-
- if (withBroadcast) {
- serviceBroadcasts?.broadcastNodeChange(info.toNodeInfo())
- }
- }
-
- fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) {
- scope.handledLaunch { nodeRepository?.insertMetadata(MetadataEntity(nodeNum, metadata)) }
- }
-
- fun handleReceivedUser(fromNum: Int, p: User, channel: Int = 0, manuallyVerified: Boolean = false) {
- updateNodeInfo(fromNum) {
- val newNode = (it.isUnknownUser && p.hw_model != HardwareModel.UNSET)
- val shouldPreserve = shouldPreserveExistingUser(it.user, p)
-
- if (shouldPreserve) {
- it.longName = it.user.long_name
- it.shortName = it.user.short_name
- it.channel = channel
- it.manuallyVerified = manuallyVerified
- } else {
- val keyMatch = !it.hasPKC || it.user.public_key == p.public_key
- it.user = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY)
- it.longName = p.long_name
- it.shortName = p.short_name
- it.channel = channel
- it.manuallyVerified = manuallyVerified
- if (newNode) {
- serviceNotifications?.showNewNodeSeenNotification(it)
- }
- }
- }
- }
-
- fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long = nowMillis) {
- if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) {
- Logger.d { "Ignoring nop position update for the local node" }
- } else {
- updateNodeInfo(fromNum) { it.setPosition(p, (defaultTime / TIME_MS_TO_S).toInt()) }
- }
- }
-
- fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) {
- updateNodeInfo(fromNum) { nodeEntity ->
- when {
- telemetry.device_metrics != null -> nodeEntity.deviceTelemetry = telemetry
- telemetry.environment_metrics != null -> nodeEntity.environmentTelemetry = telemetry
- telemetry.power_metrics != null -> nodeEntity.powerTelemetry = telemetry
- }
- }
- }
-
- fun handleReceivedPaxcounter(fromNum: Int, p: Paxcount) {
- updateNodeInfo(fromNum) { it.paxcounter = p }
- }
-
- fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) {
- updateNodeStatus(fromNum, s.status)
- }
-
- fun updateNodeStatus(nodeNum: Int, status: String?) {
- updateNodeInfo(nodeNum) { it.nodeStatus = status?.takeIf { s -> s.isNotEmpty() } }
- }
-
- fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean = true) {
- updateNodeInfo(info.num, withBroadcast = withBroadcast) { entity ->
- val user = info.user
- if (user != null) {
- if (shouldPreserveExistingUser(entity.user, user)) {
- entity.longName = entity.user.long_name
- entity.shortName = entity.user.short_name
- } else {
- var newUser = user.let { if (it.is_licensed) it.copy(public_key = ByteString.EMPTY) else it }
- if (info.via_mqtt) {
- newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)")
- }
- entity.user = newUser
- entity.longName = newUser.long_name
- entity.shortName = newUser.short_name
- }
- }
- val position = info.position
- if (position != null) {
- entity.position = position
- entity.latitude = Position.degD(position.latitude_i ?: 0)
- entity.longitude = Position.degD(position.longitude_i ?: 0)
- }
- entity.lastHeard = info.last_heard
- if (info.device_metrics != null) {
- entity.deviceTelemetry = Telemetry(device_metrics = info.device_metrics)
- }
- entity.channel = info.channel
- entity.viaMqtt = info.via_mqtt
- entity.hopsAway = info.hops_away ?: -1
- entity.isFavorite = info.is_favorite
- entity.isIgnored = info.is_ignored
- entity.isMuted = info.is_muted
- }
- }
-
- private fun shouldPreserveExistingUser(existing: User, incoming: User): Boolean {
- val isDefaultName = (incoming.long_name).matches(Regex("^Meshtastic [0-9a-fA-F]{4}$"))
- val isDefaultHwModel = incoming.hw_model == HardwareModel.UNSET
- val hasExistingUser = (existing.id).isNotEmpty() && existing.hw_model != HardwareModel.UNSET
- return hasExistingUser && isDefaultName && isDefaultHwModel
- }
-
- override fun toNodeID(nodeNum: Int): String = if (nodeNum == DataPacket.NODENUM_BROADCAST) {
- DataPacket.ID_BROADCAST
- } else {
- nodeDBbyNodeNum[nodeNum]?.user?.id ?: DataPacket.nodeNumToDefaultId(nodeNum)
- }
-}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt b/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt
deleted file mode 100644
index b61bb6e02e..0000000000
--- a/app/src/main/java/com/geeksville/mesh/service/MeshRouter.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (c) 2025-2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * 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.service
-
-import kotlinx.coroutines.CoroutineScope
-import javax.inject.Inject
-import javax.inject.Singleton
-
-/**
- * Orchestrates the specialized packet handlers for the [MeshService]. This class serves as a central registry and
- * lifecycle manager for all routing sub-components.
- */
-@Suppress("LongParameterList")
-@Singleton
-class MeshRouter
-@Inject
-constructor(
- val dataHandler: MeshDataHandler,
- val configHandler: MeshConfigHandler,
- val tracerouteHandler: MeshTracerouteHandler,
- val neighborInfoHandler: MeshNeighborInfoHandler,
- val configFlowManager: MeshConfigFlowManager,
- val mqttManager: MeshMqttManager,
- val actionHandler: MeshActionHandler,
-) {
- fun start(scope: CoroutineScope) {
- dataHandler.start(scope)
- configHandler.start(scope)
- tracerouteHandler.start(scope)
- neighborInfoHandler.start(scope)
- configFlowManager.start(scope)
- actionHandler.start(scope)
- }
-}
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 2f01f33680..cf97cd5c24 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -25,7 +25,6 @@ import android.os.IBinder
import androidx.core.app.ServiceCompat
import co.touchlab.kermit.Logger
import com.geeksville.mesh.BuildConfig
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
@@ -36,17 +35,27 @@ import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.common.hasLocationPermission
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.toRemoteExceptions
-import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.DeviceVersion
import org.meshtastic.core.model.MeshUser
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.Position
+import org.meshtastic.core.model.RadioNotConnectedException
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.MeshConnectionManager
+import org.meshtastic.core.repository.MeshLocationManager
+import org.meshtastic.core.repository.MeshMessageProcessor
+import org.meshtastic.core.repository.MeshRouter
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.RadioInterfaceService
+import org.meshtastic.core.repository.SERVICE_NOTIFY_ID
+import org.meshtastic.core.repository.ServiceBroadcasts
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.service.IMeshService
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.SERVICE_NOTIFY_ID
-import org.meshtastic.core.service.ServiceRepository
import org.meshtastic.proto.PortNum
import javax.inject.Inject
@@ -58,17 +67,15 @@ class MeshService : Service() {
@Inject lateinit var serviceRepository: ServiceRepository
- @Inject lateinit var connectionStateHolder: ConnectionStateHandler
-
@Inject lateinit var packetHandler: PacketHandler
- @Inject lateinit var serviceBroadcasts: MeshServiceBroadcasts
+ @Inject lateinit var serviceBroadcasts: ServiceBroadcasts
- @Inject lateinit var nodeManager: MeshNodeManager
+ @Inject lateinit var nodeManager: NodeManager
@Inject lateinit var messageProcessor: MeshMessageProcessor
- @Inject lateinit var commandSender: MeshCommandSender
+ @Inject lateinit var commandSender: CommandSender
@Inject lateinit var locationManager: MeshLocationManager
@@ -90,7 +97,7 @@ class MeshService : Service() {
fun actionReceived(portNum: Int): String {
val portType = PortNum.fromValue(portNum)
val portStr = portType?.toString() ?: portNum.toString()
- return com.geeksville.mesh.service.actionReceived(portStr)
+ return actionReceived(portStr)
}
fun createIntent(context: Context) = Intent(context, MeshService::class.java)
@@ -143,7 +150,7 @@ class MeshService : Service() {
val a = radioInterfaceService.getDeviceAddress()
val wantForeground = a != null && a != NO_DEVICE_SELECTED
- val notification = connectionManager.updateStatusNotification()
+ val notification = connectionManager.updateStatusNotification() as android.app.Notification
val foregroundServiceType =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -311,7 +318,7 @@ class MeshService : Service() {
override fun getNodes(): List = nodeManager.getNodes()
- override fun connectionState(): String = connectionStateHolder.connectionState.value.toString()
+ override fun connectionState(): String = serviceRepository.connectionState.value.toString()
override fun startProvideLocation() {
locationManager.start(serviceScope) { commandSender.sendPosition(it) }
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
index babdc55650..47b0a7fb28 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotificationsImpl.kt
@@ -47,13 +47,15 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.data.repository.NodeRepository
-import org.meshtastic.core.data.repository.PacketRepository
-import org.meshtastic.core.database.entity.NodeEntity
-import org.meshtastic.core.database.model.Message
import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.model.Message
+import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.formatUptime
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.SERVICE_NOTIFY_ID
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.client_notification
import org.meshtastic.core.resources.getString
@@ -86,8 +88,6 @@ import org.meshtastic.core.resources.no_local_stats
import org.meshtastic.core.resources.powered
import org.meshtastic.core.resources.reply
import org.meshtastic.core.resources.you
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.SERVICE_NOTIFY_ID
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.DeviceMetrics
import org.meshtastic.proto.LocalStats
@@ -309,16 +309,14 @@ constructor(
if (myNodeNum != null) {
// We use runBlocking here because this is called from MeshConnectionManager's synchronous methods,
// and we only do this once if the cache is empty.
- val nodes = runBlocking { repo.getNodeEntityDBbyNumFlow().first() }
- nodes[myNodeNum]?.let { entity ->
+ val nodes = runBlocking { repo.nodeDBbyNum.first() }
+ nodes[myNodeNum]?.let { node ->
if (cachedDeviceMetrics == null) {
- cachedDeviceMetrics = entity.deviceTelemetry.device_metrics
+ cachedDeviceMetrics = node.deviceMetrics
}
if (cachedLocalStats == null) {
// Fallback to DB stats if repository hasn't received any fresh ones yet
- cachedLocalStats =
- repo.localStats.value.takeIf { it.uptime_seconds != 0 }
- ?: entity.deviceTelemetry.local_stats
+ cachedLocalStats = repo.localStats.value.takeIf { it.uptime_seconds != 0 }
}
}
}
@@ -477,12 +475,12 @@ constructor(
notificationManager.notify(name.hashCode(), notification)
}
- override fun showNewNodeSeenNotification(node: NodeEntity) {
+ override fun showNewNodeSeenNotification(node: Node) {
val notification = createNewNodeSeenNotification(node.user.short_name, node.user.long_name, node.num)
notificationManager.notify(node.num, notification)
}
- override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {
+ override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {
val notification = createLowBatteryNotification(node, isRemote)
notificationManager.notify(node.num, notification)
}
@@ -495,7 +493,7 @@ constructor(
override fun cancelMessageNotification(contactKey: String) = notificationManager.cancel(contactKey.hashCode())
- override fun cancelLowBatteryNotification(node: NodeEntity) = notificationManager.cancel(node.num)
+ override fun cancelLowBatteryNotification(node: Node) = notificationManager.cancel(node.num)
override fun clearClientNotification(notification: ClientNotification) =
notificationManager.cancel(notification.toString().hashCode())
@@ -673,11 +671,11 @@ constructor(
return builder.build()
}
- private fun createLowBatteryNotification(node: NodeEntity, isRemote: Boolean): Notification {
+ private fun createLowBatteryNotification(node: Node, isRemote: Boolean): Notification {
val type = if (isRemote) NotificationType.LowBatteryRemote else NotificationType.LowBatteryLocal
- val title = getString(Res.string.low_battery_title).format(node.shortName)
- val batteryLevel = node.deviceMetrics?.battery_level ?: 0
- val message = getString(Res.string.low_battery_message).format(node.longName, batteryLevel)
+ val title = getString(Res.string.low_battery_title).format(node.user.short_name)
+ val batteryLevel = node.deviceMetrics.battery_level ?: 0
+ val message = getString(Res.string.low_battery_message).format(node.user.long_name, batteryLevel)
return commonBuilder(type, createOpenNodeDetailIntent(node.num))
.setCategory(Notification.CATEGORY_STATUS)
diff --git a/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt b/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt
index 8462d8ec96..bea76c1478 100644
--- a/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/ReactionReceiver.kt
@@ -25,8 +25,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
-import org.meshtastic.core.service.ServiceAction
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.model.service.ServiceAction
+import org.meshtastic.core.repository.ServiceRepository
import javax.inject.Inject
@AndroidEntryPoint
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 a80839176c..e210396708 100644
--- a/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/ReplyReceiver.kt
@@ -17,12 +17,19 @@
package com.geeksville.mesh.service
import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
import androidx.core.app.RemoteInput
import dagger.hilt.android.AndroidEntryPoint
import jakarta.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
import org.meshtastic.core.model.DataPacket
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.model.RadioController
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.ServiceRepository
/**
* A [BroadcastReceiver] that handles inline replies from notifications.
@@ -33,32 +40,42 @@ import org.meshtastic.core.service.ServiceRepository
*/
@AndroidEntryPoint
class ReplyReceiver : BroadcastReceiver() {
- @Inject lateinit var serviceRepository: ServiceRepository
+ @Inject lateinit var radioController: RadioController
@Inject lateinit var meshServiceNotifications: MeshServiceNotifications
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
companion object {
const val REPLY_ACTION = "com.geeksville.mesh.REPLY_ACTION"
const val CONTACT_KEY = "contactKey"
const val KEY_TEXT_REPLY = "key_text_reply"
}
- private fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
- // contactKey: unique contact key filter (channel)+(nodeId)
- val channel = contactKey[0].digitToIntOrNull()
- val dest = if (channel != null) contactKey.substring(1) else contactKey
- val p = DataPacket(dest, channel ?: 0, str)
- serviceRepository.meshService?.send(p)
- }
-
- override fun onReceive(context: android.content.Context, intent: android.content.Intent) {
+ override fun onReceive(context: Context, intent: Intent) {
val remoteInput = RemoteInput.getResultsFromIntent(intent)
if (remoteInput != null) {
val contactKey = intent.getStringExtra(CONTACT_KEY) ?: ""
val message = remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString() ?: ""
- sendMessage(message, contactKey)
- meshServiceNotifications.cancelMessageNotification(contactKey)
+
+ val pendingResult = goAsync()
+ scope.launch {
+ try {
+ sendMessage(message, contactKey)
+ meshServiceNotifications.cancelMessageNotification(contactKey)
+ } finally {
+ pendingResult.finish()
+ }
+ }
}
}
+
+ private suspend fun sendMessage(str: String, contactKey: String) {
+ // contactKey: unique contact key filter (channel)+(nodeId)
+ val channel = contactKey.getOrNull(0)?.digitToIntOrNull()
+ val dest = if (channel != null) contactKey.substring(1) else contactKey
+ val p = DataPacket(dest, channel ?: 0, str)
+ radioController.sendMessage(p)
+ }
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt
similarity index 62%
rename from app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
rename to app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt
index 34ce09dec3..99d0bc7246 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/ServiceBroadcasts.kt
@@ -24,57 +24,99 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
+import org.meshtastic.core.model.Node
import org.meshtastic.core.model.NodeInfo
import org.meshtastic.core.model.util.toPIIString
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.ServiceRepository
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
+import org.meshtastic.core.repository.ServiceBroadcasts as SharedServiceBroadcasts
@Singleton
-class MeshServiceBroadcasts
+class ServiceBroadcasts
@Inject
constructor(
@ApplicationContext private val context: Context,
- private val connectionStateHolder: ConnectionStateHandler,
private val serviceRepository: ServiceRepository,
-) {
+) : SharedServiceBroadcasts {
// A mapping of receiver class name to package name - used for explicit broadcasts
private val clientPackages = mutableMapOf()
- fun subscribeReceiver(receiverName: String, packageName: String) {
+ override fun subscribeReceiver(receiverName: String, packageName: String) {
clientPackages[receiverName] = packageName
}
/** Broadcast some received data Payload will be a DataPacket */
- fun broadcastReceivedData(payload: DataPacket) {
- val action = MeshService.actionReceived(payload.dataType)
- explicitBroadcast(Intent(action).putExtra(EXTRA_PAYLOAD, payload))
+ override fun broadcastReceivedData(dataPacket: DataPacket) {
+ val action = MeshService.actionReceived(dataPacket.dataType)
+ explicitBroadcast(Intent(action).putExtra(EXTRA_PAYLOAD, dataPacket))
// Also broadcast with the numeric port number for backwards compatibility with some apps
- val numericAction = actionReceived(payload.dataType.toString())
+ val numericAction = actionReceived(dataPacket.dataType.toString())
if (numericAction != action) {
- explicitBroadcast(Intent(numericAction).putExtra(EXTRA_PAYLOAD, payload))
+ explicitBroadcast(Intent(numericAction).putExtra(EXTRA_PAYLOAD, dataPacket))
}
}
- fun broadcastNodeChange(info: NodeInfo) {
- Logger.d { "Broadcasting node change ${info.user?.toPIIString()}" }
- val intent = Intent(ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, info)
+ override fun broadcastNodeChange(node: Node) {
+ Logger.d { "Broadcasting node change ${node.user.toPIIString()}" }
+ val legacy = node.toLegacy()
+ val intent = Intent(ACTION_NODE_CHANGE).putExtra(EXTRA_NODEINFO, legacy)
explicitBroadcast(intent)
}
- fun broadcastMessageStatus(p: DataPacket) = broadcastMessageStatus(p.id, p.status)
+ private fun Node.toLegacy(): NodeInfo = NodeInfo(
+ num = num,
+ user =
+ org.meshtastic.core.model.MeshUser(
+ id = user.id,
+ longName = user.long_name,
+ shortName = user.short_name,
+ hwModel = user.hw_model,
+ role = user.role.value,
+ ),
+ position =
+ org.meshtastic.core.model
+ .Position(
+ latitude = latitude,
+ longitude = longitude,
+ altitude = position.altitude ?: 0,
+ time = position.time,
+ satellitesInView = position.sats_in_view ?: 0,
+ groundSpeed = position.ground_speed ?: 0,
+ groundTrack = position.ground_track ?: 0,
+ precisionBits = position.precision_bits ?: 0,
+ )
+ .takeIf { latitude != 0.0 || longitude != 0.0 },
+ snr = snr,
+ rssi = rssi,
+ lastHeard = lastHeard,
+ deviceMetrics =
+ org.meshtastic.core.model.DeviceMetrics(
+ batteryLevel = deviceMetrics.battery_level ?: 0,
+ voltage = deviceMetrics.voltage ?: 0f,
+ channelUtilization = deviceMetrics.channel_utilization ?: 0f,
+ airUtilTx = deviceMetrics.air_util_tx ?: 0f,
+ uptimeSeconds = deviceMetrics.uptime_seconds ?: 0,
+ ),
+ channel = channel,
+ environmentMetrics = org.meshtastic.core.model.EnvironmentMetrics.fromTelemetryProto(environmentMetrics, 0),
+ hopsAway = hopsAway,
+ nodeStatus = nodeStatus,
+ )
- fun broadcastMessageStatus(id: Int, status: MessageStatus?) {
- if (id == 0) {
+ fun broadcastMessageStatus(p: DataPacket) = broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN)
+
+ override fun broadcastMessageStatus(packetId: Int, status: MessageStatus) {
+ if (packetId == 0) {
Logger.d { "Ignoring anonymous packet status" }
} else {
// Do not log, contains PII possibly
// MeshService.Logger.d { "Broadcasting message status $p" }
val intent =
Intent(ACTION_MESSAGE_STATUS).apply {
- putExtra(EXTRA_PACKET_ID, id)
+ putExtra(EXTRA_PACKET_ID, packetId)
putExtra(EXTRA_STATUS, status as Parcelable)
}
explicitBroadcast(intent)
@@ -82,14 +124,13 @@ constructor(
}
/** Broadcast our current connection status */
- fun broadcastConnection() {
- val connectionState = connectionStateHolder.connectionState.value
+ override fun broadcastConnection() {
+ val connectionState = serviceRepository.connectionState.value
// ATAK expects a String: "CONNECTED" or "DISCONNECTED"
// It uses equalsIgnoreCase, but we'll use uppercase to be specific.
val stateStr = connectionState.toString().uppercase(Locale.ROOT)
val intent = Intent(ACTION_MESH_CONNECTED).apply { putExtra(EXTRA_CONNECTED, stateStr) }
- serviceRepository.setConnectionState(connectionState)
explicitBroadcast(intent)
if (connectionState == ConnectionState.Disconnected) {
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 f28f98114f..f41dcd8e1b 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt
@@ -64,7 +64,6 @@ import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -85,7 +84,6 @@ import com.geeksville.mesh.navigation.firmwareGraph
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.MeshService
import com.geeksville.mesh.ui.connections.DeviceType
import com.geeksville.mesh.ui.connections.ScannerViewModel
@@ -98,6 +96,7 @@ import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceVersion
+import org.meshtastic.core.model.MeshActivity
import org.meshtastic.core.navigation.ConnectionsRoutes
import org.meshtastic.core.navigation.ContactsRoutes
import org.meshtastic.core.navigation.MapRoutes
@@ -464,7 +463,6 @@ fun MainScreen(uIViewModel: UIViewModel = hiltViewModel(), scanModel: ScannerVie
private fun VersionChecks(viewModel: UIViewModel) {
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle()
- val context = LocalContext.current
val myFirmwareVersion = myNodeInfo?.firmwareVersion
@@ -499,10 +497,7 @@ private fun VersionChecks(viewModel: UIViewModel) {
viewModel.showAlert(
titleRes = Res.string.app_too_old,
messageRes = Res.string.must_update,
- onConfirm = {
- val service = viewModel.meshService ?: return@showAlert
- MeshService.changeDeviceAddress(context, service, "n")
- },
+ onConfirm = { viewModel.setDeviceAddress("n") },
)
} else {
myFirmwareVersion
@@ -526,10 +521,7 @@ private fun VersionChecks(viewModel: UIViewModel) {
viewModel.showAlert(
title = title,
html = message,
- onConfirm = {
- val service = viewModel.meshService ?: return@showAlert
- MeshService.changeDeviceAddress(context, service, "n")
- },
+ onConfirm = { viewModel.setDeviceAddress("n") },
)
} else if (curVer < MeshService.minDeviceVersion) {
Logger.w {
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 88e9391f58..b17281ff6b 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,12 +21,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import org.meshtastic.core.data.repository.NodeRepository
-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.model.MyNodeInfo
+import org.meshtastic.core.model.Node
import org.meshtastic.core.prefs.ui.UiPrefs
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalConfig
import javax.inject.Inject
@@ -46,7 +46,7 @@ constructor(
val connectionState = serviceRepository.connectionState
- val myNodeInfo: StateFlow = nodeRepository.myNodeInfo
+ val myNodeInfo: StateFlow = nodeRepository.myNodeInfo
val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt
index 131eb33e8f..0bfba1faf0 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ScannerViewModel.kt
@@ -16,18 +16,13 @@
*/
package com.geeksville.mesh.ui.connections
-import android.app.Application
-import android.content.Context
-import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.geeksville.mesh.domain.usecase.GetDiscoveredDevicesUseCase
import com.geeksville.mesh.model.DeviceListEntry
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.usb.UsbRepository
-import com.geeksville.mesh.service.MeshService
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -42,8 +37,10 @@ import kotlinx.coroutines.launch
import org.meshtastic.core.ble.BluetoothRepository
import org.meshtastic.core.datastore.RecentAddressesDataSource
import org.meshtastic.core.datastore.model.RecentAddress
+import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.util.anonymize
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.RadioInterfaceService
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import javax.inject.Inject
@@ -52,17 +49,14 @@ import javax.inject.Inject
class ScannerViewModel
@Inject
constructor(
- private val application: Application,
private val serviceRepository: ServiceRepository,
+ private val radioController: RadioController,
private val bluetoothRepository: BluetoothRepository,
private val usbRepository: UsbRepository,
private val radioInterfaceService: RadioInterfaceService,
private val recentAddressesDataSource: RecentAddressesDataSource,
private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
) : ViewModel() {
- private val context: Context
- get() = application.applicationContext
-
val showMockInterface: StateFlow = MutableStateFlow(radioInterfaceService.isMockInterface()).asStateFlow()
private val _errorText = MutableStateFlow(null)
@@ -117,11 +111,8 @@ constructor(
}
private fun changeDeviceAddress(address: String) {
- try {
- serviceRepository.meshService?.let { service -> MeshService.changeDeviceAddress(context, service, address) }
- } catch (ex: RemoteException) {
- Logger.e(ex) { "changeDeviceSelection failed, probably it is shutting down" }
- }
+ Logger.i { "Attempting to change device address to ${address.anonymize()}" }
+ radioController.setDeviceAddress(address)
}
/** Initiates the bonding process and connects to the device upon success. */
diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt
index eb359ca00d..9bf5f3fbc9 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/connections/components/CurrentlyConnectedInfo.kt
@@ -47,7 +47,7 @@ import no.nordicsemi.android.common.ui.view.RssiIcon
import no.nordicsemi.kotlin.ble.client.exception.OperationFailedException
import no.nordicsemi.kotlin.ble.client.exception.PeripheralNotConnectedException
import org.jetbrains.compose.resources.stringResource
-import org.meshtastic.core.database.model.Node
+import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.disconnect
import org.meshtastic.core.resources.firmware_version
diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt
index dc5f2a7b45..c5ba9bec44 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ChannelViewModel.kt
@@ -17,7 +17,6 @@
package com.geeksville.mesh.ui.sharing
import android.net.Uri
-import android.os.RemoteException
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
@@ -27,9 +26,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.meshtastic.core.analytics.DataPair
import org.meshtastic.core.analytics.platform.PlatformAnalytics
-import org.meshtastic.core.data.repository.RadioConfigRepository
+import org.meshtastic.core.model.RadioController
import org.meshtastic.core.model.util.toChannelSet
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.ui.util.getChannelList
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.Channel
@@ -42,12 +41,12 @@ import javax.inject.Inject
class ChannelViewModel
@Inject
constructor(
- private val serviceRepository: ServiceRepository,
+ private val radioController: RadioController,
private val radioConfigRepository: RadioConfigRepository,
private val analytics: PlatformAnalytics,
) : ViewModel() {
- val connectionState = serviceRepository.connectionState
+ val connectionState = radioController.connectionState
val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
@@ -95,20 +94,12 @@ constructor(
}
fun setChannel(channel: Channel) {
- try {
- serviceRepository.meshService?.setChannel(channel.encode())
- } catch (ex: RemoteException) {
- Logger.e(ex) { "Set channel error" }
- }
+ viewModelScope.launch { radioController.setLocalChannel(channel) }
}
// Set the radio config (also updates our saved copy in preferences)
fun setConfig(config: Config) {
- try {
- serviceRepository.meshService?.setConfig(config.encode())
- } catch (ex: RemoteException) {
- Logger.e(ex) { "Set config error" }
- }
+ viewModelScope.launch { radioController.setLocalConfig(config) }
}
fun trackShare() {
diff --git a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt
index eafbe38a2f..1f28a65f7c 100644
--- a/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt
+++ b/app/src/main/java/com/geeksville/mesh/widget/LocalStatsWidgetState.kt
@@ -28,11 +28,11 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.data.repository.NodeRepository
-import org.meshtastic.core.database.model.Node
import org.meshtastic.core.model.ConnectionState
+import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.onlineTimeThreshold
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.LocalStats
import javax.inject.Inject
import javax.inject.Singleton
diff --git a/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt b/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt
index 16d6b566ed..6a044c90e5 100644
--- a/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt
+++ b/app/src/main/java/com/geeksville/mesh/widget/RefreshLocalStatsAction.kt
@@ -20,22 +20,22 @@ import android.content.Context
import androidx.glance.GlanceId
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
-import com.geeksville.mesh.service.MeshCommandSender
-import com.geeksville.mesh.service.MeshNodeManager
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import org.meshtastic.core.model.TelemetryType
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.NodeManager
class RefreshLocalStatsAction : ActionCallback {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface RefreshLocalStatsEntryPoint {
- fun commandSender(): MeshCommandSender
+ fun commandSender(): CommandSender
- fun nodeManager(): MeshNodeManager
+ fun nodeManager(): NodeManager
}
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
diff --git a/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt b/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt
index d980d265e3..a468896fb3 100644
--- a/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt
+++ b/app/src/main/java/com/geeksville/mesh/worker/ServiceKeepAliveWorker.kt
@@ -31,8 +31,8 @@ import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.startService
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.SERVICE_NOTIFY_ID
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.SERVICE_NOTIFY_ID
/**
* A worker whose sole purpose is to start the MeshService from the background. This is used as a fallback when
diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt
index eb4ac385d1..41cceafe24 100644
--- a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt
+++ b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceRetryTest.kt
@@ -44,6 +44,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
+import org.meshtastic.core.repository.RadioInterfaceService
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalCoroutinesApi::class)
diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt
index 2974d30295..1ee5ff9eed 100644
--- a/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt
+++ b/app/src/test/java/com/geeksville/mesh/repository/radio/NordicBleInterfaceTest.kt
@@ -47,6 +47,7 @@ import org.meshtastic.core.ble.MeshtasticBleConstants.FROMRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.LOGRADIO_CHARACTERISTIC
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
import org.meshtastic.core.ble.MeshtasticBleConstants.TORADIO_CHARACTERISTIC
+import org.meshtastic.core.repository.RadioInterfaceService
import kotlin.time.Duration.Companion.milliseconds
@OptIn(ExperimentalCoroutinesApi::class)
@@ -662,7 +663,7 @@ class NordicBleInterfaceTest {
advanceUntilIdle()
// Verify handleFromRadio was called directly with the payload
- verify(timeout = 2000) { service.handleFromRadio(p = payload) }
+ verify(timeout = 2000) { service.handleFromRadio(payload) }
nordicInterface.close()
}
diff --git a/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt b/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt
index b0ddc037ea..868c5197f3 100644
--- a/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt
+++ b/app/src/test/java/com/geeksville/mesh/repository/radio/StreamInterfaceTest.kt
@@ -20,6 +20,7 @@ import io.mockk.confirmVerified
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test
+import org.meshtastic.core.repository.RadioInterfaceService
class StreamInterfaceTest {
diff --git a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt
index 19b187bdc0..86ecc7fb93 100644
--- a/app/src/test/java/com/geeksville/mesh/service/Fakes.kt
+++ b/app/src/test/java/com/geeksville/mesh/service/Fakes.kt
@@ -17,10 +17,10 @@
package com.geeksville.mesh.service
import android.app.Notification
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
import io.mockk.mockk
-import org.meshtastic.core.database.entity.NodeEntity
-import org.meshtastic.core.service.MeshServiceNotifications
+import org.meshtastic.core.model.Node
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.proto.ClientNotification
import org.meshtastic.proto.Telemetry
@@ -64,15 +64,15 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
override fun showAlertNotification(contactKey: String, name: String, alert: String) {}
- override fun showNewNodeSeenNotification(node: NodeEntity) {}
+ override fun showNewNodeSeenNotification(node: Node) {}
- override fun showOrUpdateLowBatteryNotification(node: NodeEntity, isRemote: Boolean) {}
+ override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {}
override fun showClientNotification(clientNotification: ClientNotification) {}
override fun cancelMessageNotification(contactKey: String) {}
- override fun cancelLowBatteryNotification(node: NodeEntity) {}
+ override fun cancelLowBatteryNotification(node: Node) {}
override fun clearClientNotification(notification: ClientNotification) {}
}
diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt b/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt
deleted file mode 100644
index 9b3aa4cfcc..0000000000
--- a/app/src/test/java/com/geeksville/mesh/service/MeshMessageProcessorTest.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (c) 2026 Meshtastic LLC
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * 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.service
-
-import io.mockk.coVerify
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.verify
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.meshtastic.core.data.repository.MeshLogRepository
-import org.meshtastic.core.database.entity.MeshLog
-import org.meshtastic.core.service.ServiceRepository
-import org.meshtastic.proto.Data
-import org.meshtastic.proto.MeshPacket
-import org.meshtastic.proto.PortNum
-
-class MeshMessageProcessorTest {
-
- private val nodeManager: MeshNodeManager = mockk(relaxed = true)
- private val serviceRepository: ServiceRepository = mockk(relaxed = true)
- private val meshLogRepository: MeshLogRepository = mockk(relaxed = true)
- private val router: MeshRouter = mockk(relaxed = true)
- private val fromRadioDispatcher: FromRadioPacketHandler = mockk(relaxed = true)
- private val meshLogRepositoryLazy = dagger.Lazy { meshLogRepository }
- private val dataHandler: MeshDataHandler = mockk(relaxed = true)
-
- private val isNodeDbReady = MutableStateFlow(false)
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
-
- private lateinit var processor: MeshMessageProcessor
-
- @Before
- fun setUp() {
- every { nodeManager.isNodeDbReady } returns isNodeDbReady
- every { router.dataHandler } returns dataHandler
- processor =
- MeshMessageProcessor(nodeManager, serviceRepository, meshLogRepositoryLazy, router, fromRadioDispatcher)
- processor.start(testScope)
- }
-
- @Test
- fun `early packets are buffered and flushed when DB is ready`() = runTest(testDispatcher) {
- val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
-
- // 1. Database is NOT ready
- isNodeDbReady.value = false
- testScheduler.runCurrent() // trigger start() onEach
-
- processor.handleReceivedMeshPacket(packet, 999)
-
- // Verify that handleReceivedData has NOT been called yet
- verify(exactly = 0) { dataHandler.handleReceivedData(any(), any(), any(), any()) }
-
- // 2. Database becomes ready
- isNodeDbReady.value = true
- testScheduler.runCurrent() // trigger onEach(true)
-
- // Verify that handleReceivedData is now called with the buffered packet
- verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 123 }, any(), any(), any()) }
- }
-
- @Test
- fun `packets are processed immediately if DB is already ready`() = runTest(testDispatcher) {
- val packet = MeshPacket(id = 456, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
-
- isNodeDbReady.value = true
- testScheduler.runCurrent()
-
- processor.handleReceivedMeshPacket(packet, 999)
-
- verify(exactly = 1) { dataHandler.handleReceivedData(match { it.id == 456 }, any(), any(), any()) }
- }
-
- @Test
- fun `packets from local node are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) {
- val myNodeNum = 1234
- val packet = MeshPacket(from = myNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
-
- isNodeDbReady.value = true
- testScheduler.runCurrent()
-
- processor.handleReceivedMeshPacket(packet, myNodeNum)
- testScheduler.runCurrent() // wait for log insert job
-
- coVerify { meshLogRepository.insert(match { log -> log.fromNum == MeshLog.NODE_NUM_LOCAL }) }
- }
-
- @Test
- fun `packets from remote nodes are logged with their node number`() = runTest(testDispatcher) {
- val myNodeNum = 1234
- val remoteNodeNum = 5678
- val packet = MeshPacket(from = remoteNodeNum, id = 789, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
-
- isNodeDbReady.value = true
- testScheduler.runCurrent()
-
- processor.handleReceivedMeshPacket(packet, myNodeNum)
- testScheduler.runCurrent()
-
- coVerify { meshLogRepository.insert(match { log -> log.fromNum == remoteNodeNum }) }
- }
-}
diff --git a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt b/app/src/test/java/com/geeksville/mesh/service/ServiceBroadcastsTest.kt
similarity index 82%
rename from app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt
rename to app/src/test/java/com/geeksville/mesh/service/ServiceBroadcastsTest.kt
index 88cee4a4bc..3ddfecd61b 100644
--- a/app/src/test/java/com/geeksville/mesh/service/MeshServiceBroadcastsTest.kt
+++ b/app/src/test/java/com/geeksville/mesh/service/ServiceBroadcastsTest.kt
@@ -19,35 +19,36 @@ package com.geeksville.mesh.service
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
+import io.mockk.every
import io.mockk.mockk
+import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.model.ConnectionState
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.ServiceRepository
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
-class MeshServiceBroadcastsTest {
+class ServiceBroadcastsTest {
private lateinit var context: Context
- private val connectionStateHolder = ConnectionStateHandler()
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
- private lateinit var broadcasts: MeshServiceBroadcasts
+ private lateinit var broadcasts: ServiceBroadcasts
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
- broadcasts = MeshServiceBroadcasts(context, connectionStateHolder, serviceRepository)
+ broadcasts = ServiceBroadcasts(context, serviceRepository)
}
@Test
fun `broadcastConnection sends uppercase state string for ATAK`() {
- connectionStateHolder.setState(ConnectionState.Connected)
+ every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected)
broadcasts.broadcastConnection()
@@ -58,7 +59,7 @@ class MeshServiceBroadcastsTest {
@Test
fun `broadcastConnection sends legacy connection intent`() {
- connectionStateHolder.setState(ConnectionState.Connected)
+ every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Connected)
broadcasts.broadcastConnection()
diff --git a/compose_compiler_config.conf b/compose_compiler_config.conf
index 5952a81bdf..032dc04e05 100644
--- a/compose_compiler_config.conf
+++ b/compose_compiler_config.conf
@@ -3,8 +3,8 @@
// For more information, check https://developer.android.com/jetpack/compose/performance/stability/fix#configuration-file
// Meshtastic Models
-org.meshtastic.core.database.model.Node
-org.meshtastic.core.database.model.Message
+org.meshtastic.core.model.Node
+org.meshtastic.core.model.Message
org.meshtastic.core.database.entity.Reaction
org.meshtastic.core.database.entity.ReactionEntity
org.meshtastic.core.model.**
diff --git a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt b/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt
index e58e804b68..8861b8a11b 100644
--- a/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt
+++ b/core/ble/src/main/kotlin/org/meshtastic/core/ble/BluetoothRepository.kt
@@ -81,7 +81,7 @@ constructor(
@SuppressLint("MissingPermission")
suspend fun bond(peripheral: Peripheral) {
peripheral.createBond()
- refreshState()
+ updateBluetoothState()
}
internal suspend fun updateBluetoothState() {
@@ -112,6 +112,24 @@ constructor(
emptyList()
}
+ /** @return true if the given address is currently bonded to the system. */
+ @SuppressLint("MissingPermission")
+ fun isBonded(address: String): Boolean {
+ val enabled = androidEnvironment.isBluetoothEnabled
+ val hasPerms =
+ if (androidEnvironment.requiresBluetoothRuntimePermissions) {
+ androidEnvironment.isBluetoothScanPermissionGranted &&
+ androidEnvironment.isBluetoothConnectPermissionGranted
+ } else {
+ androidEnvironment.isLocationPermissionGranted
+ }
+ return if (enabled && hasPerms) {
+ centralManager.getBondedPeripherals().any { it.address == address }
+ } else {
+ false
+ }
+ }
+
/** Checks if a peripheral is one of ours, either by its advertised name or by the services it provides. */
private fun isMatchingPeripheral(peripheral: Peripheral): Boolean {
val nameMatches = peripheral.name?.matches(Regex(BLE_NAME_PATTERN)) ?: false
diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts
index 1f06437b66..90a438478e 100644
--- a/core/data/build.gradle.kts
+++ b/core/data/build.gradle.kts
@@ -26,6 +26,7 @@ plugins {
configure { namespace = "org.meshtastic.core.data" }
dependencies {
+ api(projects.core.repository)
implementation(projects.core.analytics)
implementation(projects.core.common)
implementation(projects.core.database)
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt
new file mode 100644
index 0000000000..333398c103
--- /dev/null
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.data.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.meshtastic.core.data.manager.CommandSenderImpl
+import org.meshtastic.core.data.manager.FromRadioPacketHandlerImpl
+import org.meshtastic.core.data.manager.HistoryManagerImpl
+import org.meshtastic.core.data.manager.MeshActionHandlerImpl
+import org.meshtastic.core.data.manager.MeshConfigFlowManagerImpl
+import org.meshtastic.core.data.manager.MeshConfigHandlerImpl
+import org.meshtastic.core.data.manager.MeshConnectionManagerImpl
+import org.meshtastic.core.data.manager.MeshDataHandlerImpl
+import org.meshtastic.core.data.manager.MeshMessageProcessorImpl
+import org.meshtastic.core.data.manager.MeshRouterImpl
+import org.meshtastic.core.data.manager.MessageFilterImpl
+import org.meshtastic.core.data.manager.MqttManagerImpl
+import org.meshtastic.core.data.manager.NeighborInfoHandlerImpl
+import org.meshtastic.core.data.manager.NodeManagerImpl
+import org.meshtastic.core.data.manager.PacketHandlerImpl
+import org.meshtastic.core.data.manager.TracerouteHandlerImpl
+import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl
+import org.meshtastic.core.data.repository.NodeRepositoryImpl
+import org.meshtastic.core.data.repository.PacketRepositoryImpl
+import org.meshtastic.core.data.repository.RadioConfigRepositoryImpl
+import org.meshtastic.core.model.util.MeshDataMapper
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.DeviceHardwareRepository
+import org.meshtastic.core.repository.FromRadioPacketHandler
+import org.meshtastic.core.repository.HistoryManager
+import org.meshtastic.core.repository.MeshActionHandler
+import org.meshtastic.core.repository.MeshConfigFlowManager
+import org.meshtastic.core.repository.MeshConfigHandler
+import org.meshtastic.core.repository.MeshConnectionManager
+import org.meshtastic.core.repository.MeshDataHandler
+import org.meshtastic.core.repository.MeshMessageProcessor
+import org.meshtastic.core.repository.MeshRouter
+import org.meshtastic.core.repository.MessageFilter
+import org.meshtastic.core.repository.MqttManager
+import org.meshtastic.core.repository.NeighborInfoHandler
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.TracerouteHandler
+import javax.inject.Singleton
+
+@Suppress("TooManyFunctions")
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class RepositoryModule {
+
+ @Binds @Singleton
+ abstract fun bindNodeRepository(nodeRepositoryImpl: NodeRepositoryImpl): NodeRepository
+
+ @Binds
+ @Singleton
+ abstract fun bindRadioConfigRepository(radioConfigRepositoryImpl: RadioConfigRepositoryImpl): RadioConfigRepository
+
+ @Binds
+ @Singleton
+ abstract fun bindDeviceHardwareRepository(
+ deviceHardwareRepositoryImpl: DeviceHardwareRepositoryImpl,
+ ): DeviceHardwareRepository
+
+ @Binds @Singleton
+ abstract fun bindPacketRepository(packetRepositoryImpl: PacketRepositoryImpl): PacketRepository
+
+ @Binds @Singleton
+ abstract fun bindNodeManager(nodeManagerImpl: NodeManagerImpl): NodeManager
+
+ @Binds @Singleton
+ abstract fun bindCommandSender(commandSenderImpl: CommandSenderImpl): CommandSender
+
+ @Binds @Singleton
+ abstract fun bindHistoryManager(historyManagerImpl: HistoryManagerImpl): HistoryManager
+
+ @Binds
+ @Singleton
+ abstract fun bindTracerouteHandler(tracerouteHandlerImpl: TracerouteHandlerImpl): TracerouteHandler
+
+ @Binds
+ @Singleton
+ abstract fun bindNeighborInfoHandler(neighborInfoHandlerImpl: NeighborInfoHandlerImpl): NeighborInfoHandler
+
+ @Binds @Singleton
+ abstract fun bindMqttManager(mqttManagerImpl: MqttManagerImpl): MqttManager
+
+ @Binds @Singleton
+ abstract fun bindPacketHandler(packetHandlerImpl: PacketHandlerImpl): PacketHandler
+
+ @Binds
+ @Singleton
+ abstract fun bindMeshConnectionManager(meshConnectionManagerImpl: MeshConnectionManagerImpl): MeshConnectionManager
+
+ @Binds @Singleton
+ abstract fun bindMeshDataHandler(meshDataHandlerImpl: MeshDataHandlerImpl): MeshDataHandler
+
+ @Binds
+ @Singleton
+ abstract fun bindMeshActionHandler(meshActionHandlerImpl: MeshActionHandlerImpl): MeshActionHandler
+
+ @Binds
+ @Singleton
+ abstract fun bindMeshMessageProcessor(meshMessageProcessorImpl: MeshMessageProcessorImpl): MeshMessageProcessor
+
+ @Binds @Singleton
+ abstract fun bindMeshRouter(meshRouterImpl: MeshRouterImpl): MeshRouter
+
+ @Binds
+ @Singleton
+ abstract fun bindFromRadioPacketHandler(
+ fromRadioPacketHandlerImpl: FromRadioPacketHandlerImpl,
+ ): FromRadioPacketHandler
+
+ @Binds
+ @Singleton
+ abstract fun bindMeshConfigHandler(meshConfigHandlerImpl: MeshConfigHandlerImpl): MeshConfigHandler
+
+ @Binds
+ @Singleton
+ abstract fun bindMeshConfigFlowManager(meshConfigFlowManagerImpl: MeshConfigFlowManagerImpl): MeshConfigFlowManager
+
+ @Binds @Singleton
+ abstract fun bindMessageFilter(messageFilterImpl: MessageFilterImpl): MessageFilter
+
+ companion object {
+ @Provides
+ @Singleton
+ fun provideMeshDataMapper(nodeManager: NodeManager): MeshDataMapper = MeshDataMapper(nodeManager)
+ }
+}
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt
new file mode 100644
index 0000000000..8093d73e91
--- /dev/null
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/UseCaseModule.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.data.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import org.meshtastic.core.model.RadioController
+import org.meshtastic.core.repository.HomoglyphPrefs
+import org.meshtastic.core.repository.MessageQueue
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.usecase.SendMessageUseCase
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object UseCaseModule {
+
+ @Provides
+ @Singleton
+ fun provideSendMessageUseCase(
+ nodeRepository: NodeRepository,
+ packetRepository: PacketRepository,
+ radioController: RadioController,
+ homoglyphEncodingPrefs: HomoglyphPrefs,
+ messageQueue: MessageQueue,
+ ): SendMessageUseCase =
+ SendMessageUseCase(nodeRepository, packetRepository, radioController, homoglyphEncodingPrefs, messageQueue)
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
similarity index 77%
rename from app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
index 6e98b253ee..4f262071c7 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshCommandSender.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/CommandSenderImpl.kt
@@ -14,10 +14,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
-import android.os.RemoteException
-import androidx.annotation.VisibleForTesting
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -28,14 +26,15 @@ import kotlinx.coroutines.flow.onEach
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.common.util.nowSeconds
-import org.meshtastic.core.data.repository.RadioConfigRepository
-import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Position
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.util.isWithinSizeLimit
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.Constants
@@ -54,55 +53,56 @@ import javax.inject.Singleton
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.hours
-@Suppress("TooManyFunctions")
+@Suppress("TooManyFunctions", "CyclomaticComplexMethod")
@Singleton
-class MeshCommandSender
+class CommandSenderImpl
@Inject
constructor(
- private val packetHandler: PacketHandler?,
- private val nodeManager: MeshNodeManager?,
- private val connectionStateHolder: ConnectionStateHandler?,
- private val radioConfigRepository: RadioConfigRepository?,
-) {
+ private val packetHandler: PacketHandler,
+ private val nodeManager: NodeManager,
+ private val radioConfigRepository: RadioConfigRepository,
+) : CommandSender {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val currentPacketId = AtomicLong(java.util.Random(nowMillis).nextLong().absoluteValue)
private val sessionPasskey = AtomicReference(ByteString.EMPTY)
- val tracerouteStartTimes = ConcurrentHashMap()
- val neighborInfoStartTimes = ConcurrentHashMap()
+ override val tracerouteStartTimes = ConcurrentHashMap()
+ override val neighborInfoStartTimes = ConcurrentHashMap()
private val localConfig = MutableStateFlow(LocalConfig())
private val channelSet = MutableStateFlow(ChannelSet())
- @Volatile var lastNeighborInfo: NeighborInfo? = null
+ override var lastNeighborInfo: NeighborInfo? = null
- fun start(scope: CoroutineScope) {
+ // We'll need a way to track connection state in shared code,
+ // maybe via ServiceRepository or similar.
+ // For now I'll assume it's injected or available.
+
+ override fun start(scope: CoroutineScope) {
this.scope = scope
- radioConfigRepository?.localConfigFlow?.onEach { localConfig.value = it }?.launchIn(scope)
- radioConfigRepository?.channelSetFlow?.onEach { channelSet.value = it }?.launchIn(scope)
+ radioConfigRepository.localConfigFlow.onEach { localConfig.value = it }.launchIn(scope)
+ radioConfigRepository.channelSetFlow.onEach { channelSet.value = it }.launchIn(scope)
}
- fun getCachedLocalConfig(): LocalConfig = localConfig.value
-
- fun getCachedChannelSet(): ChannelSet = channelSet.value
+ override fun getCachedLocalConfig(): LocalConfig = localConfig.value
- @VisibleForTesting internal constructor() : this(null, null, null, null)
+ override fun getCachedChannelSet(): ChannelSet = channelSet.value
- fun getCurrentPacketId(): Long = currentPacketId.get()
+ override fun getCurrentPacketId(): Long = currentPacketId.get()
- fun generatePacketId(): Int {
+ override fun generatePacketId(): Int {
val numPacketIds = ((1L shl PACKET_ID_SHIFT_BITS) - 1)
val next = currentPacketId.incrementAndGet() and PACKET_ID_MASK
return ((next % numPacketIds) + 1L).toInt()
}
- fun setSessionPasskey(key: ByteString) {
+ override fun setSessionPasskey(key: ByteString) {
sessionPasskey.set(key)
}
private fun computeHopLimit(): Int = (localConfig.value.lora?.hop_limit ?: 0).takeIf { it > 0 } ?: DEFAULT_HOP_LIMIT
private fun getAdminChannelIndex(toNum: Int): Int {
- val myNum = nodeManager?.myNodeNum ?: return 0
+ val myNum = nodeManager.myNodeNum ?: return 0
val myNode = nodeManager.nodeDBbyNodeNum[myNum]
val destNode = nodeManager.nodeDBbyNodeNum[toNum]
@@ -118,7 +118,7 @@ constructor(
return adminChannelIndex
}
- fun sendData(p: DataPacket) {
+ override fun sendData(p: DataPacket) {
if (p.id == 0) p.id = generatePacketId()
val bytes = p.bytes ?: ByteString.EMPTY
require(p.dataType != 0) { "Port numbers must be non-zero!" }
@@ -135,16 +135,15 @@ constructor(
if (!Data.ADAPTER.isWithinSizeLimit(data, Constants.DATA_PAYLOAD_LEN.value)) {
val actualSize = Data.ADAPTER.encodedSize(data)
p.status = MessageStatus.ERROR
- throw RemoteException("Message too long: $actualSize bytes (max ${Constants.DATA_PAYLOAD_LEN.value})")
+ // throw RemoteException("Message too long: $actualSize bytes (max ${Constants.DATA_PAYLOAD_LEN.value})")
+ // RemoteException is Android specific. For KMP we might want a custom exception.
+ error("Message too long: $actualSize bytes")
} else {
p.status = MessageStatus.QUEUED
}
- if (connectionStateHolder?.connectionState?.value == ConnectionState.Connected) {
- sendNow(p)
- } else {
- error("Radio is not connected")
- }
+ // TODO: Check connection state
+ sendNow(p)
}
private fun sendNow(p: DataPacket) {
@@ -164,31 +163,26 @@ constructor(
),
)
p.time = nowMillis
- packetHandler?.sendToRadio(meshPacket)
+ packetHandler.sendToRadio(meshPacket)
}
- fun sendAdmin(
- destNum: Int,
- requestId: Int = generatePacketId(),
- wantResponse: Boolean = false,
- initFn: () -> AdminMessage,
- ) {
+ override fun sendAdmin(destNum: Int, requestId: Int, wantResponse: Boolean, initFn: () -> AdminMessage) {
val adminMsg = initFn().copy(session_passkey = sessionPasskey.get())
val packet =
buildAdminPacket(to = destNum, id = requestId, wantResponse = wantResponse, adminMessage = adminMsg)
- packetHandler?.sendToRadio(packet)
+ packetHandler.sendToRadio(packet)
}
- fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int? = null, wantResponse: Boolean = false) {
- val myNum = nodeManager?.myNodeNum ?: return
+ override fun sendPosition(pos: org.meshtastic.proto.Position, destNum: Int?, wantResponse: Boolean) {
+ val myNum = nodeManager.myNodeNum ?: return
val idNum = destNum ?: myNum
Logger.d { "Sending our position/time to=$idNum $pos" }
if (localConfig.value.position?.fixed_position != true) {
- nodeManager.handleReceivedPosition(myNum, myNum, pos)
+ nodeManager.handleReceivedPosition(myNum, myNum, pos, nowMillis)
}
- packetHandler?.sendToRadio(
+ packetHandler.sendToRadio(
buildMeshPacket(
to = idNum,
channel = if (destNum == null) 0 else nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
@@ -203,18 +197,18 @@ constructor(
)
}
- fun requestPosition(destNum: Int, currentPosition: Position) {
+ override fun requestPosition(destNum: Int, currentPosition: Position) {
val meshPosition =
org.meshtastic.proto.Position(
latitude_i = Position.degI(currentPosition.latitude),
longitude_i = Position.degI(currentPosition.longitude),
altitude = currentPosition.altitude,
- time = nowSeconds.toInt(),
+ time = (nowMillis / 1000L).toInt(),
)
- packetHandler?.sendToRadio(
+ packetHandler.sendToRadio(
buildMeshPacket(
to = destNum,
- channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
+ channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
priority = MeshPacket.Priority.BACKGROUND,
decoded =
Data(
@@ -226,7 +220,7 @@ constructor(
)
}
- fun setFixedPosition(destNum: Int, pos: Position) {
+ override fun setFixedPosition(destNum: Int, pos: Position) {
val meshPos =
org.meshtastic.proto.Position(
latitude_i = Position.degI(pos.latitude),
@@ -240,13 +234,13 @@ constructor(
AdminMessage(remove_fixed_position = true)
}
}
- nodeManager?.handleReceivedPosition(destNum, nodeManager.myNodeNum ?: 0, meshPos)
+ nodeManager.handleReceivedPosition(destNum, nodeManager.myNodeNum ?: 0, meshPos, nowMillis)
}
- fun requestUserInfo(destNum: Int) {
- val myNum = nodeManager?.myNodeNum ?: return
- val myNode = nodeManager.getOrCreateNodeInfo(myNum)
- packetHandler?.sendToRadio(
+ override fun requestUserInfo(destNum: Int) {
+ val myNum = nodeManager.myNodeNum ?: return
+ val myNode = nodeManager.nodeDBbyNodeNum[myNum] ?: return
+ packetHandler.sendToRadio(
buildMeshPacket(
to = destNum,
channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
@@ -260,20 +254,20 @@ constructor(
)
}
- fun requestTraceroute(requestId: Int, destNum: Int) {
+ override fun requestTraceroute(requestId: Int, destNum: Int) {
tracerouteStartTimes[requestId] = nowMillis
- packetHandler?.sendToRadio(
+ packetHandler.sendToRadio(
buildMeshPacket(
to = destNum,
wantAck = true,
id = requestId,
- channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
+ channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
decoded = Data(portnum = PortNum.TRACEROUTE_APP, want_response = true),
),
)
}
- fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) {
+ override fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) {
val type = TelemetryType.entries.getOrNull(typeValue) ?: TelemetryType.DEVICE
val portNum: PortNum
@@ -301,19 +295,19 @@ constructor(
.toByteString()
}
- packetHandler?.sendToRadio(
+ packetHandler.sendToRadio(
buildMeshPacket(
to = destNum,
id = requestId,
- channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
+ channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
decoded = Data(portnum = portNum, payload = payloadBytes, want_response = true),
),
)
}
- fun requestNeighborInfo(requestId: Int, destNum: Int) {
+ override fun requestNeighborInfo(requestId: Int, destNum: Int) {
neighborInfoStartTimes[requestId] = nowMillis
- val myNum = nodeManager?.myNodeNum ?: 0
+ val myNum = nodeManager.myNodeNum ?: 0
if (destNum == myNum) {
val neighborInfoToSend =
lastNeighborInfo
@@ -329,7 +323,7 @@ constructor(
Neighbor(
node_id = 0, // Dummy node ID that can be intercepted
snr = 0f,
- last_rx_time = nowSeconds.toInt(),
+ last_rx_time = (nowMillis / 1000L).toInt(),
node_broadcast_interval_secs = oneHour,
),
),
@@ -337,12 +331,12 @@ constructor(
}
// Send the neighbor info from our connected radio to ourselves (simulated)
- packetHandler?.sendToRadio(
+ packetHandler.sendToRadio(
buildMeshPacket(
to = destNum,
wantAck = true,
id = requestId,
- channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
+ channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
decoded =
Data(
portnum = PortNum.NEIGHBORINFO_APP,
@@ -353,20 +347,19 @@ constructor(
)
} else {
// Send request to remote
- packetHandler?.sendToRadio(
+ packetHandler.sendToRadio(
buildMeshPacket(
to = destNum,
wantAck = true,
id = requestId,
- channel = nodeManager?.nodeDBbyNodeNum?.get(destNum)?.channel ?: 0,
+ channel = nodeManager.nodeDBbyNodeNum[destNum]?.channel ?: 0,
decoded = Data(portnum = PortNum.NEIGHBORINFO_APP, want_response = true),
),
)
}
}
- @VisibleForTesting
- internal fun resolveNodeNum(toId: String): Int = when (toId) {
+ fun resolveNodeNum(toId: String): Int = when (toId) {
DataPacket.ID_BROADCAST -> DataPacket.NODENUM_BROADCAST
else -> {
val numericNum =
@@ -376,7 +369,7 @@ constructor(
null
}
numericNum
- ?: nodeManager?.nodeDBbyID?.get(toId)?.num
+ ?: nodeManager.nodeDBbyID[toId]?.num
?: throw IllegalArgumentException("Unknown node ID $toId")
}
}
@@ -398,12 +391,12 @@ constructor(
if (channel == DataPacket.PKC_CHANNEL_INDEX) {
pkiEncrypted = true
- publicKey = nodeManager?.nodeDBbyNodeNum?.get(to)?.user?.public_key ?: ByteString.EMPTY
+ publicKey = nodeManager.nodeDBbyNodeNum[to]?.user?.public_key ?: ByteString.EMPTY
actualChannel = 0
}
return MeshPacket(
- from = nodeManager?.myNodeNum ?: 0,
+ from = nodeManager.myNodeNum ?: 0,
to = to,
id = id,
want_ack = wantAck,
diff --git a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt
similarity index 53%
rename from app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt
index a771b6fa2e..081d1a2078 100644
--- a/app/src/main/java/com/geeksville/mesh/service/FromRadioPacketHandler.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/FromRadioPacketHandlerImpl.kt
@@ -14,31 +14,32 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
-import co.touchlab.kermit.Logger
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.ServiceRepository
+import dagger.Lazy
+import org.meshtastic.core.repository.FromRadioPacketHandler
+import org.meshtastic.core.repository.MeshRouter
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.MqttManager
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.FromRadio
import javax.inject.Inject
import javax.inject.Singleton
-/**
- * Dispatches non-packet [FromRadio] variants to their respective handlers. This class is stateless and handles routing
- * for config, metadata, and specialized system messages.
- */
+/** Implementation of [FromRadioPacketHandler] that dispatches [FromRadio] variants to specialized handlers. */
@Singleton
-class FromRadioPacketHandler
+class FromRadioPacketHandlerImpl
@Inject
constructor(
private val serviceRepository: ServiceRepository,
- private val router: MeshRouter,
- private val mqttManager: MeshMqttManager,
+ private val router: Lazy,
+ private val mqttManager: MqttManager,
private val packetHandler: PacketHandler,
private val serviceNotifications: MeshServiceNotifications,
-) {
+) : FromRadioPacketHandler {
@Suppress("CyclomaticComplexMethod")
- fun handleFromRadio(proto: FromRadio) {
+ override fun handleFromRadio(proto: FromRadio) {
val myInfo = proto.my_info
val metadata = proto.metadata
val nodeInfo = proto.node_info
@@ -51,34 +52,23 @@ constructor(
val clientNotification = proto.clientNotification
when {
- myInfo != null -> router.configFlowManager.handleMyInfo(myInfo)
- metadata != null -> router.configFlowManager.handleLocalMetadata(metadata)
+ myInfo != null -> router.get().configFlowManager.handleMyInfo(myInfo)
+ metadata != null -> router.get().configFlowManager.handleLocalMetadata(metadata)
nodeInfo != null -> {
- router.configFlowManager.handleNodeInfo(nodeInfo)
- serviceRepository.setConnectionProgress("Nodes (${router.configFlowManager.newNodeCount})")
+ router.get().configFlowManager.handleNodeInfo(nodeInfo)
+ serviceRepository.setConnectionProgress("Nodes (${router.get().configFlowManager.newNodeCount})")
}
- configCompleteId != null -> router.configFlowManager.handleConfigComplete(configCompleteId)
+ configCompleteId != null -> router.get().configFlowManager.handleConfigComplete(configCompleteId)
mqttProxyMessage != null -> mqttManager.handleMqttProxyMessage(mqttProxyMessage)
queueStatus != null -> packetHandler.handleQueueStatus(queueStatus)
- config != null -> router.configHandler.handleDeviceConfig(config)
- moduleConfig != null -> router.configHandler.handleModuleConfig(moduleConfig)
- channel != null -> router.configHandler.handleChannel(channel)
+ config != null -> router.get().configHandler.handleDeviceConfig(config)
+ moduleConfig != null -> router.get().configHandler.handleModuleConfig(moduleConfig)
+ channel != null -> router.get().configHandler.handleChannel(channel)
clientNotification != null -> {
serviceRepository.setClientNotification(clientNotification)
serviceNotifications.showClientNotification(clientNotification)
- packetHandler.removeResponse(clientNotification.reply_id ?: 0, complete = false)
- }
- // Logging-only variants are handled by MeshMessageProcessor before dispatching here
- proto.packet != null ||
- proto.log_record != null ||
- proto.rebooted != null ||
- proto.xmodemPacket != null ||
- proto.deviceuiConfig != null ||
- proto.fileInfo != null -> {
- /* No specialized routing needed here */
+ packetHandler.removeResponse(0, complete = false)
}
-
- else -> Logger.d { "Dispatcher ignoring FromRadio variant" }
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
similarity index 74%
rename from app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
index b084433b41..a2df3d73a1 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshHistoryManager.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt
@@ -14,15 +14,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
-import android.util.Log
-import androidx.annotation.VisibleForTesting
import co.touchlab.kermit.Logger
-import com.geeksville.mesh.BuildConfig
-import com.geeksville.mesh.ui.connections.NO_DEVICE_SELECTED
import okio.ByteString.Companion.toByteString
import org.meshtastic.core.prefs.mesh.MeshPrefs
+import org.meshtastic.core.repository.HistoryManager
+import org.meshtastic.core.repository.PacketHandler
import org.meshtastic.proto.Data
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.ModuleConfig
@@ -32,19 +30,20 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class MeshHistoryManager
+class HistoryManagerImpl
@Inject
constructor(
private val meshPrefs: MeshPrefs,
private val packetHandler: PacketHandler,
-) {
+) : HistoryManager {
+
companion object {
private const val HISTORY_TAG = "HistoryReplay"
private const val DEFAULT_HISTORY_RETURN_WINDOW_MINUTES = 60 * 24
private const val DEFAULT_HISTORY_RETURN_MAX_MESSAGES = 100
+ private const val NO_DEVICE_SELECTED = "No device selected"
- @VisibleForTesting
- internal fun buildStoreForwardHistoryRequest(
+ fun buildStoreForwardHistoryRequest(
lastRequest: Int,
historyReturnWindow: Int,
historyReturnMax: Int,
@@ -58,32 +57,23 @@ constructor(
return StoreAndForward(rr = StoreAndForward.RequestResponse.CLIENT_HISTORY, history = history)
}
- @VisibleForTesting
- internal fun resolveHistoryRequestParameters(window: Int, max: Int): Pair {
+ fun resolveHistoryRequestParameters(window: Int, max: Int): Pair {
val resolvedWindow = if (window > 0) window else DEFAULT_HISTORY_RETURN_WINDOW_MINUTES
val resolvedMax = if (max > 0) max else DEFAULT_HISTORY_RETURN_MAX_MESSAGES
return resolvedWindow to resolvedMax
}
}
- private fun historyLog(priority: Int = Log.INFO, throwable: Throwable? = null, message: () -> String) {
- if (!BuildConfig.DEBUG) return
- val logger = Logger.withTag(HISTORY_TAG)
- val msg = message()
- when (priority) {
- Log.VERBOSE -> logger.v(throwable) { msg }
- Log.DEBUG -> logger.d(throwable) { msg }
- Log.INFO -> logger.i(throwable) { msg }
- Log.WARN -> logger.w(throwable) { msg }
- Log.ERROR -> logger.e(throwable) { msg }
- else -> logger.i(throwable) { msg }
- }
+ private val logger = Logger.withTag(HISTORY_TAG)
+
+ private fun historyLog(message: String, throwable: Throwable? = null) {
+ logger.i(throwable) { message }
}
private fun activeDeviceAddress(): String? =
meshPrefs.deviceAddress?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() }
- fun requestHistoryReplay(
+ override fun requestHistoryReplay(
trigger: String,
myNodeNum: Int?,
storeForwardConfig: ModuleConfig.StoreForwardConfig?,
@@ -92,7 +82,7 @@ constructor(
val address = activeDeviceAddress()
if (address == null || myNodeNum == null) {
val reason = if (address == null) "no_addr" else "no_my_node"
- historyLog { "requestHistory skipped trigger=$trigger reason=$reason" }
+ historyLog("requestHistory skipped trigger=$trigger reason=$reason")
return
}
@@ -105,10 +95,10 @@ constructor(
val request = buildStoreForwardHistoryRequest(lastRequest, window, max)
- historyLog {
+ historyLog(
"requestHistory trigger=$trigger transport=$transport addr=$address " +
- "lastRequest=$lastRequest window=$window max=$max"
- }
+ "lastRequest=$lastRequest window=$window max=$max",
+ )
runCatching {
packetHandler.sendToRadio(
@@ -120,19 +110,19 @@ constructor(
),
)
}
- .onFailure { ex -> historyLog(Log.WARN, ex) { "requestHistory failed" } }
+ .onFailure { ex -> logger.w(ex) { "requestHistory failed" } }
}
- fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) {
+ override fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) {
if (lastRequest <= 0) return
val address = activeDeviceAddress() ?: return
val current = meshPrefs.getStoreForwardLastRequest(address)
if (lastRequest != current) {
meshPrefs.setStoreForwardLastRequest(address, lastRequest)
- historyLog {
+ historyLog(
"historyMarker updated source=$source transport=$transport " +
- "addr=$address from=$current to=$lastRequest"
- }
+ "addr=$address from=$current to=$lastRequest",
+ )
}
}
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
similarity index 74%
rename from app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
index 5ac1ee1cfe..0adf6a80ee 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshActionHandler.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
@@ -26,15 +26,22 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.ignoreException
import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.data.repository.PacketRepository
-import org.meshtastic.core.database.DatabaseManager
-import org.meshtastic.core.database.entity.ReactionEntity
import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.model.MeshUser
import org.meshtastic.core.model.MessageStatus
import org.meshtastic.core.model.Position
+import org.meshtastic.core.model.Reaction
+import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.prefs.mesh.MeshPrefs
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.ServiceAction
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.DatabaseManager
+import org.meshtastic.core.repository.MeshActionHandler
+import org.meshtastic.core.repository.MeshDataHandler
+import org.meshtastic.core.repository.MeshMessageProcessor
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.ServiceBroadcasts
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config
@@ -47,23 +54,23 @@ import javax.inject.Singleton
@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
@Singleton
-class MeshActionHandler
+class MeshActionHandlerImpl
@Inject
constructor(
- private val nodeManager: MeshNodeManager,
- private val commandSender: MeshCommandSender,
+ private val nodeManager: NodeManager,
+ private val commandSender: CommandSender,
private val packetRepository: Lazy,
- private val serviceBroadcasts: MeshServiceBroadcasts,
- private val dataHandler: MeshDataHandler,
+ private val serviceBroadcasts: ServiceBroadcasts,
+ private val dataHandler: Lazy,
private val analytics: PlatformAnalytics,
private val meshPrefs: MeshPrefs,
private val databaseManager: DatabaseManager,
private val serviceNotifications: MeshServiceNotifications,
private val messageProcessor: Lazy,
-) {
+) : MeshActionHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
}
@@ -72,7 +79,7 @@ constructor(
private const val EMOJI_INDICATOR = 1
}
- fun onServiceAction(action: ServiceAction) {
+ override fun onServiceAction(action: ServiceAction) {
ignoreException {
val myNodeNum = nodeManager.myNodeNum ?: return@ignoreException
when (action) {
@@ -102,7 +109,7 @@ constructor(
AdminMessage(set_favorite_node = node.num)
}
}
- nodeManager.updateNodeInfo(node.num) { it.isFavorite = !node.isFavorite }
+ nodeManager.updateNode(node.num) { it.copy(isFavorite = !node.isFavorite) }
}
private fun handleIgnore(action: ServiceAction.Ignore, myNodeNum: Int) {
@@ -115,14 +122,14 @@ constructor(
AdminMessage(remove_ignored_node = node.num)
}
}
- nodeManager.updateNodeInfo(node.num) { it.isIgnored = newIgnoredStatus }
+ nodeManager.updateNode(node.num) { it.copy(isIgnored = newIgnoredStatus) }
scope.handledLaunch { packetRepository.get().updateFilteredBySender(node.user.id, newIgnoredStatus) }
}
private fun handleMute(action: ServiceAction.Mute, myNodeNum: Int) {
val node = action.node
commandSender.sendAdmin(myNodeNum) { AdminMessage(toggle_muted_node = node.num) }
- nodeManager.updateNodeInfo(node.num) { it.isMuted = !node.isMuted }
+ nodeManager.updateNode(node.num) { it.copy(isMuted = !node.isMuted) }
}
private fun handleReaction(action: ServiceAction.Reaction, myNodeNum: Int) {
@@ -147,7 +154,7 @@ constructor(
val verifiedContact = action.contact.copy(manually_verified = true)
commandSender.sendAdmin(myNodeNum) { AdminMessage(add_contact = verifiedContact) }
nodeManager.handleReceivedUser(
- verifiedContact.node_num ?: 0,
+ verifiedContact.node_num,
verifiedContact.user ?: User(),
manuallyVerified = true,
)
@@ -155,11 +162,11 @@ constructor(
private fun rememberReaction(action: ServiceAction.Reaction, packetId: Int, myNodeNum: Int) {
scope.handledLaunch {
+ val user = nodeManager.nodeDBbyNodeNum[myNodeNum]?.user ?: User(id = nodeManager.getMyId())
val reaction =
- ReactionEntity(
- myNodeNum = myNodeNum,
+ Reaction(
replyId = action.replyId,
- userId = nodeManager.getMyId().takeIf { it.isNotEmpty() } ?: DataPacket.ID_LOCAL,
+ user = user,
emoji = action.emoji,
timestamp = nowMillis,
snr = 0f,
@@ -170,25 +177,25 @@ constructor(
to = action.contactKey.substring(1),
channel = action.contactKey[0].digitToInt(),
)
- packetRepository.get().insertReaction(reaction)
+ packetRepository.get().insertReaction(reaction, myNodeNum)
}
}
- fun handleSetOwner(u: org.meshtastic.core.model.MeshUser, myNodeNum: Int) {
+ override fun handleSetOwner(u: MeshUser, myNodeNum: Int) {
val newUser = User(id = u.id, long_name = u.longName, short_name = u.shortName, is_licensed = u.isLicensed)
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_owner = newUser) }
nodeManager.handleReceivedUser(myNodeNum, newUser)
}
- fun handleSend(p: DataPacket, myNodeNum: Int) {
+ override fun handleSend(p: DataPacket, myNodeNum: Int) {
commandSender.sendData(p)
- serviceBroadcasts.broadcastMessageStatus(p)
- dataHandler.rememberDataPacket(p, myNodeNum, false)
+ serviceBroadcasts.broadcastMessageStatus(p.id, p.status ?: MessageStatus.UNKNOWN)
+ dataHandler.get().rememberDataPacket(p, myNodeNum, false)
val bytes = p.bytes ?: okio.ByteString.EMPTY
analytics.track("data_send", DataPair("num_bytes", bytes.size), DataPair("type", p.dataType))
}
- fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) {
+ override fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) {
if (destNum != myNodeNum) {
val provideLocation = meshPrefs.shouldProvideNodeLocation(myNodeNum)
val currentPosition =
@@ -201,32 +208,32 @@ constructor(
}
}
- fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int) {
+ override fun handleRemoveByNodenum(nodeNum: Int, requestId: Int, myNodeNum: Int) {
nodeManager.removeByNodenum(nodeNum)
commandSender.sendAdmin(myNodeNum, requestId) { AdminMessage(remove_by_nodenum = nodeNum) }
}
- fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) {
+ override fun handleSetRemoteOwner(id: Int, destNum: Int, payload: ByteArray) {
val u = User.ADAPTER.decode(payload)
commandSender.sendAdmin(destNum, id) { AdminMessage(set_owner = u) }
nodeManager.handleReceivedUser(destNum, u)
}
- fun handleGetRemoteOwner(id: Int, destNum: Int) {
+ override fun handleGetRemoteOwner(id: Int, destNum: Int) {
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_owner_request = true) }
}
- fun handleSetConfig(payload: ByteArray, myNodeNum: Int) {
+ override fun handleSetConfig(payload: ByteArray, myNodeNum: Int) {
val c = Config.ADAPTER.decode(payload)
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_config = c) }
}
- fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray) {
+ override fun handleSetRemoteConfig(id: Int, destNum: Int, payload: ByteArray) {
val c = Config.ADAPTER.decode(payload)
commandSender.sendAdmin(destNum, id) { AdminMessage(set_config = c) }
}
- fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int) {
+ override fun handleGetRemoteConfig(id: Int, destNum: Int, config: Int) {
commandSender.sendAdmin(destNum, id, wantResponse = true) {
if (config == AdminMessage.ConfigType.SESSIONKEY_CONFIG.value) {
AdminMessage(get_device_metadata_request = true)
@@ -236,104 +243,104 @@ constructor(
}
}
- fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) {
+ override fun handleSetModuleConfig(id: Int, destNum: Int, payload: ByteArray) {
val c = ModuleConfig.ADAPTER.decode(payload)
commandSender.sendAdmin(destNum, id) { AdminMessage(set_module_config = c) }
c.statusmessage?.let { sm -> nodeManager.updateNodeStatus(destNum, sm.node_status) }
}
- fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) {
+ override fun handleGetModuleConfig(id: Int, destNum: Int, config: Int) {
commandSender.sendAdmin(destNum, id, wantResponse = true) {
AdminMessage(get_module_config_request = AdminMessage.ModuleConfigType.fromValue(config))
}
}
- fun handleSetRingtone(destNum: Int, ringtone: String) {
+ override fun handleSetRingtone(destNum: Int, ringtone: String) {
commandSender.sendAdmin(destNum) { AdminMessage(set_ringtone_message = ringtone) }
}
- fun handleGetRingtone(id: Int, destNum: Int) {
+ override fun handleGetRingtone(id: Int, destNum: Int) {
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_ringtone_request = true) }
}
- fun handleSetCannedMessages(destNum: Int, messages: String) {
+ override fun handleSetCannedMessages(destNum: Int, messages: String) {
commandSender.sendAdmin(destNum) { AdminMessage(set_canned_message_module_messages = messages) }
}
- fun handleGetCannedMessages(id: Int, destNum: Int) {
+ override fun handleGetCannedMessages(id: Int, destNum: Int) {
commandSender.sendAdmin(destNum, id, wantResponse = true) {
AdminMessage(get_canned_message_module_messages_request = true)
}
}
- fun handleSetChannel(payload: ByteArray?, myNodeNum: Int) {
+ override fun handleSetChannel(payload: ByteArray?, myNodeNum: Int) {
if (payload != null) {
val c = Channel.ADAPTER.decode(payload)
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_channel = c) }
}
}
- fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?) {
+ override fun handleSetRemoteChannel(id: Int, destNum: Int, payload: ByteArray?) {
if (payload != null) {
val c = Channel.ADAPTER.decode(payload)
commandSender.sendAdmin(destNum, id) { AdminMessage(set_channel = c) }
}
}
- fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int) {
+ override fun handleGetRemoteChannel(id: Int, destNum: Int, index: Int) {
commandSender.sendAdmin(destNum, id, wantResponse = true) { AdminMessage(get_channel_request = index + 1) }
}
- fun handleRequestNeighborInfo(requestId: Int, destNum: Int) {
+ override fun handleRequestNeighborInfo(requestId: Int, destNum: Int) {
commandSender.requestNeighborInfo(requestId, destNum)
}
- fun handleBeginEditSettings(destNum: Int) {
+ override fun handleBeginEditSettings(destNum: Int) {
commandSender.sendAdmin(destNum) { AdminMessage(begin_edit_settings = true) }
}
- fun handleCommitEditSettings(destNum: Int) {
+ override fun handleCommitEditSettings(destNum: Int) {
commandSender.sendAdmin(destNum) { AdminMessage(commit_edit_settings = true) }
}
- fun handleRebootToDfu(destNum: Int) {
+ override fun handleRebootToDfu(destNum: Int) {
commandSender.sendAdmin(destNum) { AdminMessage(enter_dfu_mode_request = true) }
}
- fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int) {
+ override fun handleRequestTelemetry(requestId: Int, destNum: Int, type: Int) {
commandSender.requestTelemetry(requestId, destNum, type)
}
- fun handleRequestShutdown(requestId: Int, destNum: Int) {
+ override fun handleRequestShutdown(requestId: Int, destNum: Int) {
commandSender.sendAdmin(destNum, requestId) { AdminMessage(shutdown_seconds = DEFAULT_REBOOT_DELAY) }
}
- fun handleRequestReboot(requestId: Int, destNum: Int) {
+ override fun handleRequestReboot(requestId: Int, destNum: Int) {
commandSender.sendAdmin(destNum, requestId) { AdminMessage(reboot_seconds = DEFAULT_REBOOT_DELAY) }
}
- fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {
+ override fun handleRequestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {
val otaMode = OTAMode.fromValue(mode) ?: OTAMode.NO_REBOOT_OTA
val otaEvent =
AdminMessage.OTAEvent(reboot_ota_mode = otaMode, ota_hash = hash?.toByteString() ?: okio.ByteString.EMPTY)
commandSender.sendAdmin(destNum, requestId) { AdminMessage(ota_request = otaEvent) }
}
- fun handleRequestFactoryReset(requestId: Int, destNum: Int) {
+ override fun handleRequestFactoryReset(requestId: Int, destNum: Int) {
commandSender.sendAdmin(destNum, requestId) { AdminMessage(factory_reset_device = 1) }
}
- fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) {
+ override fun handleRequestNodedbReset(requestId: Int, destNum: Int, preserveFavorites: Boolean) {
commandSender.sendAdmin(destNum, requestId) { AdminMessage(nodedb_reset = preserveFavorites) }
}
- fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int) {
+ override fun handleGetDeviceConnectionStatus(requestId: Int, destNum: Int) {
commandSender.sendAdmin(destNum, requestId, wantResponse = true) {
AdminMessage(get_device_connection_status_request = true)
}
}
- fun handleUpdateLastAddress(deviceAddr: String?) {
+ override fun handleUpdateLastAddress(deviceAddr: String?) {
val currentAddr = meshPrefs.deviceAddress
if (deviceAddr != currentAddr) {
meshPrefs.deviceAddress = deviceAddr
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt
similarity index 73%
rename from app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt
index 1d666ca2d4..86026b9be1 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigFlowManager.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigFlowManagerImpl.kt
@@ -14,64 +14,71 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
+import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.common.util.handledLaunch
-import org.meshtastic.core.data.repository.NodeRepository
-import org.meshtastic.core.data.repository.RadioConfigRepository
-import org.meshtastic.core.database.entity.MetadataEntity
-import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.model.ConnectionState
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.MeshConfigFlowManager
+import org.meshtastic.core.repository.MeshConnectionManager
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.ServiceBroadcasts
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.Heartbeat
-import org.meshtastic.proto.MyNodeInfo
import org.meshtastic.proto.NodeInfo
import org.meshtastic.proto.ToRadio
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
+import org.meshtastic.core.model.MyNodeInfo as SharedMyNodeInfo
+import org.meshtastic.proto.MyNodeInfo as ProtoMyNodeInfo
-@Suppress("LongParameterList")
+@Suppress("LongParameterList", "TooManyFunctions")
@Singleton
-class MeshConfigFlowManager
+class MeshConfigFlowManagerImpl
@Inject
constructor(
- private val nodeManager: MeshNodeManager,
- private val connectionManager: MeshConnectionManager,
+ private val nodeManager: NodeManager,
+ private val connectionManager: Lazy,
private val nodeRepository: NodeRepository,
private val radioConfigRepository: RadioConfigRepository,
- private val connectionStateHolder: ConnectionStateHandler,
- private val serviceBroadcasts: MeshServiceBroadcasts,
+ private val serviceRepository: ServiceRepository,
+ private val serviceBroadcasts: ServiceBroadcasts,
private val analytics: PlatformAnalytics,
- private val commandSender: MeshCommandSender,
+ private val commandSender: CommandSender,
private val packetHandler: PacketHandler,
-) {
+) : MeshConfigFlowManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val configOnlyNonce = 69420
private val nodeInfoNonce = 69421
private val wantConfigDelay = 100L
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
}
private val newNodes = mutableListOf()
- val newNodeCount: Int
+ override val newNodeCount: Int
get() = newNodes.size
- private var rawMyNodeInfo: MyNodeInfo? = null
+ private var rawMyNodeInfo: ProtoMyNodeInfo? = null
private var lastMetadata: DeviceMetadata? = null
- private var newMyNodeInfo: MyNodeEntity? = null
- private var myNodeInfo: MyNodeEntity? = null
+ private var newMyNodeInfo: SharedMyNodeInfo? = null
+ private var myNodeInfo: SharedMyNodeInfo? = null
- fun handleConfigComplete(configCompleteId: Int) {
+ override fun handleConfigComplete(configCompleteId: Int) {
when (configCompleteId) {
configOnlyNonce -> handleConfigOnlyComplete()
nodeInfoNonce -> handleNodeInfoComplete()
@@ -94,7 +101,7 @@ constructor(
} else {
myNodeInfo = finalizedInfo
Logger.i { "myNodeInfo committed successfully (nodeNum=${finalizedInfo.myNodeNum})" }
- connectionManager.onRadioConfigLoaded()
+ connectionManager.get().onRadioConfigLoaded()
}
scope.handledLaunch {
@@ -102,7 +109,7 @@ constructor(
sendHeartbeat()
delay(wantConfigDelay)
Logger.i { "Requesting NodeInfo (Stage 2)" }
- connectionManager.startNodeInfoOnly()
+ connectionManager.get().startNodeInfoOnly()
}
}
@@ -129,19 +136,19 @@ constructor(
nodeRepository.installConfig(it, entities)
sendAnalytics(it)
}
- nodeManager.isNodeDbReady.value = true
- nodeManager.allowNodeDbWrites.value = true
- connectionStateHolder.setState(ConnectionState.Connected)
+ nodeManager.setNodeDbReady(true)
+ nodeManager.setAllowNodeDbWrites(true)
+ serviceRepository.setConnectionState(ConnectionState.Connected)
serviceBroadcasts.broadcastConnection()
- connectionManager.onNodeDbReady()
+ connectionManager.get().onNodeDbReady()
}
}
- private fun sendAnalytics(mi: MyNodeEntity) {
+ private fun sendAnalytics(mi: SharedMyNodeInfo) {
analytics.setDeviceAttributes(mi.firmwareVersion ?: "unknown", mi.model ?: "unknown")
}
- fun handleMyInfo(myInfo: MyNodeInfo) {
+ override fun handleMyInfo(myInfo: ProtoMyNodeInfo) {
Logger.i { "MyNodeInfo received: ${myInfo.my_node_num}" }
rawMyNodeInfo = myInfo
nodeManager.myNodeNum = myInfo.my_node_num
@@ -154,24 +161,29 @@ constructor(
}
}
- fun handleLocalMetadata(metadata: DeviceMetadata) {
+ override fun handleLocalMetadata(metadata: DeviceMetadata) {
Logger.i { "Local Metadata received: ${metadata.firmware_version}" }
lastMetadata = metadata
regenMyNodeInfo(metadata)
}
- fun handleNodeInfo(info: NodeInfo) {
+ override fun handleNodeInfo(info: NodeInfo) {
newNodes.add(info)
}
+ override fun triggerWantConfig() {
+ connectionManager.get().startConfigOnly()
+ }
+
private fun regenMyNodeInfo(metadata: DeviceMetadata? = null) {
val myInfo = rawMyNodeInfo
if (myInfo != null) {
try {
val mi =
with(myInfo) {
- MyNodeEntity(
- myNodeNum = my_node_num ?: 0,
+ SharedMyNodeInfo(
+ myNodeNum = my_node_num,
+ hasGPS = false,
model =
when (val hwModel = metadata?.hw_model) {
null,
@@ -187,12 +199,14 @@ constructor(
minAppVersion = min_app_version,
maxChannels = 8,
hasWifi = metadata?.hasWifi == true,
+ channelUtilization = 0f,
+ airUtilTx = 0f,
deviceId = device_id.utf8(),
pioEnv = myInfo.pio_env.ifEmpty { null },
)
}
if (metadata != null && metadata != DeviceMetadata()) {
- scope.handledLaunch { nodeRepository.insertMetadata(MetadataEntity(mi.myNodeNum, metadata)) }
+ scope.handledLaunch { nodeRepository.insertMetadata(mi.myNodeNum, metadata) }
}
newMyNodeInfo = mi
Logger.d { "newMyNodeInfo updated: nodeNum=${mi.myNodeNum} model=${mi.model} fw=${mi.firmwareVersion}" }
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt
similarity index 79%
rename from app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt
index 616529d145..d5ff324266 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshConfigHandler.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConfigHandlerImpl.kt
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -24,8 +24,10 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.meshtastic.core.common.util.handledLaunch
-import org.meshtastic.core.data.repository.RadioConfigRepository
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.MeshConfigHandler
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.Channel
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
@@ -35,34 +37,33 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class MeshConfigHandler
+class MeshConfigHandlerImpl
@Inject
constructor(
private val radioConfigRepository: RadioConfigRepository,
private val serviceRepository: ServiceRepository,
- private val nodeManager: MeshNodeManager,
-) {
+ private val nodeManager: NodeManager,
+) : MeshConfigHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val _localConfig = MutableStateFlow(LocalConfig())
- val localConfig = _localConfig.asStateFlow()
+ override val localConfig = _localConfig.asStateFlow()
private val _moduleConfig = MutableStateFlow(LocalModuleConfig())
- val moduleConfig = _moduleConfig.asStateFlow()
+ override val moduleConfig = _moduleConfig.asStateFlow()
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
radioConfigRepository.localConfigFlow.onEach { _localConfig.value = it }.launchIn(scope)
-
radioConfigRepository.moduleConfigFlow.onEach { _moduleConfig.value = it }.launchIn(scope)
}
- fun handleDeviceConfig(config: Config) {
+ override fun handleDeviceConfig(config: Config) {
scope.handledLaunch { radioConfigRepository.setLocalConfig(config) }
serviceRepository.setConnectionProgress("Device config received")
}
- fun handleModuleConfig(config: ModuleConfig) {
+ override fun handleModuleConfig(config: ModuleConfig) {
scope.handledLaunch { radioConfigRepository.setLocalModuleConfig(config) }
serviceRepository.setConnectionProgress("Module config received")
@@ -71,13 +72,13 @@ constructor(
}
}
- fun handleChannel(ch: Channel) {
+ override fun handleChannel(channel: Channel) {
// We always want to save channel settings we receive from the radio
- scope.handledLaunch { radioConfigRepository.updateChannelSettings(ch) }
+ scope.handledLaunch { radioConfigRepository.updateChannelSettings(channel) }
// Update status message if we have node info, otherwise use a generic one
val mi = nodeManager.getMyNodeInfo()
- val index = ch.index ?: 0
+ val index = channel.index
if (mi != null) {
serviceRepository.setConnectionProgress("Channels (${index + 1} / ${mi.maxChannels})")
} else {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
similarity index 79%
rename from app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
index eeb4882dc4..a420793dfc 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshConnectionManager.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt
@@ -14,19 +14,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
-
-import android.app.Notification
-import android.content.Context
-import androidx.glance.appwidget.updateAll
-import androidx.work.ExistingWorkPolicy
-import androidx.work.OneTimeWorkRequestBuilder
-import androidx.work.WorkManager
-import androidx.work.workDataOf
+package org.meshtastic.core.data.manager
+
import co.touchlab.kermit.Logger
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
-import com.geeksville.mesh.widget.LocalStatsWidget
-import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -43,12 +33,25 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
-import org.meshtastic.core.data.repository.NodeRepository
-import org.meshtastic.core.data.repository.PacketRepository
-import org.meshtastic.core.data.repository.RadioConfigRepository
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.prefs.ui.UiPrefs
+import org.meshtastic.core.repository.AppWidgetUpdater
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.HistoryManager
+import org.meshtastic.core.repository.MeshConnectionManager
+import org.meshtastic.core.repository.MeshLocationManager
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.MeshWorkerManager
+import org.meshtastic.core.repository.MqttManager
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.RadioInterfaceService
+import org.meshtastic.core.repository.ServiceBroadcasts
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.connected
import org.meshtastic.core.resources.connecting
@@ -56,8 +59,6 @@ import org.meshtastic.core.resources.device_sleeping
import org.meshtastic.core.resources.disconnected
import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.meshtastic_app_name
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.Config
import org.meshtastic.proto.Telemetry
@@ -70,27 +71,27 @@ import kotlin.time.DurationUnit
@Suppress("LongParameterList", "TooManyFunctions")
@Singleton
-class MeshConnectionManager
+class MeshConnectionManagerImpl
@Inject
constructor(
- @ApplicationContext private val context: Context,
private val radioInterfaceService: RadioInterfaceService,
- private val connectionStateHolder: ConnectionStateHandler,
- private val serviceBroadcasts: MeshServiceBroadcasts,
+ private val serviceRepository: ServiceRepository,
+ private val serviceBroadcasts: ServiceBroadcasts,
private val serviceNotifications: MeshServiceNotifications,
private val uiPrefs: UiPrefs,
private val packetHandler: PacketHandler,
private val nodeRepository: NodeRepository,
private val locationManager: MeshLocationManager,
- private val mqttManager: MeshMqttManager,
- private val historyManager: MeshHistoryManager,
+ private val mqttManager: MqttManager,
+ private val historyManager: HistoryManager,
private val radioConfigRepository: RadioConfigRepository,
- private val commandSender: MeshCommandSender,
- private val nodeManager: MeshNodeManager,
+ private val commandSender: CommandSender,
+ private val nodeManager: NodeManager,
private val analytics: PlatformAnalytics,
private val packetRepository: PacketRepository,
- private val workManager: WorkManager,
-) {
+ private val workerManager: MeshWorkerManager,
+ private val appWidgetUpdater: AppWidgetUpdater,
+) : MeshConnectionManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var sleepTimeout: Job? = null
private var locationRequestsJob: Job? = null
@@ -98,18 +99,16 @@ constructor(
private var connectTimeMsec = 0L
@OptIn(FlowPreview::class)
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
radioInterfaceService.connectionState.onEach(::onRadioConnectionState).launchIn(scope)
// Ensure notification title and content stay in sync with state changes
- connectionStateHolder.connectionState.onEach { updateStatusNotification() }.launchIn(scope)
+ serviceRepository.connectionState.onEach { updateStatusNotification() }.launchIn(scope)
- // Kickstart the widget composition. The widget internally uses collectAsState()
- // and its own sampled StateFlow to drive updates automatically without excessive IPC and recreation.
scope.launch {
try {
- LocalStatsWidget().updateAll(context)
+ appWidgetUpdater.updateAll()
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
Logger.e(e) { "Failed to kickstart LocalStatsWidget" }
}
@@ -154,7 +153,7 @@ constructor(
}
private fun onConnectionChanged(c: ConnectionState) {
- val current = connectionStateHolder.connectionState.value
+ val current = serviceRepository.connectionState.value
if (current == c) return
// If the transport reports 'Connected', but we are already in the middle of a handshake (Connecting)
@@ -171,7 +170,7 @@ constructor(
handshakeTimeout = null
when (c) {
- is ConnectionState.Connecting -> connectionStateHolder.setState(ConnectionState.Connecting)
+ is ConnectionState.Connecting -> serviceRepository.setConnectionState(ConnectionState.Connecting)
is ConnectionState.Connected -> handleConnected()
is ConnectionState.DeviceSleep -> handleDeviceSleep()
is ConnectionState.Disconnected -> handleDisconnected()
@@ -180,8 +179,8 @@ constructor(
private fun handleConnected() {
// The service state remains 'Connecting' until config is fully loaded
- if (connectionStateHolder.connectionState.value != ConnectionState.Connected) {
- connectionStateHolder.setState(ConnectionState.Connecting)
+ if (serviceRepository.connectionState.value != ConnectionState.Connected) {
+ serviceRepository.setConnectionState(ConnectionState.Connecting)
}
serviceBroadcasts.broadcastConnection()
Logger.i { "Starting mesh handshake (Stage 1)" }
@@ -192,12 +191,12 @@ constructor(
handshakeTimeout =
scope.handledLaunch {
delay(HANDSHAKE_TIMEOUT)
- if (connectionStateHolder.connectionState.value is ConnectionState.Connecting) {
+ if (serviceRepository.connectionState.value is ConnectionState.Connecting) {
Logger.w { "Handshake stall detected! Retrying Stage 1." }
startConfigOnly()
// Recursive timeout for one more try
delay(HANDSHAKE_TIMEOUT)
- if (connectionStateHolder.connectionState.value is ConnectionState.Connecting) {
+ if (serviceRepository.connectionState.value is ConnectionState.Connecting) {
Logger.e { "Handshake still stalled after retry. Resetting connection." }
onConnectionChanged(ConnectionState.Disconnected)
}
@@ -206,7 +205,7 @@ constructor(
}
private fun handleDeviceSleep() {
- connectionStateHolder.setState(ConnectionState.DeviceSleep)
+ serviceRepository.setConnectionState(ConnectionState.DeviceSleep)
packetHandler.stopPacketQueue()
locationManager.stop()
mqttManager.stop()
@@ -239,7 +238,7 @@ constructor(
}
private fun handleDisconnected() {
- connectionStateHolder.setState(ConnectionState.Disconnected)
+ serviceRepository.setConnectionState(ConnectionState.Disconnected)
packetHandler.stopPacketQueue()
locationManager.stop()
mqttManager.stop()
@@ -254,29 +253,20 @@ constructor(
serviceBroadcasts.broadcastConnection()
}
- fun startConfigOnly() {
+ override fun startConfigOnly() {
packetHandler.sendToRadio(ToRadio(want_config_id = CONFIG_ONLY_NONCE))
}
- fun startNodeInfoOnly() {
+ override fun startNodeInfoOnly() {
packetHandler.sendToRadio(ToRadio(want_config_id = NODE_INFO_NONCE))
}
- fun onRadioConfigLoaded() {
+ override fun onRadioConfigLoaded() {
scope.handledLaunch {
val queuedPackets = packetRepository.getQueuedPackets() ?: emptyList()
queuedPackets.forEach { packet ->
try {
- val workRequest =
- OneTimeWorkRequestBuilder()
- .setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packet.id))
- .build()
-
- workManager.enqueueUniqueWork(
- "${SendMessageWorker.WORK_NAME_PREFIX}${packet.id}",
- ExistingWorkPolicy.REPLACE,
- workRequest,
- )
+ workerManager.enqueueSendMessage(packet.id)
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
Logger.e(e) { "Failed to enqueue queued packet worker" }
}
@@ -288,7 +278,7 @@ constructor(
commandSender.sendAdmin(myNodeNum) { AdminMessage(set_time_only = nowSeconds.toInt()) }
}
- fun onNodeDbReady() {
+ override fun onNodeDbReady() {
handshakeTimeout?.cancel()
handshakeTimeout = null
@@ -329,14 +319,14 @@ constructor(
)
}
- fun updateTelemetry(telemetry: Telemetry) {
- telemetry.local_stats?.let { nodeRepository.updateLocalStats(it) }
- updateStatusNotification(telemetry)
+ override fun updateTelemetry(t: Telemetry) {
+ t.local_stats?.let { nodeRepository.updateLocalStats(it) }
+ updateStatusNotification(t)
}
- fun updateStatusNotification(telemetry: Telemetry? = null): Notification {
+ override fun updateStatusNotification(telemetry: Telemetry?): Any {
val summary =
- when (connectionStateHolder.connectionState.value) {
+ when (serviceRepository.connectionState.value) {
is ConnectionState.Connected ->
getString(Res.string.meshtastic_app_name) + ": " + getString(Res.string.connected)
is ConnectionState.Disconnected -> getString(Res.string.disconnected)
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt
similarity index 81%
rename from app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt
index 36338d4934..e84af354c7 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshDataHandler.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.kt
@@ -14,13 +14,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
-import android.util.Log
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
-import com.geeksville.mesh.BuildConfig
-import com.geeksville.mesh.repository.radio.InterfaceId
import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -33,25 +30,36 @@ import org.meshtastic.core.analytics.platform.PlatformAnalytics
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
-import org.meshtastic.core.data.repository.PacketRepository
-import org.meshtastic.core.data.repository.RadioConfigRepository
-import org.meshtastic.core.database.entity.Packet
-import org.meshtastic.core.database.entity.ReactionEntity
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
+import org.meshtastic.core.model.Node
+import org.meshtastic.core.model.Reaction
+import org.meshtastic.core.model.util.MeshDataMapper
import org.meshtastic.core.model.util.SfppHasher
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.core.model.util.toOneLiner
-import org.meshtastic.core.prefs.mesh.MeshPrefs
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.HistoryManager
+import org.meshtastic.core.repository.MeshConfigFlowManager
+import org.meshtastic.core.repository.MeshConfigHandler
+import org.meshtastic.core.repository.MeshConnectionManager
+import org.meshtastic.core.repository.MeshDataHandler
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.MessageFilter
+import org.meshtastic.core.repository.NeighborInfoHandler
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.RadioConfigRepository
+import org.meshtastic.core.repository.ServiceBroadcasts
+import org.meshtastic.core.repository.ServiceRepository
+import org.meshtastic.core.repository.TracerouteHandler
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.critical_alert
import org.meshtastic.core.resources.error_duty_cycle
import org.meshtastic.core.resources.getString
import org.meshtastic.core.resources.unknown_username
import org.meshtastic.core.resources.waypoint_received
-import org.meshtastic.core.service.MeshServiceNotifications
-import org.meshtastic.core.service.ServiceRepository
-import org.meshtastic.core.service.filter.MessageFilterService
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.Paxcount
@@ -70,33 +78,42 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.milliseconds
+/**
+ * Implementation of [MeshDataHandler] that decodes and routes incoming mesh data packets.
+ *
+ * This class handles the complexity of:
+ * 1. Mapping raw [MeshPacket] objects to domain-friendly [DataPacket] objects.
+ * 2. Routing packets to specialized handlers (e.g., Traceroute, NeighborInfo, SFPP).
+ * 3. Managing message history and persistence.
+ * 4. Triggering notifications for various packet types (Text, Waypoints, Battery).
+ * 5. Tracking received telemetry for node updates.
+ */
@Suppress("LongParameterList", "TooManyFunctions", "LargeClass", "CyclomaticComplexMethod")
@Singleton
-class MeshDataHandler
+class MeshDataHandlerImpl
@Inject
constructor(
- private val nodeManager: MeshNodeManager,
+ private val nodeManager: NodeManager,
private val packetHandler: PacketHandler,
private val serviceRepository: ServiceRepository,
private val packetRepository: Lazy,
- private val serviceBroadcasts: MeshServiceBroadcasts,
+ private val serviceBroadcasts: ServiceBroadcasts,
private val serviceNotifications: MeshServiceNotifications,
private val analytics: PlatformAnalytics,
private val dataMapper: MeshDataMapper,
- private val configHandler: MeshConfigHandler,
- private val configFlowManager: MeshConfigFlowManager,
- private val commandSender: MeshCommandSender,
- private val historyManager: MeshHistoryManager,
- private val meshPrefs: MeshPrefs,
- private val connectionManager: MeshConnectionManager,
- private val tracerouteHandler: MeshTracerouteHandler,
- private val neighborInfoHandler: MeshNeighborInfoHandler,
+ private val configHandler: Lazy,
+ private val configFlowManager: Lazy,
+ private val commandSender: CommandSender,
+ private val historyManager: HistoryManager,
+ private val connectionManager: Lazy,
+ private val tracerouteHandler: TracerouteHandler,
+ private val neighborInfoHandler: NeighborInfoHandler,
private val radioConfigRepository: RadioConfigRepository,
- private val messageFilterService: MessageFilterService,
-) {
+ private val messageFilter: MessageFilter,
+) : MeshDataHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
}
@@ -108,7 +125,7 @@ constructor(
PortNum.NODE_STATUS_APP.value,
)
- fun handleReceivedData(packet: MeshPacket, myNodeNum: Int, logUuid: String? = null, logInsertJob: Job? = null) {
+ override fun handleReceivedData(packet: MeshPacket, myNodeNum: Int, logUuid: String?, logInsertJob: Job?) {
val dataPacket = dataMapper.toDataPacket(packet) ?: return
val fromUs = myNodeNum == packet.from
dataPacket.status = MessageStatus.RECEIVED
@@ -221,7 +238,7 @@ constructor(
handleReceivedStoreAndForward(dataPacket, u, myNodeNum)
}
- @Suppress("LongMethod")
+ @Suppress("LongMethod", "ReturnCount")
private fun handleStoreForwardPlusPlus(packet: MeshPacket) {
val payload = packet.decoded?.payload ?: return
val sfpp =
@@ -340,20 +357,20 @@ constructor(
val fromNum = packet.from
u.get_module_config_response?.let { config ->
if (fromNum == myNodeNum) {
- configHandler.handleModuleConfig(config)
+ configHandler.get().handleModuleConfig(config)
} else {
config.statusmessage?.node_status?.let { nodeManager.updateNodeStatus(fromNum, it) }
}
}
if (fromNum == myNodeNum) {
- u.get_config_response?.let { configHandler.handleDeviceConfig(it) }
- u.get_channel_response?.let { configHandler.handleChannel(it) }
+ u.get_config_response?.let { configHandler.get().handleDeviceConfig(it) }
+ u.get_channel_response?.let { configHandler.get().handleChannel(it) }
}
u.get_device_metadata_response?.let { metadata ->
if (fromNum == myNodeNum) {
- configFlowManager.handleLocalMetadata(metadata)
+ configFlowManager.get().handleLocalMetadata(metadata)
} else {
nodeManager.insertMetadata(fromNum, metadata)
}
@@ -395,39 +412,43 @@ constructor(
val fromNum = packet.from
val isRemote = (fromNum != myNodeNum)
if (!isRemote) {
- connectionManager.updateTelemetry(t)
+ connectionManager.get().updateTelemetry(t)
}
- nodeManager.updateNodeInfo(fromNum) { nodeEntity ->
+ nodeManager.updateNode(fromNum) { node: Node ->
val metrics = t.device_metrics
val environment = t.environment_metrics
val power = t.power_metrics
+
+ var nextNode = node
when {
metrics != null -> {
- nodeEntity.deviceTelemetry = t
- if (fromNum == myNodeNum || (isRemote && nodeEntity.isFavorite)) {
+ nextNode = nextNode.copy(deviceMetrics = metrics)
+ if (fromNum == myNodeNum || (isRemote && node.isFavorite)) {
if (
(metrics.voltage ?: 0f) > BATTERY_PERCENT_UNSUPPORTED &&
(metrics.battery_level ?: 0) <= BATTERY_PERCENT_LOW_THRESHOLD
) {
if (shouldBatteryNotificationShow(fromNum, t, myNodeNum)) {
- serviceNotifications.showOrUpdateLowBatteryNotification(nodeEntity, isRemote)
+ serviceNotifications.showOrUpdateLowBatteryNotification(nextNode, isRemote)
}
} else {
if (batteryPercentCooldowns.containsKey(fromNum)) {
batteryPercentCooldowns.remove(fromNum)
}
- serviceNotifications.cancelLowBatteryNotification(nodeEntity)
+ serviceNotifications.cancelLowBatteryNotification(nextNode)
}
}
}
- environment != null -> nodeEntity.environmentTelemetry = t
- power != null -> nodeEntity.powerTelemetry = t
+ environment != null -> nextNode = nextNode.copy(environmentMetrics = environment)
+ power != null -> nextNode = nextNode.copy(powerMetrics = power)
}
+ nextNode
}
}
+ @Suppress("ReturnCount")
private fun shouldBatteryNotificationShow(fromNum: Int, t: Telemetry, myNodeNum: Int): Boolean {
val isRemote = (fromNum != myNodeNum)
var shouldDisplay = false
@@ -475,30 +496,26 @@ constructor(
private fun handleAckNak(requestId: Int, fromId: String, routingError: Int, relayNode: Int?) {
scope.handledLaunch {
val isAck = routingError == Routing.Error.NONE.value
- val p = packetRepository.get().getPacketById(requestId)
+ val p = packetRepository.get().getPacketByPacketId(requestId)
val reaction = packetRepository.get().getReactionByPacketId(requestId)
@Suppress("MaxLineLength")
Logger.d {
- val statusInfo = "status=${p?.data?.status ?: reaction?.status}"
+ val statusInfo = "status=${p?.status ?: reaction?.status}"
"[ackNak] req=$requestId routeErr=$routingError isAck=$isAck " +
- "packetId=${p?.packetId ?: reaction?.packetId} dataId=${p?.data?.id} $statusInfo"
+ "packetId=${p?.id ?: reaction?.packetId} dataId=${p?.id} $statusInfo"
}
val m =
when {
- isAck && (fromId == p?.data?.to || fromId == reaction?.to) -> MessageStatus.RECEIVED
+ isAck && (fromId == p?.to || fromId == reaction?.to) -> MessageStatus.RECEIVED
isAck -> MessageStatus.DELIVERED
else -> MessageStatus.ERROR
}
- if (p != null && p.data.status != MessageStatus.RECEIVED) {
- p.data.status = m
- p.routingError = routingError
- if (isAck) {
- p.data.relays += 1
- }
- p.data.relayNode = relayNode
- packetRepository.get().update(p)
+ if (p != null && p.status != MessageStatus.RECEIVED) {
+ val updatedPacket =
+ p.copy(status = m, relays = if (isAck) p.relays + 1 else p.relays, relayNode = relayNode)
+ packetRepository.get().update(updatedPacket)
}
reaction?.let { r ->
@@ -517,11 +534,11 @@ constructor(
private fun handleReceivedStoreAndForward(dataPacket: DataPacket, s: StoreAndForward, myNodeNum: Int) {
Logger.d { "StoreAndForward: variant from ${dataPacket.from}" }
- val transport = currentTransport()
+ // For now, we don't have meshPrefs in commonMain, so we use a simplified transport check or abstract it.
+ // In the original, it was used for logging.
val h = s.history
val lastRequest = h?.last_request ?: 0
- val baseContext = "transport=$transport from=${dataPacket.from}"
- historyLog { "rxStoreForward $baseContext lastRequest=$lastRequest" }
+ Logger.d { "rxStoreForward from=${dataPacket.from} lastRequest=$lastRequest" }
when {
s.stats != null -> {
val text = s.stats.toString()
@@ -533,10 +550,6 @@ constructor(
rememberDataPacket(u, myNodeNum)
}
h != null -> {
- @Suppress("MaxLineLength")
- historyLog(Log.DEBUG) {
- "routerHistory $baseContext messages=${h.history_messages} window=${h.window} lastReq=${h.last_request}"
- }
val text =
"Total messages: ${h.history_messages}\n" +
"History window: ${h.window.milliseconds.inWholeMinutes} min\n" +
@@ -547,20 +560,17 @@ constructor(
dataType = PortNum.TEXT_MESSAGE_APP.value,
)
rememberDataPacket(u, myNodeNum)
- historyManager.updateStoreForwardLastRequest("router_history", h.last_request, transport)
+ // historyManager call remains same
+ historyManager.updateStoreForwardLastRequest("router_history", h.last_request, "Unknown")
}
s.heartbeat != null -> {
val hb = s.heartbeat!!
- historyLog { "rxHeartbeat $baseContext period=${hb.period} secondary=${hb.secondary}" }
+ Logger.d { "rxHeartbeat from=${dataPacket.from} period=${hb.period} secondary=${hb.secondary}" }
}
s.text != null -> {
if (s.rr == StoreAndForward.RequestResponse.ROUTER_TEXT_BROADCAST) {
dataPacket.to = DataPacket.ID_BROADCAST
}
- @Suppress("MaxLineLength")
- historyLog(Log.DEBUG) {
- "rxText $baseContext id=${dataPacket.id} ts=${dataPacket.time} to=${dataPacket.to} decision=remember"
- }
val u = dataPacket.copy(bytes = s.text, dataType = PortNum.TEXT_MESSAGE_APP.value)
rememberDataPacket(u, myNodeNum)
}
@@ -568,7 +578,7 @@ constructor(
}
}
- fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean = true) {
+ override fun rememberDataPacket(dataPacket: DataPacket, myNodeNum: Int, updateNotification: Boolean) {
if (dataPacket.dataType !in rememberDataType) return
val fromLocal =
dataPacket.from == DataPacket.ID_LOCAL || dataPacket.from == DataPacket.nodeNumToDefaultId(myNodeNum)
@@ -594,25 +604,16 @@ constructor(
// Check if message should be filtered
val isFiltered = shouldFilterMessage(dataPacket, contactKey)
- val packetToSave =
- Packet(
- uuid = 0L,
- myNodeNum = myNodeNum,
- packetId = dataPacket.id,
- port_num = dataPacket.dataType,
- contact_key = contactKey,
- received_time = nowMillis,
- read = fromLocal || isFiltered,
- data = dataPacket,
- snr = dataPacket.snr,
- rssi = dataPacket.rssi,
- hopsAway = dataPacket.hopsAway,
- filtered = isFiltered,
- )
-
- insert(packetToSave)
+ insert(
+ dataPacket,
+ myNodeNum,
+ contactKey,
+ nowMillis,
+ read = fromLocal || isFiltered,
+ filtered = isFiltered,
+ )
if (!isFiltered) {
- handlePacketNotification(packetToSave, dataPacket, contactKey, updateNotification)
+ handlePacketNotification(dataPacket, contactKey, updateNotification)
}
}
}
@@ -625,11 +626,10 @@ constructor(
if (dataPacket.dataType != PortNum.TEXT_MESSAGE_APP.value) return false
val isFilteringDisabled = getContactSettings(contactKey).filteringDisabled
- return messageFilterService.shouldFilter(dataPacket.text.orEmpty(), isFilteringDisabled)
+ return messageFilter.shouldFilter(dataPacket.text.orEmpty(), isFilteringDisabled)
}
private suspend fun handlePacketNotification(
- packet: Packet,
dataPacket: DataPacket,
contactKey: String,
updateNotification: Boolean,
@@ -637,7 +637,7 @@ constructor(
val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted
val nodeMuted = nodeManager.nodeDBbyID[dataPacket.from]?.isMuted == true
val isSilent = conversationMuted || nodeMuted
- if (packet.port_num == PortNum.ALERT_APP.value && !isSilent) {
+ if (dataPacket.dataType == PortNum.ALERT_APP.value && !isSilent) {
serviceNotifications.showAlertNotification(
contactKey,
getSenderName(dataPacket),
@@ -696,13 +696,14 @@ constructor(
val decoded = packet.decoded ?: return@handledLaunch
val emoji = decoded.payload.toByteArray().decodeToString()
val fromId = nodeManager.toNodeID(packet.from)
- val toId = nodeManager.toNodeID(packet.to)
+
+ val fromNode = nodeManager.nodeDBbyNodeNum[packet.from] ?: Node(num = packet.from)
+ val toNode = nodeManager.nodeDBbyNodeNum[packet.to] ?: Node(num = packet.to)
val reaction =
- ReactionEntity(
- myNodeNum = nodeManager.myNodeNum ?: 0,
+ Reaction(
replyId = decoded.reply_id,
- userId = fromId,
+ user = fromNode.user,
emoji = emoji,
timestamp = nowMillis,
snr = packet.rx_snr,
@@ -715,7 +716,7 @@ constructor(
},
packetId = packet.id,
status = MessageStatus.RECEIVED,
- to = toId,
+ to = toNode.user.id,
channel = packet.channel,
)
@@ -729,25 +730,25 @@ constructor(
return@handledLaunch
}
- packetRepository.get().insertReaction(reaction)
+ packetRepository.get().insertReaction(reaction, nodeManager.myNodeNum ?: 0)
// Find the original packet to get the contactKey
- packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { original ->
+ packetRepository.get().getPacketByPacketId(decoded.reply_id)?.let { originalPacket ->
// Skip notification if the original message was filtered
- if (original.packet.filtered) return@let
-
- val contactKey = original.packet.contact_key
+ val targetId =
+ if (originalPacket.from == DataPacket.ID_LOCAL) originalPacket.to else originalPacket.from
+ val contactKey = "${originalPacket.channel}$targetId"
val conversationMuted = packetRepository.get().getContactSettings(contactKey).isMuted
val nodeMuted = nodeManager.nodeDBbyID[fromId]?.isMuted == true
val isSilent = conversationMuted || nodeMuted
if (!isSilent) {
val channelName =
- if (original.packet.data.to == DataPacket.ID_BROADCAST) {
+ if (originalPacket.to == DataPacket.ID_BROADCAST) {
radioConfigRepository.channelSetFlow
.first()
.settings
- .getOrNull(original.packet.data.channel)
+ .getOrNull(originalPacket.channel)
?.name
} else {
null
@@ -756,7 +757,7 @@ constructor(
contactKey,
getSenderName(dataMapper.toDataPacket(packet)!!),
emoji,
- original.packet.data.to == DataPacket.ID_BROADCAST,
+ originalPacket.to == DataPacket.ID_BROADCAST,
channelName,
isSilent,
)
@@ -764,33 +765,6 @@ constructor(
}
}
- private fun currentTransport(address: String? = meshPrefs.deviceAddress): String = when (address?.firstOrNull()) {
- InterfaceId.BLUETOOTH.id -> "BLE"
- InterfaceId.TCP.id -> "TCP"
- InterfaceId.SERIAL.id -> "Serial"
- InterfaceId.MOCK.id -> "Mock"
- InterfaceId.NOP.id -> "NOP"
- else -> "Unknown"
- }
-
- private inline fun historyLog(
- priority: Int = Log.INFO,
- throwable: Throwable? = null,
- crossinline message: () -> String,
- ) {
- if (!BuildConfig.DEBUG) return
- val logger = Logger.withTag("HistoryReplay")
- val msg = message()
- when (priority) {
- Log.VERBOSE -> logger.v(throwable) { msg }
- Log.DEBUG -> logger.d(throwable) { msg }
- Log.INFO -> logger.i(throwable) { msg }
- Log.WARN -> logger.w(throwable) { msg }
- Log.ERROR -> logger.e(throwable) { msg }
- else -> logger.i(throwable) { msg }
- }
- }
-
companion object {
private const val HOPS_AWAY_UNAVAILABLE = -1
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt
similarity index 75%
rename from app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt
index 7ed7980c35..1c19c8f317 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshMessageProcessor.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt
@@ -14,11 +14,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
-import android.util.Log
import co.touchlab.kermit.Logger
-import com.geeksville.mesh.BuildConfig
import dagger.Lazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -31,8 +29,13 @@ import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.database.entity.MeshLog
+import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.isLora
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.FromRadioPacketHandler
+import org.meshtastic.core.repository.MeshMessageProcessor
+import org.meshtastic.core.repository.MeshRouter
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.LogRecord
import org.meshtastic.proto.MeshPacket
@@ -44,17 +47,18 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlin.uuid.Uuid
+/** Implementation of [MeshMessageProcessor] that handles raw radio messages and prepares mesh packets for routing. */
@Suppress("TooManyFunctions")
@Singleton
-class MeshMessageProcessor
+class MeshMessageProcessorImpl
@Inject
constructor(
- private val nodeManager: MeshNodeManager,
+ private val nodeManager: NodeManager,
private val serviceRepository: ServiceRepository,
private val meshLogRepository: Lazy,
- private val router: MeshRouter,
+ private val router: Lazy,
private val fromRadioDispatcher: FromRadioPacketHandler,
-) {
+) : MeshMessageProcessor {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val logUuidByPacketId = ConcurrentHashMap()
private val logInsertJobByPacketId = ConcurrentHashMap()
@@ -62,11 +66,11 @@ constructor(
private val earlyReceivedPackets = ArrayDeque()
private val maxEarlyPacketBuffer = 10240
- fun clearEarlyPackets() {
+ override fun clearEarlyPackets() {
synchronized(earlyReceivedPackets) { earlyReceivedPackets.clear() }
}
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
nodeManager.isNodeDbReady
.onEach { ready ->
@@ -77,7 +81,7 @@ constructor(
.launchIn(scope)
}
- fun handleFromRadio(bytes: ByteArray, myNodeNum: Int?) {
+ override fun handleFromRadio(bytes: ByteArray, myNodeNum: Int?) {
runCatching { FromRadio.ADAPTER.decode(bytes) }
.onSuccess { proto -> processFromRadio(proto, myNodeNum) }
.onFailure { primaryException ->
@@ -134,7 +138,7 @@ constructor(
)
}
- fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) {
+ override fun handleReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) {
val rxTime =
if (packet.rx_time == 0) {
nowSeconds.toInt()
@@ -149,21 +153,9 @@ constructor(
synchronized(earlyReceivedPackets) {
val queueSize = earlyReceivedPackets.size
if (queueSize >= maxEarlyPacketBuffer) {
- val dropped = earlyReceivedPackets.removeFirst()
- historyLog(Log.WARN) {
- val portLabel =
- dropped.decoded?.portnum?.name ?: dropped.decoded?.portnum?.value?.toString() ?: "unknown"
- "dropEarlyPacket bufferFull size=$queueSize id=${dropped.id} port=$portLabel"
- }
+ earlyReceivedPackets.removeFirst()
}
earlyReceivedPackets.addLast(preparedPacket)
- val portLabel =
- preparedPacket.decoded?.portnum?.name
- ?: preparedPacket.decoded?.portnum?.value?.toString()
- ?: "unknown"
- historyLog {
- "queueEarlyPacket size=${earlyReceivedPackets.size} id=${preparedPacket.id} port=$portLabel"
- }
}
}
}
@@ -176,11 +168,12 @@ constructor(
earlyReceivedPackets.clear()
list
}
- historyLog { "replayEarlyPackets reason=$reason count=${packets.size}" }
+ Logger.d { "replayEarlyPackets reason=$reason count=${packets.size}" }
val myNodeNum = nodeManager.myNodeNum
packets.forEach { processReceivedMeshPacket(it, myNodeNum) }
}
+ @Suppress("LongMethod")
private fun processReceivedMeshPacket(packet: MeshPacket, myNodeNum: Int?) {
val decoded = packet.decoded ?: return
val log =
@@ -202,22 +195,24 @@ constructor(
myNodeNum?.let { myNum ->
val from = packet.from
val isOtherNode = myNum != from
- nodeManager.updateNodeInfo(myNum, withBroadcast = isOtherNode) { it.lastHeard = nowSeconds.toInt() }
- nodeManager.updateNodeInfo(from, withBroadcast = false, channel = packet.channel) {
- it.lastHeard = packet.rx_time
- it.viaMqtt = packet.via_mqtt == true
- it.lastTransport = packet.transport_mechanism.value
-
+ nodeManager.updateNode(myNum, withBroadcast = isOtherNode) { node: Node ->
+ node.copy(lastHeard = nowSeconds.toInt())
+ }
+ nodeManager.updateNode(from, withBroadcast = false, channel = packet.channel) { node: Node ->
+ val viaMqtt = packet.via_mqtt == true
val isDirect = packet.hop_start == packet.hop_limit
- if (isDirect && packet.isLora() && !it.viaMqtt) {
- it.snr = packet.rx_snr
- it.rssi = packet.rx_rssi
+
+ var snr = node.snr
+ var rssi = node.rssi
+ if (isDirect && packet.isLora() && !viaMqtt) {
+ snr = packet.rx_snr
+ rssi = packet.rx_rssi
}
- it.hopsAway =
+ val hopsAway =
if (decoded.portnum == PortNum.RANGE_TEST_APP) {
0
- } else if (it.viaMqtt) {
+ } else if (viaMqtt) {
-1
} else if (packet.hop_start == 0 && (decoded.bitfield ?: 0) == 0) {
-1
@@ -226,10 +221,19 @@ constructor(
} else {
packet.hop_start - packet.hop_limit
}
+
+ node.copy(
+ lastHeard = packet.rx_time,
+ viaMqtt = viaMqtt,
+ lastTransport = packet.transport_mechanism.value,
+ snr = snr,
+ rssi = rssi,
+ hopsAway = hopsAway,
+ )
}
try {
- router.dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob)
+ router.get().dataHandler.handleReceivedData(packet, myNum, log.uuid, logJob)
} finally {
logUuidByPacketId.remove(packet.id)
logInsertJobByPacketId.remove(packet.id)
@@ -239,24 +243,6 @@ constructor(
private fun insertMeshLog(log: MeshLog): Job = scope.handledLaunch { meshLogRepository.get().insert(log) }
- private inline fun historyLog(
- priority: Int = Log.INFO,
- throwable: Throwable? = null,
- crossinline message: () -> String,
- ) {
- if (!BuildConfig.DEBUG) return
- val logger = Logger.withTag("HistoryReplay")
- val msg = message()
- when (priority) {
- Log.VERBOSE -> logger.v(throwable) { msg }
- Log.DEBUG -> logger.d(throwable) { msg }
- Log.INFO -> logger.i(throwable) { msg }
- Log.WARN -> logger.w(throwable) { msg }
- Log.ERROR -> logger.e(throwable) { msg }
- else -> logger.i(throwable) { msg }
- }
- }
-
private fun ByteArray.toHexString(): String =
this.joinToString(",") { byte -> String.format(Locale.US, "0x%02x", byte) }
}
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt
new file mode 100644
index 0000000000..b079b1d868
--- /dev/null
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshRouterImpl.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.data.manager
+
+import dagger.Lazy
+import kotlinx.coroutines.CoroutineScope
+import org.meshtastic.core.repository.MeshActionHandler
+import org.meshtastic.core.repository.MeshConfigFlowManager
+import org.meshtastic.core.repository.MeshConfigHandler
+import org.meshtastic.core.repository.MeshDataHandler
+import org.meshtastic.core.repository.MeshRouter
+import org.meshtastic.core.repository.MqttManager
+import org.meshtastic.core.repository.NeighborInfoHandler
+import org.meshtastic.core.repository.TracerouteHandler
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Implementation of [MeshRouter] that orchestrates specialized mesh packet handlers. */
+@Suppress("LongParameterList")
+@Singleton
+class MeshRouterImpl
+@Inject
+constructor(
+ private val dataHandlerLazy: Lazy,
+ private val configHandlerLazy: Lazy,
+ private val tracerouteHandlerLazy: Lazy,
+ private val neighborInfoHandlerLazy: Lazy,
+ private val configFlowManagerLazy: Lazy,
+ private val mqttManagerLazy: Lazy,
+ private val actionHandlerLazy: Lazy,
+) : MeshRouter {
+ override val dataHandler: MeshDataHandler
+ get() = dataHandlerLazy.get()
+
+ override val configHandler: MeshConfigHandler
+ get() = configHandlerLazy.get()
+
+ override val tracerouteHandler: TracerouteHandler
+ get() = tracerouteHandlerLazy.get()
+
+ override val neighborInfoHandler: NeighborInfoHandler
+ get() = neighborInfoHandlerLazy.get()
+
+ override val configFlowManager: MeshConfigFlowManager
+ get() = configFlowManagerLazy.get()
+
+ override val mqttManager: MqttManager
+ get() = mqttManagerLazy.get()
+
+ override val actionHandler: MeshActionHandler
+ get() = actionHandlerLazy.get()
+
+ override fun start(scope: CoroutineScope) {
+ dataHandler.start(scope)
+ configHandler.start(scope)
+ tracerouteHandler.start(scope)
+ neighborInfoHandler.start(scope)
+ configFlowManager.start(scope)
+ actionHandler.start(scope)
+ }
+}
diff --git a/core/service/src/main/kotlin/org/meshtastic/core/service/filter/MessageFilterService.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt
similarity index 69%
rename from core/service/src/main/kotlin/org/meshtastic/core/service/filter/MessageFilterService.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt
index bb8a773aac..906e615ae9 100644
--- a/core/service/src/main/kotlin/org/meshtastic/core/service/filter/MessageFilterService.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt
@@ -14,34 +14,25 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.meshtastic.core.service.filter
+package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import org.meshtastic.core.prefs.filter.FilterPrefs
+import org.meshtastic.core.repository.MessageFilter
import java.util.regex.PatternSyntaxException
import javax.inject.Inject
import javax.inject.Singleton
-/**
- * Service for filtering messages based on user-configured filter words. Supports both plain text word matching and
- * regex patterns.
- */
+/** Implementation of [MessageFilter] that uses regex and plain text matching. */
@Singleton
-class MessageFilterService @Inject constructor(private val filterPrefs: FilterPrefs) {
+class MessageFilterImpl @Inject constructor(private val filterPrefs: FilterPrefs) : MessageFilter {
private var compiledPatterns: List = emptyList()
init {
rebuildPatterns()
}
- /**
- * Determines if a message should be filtered based on the configured filter words.
- *
- * @param message The message text to check.
- * @param isFilteringDisabled Whether filtering is disabled for this contact.
- * @return true if the message should be filtered, false otherwise.
- */
- fun shouldFilter(message: String, isFilteringDisabled: Boolean = false): Boolean {
+ override fun shouldFilter(message: String, isFilteringDisabled: Boolean): Boolean {
if (!filterPrefs.filterEnabled || compiledPatterns.isEmpty() || isFilteringDisabled) {
return false
}
@@ -49,11 +40,7 @@ class MessageFilterService @Inject constructor(private val filterPrefs: FilterPr
return compiledPatterns.any { it.containsMatchIn(textToCheck) }
}
- /**
- * Rebuilds the compiled regex patterns from the current filter words. Should be called whenever the filter words
- * are updated.
- */
- fun rebuildPatterns() {
+ override fun rebuildPatterns() {
compiledPatterns =
filterPrefs.filterWords.mapNotNull { word ->
try {
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
similarity index 84%
rename from app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
index 314b7c99c1..7684ebd205 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshMqttManager.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
@@ -14,11 +14,10 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
-import com.geeksville.mesh.repository.network.MQTTRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -26,24 +25,27 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.network.repository.MQTTRepository
+import org.meshtastic.core.repository.MqttManager
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.MqttClientProxyMessage
import org.meshtastic.proto.ToRadio
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class MeshMqttManager
+class MqttManagerImpl
@Inject
constructor(
private val mqttRepository: MQTTRepository,
private val packetHandler: PacketHandler,
private val serviceRepository: ServiceRepository,
-) {
+) : MqttManager {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var mqttMessageFlow: Job? = null
- fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) {
+ override fun start(scope: CoroutineScope, enabled: Boolean, proxyToClientEnabled: Boolean) {
this.scope = scope
if (mqttMessageFlow?.isActive == true) return
if (enabled && proxyToClientEnabled) {
@@ -60,7 +62,7 @@ constructor(
}
}
- fun stop() {
+ override fun stop() {
if (mqttMessageFlow?.isActive == true) {
Logger.i { "Stopping MqttClientProxy" }
mqttMessageFlow?.cancel()
@@ -68,7 +70,7 @@ constructor(
}
}
- fun handleMqttProxyMessage(message: MqttClientProxyMessage) {
+ override fun handleMqttProxyMessage(message: MqttClientProxyMessage) {
val topic = message.topic ?: ""
Logger.d { "[mqttClientProxyMessage] $topic" }
val retained = message.retained == true
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
similarity index 77%
rename from app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
index 3574bf6e1c..df19abacf6 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshNeighborInfoHandler.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.kt
@@ -14,17 +14,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.resources.Res
-import org.meshtastic.core.resources.getString
-import org.meshtastic.core.resources.unknown_username
-import org.meshtastic.core.service.ServiceRepository
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.NeighborInfoHandler
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.ServiceBroadcasts
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.NeighborInfo
import java.util.Locale
@@ -32,21 +33,21 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class MeshNeighborInfoHandler
+class NeighborInfoHandlerImpl
@Inject
constructor(
- private val nodeManager: MeshNodeManager,
+ private val nodeManager: NodeManager,
private val serviceRepository: ServiceRepository,
- private val commandSender: MeshCommandSender,
- private val serviceBroadcasts: MeshServiceBroadcasts,
-) {
+ private val commandSender: CommandSender,
+ private val serviceBroadcasts: ServiceBroadcasts,
+) : NeighborInfoHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
}
- fun handleNeighborInfo(packet: MeshPacket) {
+ override fun handleNeighborInfo(packet: MeshPacket) {
val payload = packet.decoded?.payload ?: return
val ni = NeighborInfo.ADAPTER.decode(payload)
@@ -58,7 +59,7 @@ constructor(
}
// Update Node DB
- nodeManager.nodeDBbyNodeNum[from]?.let { serviceBroadcasts.broadcastNodeChange(it.toNodeInfo()) }
+ nodeManager.nodeDBbyNodeNum[from]?.let { serviceBroadcasts.broadcastNodeChange(it) }
// Format for UI response
val requestId = packet.decoded?.request_id ?: 0
@@ -67,11 +68,11 @@ constructor(
val neighbors =
ni.neighbors.joinToString("\n") { n ->
val node = nodeManager.nodeDBbyNodeNum[n.node_id]
- val name = node?.let { "${it.longName} (${it.shortName})" } ?: getString(Res.string.unknown_username)
+ val name = node?.let { "${it.user.long_name} (${it.user.short_name})" } ?: "Unknown"
"• $name (SNR: ${n.snr})"
}
- val formatted = "Neighbors of ${nodeManager.nodeDBbyNodeNum[from]?.longName ?: "Unknown"}:\n$neighbors"
+ val formatted = "Neighbors of ${nodeManager.nodeDBbyNodeNum[from]?.user?.long_name ?: "Unknown"}:\n$neighbors"
val responseText =
if (start != null) {
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt
new file mode 100644
index 0000000000..e9172809b0
--- /dev/null
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/NodeManagerImpl.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * 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.data.manager
+
+import co.touchlab.kermit.Logger
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import okio.ByteString
+import org.meshtastic.core.common.util.handledLaunch
+import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.model.DeviceMetrics
+import org.meshtastic.core.model.EnvironmentMetrics
+import org.meshtastic.core.model.MeshUser
+import org.meshtastic.core.model.MyNodeInfo
+import org.meshtastic.core.model.Node
+import org.meshtastic.core.model.NodeInfo
+import org.meshtastic.core.model.Position
+import org.meshtastic.core.repository.MeshServiceNotifications
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.ServiceBroadcasts
+import org.meshtastic.proto.DeviceMetadata
+import org.meshtastic.proto.HardwareModel
+import org.meshtastic.proto.Paxcount
+import org.meshtastic.proto.StatusMessage
+import org.meshtastic.proto.Telemetry
+import org.meshtastic.proto.User
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import javax.inject.Singleton
+import org.meshtastic.proto.NodeInfo as ProtoNodeInfo
+import org.meshtastic.proto.Position as ProtoPosition
+
+/**
+ * Implementation of [NodeManager] that maintains an in-memory database of the mesh.
+ *
+ * This component acts as the "brain" for node-related data during a connection session. It manages:
+ * 1. In-memory maps for fast node lookup by number or ID.
+ * 2. Synchronization of node data between the radio and the persistent database.
+ * 3. Processing of incoming node-related packets (User, Position, Telemetry).
+ * 4. Broadcasting changes to the rest of the application.
+ */
+@Suppress("LongParameterList", "TooManyFunctions", "CyclomaticComplexMethod")
+@Singleton
+class NodeManagerImpl
+@Inject
+constructor(
+ private val nodeRepository: NodeRepository,
+ private val serviceBroadcasts: ServiceBroadcasts,
+ private val serviceNotifications: MeshServiceNotifications,
+) : NodeManager {
+ private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+
+ override val nodeDBbyNodeNum = ConcurrentHashMap()
+ override val nodeDBbyID = ConcurrentHashMap()
+
+ override val isNodeDbReady = MutableStateFlow(false)
+ override val allowNodeDbWrites = MutableStateFlow(false)
+
+ override fun setNodeDbReady(ready: Boolean) {
+ isNodeDbReady.value = ready
+ }
+
+ override fun setAllowNodeDbWrites(allowed: Boolean) {
+ allowNodeDbWrites.value = allowed
+ }
+
+ override var myNodeNum: Int? = null
+
+ override fun start(scope: CoroutineScope) {
+ this.scope = scope
+ }
+
+ companion object {
+ private const val TIME_MS_TO_S = 1000L
+ }
+
+ override fun loadCachedNodeDB() {
+ scope.handledLaunch {
+ val nodes = nodeRepository.nodeDBbyNum.first()
+ nodeDBbyNodeNum.putAll(nodes)
+ nodes.values.forEach { nodeDBbyID[it.user.id] = it }
+ myNodeNum = nodeRepository.myNodeInfo.value?.myNodeNum
+ }
+ }
+
+ override fun clear() {
+ nodeDBbyNodeNum.clear()
+ nodeDBbyID.clear()
+ isNodeDbReady.value = false
+ allowNodeDbWrites.value = false
+ myNodeNum = null
+ }
+
+ override fun getMyNodeInfo(): MyNodeInfo? {
+ val mi = nodeRepository.myNodeInfo.value ?: return null
+ val myNode = nodeDBbyNodeNum[mi.myNodeNum]
+ return MyNodeInfo(
+ myNodeNum = mi.myNodeNum,
+ hasGPS = (myNode?.position?.latitude_i ?: 0) != 0,
+ model = mi.model ?: myNode?.user?.hw_model?.name,
+ firmwareVersion = mi.firmwareVersion,
+ couldUpdate = mi.couldUpdate,
+ shouldUpdate = mi.shouldUpdate,
+ currentPacketId = mi.currentPacketId,
+ messageTimeoutMsec = mi.messageTimeoutMsec,
+ minAppVersion = mi.minAppVersion,
+ maxChannels = mi.maxChannels,
+ hasWifi = mi.hasWifi,
+ channelUtilization = 0f,
+ airUtilTx = 0f,
+ deviceId = mi.deviceId ?: myNode?.user?.id,
+ )
+ }
+
+ override fun getMyId(): String {
+ val num = myNodeNum ?: nodeRepository.myNodeInfo.value?.myNodeNum ?: return ""
+ return nodeDBbyNodeNum[num]?.user?.id ?: ""
+ }
+
+ override fun getNodes(): List = nodeDBbyNodeNum.values.map { it.toNodeInfo() }
+
+ override fun removeByNodenum(nodeNum: Int) {
+ nodeDBbyNodeNum.remove(nodeNum)?.let { nodeDBbyID.remove(it.user.id) }
+ }
+
+ fun getOrCreateNode(n: Int, channel: Int = 0): Node = nodeDBbyNodeNum.getOrPut(n) {
+ val userId = DataPacket.nodeNumToDefaultId(n)
+ val defaultUser =
+ User(
+ id = userId,
+ long_name = "Meshtastic ${userId.takeLast(n = 4)}",
+ short_name = userId.takeLast(n = 4),
+ hw_model = HardwareModel.UNSET,
+ )
+
+ Node(num = n, user = defaultUser, channel = channel)
+ }
+
+ override fun updateNode(nodeNum: Int, withBroadcast: Boolean, channel: Int, transform: (Node) -> Node) {
+ val current = nodeDBbyNodeNum[nodeNum] ?: getOrCreateNode(nodeNum, channel)
+ val next = transform(current)
+ nodeDBbyNodeNum[nodeNum] = next
+ if (next.user.id.isNotEmpty()) {
+ nodeDBbyID[next.user.id] = next
+ }
+
+ if (next.user.id.isNotEmpty() && isNodeDbReady.value) {
+ scope.handledLaunch { nodeRepository.upsert(next) }
+ }
+
+ if (withBroadcast) {
+ serviceBroadcasts.broadcastNodeChange(next)
+ }
+ }
+
+ override fun handleReceivedUser(fromNum: Int, p: User, channel: Int, manuallyVerified: Boolean) {
+ updateNode(fromNum) { node ->
+ val newNode = (node.isUnknownUser && p.hw_model != HardwareModel.UNSET)
+ val shouldPreserve = shouldPreserveExistingUser(node.user, p)
+
+ val next =
+ if (shouldPreserve) {
+ node.copy(channel = channel, manuallyVerified = manuallyVerified)
+ } else {
+ val keyMatch = !node.hasPKC || node.user.public_key == p.public_key
+ val newUser = if (keyMatch) p else p.copy(public_key = ByteString.EMPTY)
+ node.copy(user = newUser, channel = channel, manuallyVerified = manuallyVerified)
+ }
+ if (newNode && !shouldPreserve) {
+ serviceNotifications.showNewNodeSeenNotification(next)
+ }
+ next
+ }
+ }
+
+ override fun handleReceivedPosition(fromNum: Int, myNodeNum: Int, p: ProtoPosition, defaultTime: Long) {
+ if (myNodeNum == fromNum && (p.latitude_i ?: 0) == 0 && (p.longitude_i ?: 0) == 0) {
+ Logger.d { "Ignoring nop position update for the local node" }
+ } else {
+ updateNode(fromNum) { node ->
+ node.copy(position = p.copy(time = if (p.time != 0) p.time else (defaultTime / TIME_MS_TO_S).toInt()))
+ }
+ }
+ }
+
+ override fun handleReceivedTelemetry(fromNum: Int, telemetry: Telemetry) {
+ updateNode(fromNum) { node ->
+ when {
+ telemetry.device_metrics != null -> node.copy(deviceMetrics = telemetry.device_metrics!!)
+ telemetry.environment_metrics != null -> node.copy(environmentMetrics = telemetry.environment_metrics!!)
+ telemetry.power_metrics != null -> node.copy(powerMetrics = telemetry.power_metrics!!)
+ else -> node
+ }
+ }
+ }
+
+ override fun handleReceivedPaxcounter(fromNum: Int, p: Paxcount) {
+ updateNode(fromNum) { it.copy(paxcounter = p) }
+ }
+
+ override fun handleReceivedNodeStatus(fromNum: Int, s: StatusMessage) {
+ updateNodeStatus(fromNum, s.status)
+ }
+
+ override fun updateNodeStatus(nodeNum: Int, status: String?) {
+ updateNode(nodeNum) { it.copy(nodeStatus = status?.takeIf { s -> s.isNotEmpty() }) }
+ }
+
+ override fun installNodeInfo(info: ProtoNodeInfo, withBroadcast: Boolean) {
+ updateNode(info.num, withBroadcast = withBroadcast) { node ->
+ var next = node
+ val user = info.user
+ if (user != null) {
+ if (shouldPreserveExistingUser(node.user, user)) {
+ // keep existing names
+ } else {
+ var newUser = user.let { if (it.is_licensed) it.copy(public_key = ByteString.EMPTY) else it }
+ if (info.via_mqtt) {
+ newUser = newUser.copy(long_name = "${newUser.long_name} (MQTT)")
+ }
+ next = next.copy(user = newUser)
+ }
+ }
+ val position = info.position
+ if (position != null) {
+ next = next.copy(position = position)
+ }
+ next =
+ next.copy(
+ lastHeard = info.last_heard,
+ deviceMetrics = info.device_metrics ?: next.deviceMetrics,
+ channel = info.channel,
+ viaMqtt = info.via_mqtt,
+ hopsAway = info.hops_away ?: -1,
+ isFavorite = info.is_favorite,
+ isIgnored = info.is_ignored,
+ isMuted = info.is_muted,
+ )
+ next
+ }
+ }
+
+ override fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) {
+ scope.handledLaunch { nodeRepository.insertMetadata(nodeNum, metadata) }
+ }
+
+ private fun shouldPreserveExistingUser(existing: User, incoming: User): Boolean {
+ val isDefaultName = (incoming.long_name).matches(Regex("^Meshtastic [0-9a-fA-F]{4}$"))
+ val isDefaultHwModel = incoming.hw_model == HardwareModel.UNSET
+ val hasExistingUser = (existing.id).isNotEmpty() && existing.hw_model != HardwareModel.UNSET
+ return hasExistingUser && isDefaultName && isDefaultHwModel
+ }
+
+ override fun toNodeID(nodeNum: Int): String = if (nodeNum == DataPacket.NODENUM_BROADCAST) {
+ DataPacket.ID_BROADCAST
+ } else {
+ nodeDBbyNodeNum[nodeNum]?.user?.id ?: DataPacket.nodeNumToDefaultId(nodeNum)
+ }
+
+ private fun Node.toNodeInfo(): NodeInfo = NodeInfo(
+ num = num,
+ user =
+ MeshUser(
+ id = user.id,
+ longName = user.long_name,
+ shortName = user.short_name,
+ hwModel = user.hw_model,
+ role = user.role.value,
+ ),
+ position =
+ Position(
+ latitude = latitude,
+ longitude = longitude,
+ altitude = position.altitude ?: 0,
+ time = position.time,
+ satellitesInView = position.sats_in_view ?: 0,
+ groundSpeed = position.ground_speed ?: 0,
+ groundTrack = position.ground_track ?: 0,
+ precisionBits = position.precision_bits ?: 0,
+ )
+ .takeIf { latitude != 0.0 || longitude != 0.0 },
+ snr = snr,
+ rssi = rssi,
+ lastHeard = lastHeard,
+ deviceMetrics =
+ DeviceMetrics(
+ batteryLevel = deviceMetrics.battery_level ?: 0,
+ voltage = deviceMetrics.voltage ?: 0f,
+ channelUtilization = deviceMetrics.channel_utilization ?: 0f,
+ airUtilTx = deviceMetrics.air_util_tx ?: 0f,
+ uptimeSeconds = deviceMetrics.uptime_seconds ?: 0,
+ ),
+ channel = channel,
+ environmentMetrics = EnvironmentMetrics.fromTelemetryProto(environmentMetrics, 0),
+ hopsAway = hopsAway,
+ nodeStatus = nodeStatus,
+ )
+}
diff --git a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
similarity index 77%
rename from app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
index d85edd7ad2..a29cfed986 100644
--- a/app/src/main/java/com/geeksville/mesh/service/PacketHandler.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt
@@ -14,10 +14,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
-import com.geeksville.mesh.repository.radio.RadioInterfaceService
import dagger.Lazy
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -30,13 +29,18 @@ import kotlinx.coroutines.withTimeoutOrNull
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.repository.MeshLogRepository
-import org.meshtastic.core.data.repository.PacketRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.MessageStatus
+import org.meshtastic.core.model.RadioNotConnectedException
import org.meshtastic.core.model.util.toOneLineString
import org.meshtastic.core.model.util.toPIIString
+import org.meshtastic.core.repository.PacketHandler
+import org.meshtastic.core.repository.PacketRepository
+import org.meshtastic.core.repository.RadioInterfaceService
+import org.meshtastic.core.repository.ServiceBroadcasts
+import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.FromRadio
import org.meshtastic.proto.MeshPacket
import org.meshtastic.proto.QueueStatus
@@ -51,18 +55,18 @@ import kotlin.uuid.Uuid
@Suppress("TooManyFunctions")
@Singleton
-class PacketHandler
+class PacketHandlerImpl
@Inject
constructor(
private val packetRepository: Lazy,
- private val serviceBroadcasts: MeshServiceBroadcasts,
+ private val serviceBroadcasts: ServiceBroadcasts,
private val radioInterfaceService: RadioInterfaceService,
private val meshLogRepository: Lazy,
- private val connectionStateHolder: ConnectionStateHandler,
-) {
+ private val serviceRepository: ServiceRepository,
+) : PacketHandler {
companion object {
- private val TIMEOUT = 5.seconds // Increased from 250ms to be more tolerant
+ private val TIMEOUT = 5.seconds
}
private var queueJob: Job? = null
@@ -71,15 +75,11 @@ constructor(
private val queuedPackets = ConcurrentLinkedQueue()
private val queueResponse = ConcurrentHashMap>()
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
}
- /**
- * Send a command/packet to our radio. But cope with the possibility that we might start up before we are fully
- * bound to the RadioInterfaceService
- */
- fun sendToRadio(p: ToRadio) {
+ override fun sendToRadio(p: ToRadio) {
Logger.d { "Sending to radio ${p.toPIIString()}" }
val b = p.encode()
@@ -94,7 +94,7 @@ constructor(
message_type = "Packet",
received_date = nowMillis,
raw_message = packet.toString(),
- fromNum = MeshLog.NODE_NUM_LOCAL, // Outgoing packets are always from the local node
+ fromNum = MeshLog.NODE_NUM_LOCAL,
portNum = packet.decoded?.portnum?.value ?: 0,
fromRadio = FromRadio(packet = packet),
)
@@ -102,16 +102,12 @@ constructor(
}
}
- /**
- * Send a mesh packet to the radio, if the radio is not currently connected this function will throw
- * NotConnectedException
- */
- fun sendToRadio(packet: MeshPacket) {
+ override fun sendToRadio(packet: MeshPacket) {
queuedPackets.add(packet)
startPacketQueue()
}
- fun stopPacketQueue() {
+ override fun stopPacketQueue() {
if (queueJob?.isActive == true) {
Logger.i { "Stopping packet queueJob" }
queueJob?.cancel()
@@ -122,33 +118,30 @@ constructor(
}
}
- fun handleQueueStatus(queueStatus: QueueStatus) {
+ override fun handleQueueStatus(queueStatus: QueueStatus) {
Logger.d { "[queueStatus] ${queueStatus.toOneLineString()}" }
val (success, isFull, requestId) = with(queueStatus) { Triple(res == 0, free == 0, mesh_packet_id) }
- if (success && isFull) return // Queue is full, wait for free != 0
+ if (success && isFull) return
if (requestId != 0) {
queueResponse.remove(requestId)?.complete(success)
} else {
- // This is slightly suboptimal but matches legacy behavior for packets without IDs
queueResponse.values.firstOrNull { !it.isCompleted }?.complete(success)
}
}
- fun removeResponse(dataRequestId: Int, complete: Boolean) {
+ override fun removeResponse(dataRequestId: Int, complete: Boolean) {
queueResponse.remove(dataRequestId)?.complete(complete)
}
- @Suppress("TooGenericExceptionCaught", "SwallowedException")
private fun startPacketQueue() {
if (queueJob?.isActive == true) return
queueJob =
scope.handledLaunch {
Logger.d { "packet queueJob started" }
- while (connectionStateHolder.connectionState.value == ConnectionState.Connected) {
- // take the first packet from the queue head
+ while (serviceRepository.connectionState.value == ConnectionState.Connected) {
val packet = queuedPackets.poll() ?: break
+ @Suppress("TooGenericExceptionCaught", "SwallowedException")
try {
- // send packet to the radio and wait for response
val response = sendPacket(packet)
Logger.d { "queueJob packet id=${packet.id.toUInt()} waiting" }
val success = withTimeout(TIMEOUT) { response.await() }
@@ -164,7 +157,6 @@ constructor(
}
}
- /** Change the status on a DataPacket and update watchers */
private fun changeStatus(packetId: Int, m: MessageStatus) = scope.handledLaunch {
if (packetId != 0) {
getDataPacketById(packetId)?.let { p ->
@@ -175,11 +167,10 @@ constructor(
}
}
- @Suppress("MagicNumber")
private suspend fun getDataPacketById(packetId: Int): DataPacket? = withTimeoutOrNull(1.seconds) {
var dataPacket: DataPacket? = null
while (dataPacket == null) {
- dataPacket = packetRepository.get().getPacketById(packetId)?.data
+ dataPacket = packetRepository.get().getPacketById(packetId)
if (dataPacket == null) delay(100.milliseconds)
}
dataPacket
@@ -187,17 +178,14 @@ constructor(
@Suppress("TooGenericExceptionCaught")
private fun sendPacket(packet: MeshPacket): CompletableDeferred {
- // send the packet to the radio and return a CompletableDeferred that will be completed with
- // the result
val deferred = CompletableDeferred()
queueResponse[packet.id] = deferred
try {
- if (connectionStateHolder.connectionState.value != ConnectionState.Connected) {
+ if (serviceRepository.connectionState.value != ConnectionState.Connected) {
throw RadioNotConnectedException()
}
sendToRadio(ToRadio(packet = packet))
} catch (ex: RadioNotConnectedException) {
- // Expected when radio is not connected, log as warning to avoid Crashlytics noise
Logger.w(ex) { "sendToRadio skipped: Not connected to radio" }
deferred.complete(false)
} catch (ex: Exception) {
@@ -209,8 +197,6 @@ constructor(
private fun insertMeshLog(packetToSave: MeshLog) {
scope.handledLaunch {
- // Do not log, because might contain PII
-
Logger.d {
"insert: ${packetToSave.message_type} = " +
"${packetToSave.raw_message.toOneLineString()} from=${packetToSave.fromNum}"
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
similarity index 74%
rename from app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
index 0ca3e3947c..2524e83018 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshTracerouteHandler.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.kt
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package com.geeksville.mesh.service
+package org.meshtastic.core.data.manager
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
@@ -22,48 +22,47 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
-import org.meshtastic.core.data.repository.NodeRepository
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
+import org.meshtastic.core.model.Node
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getFullTracerouteResponse
-import org.meshtastic.core.resources.Res
-import org.meshtastic.core.resources.getString
-import org.meshtastic.core.resources.traceroute_duration
-import org.meshtastic.core.resources.traceroute_route_back_to_us
-import org.meshtastic.core.resources.traceroute_route_towards_dest
-import org.meshtastic.core.resources.unknown_username
-import org.meshtastic.core.service.ServiceRepository
-import org.meshtastic.core.service.TracerouteResponse
+import org.meshtastic.core.model.service.TracerouteResponse
+import org.meshtastic.core.repository.CommandSender
+import org.meshtastic.core.repository.NodeManager
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.core.repository.ServiceRepository
+import org.meshtastic.core.repository.TracerouteHandler
import org.meshtastic.proto.MeshPacket
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class MeshTracerouteHandler
+class TracerouteHandlerImpl
@Inject
constructor(
- private val nodeManager: MeshNodeManager,
+ private val nodeManager: NodeManager,
private val serviceRepository: ServiceRepository,
private val tracerouteSnapshotRepository: TracerouteSnapshotRepository,
private val nodeRepository: NodeRepository,
- private val commandSender: MeshCommandSender,
-) {
+ private val commandSender: CommandSender,
+) : TracerouteHandler {
private var scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
- fun start(scope: CoroutineScope) {
+ override fun start(scope: CoroutineScope) {
this.scope = scope
}
- fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) {
+ override fun handleTraceroute(packet: MeshPacket, logUuid: String?, logInsertJob: kotlinx.coroutines.Job?) {
val full =
packet.getFullTracerouteResponse(
getUser = { num ->
- nodeManager.nodeDBbyNodeNum[num]?.let { "${it.longName} (${it.shortName})" }
- ?: getString(Res.string.unknown_username)
+ nodeManager.nodeDBbyNodeNum[num]?.let { node: Node ->
+ "${node.user.long_name} (${node.user.short_name})"
+ } ?: "Unknown" // We don't have strings in core:data yet, but we can fix this later
},
- headerTowards = getString(Res.string.traceroute_route_towards_dest),
- headerBack = getString(Res.string.traceroute_route_back_to_us),
+ headerTowards = "Route towards destination:",
+ headerBack = "Route back to us:",
) ?: return
val requestId = packet.decoded?.request_id ?: 0
@@ -87,7 +86,7 @@ constructor(
val elapsedMs = nowMillis - start
val seconds = elapsedMs / MILLIS_PER_SECOND
Logger.i { "Traceroute $requestId complete in $seconds s" }
- val durationText = getString(Res.string.traceroute_duration, "%.1f".format(Locale.US, seconds))
+ val durationText = "Duration: %.1f s".format(Locale.US, seconds)
"$full\n\n$durationText"
} else {
full
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
similarity index 97%
rename from core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
index d189f19f77..d4901d02b1 100644
--- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepository.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/DeviceHardwareRepositoryImpl.kt
@@ -29,12 +29,13 @@ import org.meshtastic.core.model.BootloaderOtaQuirk
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.util.TimeConstants
import org.meshtastic.core.network.DeviceHardwareRemoteDataSource
+import org.meshtastic.core.repository.DeviceHardwareRepository
import javax.inject.Inject
import javax.inject.Singleton
// Annotating with Singleton to ensure a single instance manages the cache
@Singleton
-class DeviceHardwareRepository
+class DeviceHardwareRepositoryImpl
@Inject
constructor(
private val remoteDataSource: DeviceHardwareRemoteDataSource,
@@ -42,7 +43,7 @@ constructor(
private val jsonDataSource: DeviceHardwareJsonDataSource,
private val bootloaderOtaQuirksJsonDataSource: BootloaderOtaQuirksJsonDataSource,
private val dispatchers: CoroutineDispatchers,
-) {
+) : DeviceHardwareRepository {
/**
* Retrieves device hardware information by its model ID and optional target string.
@@ -59,10 +60,10 @@ constructor(
* @return A [Result] containing the [DeviceHardware] on success (or null if not found), or an exception on failure.
*/
@Suppress("LongMethod", "detekt:CyclomaticComplexMethod")
- suspend fun getDeviceHardwareByModel(
+ override suspend fun getDeviceHardwareByModel(
hwModel: Int,
- target: String? = null,
- forceRefresh: Boolean = false,
+ target: String?,
+ forceRefresh: Boolean,
): Result = withContext(dispatchers.io) {
Logger.d {
"DeviceHardwareRepository: getDeviceHardwareByModel(hwModel=$hwModel," +
diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt
similarity index 67%
rename from core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepository.kt
rename to core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt
index 53729ce489..a6af8c51e8 100644
--- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepository.kt
+++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.kt
@@ -40,13 +40,16 @@ import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.MetadataEntity
import org.meshtastic.core.database.entity.MyNodeEntity
import org.meshtastic.core.database.entity.NodeEntity
-import org.meshtastic.core.database.model.Node
-import org.meshtastic.core.database.model.NodeSortOption
import org.meshtastic.core.datastore.LocalStatsDataSource
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.di.ProcessLifecycle
import org.meshtastic.core.model.DataPacket
+import org.meshtastic.core.model.MyNodeInfo
+import org.meshtastic.core.model.Node
+import org.meshtastic.core.model.NodeSortOption
import org.meshtastic.core.model.util.onlineTimeThreshold
+import org.meshtastic.core.repository.NodeRepository
+import org.meshtastic.proto.DeviceMetadata
import org.meshtastic.proto.HardwareModel
import org.meshtastic.proto.LocalStats
import org.meshtastic.proto.User
@@ -56,7 +59,7 @@ import javax.inject.Singleton
/** Repository for managing node-related data, including hardware info, node database, and identity. */
@Singleton
@Suppress("TooManyFunctions")
-open class NodeRepository
+class NodeRepositoryImpl
@Inject
constructor(
@ProcessLifecycle private val processLifecycle: Lifecycle,
@@ -64,28 +67,29 @@ constructor(
private val nodeInfoWriteDataSource: NodeInfoWriteDataSource,
private val dispatchers: CoroutineDispatchers,
private val localStatsDataSource: LocalStatsDataSource,
-) {
+) : NodeRepository {
/** Hardware info about our local device (can be null if not connected). */
- open val myNodeInfo: StateFlow =
+ override val myNodeInfo: StateFlow =
nodeInfoReadDataSource
.myNodeInfoFlow()
+ .map { it?.toMyNodeInfo() }
.flowOn(dispatchers.io)
.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, null)
private val _ourNodeInfo = MutableStateFlow(null)
/** Information about the locally connected node, as seen from the mesh. */
- open val ourNodeInfo: StateFlow
+ override val ourNodeInfo: StateFlow
get() = _ourNodeInfo
private val _myId = MutableStateFlow(null)
/** The unique userId (hex string) of our local node. */
- val myId: StateFlow
+ override val myId: StateFlow
get() = _myId
/** The latest local stats telemetry received from the locally connected node. */
- val localStats: StateFlow =
+ override val localStats: StateFlow =
localStatsDataSource.localStatsFlow.stateIn(
processLifecycle.coroutineScope,
SharingStarted.Eagerly,
@@ -93,12 +97,12 @@ constructor(
)
/** Update the cached local stats telemetry. */
- fun updateLocalStats(stats: LocalStats) {
+ override fun updateLocalStats(stats: LocalStats) {
processLifecycle.coroutineScope.launch { localStatsDataSource.setLocalStats(stats) }
}
/** A reactive map from nodeNum to [Node] objects, representing the entire mesh. */
- val nodeDBbyNum: StateFlow