mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Add core data modules (#3169)
This commit is contained in:
parent
bb2e6b9a7d
commit
53fdda3a9c
17 changed files with 213 additions and 193 deletions
|
|
@ -28,8 +28,6 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
||||
import com.geeksville.mesh.repository.datastore.recentaddresses.RecentAddress
|
||||
import com.geeksville.mesh.repository.datastore.recentaddresses.RecentAddressesRepository
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository
|
||||
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
|
||||
import com.geeksville.mesh.repository.radio.InterfaceId
|
||||
|
|
@ -52,6 +50,8 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.datastore.model.RecentAddress
|
||||
import org.meshtastic.core.strings.R
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ constructor(
|
|||
private val usbManagerLazy: dagger.Lazy<UsbManager>,
|
||||
private val networkRepository: NetworkRepository,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val recentAddressesRepository: RecentAddressesRepository,
|
||||
private val recentAddressesDataSource: RecentAddressesDataSource,
|
||||
) : ViewModel(),
|
||||
Logging {
|
||||
private val context: Context
|
||||
|
|
@ -125,7 +125,7 @@ constructor(
|
|||
|
||||
// Flow for discovered TCP devices, using recent addresses for potential name enrichment
|
||||
private val processedDiscoveredTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> =
|
||||
combine(networkRepository.resolvedList, recentAddressesRepository.recentAddresses) { tcpServices, recentList ->
|
||||
combine(networkRepository.resolvedList, recentAddressesDataSource.recentAddresses) { tcpServices, recentList ->
|
||||
val recentMap = recentList.associateBy({ it.address }, { it.name })
|
||||
tcpServices
|
||||
.map { service ->
|
||||
|
|
@ -149,7 +149,7 @@ constructor(
|
|||
|
||||
// Flow for recent TCP devices, filtered to exclude any currently discovered devices
|
||||
private val filteredRecentTcpDevicesFlow: StateFlow<List<DeviceListEntry.Tcp>> =
|
||||
combine(recentAddressesRepository.recentAddresses, processedDiscoveredTcpDevicesFlow) {
|
||||
combine(recentAddressesDataSource.recentAddresses, processedDiscoveredTcpDevicesFlow) {
|
||||
recentList,
|
||||
discoveredDevices,
|
||||
->
|
||||
|
|
@ -328,11 +328,11 @@ constructor(
|
|||
|
||||
fun addRecentAddress(address: String, name: String) {
|
||||
if (!address.startsWith("t")) return
|
||||
viewModelScope.launch { recentAddressesRepository.add(RecentAddress(address, name)) }
|
||||
viewModelScope.launch { recentAddressesDataSource.add(RecentAddress(address, name)) }
|
||||
}
|
||||
|
||||
fun removeRecentAddress(address: String) {
|
||||
viewModelScope.launch { recentAddressesRepository.remove(address) }
|
||||
viewModelScope.launch { recentAddressesDataSource.remove(address) }
|
||||
}
|
||||
|
||||
// Called by the GUI when a new device has been selected by the user
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.geeksville.mesh.ChannelProtos.Channel
|
||||
import com.geeksville.mesh.ChannelProtos.ChannelSettings
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Class that handles saving and retrieving [ChannelSet] data.
|
||||
*/
|
||||
class ChannelSetRepository @Inject constructor(
|
||||
private val channelSetStore: DataStore<ChannelSet>
|
||||
) : Logging {
|
||||
val channelSetFlow: Flow<ChannelSet> = channelSetStore.data
|
||||
.catch { exception ->
|
||||
// dataStore.data throws an IOException when an error is encountered when reading data
|
||||
if (exception is IOException) {
|
||||
errormsg("Error reading DeviceConfig settings: ${exception.message}")
|
||||
emit(ChannelSet.getDefaultInstance())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearChannelSet() {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().clear().build()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearSettings() {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().clearSettings().build()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addAllSettings(settingsList: List<ChannelSettings>) {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().addAllSettings(settingsList).build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the [ChannelSettings] list with the provided channel.
|
||||
*/
|
||||
suspend fun updateChannelSettings(channel: Channel) {
|
||||
if (channel.role == Channel.Role.DISABLED) return
|
||||
channelSetStore.updateData { preference ->
|
||||
val builder = preference.toBuilder()
|
||||
// Resize to fit channel
|
||||
while (builder.settingsCount <= channel.index) {
|
||||
builder.addSettings(ChannelSettings.getDefaultInstance())
|
||||
}
|
||||
// use setSettings() to ensure settingsList and channel indexes match
|
||||
builder.setSettings(channel.index, channel.settings).build()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) {
|
||||
channelSetStore.updateData { preference ->
|
||||
preference.toBuilder().setLoraConfig(config).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Serializer for the [ChannelSet] object defined in apponly.proto.
|
||||
*/
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object ChannelSetSerializer : Serializer<ChannelSet> {
|
||||
override val defaultValue: ChannelSet = ChannelSet.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): ChannelSet {
|
||||
try {
|
||||
return ChannelSet.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: ChannelSet, output: OutputStream) = t.writeTo(output)
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import androidx.datastore.preferences.SharedPreferencesMigration
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||
import com.geeksville.mesh.AppOnlyProtos.ChannelSet
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.geeksville.mesh.repository.datastore.recentaddresses.RecentAddressesRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
||||
private const val USER_PREFERENCES_NAME = "user_preferences"
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
object DataStoreModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providePreferencesDataStore(
|
||||
@ApplicationContext appContext: Context
|
||||
): DataStore<Preferences> =
|
||||
PreferenceDataStoreFactory.create(
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
|
||||
migrations = listOf(SharedPreferencesMigration(appContext, USER_PREFERENCES_NAME)),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideRecentAddressesRepository(
|
||||
@ApplicationContext context: Context,
|
||||
dataStore: DataStore<Preferences>,
|
||||
): RecentAddressesRepository = RecentAddressesRepository(context, dataStore)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideLocalConfigDataStore(
|
||||
@ApplicationContext appContext: Context
|
||||
): DataStore<LocalConfig> =
|
||||
DataStoreFactory.create(
|
||||
serializer = LocalConfigSerializer,
|
||||
produceFile = { appContext.dataStoreFile("local_config.pb") },
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(produceNewData = { LocalConfig.getDefaultInstance() }),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideModuleConfigDataStore(
|
||||
@ApplicationContext appContext: Context
|
||||
): DataStore<LocalModuleConfig> =
|
||||
DataStoreFactory.create(
|
||||
serializer = ModuleConfigSerializer,
|
||||
produceFile = { appContext.dataStoreFile("module_config.pb") },
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(
|
||||
produceNewData = { LocalModuleConfig.getDefaultInstance() }
|
||||
),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideChannelSetDataStore(@ApplicationContext appContext: Context): DataStore<ChannelSet> =
|
||||
DataStoreFactory.create(
|
||||
serializer = ChannelSetSerializer,
|
||||
produceFile = { appContext.dataStoreFile("channel_set.pb") },
|
||||
corruptionHandler =
|
||||
ReplaceFileCorruptionHandler(produceNewData = { ChannelSet.getDefaultInstance() }),
|
||||
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.ConfigProtos.Config
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Class that handles saving and retrieving [LocalConfig] data.
|
||||
*/
|
||||
class LocalConfigRepository @Inject constructor(
|
||||
private val localConfigStore: DataStore<LocalConfig>,
|
||||
) : Logging {
|
||||
val localConfigFlow: Flow<LocalConfig> = localConfigStore.data
|
||||
.catch { exception ->
|
||||
// dataStore.data throws an IOException when an error is encountered when reading data
|
||||
if (exception is IOException) {
|
||||
errormsg("Error reading LocalConfig settings: ${exception.message}")
|
||||
emit(LocalConfig.getDefaultInstance())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearLocalConfig() {
|
||||
localConfigStore.updateData { preference ->
|
||||
preference.toBuilder().clear().build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates [LocalConfig] from each [Config] oneOf.
|
||||
*/
|
||||
suspend fun setLocalConfig(config: Config) = localConfigStore.updateData {
|
||||
val builder = it.toBuilder()
|
||||
config.allFields.forEach { (field, value) ->
|
||||
val localField = it.descriptorForType.findFieldByName(field.name)
|
||||
if (localField != null) {
|
||||
builder.setField(localField, value)
|
||||
} else {
|
||||
errormsg("Error writing LocalConfig settings: ${config.payloadVariantCase}")
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Serializer for the [LocalConfig] object defined in localonly.proto.
|
||||
*/
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object LocalConfigSerializer : Serializer<LocalConfig> {
|
||||
override val defaultValue: LocalConfig = LocalConfig.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): LocalConfig {
|
||||
try {
|
||||
return LocalConfig.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: LocalConfig, output: OutputStream) = t.writeTo(output)
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.ModuleConfigProtos.ModuleConfig
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Class that handles saving and retrieving [LocalModuleConfig] data.
|
||||
*/
|
||||
class ModuleConfigRepository @Inject constructor(
|
||||
private val moduleConfigStore: DataStore<LocalModuleConfig>,
|
||||
) : Logging {
|
||||
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigStore.data
|
||||
.catch { exception ->
|
||||
// dataStore.data throws an IOException when an error is encountered when reading data
|
||||
if (exception is IOException) {
|
||||
errormsg("Error reading LocalModuleConfig settings: ${exception.message}")
|
||||
emit(LocalModuleConfig.getDefaultInstance())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearLocalModuleConfig() {
|
||||
moduleConfigStore.updateData { preference ->
|
||||
preference.toBuilder().clear().build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates [LocalModuleConfig] from each [ModuleConfig] oneOf.
|
||||
*/
|
||||
suspend fun setLocalModuleConfig(config: ModuleConfig) = moduleConfigStore.updateData {
|
||||
val builder = it.toBuilder()
|
||||
config.allFields.forEach { (field, value) ->
|
||||
val localField = it.descriptorForType.findFieldByName(field.name)
|
||||
if (localField != null) {
|
||||
builder.setField(localField, value)
|
||||
} else {
|
||||
errormsg("Error writing LocalModuleConfig settings: ${config.payloadVariantCase}")
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore
|
||||
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.Serializer
|
||||
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Serializer for the [LocalModuleConfig] object defined in localonly.proto.
|
||||
*/
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
object ModuleConfigSerializer : Serializer<LocalModuleConfig> {
|
||||
override val defaultValue: LocalModuleConfig = LocalModuleConfig.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): LocalModuleConfig {
|
||||
try {
|
||||
return LocalModuleConfig.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: LocalModuleConfig, output: OutputStream) = t.writeTo(output)
|
||||
}
|
||||
|
|
@ -45,6 +45,9 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.meshtastic.core.datastore.ChannelSetDataSource
|
||||
import org.meshtastic.core.datastore.LocalConfigDataSource
|
||||
import org.meshtastic.core.datastore.ModuleConfigDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
|
@ -56,9 +59,9 @@ class RadioConfigRepository
|
|||
constructor(
|
||||
private val serviceRepository: ServiceRepository,
|
||||
private val nodeDB: NodeRepository,
|
||||
private val channelSetRepository: ChannelSetRepository,
|
||||
private val localConfigRepository: LocalConfigRepository,
|
||||
private val moduleConfigRepository: ModuleConfigRepository,
|
||||
private val channelSetDataSource: ChannelSetDataSource,
|
||||
private val localConfigDataSource: LocalConfigDataSource,
|
||||
private val moduleConfigDataSource: ModuleConfigDataSource,
|
||||
) {
|
||||
val meshService: IMeshService?
|
||||
get() = serviceRepository.meshService
|
||||
|
|
@ -104,17 +107,17 @@ constructor(
|
|||
}
|
||||
|
||||
/** Flow representing the [ChannelSet] data store. */
|
||||
val channelSetFlow: Flow<ChannelSet> = channelSetRepository.channelSetFlow
|
||||
val channelSetFlow: Flow<ChannelSet> = channelSetDataSource.channelSetFlow
|
||||
|
||||
/** Clears the [ChannelSet] data in the data store. */
|
||||
suspend fun clearChannelSet() {
|
||||
channelSetRepository.clearChannelSet()
|
||||
channelSetDataSource.clearChannelSet()
|
||||
}
|
||||
|
||||
/** Replaces the [ChannelSettings] list with a new [settingsList]. */
|
||||
suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) {
|
||||
channelSetRepository.clearSettings()
|
||||
channelSetRepository.addAllSettings(settingsList)
|
||||
channelSetDataSource.clearSettings()
|
||||
channelSetDataSource.addAllSettings(settingsList)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -124,14 +127,14 @@ constructor(
|
|||
* @param channel The [Channel] provided.
|
||||
* @return the index of the admin channel after the update (if not found, returns 0).
|
||||
*/
|
||||
suspend fun updateChannelSettings(channel: Channel) = channelSetRepository.updateChannelSettings(channel)
|
||||
suspend fun updateChannelSettings(channel: Channel) = channelSetDataSource.updateChannelSettings(channel)
|
||||
|
||||
/** Flow representing the [LocalConfig] data store. */
|
||||
val localConfigFlow: Flow<LocalConfig> = localConfigRepository.localConfigFlow
|
||||
val localConfigFlow: Flow<LocalConfig> = localConfigDataSource.localConfigFlow
|
||||
|
||||
/** Clears the [LocalConfig] data in the data store. */
|
||||
suspend fun clearLocalConfig() {
|
||||
localConfigRepository.clearLocalConfig()
|
||||
localConfigDataSource.clearLocalConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,16 +143,16 @@ constructor(
|
|||
* @param config The [Config] to be set.
|
||||
*/
|
||||
suspend fun setLocalConfig(config: Config) {
|
||||
localConfigRepository.setLocalConfig(config)
|
||||
if (config.hasLora()) channelSetRepository.setLoraConfig(config.lora)
|
||||
localConfigDataSource.setLocalConfig(config)
|
||||
if (config.hasLora()) channelSetDataSource.setLoraConfig(config.lora)
|
||||
}
|
||||
|
||||
/** Flow representing the [LocalModuleConfig] data store. */
|
||||
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigRepository.moduleConfigFlow
|
||||
val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigDataSource.moduleConfigFlow
|
||||
|
||||
/** Clears the [LocalModuleConfig] data in the data store. */
|
||||
suspend fun clearLocalModuleConfig() {
|
||||
moduleConfigRepository.clearLocalModuleConfig()
|
||||
moduleConfigDataSource.clearLocalModuleConfig()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -158,7 +161,7 @@ constructor(
|
|||
* @param config The [ModuleConfig] to be set.
|
||||
*/
|
||||
suspend fun setLocalModuleConfig(config: ModuleConfig) {
|
||||
moduleConfigRepository.setLocalModuleConfig(config)
|
||||
moduleConfigDataSource.setLocalModuleConfig(config)
|
||||
}
|
||||
|
||||
/** Flow representing the combined [DeviceProfile] protobuf. */
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore.recentaddresses
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable data class RecentAddress(val address: String, val name: String)
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.repository.datastore.recentaddresses
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.meshtastic.core.strings.R
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecentAddressesRepository
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : Logging {
|
||||
private object PreferencesKeys {
|
||||
val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses")
|
||||
}
|
||||
|
||||
val recentAddresses: Flow<List<RecentAddress>> =
|
||||
dataStore.data.map { preferences ->
|
||||
val jsonString = preferences[PreferencesKeys.RECENT_IP_ADDRESSES]
|
||||
if (jsonString != null) {
|
||||
try {
|
||||
Json.decodeFromString<List<RecentAddress>>(jsonString)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
warn("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
|
||||
// Fallback to legacy parsing
|
||||
parseLegacyRecentAddresses(jsonString)
|
||||
} catch (e: SerializationException) {
|
||||
warn("Could not parse recent addresses, falling back to legacy parsing: ${e.message}")
|
||||
// Fallback to legacy parsing
|
||||
parseLegacyRecentAddresses(jsonString)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseLegacyRecentAddresses(jsonAddresses: String): List<RecentAddress> {
|
||||
val jsonArray = JSONArray(jsonAddresses)
|
||||
return (0 until jsonArray.length()).mapNotNull { i ->
|
||||
when (val item = jsonArray.get(i)) {
|
||||
is JSONObject -> {
|
||||
// Modern format: JSONObject with address and name
|
||||
RecentAddress(address = item.getString("address"), name = item.getString("name"))
|
||||
}
|
||||
is String -> {
|
||||
// Old format: just the address string
|
||||
RecentAddress(address = item, name = context.getString(R.string.meshtastic))
|
||||
}
|
||||
else -> {
|
||||
// Unknown format, log or handle as an error if necessary
|
||||
warn("Unknown item type in recent IP addresses: $item")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun setRecentAddresses(addresses: List<RecentAddress>) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PreferencesKeys.RECENT_IP_ADDRESSES] = Json.encodeToString(addresses)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun add(address: RecentAddress) {
|
||||
val currentAddresses = recentAddresses.first()
|
||||
val updatedList = mutableListOf(address)
|
||||
currentAddresses.filterTo(updatedList) { it.address != address.address }
|
||||
setRecentAddresses(updatedList.take(CACHE_CAPACITY))
|
||||
}
|
||||
|
||||
suspend fun remove(address: String) {
|
||||
val currentAddresses = recentAddresses.first()
|
||||
val updatedList = currentAddresses.filter { it.address != address }
|
||||
setRecentAddresses(updatedList)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CACHE_CAPACITY = 3
|
||||
|
|
@ -44,15 +44,14 @@ import javax.net.ssl.SSLContext
|
|||
import javax.net.ssl.TrustManager
|
||||
|
||||
@Singleton
|
||||
class MQTTRepository @Inject constructor(
|
||||
private val radioConfigRepository: RadioConfigRepository,
|
||||
) : Logging {
|
||||
class MQTTRepository @Inject constructor(private val radioConfigRepository: RadioConfigRepository) : Logging {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Quality of Service (QoS) levels in MQTT:
|
||||
* - QoS 0: "at most once". Packets are sent once without validation if it has been received.
|
||||
* - QoS 1: "at least once". Packets are sent and stored until the client receives confirmation from the server. MQTT ensures delivery, but duplicates may occur.
|
||||
* - QoS 1: "at least once". Packets are sent and stored until the client receives confirmation from the server.
|
||||
* MQTT ensures delivery, but duplicates may occur.
|
||||
* - QoS 2: "exactly once". Similar to QoS 1, but with no duplicates.
|
||||
*/
|
||||
private const val DEFAULT_QOS = 1
|
||||
|
|
@ -84,63 +83,72 @@ class MQTTRepository @Inject constructor(
|
|||
|
||||
val rootTopic = mqttConfig.root.ifEmpty { DEFAULT_TOPIC_ROOT }
|
||||
|
||||
val connectOptions = MqttConnectOptions().apply {
|
||||
userName = mqttConfig.username
|
||||
password = mqttConfig.password.toCharArray()
|
||||
isAutomaticReconnect = true
|
||||
if (mqttConfig.tlsEnabled) {
|
||||
socketFactory = sslContext.socketFactory
|
||||
}
|
||||
}
|
||||
|
||||
val bufferOptions = DisconnectedBufferOptions().apply {
|
||||
isBufferEnabled = true
|
||||
bufferSize = 512
|
||||
isPersistBuffer = false
|
||||
isDeleteOldestMessages = true
|
||||
}
|
||||
|
||||
val callback = object : MqttCallbackExtended {
|
||||
override fun connectComplete(reconnect: Boolean, serverURI: String) {
|
||||
info("MQTT connectComplete: $serverURI reconnect: $reconnect")
|
||||
channelSet.subscribeList.ifEmpty { return }.forEach { globalId ->
|
||||
subscribe("$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+")
|
||||
if (mqttConfig.jsonEnabled) subscribe("$rootTopic$JSON_TOPIC_LEVEL$globalId/+")
|
||||
val connectOptions =
|
||||
MqttConnectOptions().apply {
|
||||
userName = mqttConfig.username
|
||||
password = mqttConfig.password.toCharArray()
|
||||
isAutomaticReconnect = true
|
||||
if (mqttConfig.tlsEnabled) {
|
||||
socketFactory = sslContext.socketFactory
|
||||
}
|
||||
subscribe("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+")
|
||||
}
|
||||
|
||||
override fun connectionLost(cause: Throwable) {
|
||||
info("MQTT connectionLost cause: $cause")
|
||||
if (cause is IllegalArgumentException) close(cause)
|
||||
val bufferOptions =
|
||||
DisconnectedBufferOptions().apply {
|
||||
isBufferEnabled = true
|
||||
bufferSize = 512
|
||||
isPersistBuffer = false
|
||||
isDeleteOldestMessages = true
|
||||
}
|
||||
|
||||
override fun messageArrived(topic: String, message: MqttMessage) {
|
||||
trySend(mqttClientProxyMessage {
|
||||
this.topic = topic
|
||||
data = ByteString.copyFrom(message.payload)
|
||||
retained = message.isRetained
|
||||
})
|
||||
}
|
||||
val callback =
|
||||
object : MqttCallbackExtended {
|
||||
override fun connectComplete(reconnect: Boolean, serverURI: String) {
|
||||
info("MQTT connectComplete: $serverURI reconnect: $reconnect")
|
||||
channelSet.subscribeList
|
||||
.ifEmpty {
|
||||
return
|
||||
}
|
||||
.forEach { globalId ->
|
||||
subscribe("$rootTopic$DEFAULT_TOPIC_LEVEL$globalId/+")
|
||||
if (mqttConfig.jsonEnabled) subscribe("$rootTopic$JSON_TOPIC_LEVEL$globalId/+")
|
||||
}
|
||||
subscribe("$rootTopic${DEFAULT_TOPIC_LEVEL}PKI/+")
|
||||
}
|
||||
|
||||
override fun deliveryComplete(token: IMqttDeliveryToken?) {
|
||||
info("MQTT deliveryComplete messageId: ${token?.messageId}")
|
||||
override fun connectionLost(cause: Throwable) {
|
||||
info("MQTT connectionLost cause: $cause")
|
||||
if (cause is IllegalArgumentException) close(cause)
|
||||
}
|
||||
|
||||
override fun messageArrived(topic: String, message: MqttMessage) {
|
||||
trySend(
|
||||
mqttClientProxyMessage {
|
||||
this.topic = topic
|
||||
data = ByteString.copyFrom(message.payload)
|
||||
retained = message.isRetained
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun deliveryComplete(token: IMqttDeliveryToken?) {
|
||||
info("MQTT deliveryComplete messageId: ${token?.messageId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scheme = if (mqttConfig.tlsEnabled) "ssl" else "tcp"
|
||||
val (host, port) = mqttConfig.address.ifEmpty { DEFAULT_SERVER_ADDRESS }
|
||||
.split(":", limit = 2).let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1) }
|
||||
val (host, port) =
|
||||
mqttConfig.address
|
||||
.ifEmpty { DEFAULT_SERVER_ADDRESS }
|
||||
.split(":", limit = 2)
|
||||
.let { it[0] to (it.getOrNull(1)?.toIntOrNull() ?: -1) }
|
||||
|
||||
mqttClient = MqttAsyncClient(
|
||||
URI(scheme, null, host, port, "", "", "").toString(),
|
||||
ownerId,
|
||||
MemoryPersistence(),
|
||||
).apply {
|
||||
setCallback(callback)
|
||||
setBufferOpts(bufferOptions)
|
||||
connect(connectOptions)
|
||||
}
|
||||
mqttClient =
|
||||
MqttAsyncClient(URI(scheme, null, host, port, "", "", "").toString(), ownerId, MemoryPersistence()).apply {
|
||||
setCallback(callback)
|
||||
setBufferOpts(bufferOptions)
|
||||
connect(connectOptions)
|
||||
}
|
||||
|
||||
awaitClose { disconnect() }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue