diff --git a/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt b/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt index 8788167b2..9bd4f0cf5 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/ChannelTest.kt @@ -1,6 +1,7 @@ package com.geeksville.mesh import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.ChannelSet import org.junit.Assert import org.junit.Test @@ -15,4 +16,18 @@ class ChannelTest { Assert.assertTrue(ch.getChannelUrl().toString().startsWith(ChannelSet.prefix)) Assert.assertEquals(ChannelSet(ch.getChannelUrl()), ch) } -} \ No newline at end of file + + @Test + fun channelNumGood() { + val ch = Channel.default + + Assert.assertEquals(20, ch.channelNum) + } + + @Test + fun radioFreqGood() { + val ch = Channel.default + + Assert.assertEquals(906.875f, ch.radioFreq) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/model/Channel.kt b/app/src/main/java/com/geeksville/mesh/model/Channel.kt index cd2cbd4d2..cfac363c8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -43,9 +43,7 @@ data class Channel( val name: String 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 - if (loraConfig.bandwidth != 0) - "Unset" - else when (loraConfig.modemPreset) { + if (loraConfig.usePreset) when (loraConfig.modemPreset) { ModemPreset.SHORT_FAST -> "ShortFast" ModemPreset.SHORT_SLOW -> "ShortSlow" ModemPreset.MEDIUM_FAST -> "MediumFast" @@ -55,7 +53,7 @@ data class Channel( ModemPreset.LONG_MODERATE -> "LongMod" ModemPreset.VERY_LONG_SLOW -> "VLongSlow" else -> "Invalid" - } + } else "Custom" } val psk: ByteString @@ -90,6 +88,26 @@ data class Channel( return "#${name}-${suffix}" } + /** + * hash a string into an integer using the djb2 algorithm by Dan Bernstein + * http://www.cse.yorku.ca/~oz/hash.html + */ + val hash: UInt // using UInt instead of Long to match RadioInterface.cpp results + get() { + var hash: UInt = 5381u + for (c in name) { + hash += (hash shl 5) + c.code.toUInt() + print("$c ${c.code} $hash ") + } + return hash + } + + val channelNum: Int + get() = if (loraConfig.channelNum != 0) loraConfig.channelNum + else (hash % RegionInfo.numChannels(loraConfig).toUInt()).toInt() + 1 + + val radioFreq: Float get() = RegionInfo.radioFreq(loraConfig, channelNum) + override fun equals(other: Any?): Boolean = (other is Channel) && psk.toByteArray() contentEquals other.psk.toByteArray() && name == other.name diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt index d451da790..675ece351 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt @@ -1,27 +1,85 @@ package com.geeksville.mesh.model +import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.ModemPreset +import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig.RegionCode import com.geeksville.mesh.R +fun LoRaConfig.bandwidth() = if (usePreset) { + val wideLora = region == RegionCode.LORA_24 + ChannelOption.bandwidth(modemPreset) * if (wideLora) 3.25f else 1f +} else when (bandwidth) { + 31 -> .03125f + 62 -> .0625f + 200 -> .203125f + 400 -> .40625f + 800 -> .8125f + 1600 -> 1.6250f + else -> bandwidth / 1000f +} + +enum class RegionInfo( + val regionCode: RegionCode, + val freqStart: Float, + val freqEnd: Float, +) { + US(RegionCode.US, 902.0f, 928.0f), + EU_433(RegionCode.EU_433, 433.0f, 434.0f), + EU_868(RegionCode.EU_868, 869.4f, 869.65f), + CN(RegionCode.CN, 470.0f, 510.0f), + JP(RegionCode.JP, 920.8f, 927.8f), + ANZ(RegionCode.ANZ, 915.0f, 928.0f), + RU(RegionCode.RU, 868.7f, 869.2f), + KR(RegionCode.KR, 920.0f, 923.0f), + TW(RegionCode.TW, 920.0f, 925.0f), + IN(RegionCode.IN, 865.0f, 867.0f), + NZ_865(RegionCode.NZ_865, 864.0f, 868.0f), + TH(RegionCode.TH, 920.0f, 925.0f), + UA_433(RegionCode.UA_433, 433.0f, 434.7f), + UA_868(RegionCode.UA_868, 868.0f, 868.6f), + LORA_24(RegionCode.LORA_24, 2400.0f, 2483.5f), + UNSET(RegionCode.UNSET, 902.0f, 928.0f); + + companion object { + fun numChannels(loraConfig: LoRaConfig): Int { + for (option in values()) { + if (option.regionCode == loraConfig.region) + return ((option.freqEnd - option.freqStart) / loraConfig.bandwidth()).toInt() + } + return 0 + } + + fun radioFreq(loraConfig: LoRaConfig, channelNum: Int): Float = with(loraConfig) { + if (overrideFrequency != 0f) return overrideFrequency + frequencyOffset + for (option in values()) { + if (option.regionCode == region) + return (option.freqStart + bandwidth() / 2) + (channelNum - 1) * bandwidth() + } + return 0f + } + } +} + enum class ChannelOption( val modemPreset: ModemPreset, val configRes: Int, + val bandwidth: Float, ) { - SHORT_FAST(ModemPreset.SHORT_FAST, R.string.modem_config_short), - SHORT_SLOW(ModemPreset.SHORT_SLOW, R.string.modem_config_slow_short), - MEDIUM_FAST(ModemPreset.MEDIUM_FAST, R.string.modem_config_medium), - MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, R.string.modem_config_slow_medium), - LONG_FAST(ModemPreset.LONG_FAST, R.string.modem_config_long), - LONG_MODERATE(ModemPreset.LONG_MODERATE, R.string.modem_config_mod_long), - LONG_SLOW(ModemPreset.LONG_SLOW, R.string.modem_config_slow_long), - VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, R.string.modem_config_very_long); + LONG_SLOW(ModemPreset.LONG_SLOW, R.string.modem_config_slow_long, .125f), + VERY_LONG_SLOW(ModemPreset.VERY_LONG_SLOW, R.string.modem_config_very_long, .0625f), + MEDIUM_SLOW(ModemPreset.MEDIUM_SLOW, R.string.modem_config_slow_medium, .250f), + MEDIUM_FAST(ModemPreset.MEDIUM_FAST, R.string.modem_config_medium, .250f), + SHORT_SLOW(ModemPreset.SHORT_SLOW, R.string.modem_config_slow_short, .250f), + SHORT_FAST(ModemPreset.SHORT_FAST, R.string.modem_config_short, .250f), + LONG_MODERATE(ModemPreset.LONG_MODERATE, R.string.modem_config_mod_long, .125f), + LONG_FAST(ModemPreset.LONG_FAST, R.string.modem_config_long, .250f); companion object { - fun fromConfig(modemPreset: ModemPreset?): ChannelOption? { + fun bandwidth(modemPreset: ModemPreset?): Float { for (option in values()) { - if (option.modemPreset == modemPreset) return option + if (option.modemPreset == modemPreset) return option.bandwidth } - return null + return 0f } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt index 9332637eb..896c94eb0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/DeviceSettingsFragment.kt @@ -258,6 +258,8 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { viewModel.clearPacketResponse() val parsed = AdminProtos.AdminMessage.parseFrom(data.payload) debug("packet for destNum ${destNum.toUInt()} received ${parsed.payloadVariantCase} from $from") + // check destination: lora config or channel editor + val goChannels = (packetResponseState as PacketResponseState.Loading).total > 2 when (parsed.payloadVariantCase) { AdminProtos.AdminMessage.PayloadVariantCase.GET_CHANNEL_RESPONSE -> { val response = parsed.getChannelResponse @@ -265,7 +267,7 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { // Stop once we get to the first disabled entry if (response.role != ChannelProtos.Channel.Role.DISABLED) { channelList.add(response.index, response.settings) - if (response.index + 1 < maxChannels) { + if (response.index + 1 < maxChannels && goChannels) { // Not done yet, request next channel viewModel.getChannel(destNum, response.index + 1) } else { @@ -285,8 +287,6 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { } AdminProtos.AdminMessage.PayloadVariantCase.GET_CONFIG_RESPONSE -> { - // check destination: lora config or channel editor - val goChannels = (packetResponseState as PacketResponseState.Loading).total > 1 packetResponseState = PacketResponseState.Empty val response = parsed.getConfigResponse radioConfig = response @@ -370,6 +370,11 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { viewModel.requestNodedbReset(destNum) } + ConfigType.LORA_CONFIG -> { + (packetResponseState as PacketResponseState.Loading).total = 2 + channelList.clear() + viewModel.getChannel(destNum, 0) + } is ConfigType -> { viewModel.getConfig(destNum, configType.number) } @@ -491,6 +496,7 @@ fun RadioConfigNavHost(node: NodeInfo, viewModel: UIViewModel = viewModel()) { composable("lora") { LoRaConfigItemList( loraConfig = radioConfig.lora, + primarySettings = channelList.getOrNull(0) ?: return@composable, enabled = connected, focusManager = focusManager, onSaveClicked = { loraInput -> diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt b/app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt index b36218527..1620f83bf 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/EditTextPreference.kt @@ -34,6 +34,7 @@ fun EditTextPreference( keyboardActions: KeyboardActions, onValueChanged: (Int) -> Unit, modifier: Modifier = Modifier, + onFocusChanged: (FocusState) -> Unit = {}, trailingIcon: (@Composable () -> Unit)? = null, ) { var valueState by remember(value) { mutableStateOf(value.toUInt().toString()) } @@ -54,7 +55,7 @@ fun EditTextPreference( onValueChanged(int) } }, - onFocusChanged = {}, + onFocusChanged = onFocusChanged, modifier = modifier, trailingIcon = trailingIcon ) @@ -68,7 +69,8 @@ fun EditTextPreference( keyboardActions: KeyboardActions, onValueChanged: (Float) -> Unit, modifier: Modifier = Modifier, -) { + onFocusChanged: (FocusState) -> Unit = {}, + ) { var valueState by remember(value) { mutableStateOf(value.toString()) } EditTextPreference( @@ -87,7 +89,7 @@ fun EditTextPreference( onValueChanged(float) } }, - onFocusChanged = {}, + onFocusChanged = onFocusChanged, modifier = modifier ) } diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/config/LoRaConfigItemList.kt b/app/src/main/java/com/geeksville/mesh/ui/components/config/LoRaConfigItemList.kt index c3a7a834b..d7ea369de 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/config/LoRaConfigItemList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/config/LoRaConfigItemList.kt @@ -13,8 +13,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.tooling.preview.Preview +import com.geeksville.mesh.ChannelProtos.ChannelSettings import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig import com.geeksville.mesh.copy +import com.geeksville.mesh.model.Channel +import com.geeksville.mesh.model.RegionInfo import com.geeksville.mesh.ui.components.DropDownPreference import com.geeksville.mesh.ui.components.EditListPreference import com.geeksville.mesh.ui.components.EditTextPreference @@ -25,11 +28,13 @@ import com.geeksville.mesh.ui.components.SwitchPreference @Composable fun LoRaConfigItemList( loraConfig: LoRaConfig, + primarySettings: ChannelSettings, enabled: Boolean, focusManager: FocusManager, onSaveClicked: (LoRaConfig) -> Unit, ) { var loraInput by remember(loraConfig) { mutableStateOf(loraConfig) } + val primaryChannel = Channel(primarySettings, loraInput) LazyColumn( modifier = Modifier.fillMaxSize() @@ -125,11 +130,16 @@ fun LoRaConfigItemList( } item { + var isFocused by remember { mutableStateOf(false) } EditTextPreference(title = "Channel number", - value = loraInput.channelNum, + value = if (isFocused || loraInput.channelNum != 0) loraInput.channelNum else primaryChannel.channelNum, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), - onValueChanged = { loraInput = loraInput.copy { channelNum = it } }) + onFocusChanged = { isFocused = it.isFocused }, + onValueChanged = { + if (it <= RegionInfo.numChannels(loraInput)) // max numChannels + loraInput = loraInput.copy { channelNum = it } + }) } item { @@ -163,10 +173,12 @@ fun LoRaConfigItemList( item { Divider() } item { + var isFocused by remember { mutableStateOf(false) } EditTextPreference(title = "Override frequency (MHz)", - value = loraInput.overrideFrequency, + value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq, enabled = enabled, keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + onFocusChanged = { isFocused = it.isFocused }, onValueChanged = { loraInput = loraInput.copy { overrideFrequency = it } }) } @@ -185,9 +197,10 @@ fun LoRaConfigItemList( @Preview(showBackground = true) @Composable -fun LoRaConfigPreview(){ +fun LoRaConfigPreview() { LoRaConfigItemList( - loraConfig = LoRaConfig.getDefaultInstance(), + loraConfig = Channel.default.loraConfig, + primarySettings = Channel.default.settings, enabled = true, focusManager = LocalFocusManager.current, onSaveClicked = { }, diff --git a/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt b/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt index 0613d0478..5b891a91d 100644 --- a/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt +++ b/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt @@ -8,7 +8,7 @@ class ChannelSetTest { /** make sure we match the python and device code behavior */ @Test fun matchPython() { - val url = Uri.parse("https://meshtastic.org/e/#CgMSAQESAA") + val url = Uri.parse("https://meshtastic.org/e/#CgMSAQESBggBQANIAQ") val cs = ChannelSet(url) Assert.assertEquals("LongFast", cs.primaryChannel!!.name) Assert.assertEquals("#LongFast-I", cs.primaryChannel!!.humanName)