feat(wire): migrate from protobuf -> wire (#4401)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-02-03 18:01:12 -06:00 committed by GitHub
parent 9dbc8b7fbf
commit 25657e8f8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
239 changed files with 7149 additions and 6144 deletions

View file

@ -20,11 +20,11 @@ import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ChannelProtos.Channel
import org.meshtastic.proto.ChannelProtos.ChannelSettings
import org.meshtastic.proto.ConfigProtos
import java.io.IOException
import okio.IOException
import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
import org.meshtastic.proto.Config
import javax.inject.Inject
import javax.inject.Singleton
@ -36,38 +36,37 @@ class ChannelSetDataSource @Inject constructor(private val channelSetStore: Data
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Logger.e { "Error reading DeviceConfig settings: ${exception.message}" }
emit(ChannelSet.getDefaultInstance())
emit(ChannelSet())
} else {
throw exception
}
}
suspend fun clearChannelSet() {
channelSetStore.updateData { preference -> preference.toBuilder().clear().build() }
channelSetStore.updateData { ChannelSet() }
}
/** Replaces all [ChannelSettings] in a single atomic operation. */
suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) {
channelSetStore.updateData { preference ->
preference.toBuilder().clearSettings().addAllSettings(settingsList).build()
}
channelSetStore.updateData { it.copy(settings = settingsList) }
}
/** 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()
val settings = preference.settings.toMutableList()
// Resize to fit channel
while (builder.settingsCount <= channel.index) {
builder.addSettings(ChannelSettings.getDefaultInstance())
while (settings.size <= channel.index) {
settings.add(ChannelSettings())
}
// use setSettings() to ensure settingsList and channel indexes match
builder.setSettings(channel.index, channel.settings).build()
settings[channel.index] = channel.settings ?: ChannelSettings()
preference.copy(settings = settings)
}
}
suspend fun setLoraConfig(config: ConfigProtos.Config.LoRaConfig) {
channelSetStore.updateData { preference -> preference.toBuilder().setLoraConfig(config).build() }
suspend fun setLoraConfig(config: Config.LoRaConfig) {
channelSetStore.updateData { it.copy(lora_config = config) }
}
}

View file

@ -20,9 +20,9 @@ import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.ConfigProtos.Config
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import java.io.IOException
import okio.IOException
import org.meshtastic.proto.Config
import org.meshtastic.proto.LocalConfig
import javax.inject.Inject
import javax.inject.Singleton
@ -34,28 +34,28 @@ class LocalConfigDataSource @Inject constructor(private val localConfigStore: Da
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Logger.e { "Error reading LocalConfig settings: ${exception.message}" }
emit(LocalConfig.getDefaultInstance())
emit(LocalConfig())
} else {
throw exception
}
}
suspend fun clearLocalConfig() {
localConfigStore.updateData { preference -> preference.toBuilder().clear().build() }
localConfigStore.updateData { LocalConfig() }
}
/** 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 {
// Some fields like SESSIONKEY are not intended to be persisted in LocalConfig
Logger.d { "Skipping non-persistent LocalConfig field: ${field.name}" }
}
suspend fun setLocalConfig(config: Config) = localConfigStore.updateData { current ->
when {
config.device != null -> current.copy(device = config.device)
config.position != null -> current.copy(position = config.position)
config.power != null -> current.copy(power = config.power)
config.network != null -> current.copy(network = config.network)
config.display != null -> current.copy(display = config.display)
config.lora != null -> current.copy(lora = config.lora)
config.bluetooth != null -> current.copy(bluetooth = config.bluetooth)
config.security != null -> current.copy(security = config.security)
else -> current
}
builder.build()
}
}

View file

@ -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,16 +14,15 @@
* 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.core.datastore
import androidx.datastore.core.DataStore
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import org.meshtastic.proto.LocalOnlyProtos.LocalModuleConfig
import org.meshtastic.proto.ModuleConfigProtos.ModuleConfig
import java.io.IOException
import okio.IOException
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.ModuleConfig
import javax.inject.Inject
import javax.inject.Singleton
@ -35,27 +34,35 @@ class ModuleConfigDataSource @Inject constructor(private val moduleConfigStore:
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Logger.e { "Error reading LocalModuleConfig settings: ${exception.message}" }
emit(LocalModuleConfig.getDefaultInstance())
emit(LocalModuleConfig())
} else {
throw exception
}
}
suspend fun clearLocalModuleConfig() {
moduleConfigStore.updateData { preference -> preference.toBuilder().clear().build() }
moduleConfigStore.updateData { LocalModuleConfig() }
}
/** 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 {
Logger.e { "Error writing LocalModuleConfig settings: ${config.payloadVariantCase}" }
}
suspend fun setLocalModuleConfig(config: ModuleConfig) = moduleConfigStore.updateData { current ->
when {
config.mqtt != null -> current.copy(mqtt = config.mqtt)
config.serial != null -> current.copy(serial = config.serial)
config.external_notification != null ->
current.copy(external_notification = config.external_notification)
config.store_forward != null -> current.copy(store_forward = config.store_forward)
config.range_test != null -> current.copy(range_test = config.range_test)
config.telemetry != null -> current.copy(telemetry = config.telemetry)
config.canned_message != null -> current.copy(canned_message = config.canned_message)
config.audio != null -> current.copy(audio = config.audio)
config.remote_hardware != null -> current.copy(remote_hardware = config.remote_hardware)
config.neighbor_info != null -> current.copy(neighbor_info = config.neighbor_info)
config.ambient_lighting != null -> current.copy(ambient_lighting = config.ambient_lighting)
config.detection_sensor != null -> current.copy(detection_sensor = config.detection_sensor)
config.paxcounter != null -> current.copy(paxcounter = config.paxcounter)
config.statusmessage != null -> current.copy(statusmessage = config.statusmessage)
else -> current
}
builder.build()
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.datastore.di
import android.content.Context
@ -45,9 +44,9 @@ import org.meshtastic.core.datastore.KEY_THEME
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import org.meshtastic.proto.LocalOnlyProtos.LocalModuleConfig
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import javax.inject.Qualifier
import javax.inject.Singleton
@ -103,7 +102,7 @@ object DataStoreModule {
): DataStore<LocalConfig> = DataStoreFactory.create(
serializer = LocalConfigSerializer,
produceFile = { appContext.dataStoreFile("local_config.pb") },
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig.getDefaultInstance() }),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig() }),
scope = scope,
)
@ -115,8 +114,7 @@ object DataStoreModule {
): DataStore<LocalModuleConfig> = DataStoreFactory.create(
serializer = ModuleConfigSerializer,
produceFile = { appContext.dataStoreFile("module_config.pb") },
corruptionHandler =
ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig.getDefaultInstance() }),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig() }),
scope = scope,
)
@ -128,7 +126,7 @@ object DataStoreModule {
): DataStore<ChannelSet> = DataStoreFactory.create(
serializer = ChannelSetSerializer,
produceFile = { appContext.dataStoreFile("channel_set.pb") },
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet.getDefaultInstance() }),
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet() }),
scope = scope,
)
}

View file

@ -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,28 +14,27 @@
* 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.core.datastore.serializer
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import okio.IOException
import org.meshtastic.proto.ChannelSet
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 val defaultValue: ChannelSet = ChannelSet()
override suspend fun readFrom(input: InputStream): ChannelSet {
try {
return ChannelSet.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
return ChannelSet.ADAPTER.decode(input)
} catch (exception: IOException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: ChannelSet, output: OutputStream) = t.writeTo(output)
override suspend fun writeTo(t: ChannelSet, output: OutputStream) = ChannelSet.ADAPTER.encode(output, t)
}

View file

@ -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,28 +14,27 @@
* 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.core.datastore.serializer
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import org.meshtastic.proto.LocalOnlyProtos.LocalConfig
import okio.IOException
import org.meshtastic.proto.LocalConfig
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 val defaultValue: LocalConfig = LocalConfig()
override suspend fun readFrom(input: InputStream): LocalConfig {
try {
return LocalConfig.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
return LocalConfig.ADAPTER.decode(input)
} catch (exception: IOException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: LocalConfig, output: OutputStream) = t.writeTo(output)
override suspend fun writeTo(t: LocalConfig, output: OutputStream) = LocalConfig.ADAPTER.encode(output, t)
}

View file

@ -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,28 +14,28 @@
* 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.core.datastore.serializer
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import org.meshtastic.proto.LocalOnlyProtos.LocalModuleConfig
import okio.IOException
import org.meshtastic.proto.LocalModuleConfig
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 val defaultValue: LocalModuleConfig = LocalModuleConfig()
override suspend fun readFrom(input: InputStream): LocalModuleConfig {
try {
return LocalModuleConfig.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
return LocalModuleConfig.ADAPTER.decode(input)
} catch (exception: IOException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: LocalModuleConfig, output: OutputStream) = t.writeTo(output)
override suspend fun writeTo(t: LocalModuleConfig, output: OutputStream) =
LocalModuleConfig.ADAPTER.encode(output, t)
}