Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .skills/compose-ui/strings-index.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -84,6 +91,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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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.",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,8 @@
<string name="label_short_fast">Short Range - Fast</string>
<string name="label_short_slow">Short Range - Slow</string>
<string name="label_short_turbo">Short Range - Turbo</string>
<string name="label_tiny_fast">Tiny - Fast</string>
<string name="label_tiny_slow">Tiny - Slow</string>
<string name="label_very_long_slow">Very Long Range - Slow</string>
<string name="last_heard_filter_label">Filter by Last Heard time: %1$s</string>
<string name="last_position_update">Last position update</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading