From 8ba9347b8f89171433cd2ef6d5b4ed68bf50cb88 Mon Sep 17 00:00:00 2001 From: James Rich Date: Sat, 13 Jun 2026 10:40:35 -0500 Subject: [PATCH 1/3] chore(deps): track protobufs develop-SNAPSHOT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump org.meshtastic:protobufs from 2.7.25 to develop-SNAPSHOT to pick up unreleased proto changes for evaluation (sonatype snapshot repo is already wired in settings.gradle.kts). The only source change required to compile is in Channel.kt: the develop protos add two new amateur-band ModemPreset values (TINY_FAST, TINY_SLOW), which the exhaustive `when` over modem_preset must now cover. All other deltas are additive (new HardwareModels, ITU RegionCodes, XEdDSA signing fields, lockdown additions) and the restructured deviceonly NodeInfoLite types are firmware-only and not referenced by this app. NOTE: this pins a moving snapshot — revert to a released version before merging anywhere near a release branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt | 2 ++ gradle/libs.versions.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt index 51f27fdbcc..660324eb0a 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt @@ -84,6 +84,8 @@ data class Channel(val settings: ChannelSettings = default.settings, val loraCon ModemPreset.LITE_SLOW -> "LiteSlow" ModemPreset.NARROW_FAST -> "NarrowFast" ModemPreset.NARROW_SLOW -> "NarrowSlow" + ModemPreset.TINY_FAST -> "TinyFast" + ModemPreset.TINY_SLOW -> "TinySlow" } } else { "Custom" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a828f60c3..9f622b1316 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -88,7 +88,7 @@ mqttastic = "0.3.8" jmdns = "3.6.3" qrcode-kotlin = "4.5.0" takpacket-sdk = "0.5.3" -meshtastic-protobufs = "2.7.25" +meshtastic-protobufs = "develop-SNAPSHOT" # Gradle Plugins develocity = "4.4.2" From 0a03b1e2e445c3b1d06d11da3e3b74335998d920 Mon Sep 17 00:00:00 2001 From: James Rich Date: Sat, 13 Jun 2026 11:50:25 -0500 Subject: [PATCH 2/3] feat: wire TINY_FAST/TINY_SLOW presets + force protobufs snapshot for preview Fully integrate the two new amateur-radio modem presets from the develop-SNAPSHOT protos (firmware PR #10597): - ChannelOption: add TINY_FAST/TINY_SLOW with the firmware-accurate 15.625 kHz bandwidth (0.015625f). The proto comment's "20kHz" is the padded channel spacing, not the modem bandwidth that feeds numChannels/radioFreq, so using it would skew the frequency math. - ModelExtensions.labelRes + label_tiny_fast/label_tiny_slow strings for the preset dropdown. - Channel.kt: document that the preset->name `when` is interop-critical (the name feeds the channel hash, channel number/frequency and MQTT topic), name the firmware source of truth (DisplayFormatters::getModemPresetDisplayName), and flag the two deliberately-abbreviated names so nobody "fixes" the intentional compile tripwire with an auto-deriving else. Also force org.meshtastic:protobufs* to develop-SNAPSHOT across the build (root build.gradle.kts). takpacket-sdk-jvm:0.5.3 transitively pins protobufs:2.7.25, and Gradle ranks 2.7.25 > develop-SNAPSHOT, so the test runtime classpath was downgrading to 2.7.25 while compilation used the snapshot -> NoSuchFieldError/NoSuchMethodError on the proto classes (assembleDebug/detekt miss it; test/allTests catch it). The force is safe because atak.proto is unchanged, so takpacket's message ABI stays compatible with the newer protobufs. NOTE: both the snapshot pin and this force are preview-only. Revert to a released protobufs and drop the force once takpacket-sdk (and mqtt) are republished against the new protos. Co-Authored-By: Claude Opus 4.8 (1M context) --- .skills/compose-ui/strings-index.txt | 2 ++ build.gradle.kts | 20 +++++++++++++++++++ .../org/meshtastic/core/model/Channel.kt | 9 ++++++++- .../meshtastic/core/model/ChannelOption.kt | 5 +++++ .../composeResources/values/strings.xml | 2 ++ .../core/ui/util/ModelExtensions.kt | 4 ++++ 6 files changed, 41 insertions(+), 1 deletion(-) diff --git a/.skills/compose-ui/strings-index.txt b/.skills/compose-ui/strings-index.txt index f6884c99d5..e4e05df8c5 100644 --- a/.skills/compose-ui/strings-index.txt +++ b/.skills/compose-ui/strings-index.txt @@ -614,6 +614,8 @@ label_narrow_slow label_short_fast label_short_slow label_short_turbo +label_tiny_fast +label_tiny_slow label_very_long_slow last_heard_filter_label last_position_update diff --git a/build.gradle.kts b/build.gradle.kts index e6b32a9736..b5eece44ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -60,3 +60,23 @@ plugins.withId("org.meshtastic.flatpak.sources") { dependencies { dokkaPlugin(libs.dokka.android.documentation.plugin) } + +// ─── TEMPORARY: protobufs develop-SNAPSHOT preview (PR #5790) ──────────────────────────────────── +// We track the unreleased protobufs develop-SNAPSHOT. takpacket-sdk-jvm:0.5.3 transitively pins +// protobufs:2.7.25, and Gradle ranks 2.7.25 > develop-SNAPSHOT (a numeric part outranks the +// "develop" string qualifier). That downgrades the test *runtime* classpath to 2.7.25 while the +// common-metadata *compile* uses the snapshot, yielding NoSuchFieldError/NoSuchMethodError on the +// proto-generated classes at test runtime (assembleDebug/detekt don't catch it; test/allTests do). +// Force every protobufs* variant to the snapshot so compile and runtime agree. Safe because +// atak.proto is unchanged, so takpacket's own message ABI stays compatible with the newer protobufs. +// REMOVE once takpacket-sdk (and mqtt) are republished against the new protobufs. +allprojects { + configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "org.meshtastic" && requested.name.startsWith("protobufs")) { + useVersion("develop-SNAPSHOT") + because("preview #5790: override takpacket transitive protobufs:2.7.25 pin") + } + } + } +} diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt index 660324eb0a..cb058cedbe 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/Channel.kt @@ -67,7 +67,14 @@ data class Channel(val settings: ChannelSettings = default.settings, val loraCon get() = settings.name.ifEmpty { // We have a new style 'empty' channel name. Use the same logic from the device to convert that to a - // human readable name + // human readable name. + // + // INTEROP-CRITICAL: these strings must byte-match the firmware, because the channel hash, channel + // number / radio frequency, and MQTT topic all derive from this name (see `hash`, `channelNum`). + // Source of truth: firmware DisplayFormatters::getModemPresetDisplayName(preset, useShortName=false). + // Two names are deliberately abbreviated (LONG_MODERATE -> "LongMod", VERY_LONG_SLOW -> "VLongSlow"), + // so do NOT auto-derive from the enum name. This `when` is intentionally exhaustive with no `else`: a + // new firmware preset SHOULD break the build here, forcing someone to copy its exact firmware name. if (loraConfig.use_preset) { when (loraConfig.modem_preset) { ModemPreset.SHORT_TURBO -> "ShortTurbo" diff --git a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt index da6ae71cda..d64ba13de6 100644 --- a/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt +++ b/core/model/src/commonMain/kotlin/org/meshtastic/core/model/ChannelOption.kt @@ -311,6 +311,11 @@ enum class ChannelOption(val modemPreset: ModemPreset, val bandwidth: Float) { LITE_SLOW(ModemPreset.LITE_SLOW, 0.125f), NARROW_FAST(ModemPreset.NARROW_FAST, 0.0625f), NARROW_SLOW(ModemPreset.NARROW_SLOW, 0.0625f), + + // 15.625 kHz LoRa bandwidth (firmware modemPresetToParams; the proto's "20kHz" is the + // padded channel spacing, not the modem bandwidth used for numChannels/radioFreq math). + TINY_FAST(ModemPreset.TINY_FAST, 0.015625f), + TINY_SLOW(ModemPreset.TINY_SLOW, 0.015625f), ; companion object { diff --git a/core/resources/src/commonMain/composeResources/values/strings.xml b/core/resources/src/commonMain/composeResources/values/strings.xml index 6c59d355d7..92b6dffab9 100644 --- a/core/resources/src/commonMain/composeResources/values/strings.xml +++ b/core/resources/src/commonMain/composeResources/values/strings.xml @@ -638,6 +638,8 @@ Short Range - Fast Short Range - Slow Short Range - Turbo + Tiny - Fast + Tiny - Slow Very Long Range - Slow Filter by Last Heard time: %1$s Last position update diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt index a6dd1b947c..c1250f15fd 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/ModelExtensions.kt @@ -33,6 +33,8 @@ import org.meshtastic.core.resources.label_narrow_slow import org.meshtastic.core.resources.label_short_fast import org.meshtastic.core.resources.label_short_slow import org.meshtastic.core.resources.label_short_turbo +import org.meshtastic.core.resources.label_tiny_fast +import org.meshtastic.core.resources.label_tiny_slow import org.meshtastic.core.resources.label_very_long_slow import org.meshtastic.core.resources.traceroute_endpoint_missing import org.meshtastic.core.resources.traceroute_map_no_data @@ -54,6 +56,8 @@ val ChannelOption.labelRes: StringResource ChannelOption.LITE_SLOW -> Res.string.label_lite_slow ChannelOption.NARROW_FAST -> Res.string.label_narrow_fast ChannelOption.NARROW_SLOW -> Res.string.label_narrow_slow + ChannelOption.TINY_FAST -> Res.string.label_tiny_fast + ChannelOption.TINY_SLOW -> Res.string.label_tiny_slow } fun TracerouteMapAvailability.toMessageRes(): StringResource? = when (this) { From 4aba9cd3e998373c396a7774cd5243f02846db8f Mon Sep 17 00:00:00 2001 From: James Rich Date: Sat, 13 Jun 2026 11:53:34 -0500 Subject: [PATCH 3/3] test: pin ModemPreset->channel-name contract; drop vacuous name check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ChannelPresetNameTest (commonTest, so it gates every PR) pinning every ModemPreset to its exact firmware-canonical channel name from DisplayFormatters::getModemPresetDisplayName(preset, useShortName=false). The name is interop-critical (it feeds the channel hash, channel number/frequency and MQTT topic), so the test: - locks the mapping against accidental edits (e.g. "LongMod" -> "LongModerate"), - asserts set-completeness vs ModemPreset.entries, so a new preset that was added to Channel.name's `when` but not pinned here fails loudly (also catches someone silencing the intentional compile tripwire with an `else`), - covers the deprecated VERY_LONG_SLOW, which still has a real name. This backstops Channel.name's exhaustive `when` without weakening it. The numeric on-air anchors (hash/channelNum/radioFreq) stay in ChannelTest. Remove the old androidDeviceTest allModemPresetsHaveValidNames, which only asserted name != "Invalid" — a value the no-else `when` can never produce, so it gave the illusion of a name guard while testing nothing, and ran only on-device rather than on every PR. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../org/meshtastic/core/model/ChannelTest.kt | 15 ---- .../core/model/ChannelPresetNameTest.kt | 82 +++++++++++++++++++ 2 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelPresetNameTest.kt diff --git a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt index 656afc56aa..ce98ce998b 100644 --- a/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt +++ b/core/model/src/androidDeviceTest/kotlin/org/meshtastic/core/model/ChannelTest.kt @@ -24,7 +24,6 @@ import org.meshtastic.core.model.util.CHANNEL_URL_PREFIX import org.meshtastic.core.model.util.getChannelUrl import org.meshtastic.core.model.util.toChannelSet import org.meshtastic.proto.ChannelSet -import org.meshtastic.proto.Config @RunWith(AndroidJUnit4::class) class ChannelTest { @@ -64,18 +63,4 @@ class ChannelTest { Assert.assertEquals(906.875f, ch.radioFreq) } - - @Test - fun allModemPresetsHaveValidNames() { - Config.LoRaConfig.ModemPreset.entries.forEach { preset -> - // Skip UNRECOGNIZED if it exists (Wire generates it sometimes) or generic UNSET values if applicable - if (preset.name == "UNSET" || preset.name == "UNRECOGNIZED") return@forEach - - val loraConfig = Channel.default.loraConfig.copy(use_preset = true, modem_preset = preset) - val channel = Channel(loraConfig = loraConfig) - - // We want to ensure it is NOT "Invalid" - Assert.assertNotEquals("Preset ${preset.name} should typically have a valid name", "Invalid", channel.name) - } - } } diff --git a/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelPresetNameTest.kt b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelPresetNameTest.kt new file mode 100644 index 0000000000..3aa87b1db9 --- /dev/null +++ b/core/model/src/commonTest/kotlin/org/meshtastic/core/model/ChannelPresetNameTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.model + +import org.meshtastic.proto.Config.LoRaConfig.ModemPreset +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Contract test pinning the preset -> channel-name mapping in [Channel.name]. + * + * This name is INTEROP-CRITICAL: for a channel with an empty name and `use_preset = true`, the name is hashed into the + * channel hash, the channel number / radio frequency, and the MQTT topic, so it must byte-match the firmware. Source of + * truth is firmware `DisplayFormatters::getModemPresetDisplayName(preset, useShortName = false)`. Two names are + * deliberately abbreviated (LONG_MODERATE -> "LongMod", VERY_LONG_SLOW -> "VLongSlow") and must NOT be auto-derived + * from the enum name. + * + * When firmware adds a preset, [Channel.name]'s exhaustive `when` fails to compile first (by design). This test is the + * backstop: it forces the new branch to carry the EXACT firmware name, guards the mapping against accidental edits, and + * fails if a preset is left unpinned (e.g. someone silenced the compile error with an `else`). It deliberately covers + * the deprecated VERY_LONG_SLOW, which still has a real firmware name. The numeric anchors (hash/channelNum/radioFreq) + * live in ChannelTest and are the genuine on-air interop guard — keep both. + */ +class ChannelPresetNameTest { + + // Firmware-canonical names: DisplayFormatters::getModemPresetDisplayName(preset, useShortName = false). + private val expectedNames = + mapOf( + ModemPreset.SHORT_TURBO to "ShortTurbo", + ModemPreset.SHORT_FAST to "ShortFast", + ModemPreset.SHORT_SLOW to "ShortSlow", + ModemPreset.MEDIUM_FAST to "MediumFast", + ModemPreset.MEDIUM_SLOW to "MediumSlow", + ModemPreset.LONG_FAST to "LongFast", + ModemPreset.LONG_SLOW to "LongSlow", + ModemPreset.LONG_MODERATE to "LongMod", + ModemPreset.VERY_LONG_SLOW to "VLongSlow", + ModemPreset.LONG_TURBO to "LongTurbo", + ModemPreset.LITE_FAST to "LiteFast", + ModemPreset.LITE_SLOW to "LiteSlow", + ModemPreset.NARROW_FAST to "NarrowFast", + ModemPreset.NARROW_SLOW to "NarrowSlow", + ModemPreset.TINY_FAST to "TinyFast", + ModemPreset.TINY_SLOW to "TinySlow", + ) + + private fun presetChannelName(preset: ModemPreset): String = + Channel(loraConfig = Channel.default.loraConfig.copy(use_preset = true, modem_preset = preset)).name + + @Test + fun every_preset_maps_to_its_exact_firmware_name() { + expectedNames.forEach { (preset, expected) -> + assertEquals(expected, presetChannelName(preset), "Channel name for $preset must match firmware exactly") + } + } + + @Test + fun every_ModemPreset_is_pinned() { + val protoPresets = ModemPreset.entries.filter { it.name != "UNSET" && it.name != "UNRECOGNIZED" }.toSet() + assertEquals( + protoPresets, + expectedNames.keys, + "Every ModemPreset must be pinned to its firmware name here. A new preset was added to the protos " + + "(and to Channel.name's `when`) but not recorded in this contract — add it with its exact " + + "DisplayFormatters::getModemPresetDisplayName string.", + ) + } +}