mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: Migrate project to Kotlin Multiplatform (KMP) architecture (#4738)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
182ad933f4
commit
0ce322a0f5
163 changed files with 1837 additions and 877 deletions
|
|
@ -40,6 +40,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import co.touchlab.kermit.Logger
|
||||
|
|
@ -47,14 +48,23 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
import kotlinx.coroutines.launch
|
||||
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
|
||||
import no.nordicsemi.kotlin.ble.environment.android.compose.LocalEnvironmentOwner
|
||||
import org.meshtastic.app.intro.AnalyticsIntro
|
||||
import org.meshtastic.app.intro.AndroidIntroViewModel
|
||||
import org.meshtastic.app.map.getMapViewProvider
|
||||
import org.meshtastic.app.model.UIViewModel
|
||||
import org.meshtastic.app.ui.MainScreen
|
||||
import org.meshtastic.core.barcode.rememberBarcodeScanner
|
||||
import org.meshtastic.core.model.util.dispatchMeshtasticUri
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.nfc.NfcScannerEffect
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.channel_invalid
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
import org.meshtastic.core.ui.util.LocalAnalyticsIntroProvider
|
||||
import org.meshtastic.core.ui.util.LocalBarcodeScannerProvider
|
||||
import org.meshtastic.core.ui.util.LocalMapViewProvider
|
||||
import org.meshtastic.core.ui.util.LocalNfcScannerProvider
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.intro.AppIntroductionScreen
|
||||
import javax.inject.Inject
|
||||
|
|
@ -108,7 +118,13 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
|
||||
@Suppress("SpreadOperator")
|
||||
CompositionLocalProvider(*(LocalEnvironmentOwner provides androidEnvironment)) {
|
||||
CompositionLocalProvider(
|
||||
*(LocalEnvironmentOwner provides androidEnvironment),
|
||||
LocalBarcodeScannerProvider provides { onResult -> rememberBarcodeScanner(onResult) },
|
||||
LocalNfcScannerProvider provides { onResult, onDisabled -> NfcScannerEffect(onResult, onDisabled) },
|
||||
LocalAnalyticsIntroProvider provides { AnalyticsIntro() },
|
||||
LocalMapViewProvider provides getMapViewProvider(),
|
||||
) {
|
||||
AppTheme(dynamicColor = dynamic, darkTheme = dark) {
|
||||
val appIntroCompleted by model.appIntroCompleted.collectAsStateWithLifecycle()
|
||||
|
||||
|
|
@ -119,7 +135,8 @@ class MainActivity : ComponentActivity() {
|
|||
if (appIntroCompleted) {
|
||||
MainScreen(uIViewModel = model)
|
||||
} else {
|
||||
AppIntroductionScreen(onDone = { model.onAppIntroCompleted() })
|
||||
val introViewModel = hiltViewModel<AndroidIntroViewModel>()
|
||||
AppIntroductionScreen(onDone = { model.onAppIntroCompleted() }, viewModel = introViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt
Normal file
75
app/src/main/kotlin/org/meshtastic/app/di/BleModule.kt
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.di
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.native
|
||||
import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment
|
||||
import no.nordicsemi.kotlin.ble.environment.android.NativeAndroidEnvironment
|
||||
import org.meshtastic.core.ble.AndroidBleConnectionFactory
|
||||
import org.meshtastic.core.ble.AndroidBleScanner
|
||||
import org.meshtastic.core.ble.AndroidBluetoothRepository
|
||||
import org.meshtastic.core.ble.BleConnection
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class BleModule {
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindBleScanner(impl: AndroidBleScanner): BleScanner
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindBluetoothRepository(impl: AndroidBluetoothRepository): BluetoothRepository
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindBleConnectionFactory(impl: AndroidBleConnectionFactory): BleConnectionFactory
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAndroidEnvironment(@ApplicationContext context: Context): AndroidEnvironment =
|
||||
NativeAndroidEnvironment.getInstance(context, isNeverForLocationFlagSet = true)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBleSingletonCoroutineScope(dispatchers: CoroutineDispatchers): CoroutineScope =
|
||||
CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCentralManager(environment: AndroidEnvironment, coroutineScope: CoroutineScope): CentralManager =
|
||||
CentralManager.native(environment as NativeAndroidEnvironment, coroutineScope)
|
||||
|
||||
@Provides
|
||||
fun provideBleConnection(factory: BleConnectionFactory, coroutineScope: CoroutineScope): BleConnection =
|
||||
factory.create(coroutineScope, "BLE")
|
||||
}
|
||||
}
|
||||
38
app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt
Normal file
38
app/src/main/kotlin/org/meshtastic/app/di/ServiceModule.kt
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.service.AndroidRadioControllerImpl
|
||||
import org.meshtastic.core.service.AndroidServiceRepository
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class ServiceModule {
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindRadioController(impl: AndroidRadioControllerImpl): RadioController
|
||||
|
||||
@Binds @Singleton
|
||||
abstract fun bindServiceRepository(impl: AndroidServiceRepository): ServiceRepository
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ constructor(
|
|||
val matchingNode =
|
||||
if (databaseManager.hasDatabaseFor(entry.fullAddress)) {
|
||||
db.values.find { node ->
|
||||
val suffix = entry.peripheral.getMeshtasticShortName()?.lowercase(Locale.ROOT)
|
||||
val suffix = entry.device.getMeshtasticShortName()?.lowercase(Locale.ROOT)
|
||||
suffix != null && node.user.id.lowercase(Locale.ROOT).endsWith(suffix)
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.intro
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.meshtastic.feature.intro.IntroViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Android-specific Hilt wrapper for IntroViewModel. */
|
||||
@HiltViewModel class AndroidIntroViewModel @Inject constructor() : IntroViewModel()
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.feature.map.SharedMapViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AndroidSharedMapViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
mapPrefs: MapPrefs,
|
||||
nodeRepository: NodeRepository,
|
||||
packetRepository: PacketRepository,
|
||||
radioController: RadioController,
|
||||
) : SharedMapViewModel(mapPrefs, nodeRepository, packetRepository, radioController)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.map.node
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.navigation.toRoute
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.database.entity.MeshLog
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.MeshLogRepository
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.ui.util.toPosition
|
||||
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Position
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NodeMapViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
nodeRepository: NodeRepository,
|
||||
meshLogRepository: MeshLogRepository,
|
||||
buildConfigProvider: BuildConfigProvider,
|
||||
private val mapPrefs: MapPrefs,
|
||||
) : ViewModel() {
|
||||
private val destNum = savedStateHandle.toRoute<NodesRoutes.NodeDetailGraph>().destNum
|
||||
|
||||
val node =
|
||||
nodeRepository.nodeDBbyNum
|
||||
.mapLatest { it[destNum] }
|
||||
.distinctUntilChanged()
|
||||
.stateInWhileSubscribed(initialValue = null)
|
||||
|
||||
val applicationId = buildConfigProvider.applicationId
|
||||
|
||||
private val ourNodeNumFlow = nodeRepository.myNodeInfo.map { it?.myNodeNum }.distinctUntilChanged()
|
||||
|
||||
val positionLogs: StateFlow<List<Position>> =
|
||||
ourNodeNumFlow
|
||||
.map { if (destNum == it) MeshLog.NODE_NUM_LOCAL else destNum!! }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { logId ->
|
||||
meshLogRepository.getMeshPacketsFrom(logId, PortNum.POSITION_APP.value).map { packets ->
|
||||
packets
|
||||
.mapNotNull { it.toPosition() }
|
||||
.asFlow()
|
||||
.distinctUntilChanged { old, new ->
|
||||
old.time == new.time ||
|
||||
(old.latitude_i == new.latitude_i && old.longitude_i == new.longitude_i)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
.stateInWhileSubscribed(initialValue = emptyList())
|
||||
|
||||
val mapStyleId: Int
|
||||
get() = mapPrefs.mapStyle.value
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.messaging
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.feature.messaging.ui.contact.ContactsViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AndroidContactsViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
nodeRepository: NodeRepository,
|
||||
packetRepository: PacketRepository,
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
serviceRepository: ServiceRepository,
|
||||
) : ContactsViewModel(nodeRepository, packetRepository, radioConfigRepository, serviceRepository)
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.messaging
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.meshtastic.core.data.repository.QuickChatActionRepository
|
||||
import org.meshtastic.core.repository.CustomEmojiPrefs
|
||||
import org.meshtastic.core.repository.HomoglyphPrefs
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.meshtastic.core.repository.usecase.SendMessageUseCase
|
||||
import org.meshtastic.feature.messaging.MessageViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
class AndroidMessageViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
nodeRepository: NodeRepository,
|
||||
radioConfigRepository: RadioConfigRepository,
|
||||
quickChatActionRepository: QuickChatActionRepository,
|
||||
serviceRepository: ServiceRepository,
|
||||
packetRepository: PacketRepository,
|
||||
uiPrefs: UiPrefs,
|
||||
customEmojiPrefs: CustomEmojiPrefs,
|
||||
homoglyphEncodingPrefs: HomoglyphPrefs,
|
||||
meshServiceNotifications: MeshServiceNotifications,
|
||||
sendMessageUseCase: SendMessageUseCase,
|
||||
) : MessageViewModel(
|
||||
savedStateHandle,
|
||||
nodeRepository,
|
||||
radioConfigRepository,
|
||||
quickChatActionRepository,
|
||||
serviceRepository,
|
||||
packetRepository,
|
||||
uiPrefs,
|
||||
customEmojiPrefs,
|
||||
homoglyphEncodingPrefs,
|
||||
meshServiceNotifications,
|
||||
sendMessageUseCase,
|
||||
)
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.messaging
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import org.meshtastic.core.data.repository.QuickChatActionRepository
|
||||
import org.meshtastic.feature.messaging.QuickChatViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AndroidQuickChatViewModel @Inject constructor(quickChatActionRepository: QuickChatActionRepository) :
|
||||
QuickChatViewModel(quickChatActionRepository)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.messaging.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.meshtastic.app.messaging.domain.worker.WorkManagerMessageQueue
|
||||
import org.meshtastic.core.repository.MessageQueue
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class MessagingModule {
|
||||
|
||||
@Binds abstract fun bindMessageQueue(impl: WorkManagerMessageQueue): MessageQueue
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.messaging.domain.worker
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.MessageStatus
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
|
||||
@HiltWorker
|
||||
class SendMessageWorker
|
||||
@AssistedInject
|
||||
constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val radioController: RadioController,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException", "ReturnCount")
|
||||
override suspend fun doWork(): Result {
|
||||
val packetId = inputData.getInt(KEY_PACKET_ID, 0)
|
||||
if (packetId == 0) return Result.failure()
|
||||
|
||||
// Verify we are connected before attempting to send to avoid unnecessary Exception bubbling
|
||||
if (radioController.connectionState.value != ConnectionState.Connected) {
|
||||
return Result.retry()
|
||||
}
|
||||
|
||||
val packetData =
|
||||
packetRepository.getPacketByPacketId(packetId)
|
||||
?: return Result.failure() // Packet no longer exists in DB? Do not retry.
|
||||
|
||||
return try {
|
||||
radioController.sendMessage(packetData)
|
||||
packetRepository.updateMessageStatus(packetData, MessageStatus.ENROUTE)
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
packetRepository.updateMessageStatus(packetData, MessageStatus.QUEUED)
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_PACKET_ID = "packet_id"
|
||||
const val WORK_NAME_PREFIX = "send_message_"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.app.messaging.domain.worker
|
||||
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import org.meshtastic.core.repository.MessageQueue
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Android implementation of [MessageQueue] that uses [WorkManager] for reliable background transmission. */
|
||||
@Singleton
|
||||
class WorkManagerMessageQueue @Inject constructor(private val workManager: WorkManager) : MessageQueue {
|
||||
|
||||
override suspend fun enqueue(packetId: Int) {
|
||||
val workRequest =
|
||||
OneTimeWorkRequestBuilder<SendMessageWorker>()
|
||||
.setInputData(workDataOf(SendMessageWorker.KEY_PACKET_ID to packetId))
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniqueWork(
|
||||
"${SendMessageWorker.WORK_NAME_PREFIX}$packetId",
|
||||
ExistingWorkPolicy.REPLACE,
|
||||
workRequest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,7 @@ package org.meshtastic.app.model
|
|||
|
||||
import android.hardware.usb.UsbManager
|
||||
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.BleDevice
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.model.Node
|
||||
|
|
@ -50,15 +49,14 @@ sealed class DeviceListEntry(
|
|||
override fun toString(): String =
|
||||
"DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize}, bonded=$bonded, hasNode=${node != null})"
|
||||
|
||||
@Suppress("MissingPermission")
|
||||
data class Ble(val peripheral: Peripheral, override val node: Node? = null) :
|
||||
data class Ble(val device: BleDevice, override val node: Node? = null) :
|
||||
DeviceListEntry(
|
||||
name = peripheral.name ?: "unnamed-${peripheral.address}",
|
||||
fullAddress = "x${peripheral.address}",
|
||||
bonded = peripheral.bondState.value == BondState.BONDED,
|
||||
name = device.name ?: "unnamed-${device.address}",
|
||||
fullAddress = "x${device.address}",
|
||||
bonded = device.isBonded,
|
||||
node = node,
|
||||
) {
|
||||
override fun copy(node: Node?): Ble = copy(peripheral = peripheral, node = node)
|
||||
override fun copy(node: Node?): Ble = copy(device = device, node = node)
|
||||
}
|
||||
|
||||
data class Usb(
|
||||
|
|
@ -95,4 +93,4 @@ private val bleNameRegex = Regex(BLE_NAME_PATTERN)
|
|||
*
|
||||
* @return The short name (e.g., 1234) or null.
|
||||
*/
|
||||
fun Peripheral.getMeshtasticShortName(): String? = name?.let { bleNameRegex.find(it)?.groupValues?.get(1) }
|
||||
fun BleDevice.getMeshtasticShortName(): String? = name?.let { bleNameRegex.find(it)?.groupValues?.get(1) }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ import androidx.navigation.navDeepLink
|
|||
import androidx.navigation.navigation
|
||||
import androidx.navigation.toRoute
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.app.messaging.AndroidContactsViewModel
|
||||
import org.meshtastic.app.messaging.AndroidMessageViewModel
|
||||
import org.meshtastic.app.messaging.AndroidQuickChatViewModel
|
||||
import org.meshtastic.app.model.UIViewModel
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
|
|
@ -43,9 +46,13 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
|
|||
val uiViewModel: UIViewModel = hiltViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val contactsViewModel = hiltViewModel<AndroidContactsViewModel>()
|
||||
val messageViewModel = hiltViewModel<AndroidMessageViewModel>()
|
||||
|
||||
AdaptiveContactsScreen(
|
||||
navController = navController,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = messageViewModel,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
|
|
@ -67,9 +74,13 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
|
|||
val uiViewModel: UIViewModel = hiltViewModel()
|
||||
val sharedContactRequested by uiViewModel.sharedContactRequested.collectAsStateWithLifecycle()
|
||||
val requestChannelSet by uiViewModel.requestChannelSet.collectAsStateWithLifecycle()
|
||||
val contactsViewModel = hiltViewModel<AndroidContactsViewModel>()
|
||||
val messageViewModel = hiltViewModel<AndroidMessageViewModel>()
|
||||
|
||||
AdaptiveContactsScreen(
|
||||
navController = navController,
|
||||
contactsViewModel = contactsViewModel,
|
||||
messageViewModel = messageViewModel,
|
||||
scrollToTopEvents = scrollToTopEvents,
|
||||
sharedContactRequested = sharedContactRequested,
|
||||
requestChannelSet = requestChannelSet,
|
||||
|
|
@ -90,7 +101,9 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
|
|||
),
|
||||
) { backStackEntry ->
|
||||
val message = backStackEntry.toRoute<ContactsRoutes.Share>().message
|
||||
val viewModel = hiltViewModel<AndroidContactsViewModel>()
|
||||
ShareScreen(
|
||||
viewModel = viewModel,
|
||||
onConfirm = {
|
||||
navController.navigate(ContactsRoutes.Messages(it, message)) {
|
||||
popUpTo<ContactsRoutes.Share> { inclusive = true }
|
||||
|
|
@ -102,6 +115,7 @@ fun NavGraphBuilder.contactsGraph(navController: NavHostController, scrollToTopE
|
|||
composable<ContactsRoutes.QuickChat>(
|
||||
deepLinks = listOf(navDeepLink<ContactsRoutes.QuickChat>(basePath = "$DEEP_LINK_BASE_URI/quick_chat")),
|
||||
) {
|
||||
QuickChatScreen(onNavigateUp = navController::navigateUp)
|
||||
val viewModel = hiltViewModel<AndroidQuickChatViewModel>()
|
||||
QuickChatScreen(viewModel = viewModel, onNavigateUp = navController::navigateUp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,10 +16,12 @@
|
|||
*/
|
||||
package org.meshtastic.app.navigation
|
||||
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import org.meshtastic.app.map.AndroidSharedMapViewModel
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
|
|
@ -27,7 +29,9 @@ import org.meshtastic.feature.map.MapScreen
|
|||
|
||||
fun NavGraphBuilder.mapGraph(navController: NavHostController) {
|
||||
composable<MapRoutes.Map>(deepLinks = listOf(navDeepLink<MapRoutes.Map>(basePath = "$DEEP_LINK_BASE_URI/map"))) {
|
||||
val viewModel = hiltViewModel<AndroidSharedMapViewModel>()
|
||||
MapScreen(
|
||||
viewModel = viewModel,
|
||||
onClickNodeChip = {
|
||||
navController.navigate(NodesRoutes.NodeDetailGraph(it)) {
|
||||
launchSingleTop = true
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import androidx.navigation.navDeepLink
|
|||
import androidx.navigation.toRoute
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.app.map.node.NodeMapScreen
|
||||
import org.meshtastic.app.map.node.NodeMapViewModel
|
||||
import org.meshtastic.app.ui.node.AdaptiveNodeListScreen
|
||||
import org.meshtastic.core.navigation.ContactsRoutes
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
|
|
@ -57,8 +59,6 @@ import org.meshtastic.core.resources.power
|
|||
import org.meshtastic.core.resources.signal
|
||||
import org.meshtastic.core.resources.traceroute
|
||||
import org.meshtastic.core.ui.component.ScrollToTopEvent
|
||||
import org.meshtastic.feature.map.node.NodeMapScreen
|
||||
import org.meshtastic.feature.map.node.NodeMapViewModel
|
||||
import org.meshtastic.feature.node.metrics.DeviceMetricsScreen
|
||||
import org.meshtastic.feature.node.metrics.EnvironmentMetricsScreen
|
||||
import org.meshtastic.feature.node.metrics.HostMetricsLogScreen
|
||||
|
|
|
|||
|
|
@ -33,12 +33,15 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import no.nordicsemi.kotlin.ble.client.android.CentralManager
|
||||
import no.nordicsemi.kotlin.ble.client.android.Peripheral
|
||||
import no.nordicsemi.kotlin.ble.core.ConnectionState
|
||||
import no.nordicsemi.kotlin.ble.core.WriteType
|
||||
import org.meshtastic.core.ble.AndroidBleDevice
|
||||
import org.meshtastic.core.ble.AndroidBleService
|
||||
import org.meshtastic.core.ble.BleConnection
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleConnectionState
|
||||
import org.meshtastic.core.ble.BleDevice
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.ble.BleWriteType
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
|
||||
import org.meshtastic.core.ble.retryBleOperation
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
|
|
@ -62,7 +65,9 @@ private val SCAN_TIMEOUT = 5.seconds
|
|||
* - Routing raw byte packets between the radio and [RadioInterfaceService].
|
||||
*
|
||||
* @param serviceScope The coroutine scope to use for launching coroutines.
|
||||
* @param centralManager The central manager provided by Nordic BLE Library.
|
||||
* @param scanner The BLE scanner.
|
||||
* @param bluetoothRepository The Bluetooth repository.
|
||||
* @param connectionFactory The BLE connection factory.
|
||||
* @param service The [RadioInterfaceService] to use for handling radio events.
|
||||
* @param address The BLE address of the device to connect to.
|
||||
*/
|
||||
|
|
@ -71,7 +76,9 @@ class NordicBleInterface
|
|||
@AssistedInject
|
||||
constructor(
|
||||
private val serviceScope: CoroutineScope,
|
||||
private val centralManager: CentralManager,
|
||||
private val scanner: BleScanner,
|
||||
private val bluetoothRepository: BluetoothRepository,
|
||||
private val connectionFactory: BleConnectionFactory,
|
||||
private val service: RadioInterfaceService,
|
||||
@Assisted val address: String,
|
||||
) : IRadioInterface {
|
||||
|
|
@ -91,7 +98,7 @@ constructor(
|
|||
|
||||
private val connectionScope: CoroutineScope =
|
||||
CoroutineScope(serviceScope.coroutineContext + SupervisorJob() + exceptionHandler)
|
||||
private val bleConnection: BleConnection = BleConnection(centralManager, connectionScope, address)
|
||||
private val bleConnection: BleConnection = connectionFactory.create(connectionScope, address)
|
||||
private val writeMutex: Mutex = Mutex()
|
||||
|
||||
private var connectionStartTime: Long = 0
|
||||
|
|
@ -106,21 +113,19 @@ constructor(
|
|||
|
||||
// --- Connection & Discovery Logic ---
|
||||
|
||||
/** Robustly finds the peripheral. First checks bonded devices, then performs a short scan if not found. */
|
||||
private suspend fun findPeripheral(): Peripheral {
|
||||
centralManager
|
||||
.getBondedPeripherals()
|
||||
/** Robustly finds the device. First checks bonded devices, then performs a short scan if not found. */
|
||||
private suspend fun findDevice(): BleDevice {
|
||||
bluetoothRepository.state.value.bondedDevices
|
||||
.firstOrNull { it.address == address }
|
||||
?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
Logger.i { "[$address] Device not found in bonded list, scanning..." }
|
||||
val scanner = BleScanner(centralManager)
|
||||
|
||||
repeat(SCAN_RETRY_COUNT) { attempt ->
|
||||
val p = scanner.scan(SCAN_TIMEOUT).firstOrNull { it.address == address }
|
||||
if (p != null) return p
|
||||
val d = scanner.scan(SCAN_TIMEOUT).firstOrNull { it.address == address }
|
||||
if (d != null) return d
|
||||
|
||||
if (attempt < SCAN_RETRY_COUNT - 1) {
|
||||
delay(SCAN_RETRY_DELAY_MS)
|
||||
|
|
@ -138,7 +143,7 @@ constructor(
|
|||
|
||||
bleConnection.connectionState
|
||||
.onEach { state ->
|
||||
if (state is ConnectionState.Disconnected) {
|
||||
if (state is BleConnectionState.Disconnected) {
|
||||
onDisconnected(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -148,9 +153,9 @@ constructor(
|
|||
}
|
||||
.launchIn(connectionScope)
|
||||
|
||||
val p = findPeripheral()
|
||||
val state = bleConnection.connectAndAwait(p, CONNECTION_TIMEOUT_MS)
|
||||
if (state !is ConnectionState.Connected) {
|
||||
val device = findDevice()
|
||||
val state = bleConnection.connectAndAwait(device, CONNECTION_TIMEOUT_MS)
|
||||
if (state !is BleConnectionState.Connected) {
|
||||
throw RadioNotConnectedException("Failed to connect to device at address $address")
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +163,7 @@ constructor(
|
|||
discoverServicesAndSetupCharacteristics()
|
||||
} catch (e: Exception) {
|
||||
val failureTime = nowMillis - connectionStartTime
|
||||
Logger.w(e) { "[$address] Failed to connect to peripheral after ${failureTime}ms" }
|
||||
Logger.w(e) { "[$address] Failed to connect to device after ${failureTime}ms" }
|
||||
handleFailure(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -166,8 +171,9 @@ constructor(
|
|||
|
||||
private suspend fun onConnected() {
|
||||
try {
|
||||
bleConnection.peripheralFlow.first()?.let { p ->
|
||||
val rssi = retryBleOperation(tag = address) { p.readRssi() }
|
||||
bleConnection.deviceFlow.first()?.let { device ->
|
||||
val androidDevice = device as AndroidBleDevice
|
||||
val rssi = retryBleOperation(tag = address) { androidDevice.peripheral.readRssi() }
|
||||
Logger.d { "[$address] Connection confirmed. Initial RSSI: $rssi dBm" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -175,7 +181,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onDisconnected(state: ConnectionState.Disconnected) {
|
||||
private fun onDisconnected(@Suppress("UNUSED_PARAMETER") state: BleConnectionState.Disconnected) {
|
||||
radioService = null
|
||||
|
||||
val uptime =
|
||||
|
|
@ -185,26 +191,22 @@ constructor(
|
|||
0
|
||||
}
|
||||
Logger.w {
|
||||
"[$address] BLE disconnected - Reason: ${state.reason}, " +
|
||||
"[$address] BLE disconnected, " +
|
||||
"Uptime: ${uptime}ms, " +
|
||||
"Packets RX: $packetsReceived ($bytesReceived bytes), " +
|
||||
"Packets TX: $packetsSent ($bytesSent bytes)"
|
||||
}
|
||||
val (isPermanent, msg) =
|
||||
when (val reason = state.reason) {
|
||||
is ConnectionState.Disconnected.Reason.InsufficientAuthentication ->
|
||||
Pair(true, "Insufficient authentication: please unpair and repair the device")
|
||||
is ConnectionState.Disconnected.Reason.RequiredServiceNotFound ->
|
||||
Pair(false, "Required characteristic missing")
|
||||
else -> Pair(false, reason.toString())
|
||||
}
|
||||
service.onDisconnect(isPermanent, errorMessage = msg)
|
||||
|
||||
// Note: Disconnected state in commonMain doesn't currently carry a reason.
|
||||
// We might want to add that later if needed.
|
||||
service.onDisconnect(false, errorMessage = "Disconnected")
|
||||
}
|
||||
|
||||
private suspend fun discoverServicesAndSetupCharacteristics() {
|
||||
try {
|
||||
bleConnection.profile(serviceUuid = SERVICE_UUID) { service ->
|
||||
val radioService = MeshtasticRadioServiceImpl(service)
|
||||
val androidService = (service as AndroidBleService).service
|
||||
val radioService = MeshtasticRadioServiceImpl(androidService)
|
||||
|
||||
// Wire up notifications
|
||||
radioService.fromRadio
|
||||
|
|
@ -235,7 +237,7 @@ constructor(
|
|||
Logger.i { "[$address] Profile service active and characteristics subscribed" }
|
||||
|
||||
// Log negotiated MTU for diagnostics
|
||||
val maxLen = bleConnection.maximumWriteValueLength(WriteType.WITHOUT_RESPONSE)
|
||||
val maxLen = bleConnection.maximumWriteValueLength(BleWriteType.WITHOUT_RESPONSE)
|
||||
Logger.i { "[$address] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): $maxLen bytes" }
|
||||
|
||||
this@NordicBleInterface.service.onConnect()
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import androidx.work.ExistingWorkPolicy
|
|||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.workDataOf
|
||||
import org.meshtastic.app.messaging.domain.worker.SendMessageWorker
|
||||
import org.meshtastic.core.repository.MeshWorkerManager
|
||||
import org.meshtastic.feature.messaging.domain.worker.SendMessageWorker
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
|
|||
|
|
@ -117,15 +117,15 @@ constructor(
|
|||
|
||||
/** Initiates the bonding process and connects to the device upon success. */
|
||||
private fun requestBonding(entry: DeviceListEntry.Ble) {
|
||||
Logger.i { "Starting bonding for ${entry.peripheral.address.anonymize}" }
|
||||
Logger.i { "Starting bonding for ${entry.device.address.anonymize}" }
|
||||
viewModelScope.launch {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
bluetoothRepository.bond(entry.peripheral)
|
||||
Logger.i { "Bonding complete for ${entry.peripheral.address.anonymize}, selecting device..." }
|
||||
bluetoothRepository.bond(entry.device)
|
||||
Logger.i { "Bonding complete for ${entry.device.address.anonymize}, selecting device..." }
|
||||
changeDeviceAddress(entry.fullAddress)
|
||||
} catch (ex: SecurityException) {
|
||||
Logger.w(ex) { "Bonding failed for ${entry.peripheral.address.anonymize} Permissions not granted" }
|
||||
Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize} Permissions not granted" }
|
||||
serviceRepository.setErrorMessage(
|
||||
text = "Bonding failed: ${ex.message} Permissions not granted",
|
||||
severity = Severity.Warn,
|
||||
|
|
@ -135,9 +135,9 @@ constructor(
|
|||
val message = ex.message ?: ""
|
||||
if (message.contains("Received bond state changed 11")) {
|
||||
// This is a known issue where bonding is still in progress, ignore as error
|
||||
Logger.d { "Bonding still in progress for ${entry.peripheral.address.anonymize}" }
|
||||
Logger.d { "Bonding still in progress for ${entry.device.address.anonymize}" }
|
||||
} else {
|
||||
Logger.w(ex) { "Bonding failed for ${entry.peripheral.address.anonymize}" }
|
||||
Logger.w(ex) { "Bonding failed for ${entry.device.address.anonymize}" }
|
||||
serviceRepository.setErrorMessage(text = "Bonding failed: ${ex.message}", severity = Severity.Warn)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import no.nordicsemi.android.common.scanner.view.ScannerView
|
|||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.app.model.DeviceListEntry
|
||||
import org.meshtastic.app.ui.connections.ScannerViewModel
|
||||
import org.meshtastic.core.ble.AndroidBleDevice
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.BLE_NAME_PATTERN
|
||||
import org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
|
|
@ -73,12 +74,14 @@ fun BLEDevices(connectionState: ConnectionState, selectedDevice: String, scanMod
|
|||
|
||||
ScannerView(
|
||||
state = filterState,
|
||||
onScanResultSelected = { result -> scanModel.onSelected(DeviceListEntry.Ble(result.peripheral)) },
|
||||
onScanResultSelected = { result ->
|
||||
scanModel.onSelected(DeviceListEntry.Ble(AndroidBleDevice(result.peripheral)))
|
||||
},
|
||||
deviceItem = { result ->
|
||||
val device =
|
||||
remember(result.peripheral.address, bleDevices) {
|
||||
bleDevices.find { it.fullAddress == "x${result.peripheral.address}" }
|
||||
?: DeviceListEntry.Ble(result.peripheral)
|
||||
?: DeviceListEntry.Ble(AndroidBleDevice(result.peripheral))
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ import co.touchlab.kermit.Logger
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeout
|
||||
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.app.model.DeviceListEntry
|
||||
import org.meshtastic.core.model.Node
|
||||
|
|
@ -75,23 +73,14 @@ fun CurrentlyConnectedInfo(
|
|||
var rssi by remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(bleDevice) {
|
||||
if (bleDevice != null) {
|
||||
while (bleDevice.peripheral.isConnected) {
|
||||
while (bleDevice.device.isConnected) {
|
||||
try {
|
||||
rssi = withTimeout(RSSI_TIMEOUT.seconds) { bleDevice.peripheral.readRssi() }
|
||||
rssi = withTimeout(RSSI_TIMEOUT.seconds) { bleDevice.device.readRssi() }
|
||||
delay(RSSI_DELAY.seconds)
|
||||
} catch (e: PeripheralNotConnectedException) {
|
||||
Logger.w(e) { "Failed to read RSSI ${e.message}" }
|
||||
break
|
||||
} catch (e: OperationFailedException) {
|
||||
} catch (e: Exception) {
|
||||
// RSSI reading failures are common when disconnecting; log as warning to avoid Crashlytics noise
|
||||
Logger.w(e) { "Failed to read RSSI ${e.message}" }
|
||||
break
|
||||
} catch (e: SecurityException) {
|
||||
Logger.w(e) { "Failed to read RSSI ${e.message}" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Logger.w(e) { "Unexpected error reading RSSI: ${e.message}" }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue