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 7928efb9..9d5f4e1e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -1,16 +1,20 @@ package com.geeksville.mesh.model import com.geeksville.mesh.ChannelProtos +import com.geeksville.mesh.ConfigProtos import com.google.protobuf.ByteString /** Utility function to make it easy to declare byte arrays - FIXME move someplace better */ fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } +fun xorHash(b: ByteArray) = b.fold(0) { acc, x -> acc xor (x.toInt() and 0xff) } - -data class Channel(val settings: ChannelProtos.ChannelSettings) { +data class Channel( + val settings: ChannelProtos.ChannelSettings, + val loraConfig: ConfigProtos.Config.LoRaConfig +) { companion object { // These bytes must match the well known and not secret bytes used the default channel AES128 key device code - val channelDefaultKey = byteArrayOfInts( + private val channelDefaultKey = byteArrayOfInts( 0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01 ) @@ -19,19 +23,24 @@ data class Channel(val settings: ChannelProtos.ChannelSettings) { private val defaultPSK = byteArrayOfInts(1) // a shortstring code to indicate we need our default PSK - // TH=he unsecured channel that devices ship with + // The default channel that devices ship with val default = Channel( ChannelProtos.ChannelSettings.newBuilder() - // .setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.LongFast) .setPsk(ByteString.copyFrom(defaultPSK)) + .build(), + ConfigProtos.Config.LoRaConfig.newBuilder() + .setModemPreset(ConfigProtos.Config.LoRaConfig.ModemPreset.LongFast) .build() ) } /// Return the name of our channel as a human readable string. If empty string, assume "Default" per mesh.proto spec val name: String - get() = settings.name.ifEmpty { "Placeholder" /* - when (settings.modemConfig) { + 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) { ConfigProtos.Config.LoRaConfig.ModemPreset.ShortFast -> "ShortFast" ConfigProtos.Config.LoRaConfig.ModemPreset.ShortSlow -> "ShortSlow" ConfigProtos.Config.LoRaConfig.ModemPreset.MidFast -> "MidFast" @@ -40,7 +49,7 @@ data class Channel(val settings: ChannelProtos.ChannelSettings) { ConfigProtos.Config.LoRaConfig.ModemPreset.LongSlow -> "LongSlow" ConfigProtos.Config.LoRaConfig.ModemPreset.VLongSlow -> "VLongSlow" else -> "Invalid" - }*/ + } } val psk: ByteString @@ -75,9 +84,13 @@ data class Channel(val settings: ChannelProtos.ChannelSettings) { return "#${name}-${suffix}" } - override fun equals(o: Any?): Boolean = (o is Channel) - && psk.toByteArray() contentEquals o.psk.toByteArray() - && name == o.name -} + override fun equals(other: Any?): Boolean = (other is Channel) + && psk.toByteArray() contentEquals other.psk.toByteArray() + && name == other.name -fun xorHash(b: ByteArray) = b.fold(0) { acc, x -> acc xor (x.toInt() and 0xff) } \ No newline at end of file + override fun hashCode(): Int { + var result = settings.hashCode() + result = 31 * result + loraConfig.hashCode() + return result + } +} diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt index 44785c61..e41ec928 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt @@ -15,7 +15,7 @@ data class ChannelSet( ) : Logging { companion object { - const val prefix = "https://www.meshtastic.org/d/#" + const val prefix = "https://www.meshtastic.org/e/#" private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING @@ -42,7 +42,7 @@ data class ChannelSet( val primaryChannel: Channel? get() = if (protobuf.settingsCount > 0) - Channel(protobuf.getSettings(0)) + Channel(protobuf.getSettings(0), protobuf.loraConfig) else null @@ -54,10 +54,7 @@ data class ChannelSet( val channelBytes = protobuf.toByteArray() ?: ByteArray(0) // if unset just use empty val enc = Base64.encodeToString(channelBytes, base64Flags) - val p = if (upperCasePrefix) - prefix.uppercase() - else - prefix + val p = if (upperCasePrefix) prefix.uppercase() else prefix return Uri.parse("$p$enc") } diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt index 07f80271..ac7c7b75 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -350,7 +350,7 @@ class MeshService : Service(), Logging { var myNodeInfo: MyNodeInfo? = null - private var deviceConfig: ConfigProtos.Config? = null + private var localConfig: LocalOnlyProtos.LocalConfig = LocalOnlyProtos.LocalConfig.getDefaultInstance() private var channels = fixupChannelList(listOf()) @@ -472,6 +472,7 @@ class MeshService : Service(), Logging { return AppOnlyProtos.ChannelSet.newBuilder().apply { addAllSettings(cs) + loraConfig = localConfig.lora }.build() } set(value) { @@ -489,6 +490,16 @@ class MeshService : Service(), Logging { setChannel(it) } + localConfig.let { currentConfig -> + val newConfig = ConfigProtos.Config.newBuilder() + + val newPrefs = currentConfig.lora.toBuilder() + newPrefs.modemPreset = value.loraConfig.modemPreset + newConfig.lora = newPrefs.build() + + sendDeviceConfig(newConfig.build()) + } + channels = fixupChannelList(asChannels) } @@ -497,7 +508,7 @@ class MeshService : Service(), Logging { if (myNodeInfo == null) throw RadioNotConnectedException() - from = myNodeNum + from = 0 // don't add myNodeNum to = idNum } @@ -726,9 +737,21 @@ class MeshService : Service(), Logging { if (fromNodeNum == myNodeNum) { when (a.variantCase) { AdminProtos.AdminMessage.VariantCase.GET_CONFIG_RESPONSE -> { - debug("Admin: received deviceConfig") - deviceConfig = a.getConfigResponse - requestChannel(0) // Now start reading channels + val response = a.getConfigResponse + debug("Admin: received config ${response.payloadVariantCase}") + localConfig.let { currentConfig -> + val builder = currentConfig.toBuilder() + if (response.hasDevice()) builder.device = response.device + if (response.hasPosition()) builder.position = response.position + if (response.hasPower()) builder.power = response.power + if (response.hasWifi()) builder.wifi = response.wifi + if (response.hasDisplay()) builder.display = response.display + if (response.hasLora()) { + builder.lora = response.lora + requestChannel(0) // Now start reading channels + } + localConfig = builder.build() + } } AdminProtos.AdminMessage.VariantCase.GET_CHANNEL_RESPONSE -> { @@ -983,7 +1006,7 @@ class MeshService : Service(), Logging { sleepTimeout = serviceScope.handledLaunch { try { // If we have a valid timeout, wait that long (+30 seconds) otherwise, just wait 30 seconds - val timeout = (deviceConfig?.power?.lsSecs ?: 0) + 30 + val timeout = (localConfig.power?.lsSecs ?: 0) + 30 debug("Waiting for sleeping device, timeout=$timeout secs") delay(timeout * 1000L) @@ -1075,7 +1098,7 @@ class MeshService : Service(), Logging { private fun onRadioConnectionState(state: RadioServiceConnectionState) { // sleep now disabled by default on ESP32, permanent is true unless isPowerSaving enabled - val lsEnabled = deviceConfig?.power?.isPowerSaving ?: false + val lsEnabled = localConfig.power?.isPowerSaving ?: false val connected = state.isConnected val permanent = state.isPermanent || !lsEnabled onConnectionChanged( @@ -1249,7 +1272,7 @@ class MeshService : Service(), Logging { regenMyNodeInfo() // We'll need to get a new set of channels and settings now - deviceConfig = null + localConfig = LocalOnlyProtos.LocalConfig.getDefaultInstance() // prefill the channel array with null channels channels = fixupChannelList(listOf()) @@ -1259,8 +1282,8 @@ class MeshService : Service(), Logging { private fun fixupChannelList(lIn: List): Array { // When updating old firmware, we will briefly be told that there is zero channels val maxChannels = - max(myNodeInfo?.maxChannels ?: 8, 8) // If we don't have my node info, assume 8 channels - var l = lIn + max(myNodeInfo?.maxChannels ?: 10, 10) // If we don't have my node info, assume 10 channels + val l = lIn.toMutableList() while (l.size < maxChannels) { val b = ChannelProtos.Channel.newBuilder() b.index = l.size @@ -1272,15 +1295,15 @@ class MeshService : Service(), Logging { private fun setRegionOnDevice() { val curConfigRegion = - deviceConfig?.lora?.region ?: ConfigProtos.Config.LoRaConfig.RegionCode.Unset + localConfig.lora?.region ?: ConfigProtos.Config.LoRaConfig.RegionCode.Unset if (curConfigRegion.number != curRegionValue && curRegionValue != ConfigProtos.Config.LoRaConfig.RegionCode.Unset_VALUE) if (deviceVersion >= minFirmwareVersion) { info("Telling device to upgrade region") // Tell the device to set the new region field (old devices will simply ignore this) - deviceConfig?.let { currentConfig -> - val newConfig = currentConfig.toBuilder() + localConfig.let { currentConfig -> + val newConfig = ConfigProtos.Config.newBuilder() val newPrefs = currentConfig.lora.toBuilder() newPrefs.regionValue = curRegionValue @@ -1301,7 +1324,7 @@ class MeshService : Service(), Logging { // Try to pull our region code from the new preferences field // FIXME - do not check net - figuring out why board is rebooting val curConfigRegion = - deviceConfig?.lora?.region ?: ConfigProtos.Config.LoRaConfig.RegionCode.Unset + localConfig.lora?.region ?: ConfigProtos.Config.LoRaConfig.RegionCode.Unset if (curConfigRegion != ConfigProtos.Config.LoRaConfig.RegionCode.Unset) { info("Using device region $curConfigRegion (code ${curConfigRegion.number})") curRegionValue = curConfigRegion.number @@ -1364,9 +1387,11 @@ class MeshService : Service(), Logging { } private fun requestDeviceConfig() { - sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) { - getConfigRequest = AdminProtos.AdminMessage.ConfigType.DEVICE_CONFIG - }) + AdminProtos.AdminMessage.ConfigType.values().forEach { + sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) { + if (it != AdminProtos.AdminMessage.ConfigType.UNRECOGNIZED) getConfigRequest = it + }) + } } private fun requestChannel(channelIndex: Int) { @@ -1447,7 +1472,17 @@ class MeshService : Service(), Logging { }) // Update our cached copy - this@MeshService.deviceConfig = c + localConfig.let { currentConfig -> + val builder = currentConfig.toBuilder() + if (c.hasDevice()) builder.device = c.device + if (c.hasPosition()) builder.position = c.position + if (c.hasPower()) builder.power = c.power + if (c.hasWifi()) builder.wifi = c.wifi + if (c.hasDisplay()) builder.display = c.display + if (c.hasLora()) builder.lora = c.lora + this@MeshService.localConfig = builder.build() + // debug("sendDeviceConfig: localConfig ${localConfig.toOneLineString()}") + } } /** Set our radio config @@ -1691,7 +1726,7 @@ class MeshService : Service(), Logging { } override fun getDeviceConfig(): ByteArray = toRemoteExceptions { - this@MeshService.deviceConfig?.toByteArray() + this@MeshService.localConfig.toByteArray() ?: throw NoDeviceConfigException() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 194f771d..8dc87326 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -115,7 +115,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { if (bitmap != null) binding.qrView.setImageBitmap(bitmap) - val modemPreset = model.deviceConfig.value?.lora?.modemPreset + val modemPreset = channel.loraConfig.modemPreset val channelOption = ChannelOption.fromConfig(modemPreset) binding.filledExposedDropdown.setText( getString( @@ -172,9 +172,17 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } /// Send new channel settings to the device - private fun installSettings(newChannel: ChannelProtos.ChannelSettings) { + private fun installSettings( + newChannel: ChannelProtos.ChannelSettings, + newLoRaConfig: ConfigProtos.Config.LoRaConfig + ) { val newSet = - ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build()) + ChannelSet( + AppOnlyProtos.ChannelSet.newBuilder() + .addSettings(newChannel) + .setLoraConfig(newLoRaConfig) + .build() + ) // Try to change the radio, if it fails, tell the user why and throw away their edits try { model.setChannels(newSet) @@ -256,7 +264,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } .setPositiveButton(R.string.apply) { _, _ -> debug("Switching back to default channel") - installSettings(Channel.default.settings) + installSettings(Channel.default.settings, Channel.default.loraConfig) } .show() } @@ -302,15 +310,14 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { val newName = binding.channelNameEdit.text.toString().trim() // Find the new modem config - val selectedChannelOptionString = + val selectedModemPresetString = binding.filledExposedDropdown.editableText.toString() - var modemPreset = getModemPreset(selectedChannelOptionString) - // if (modemPreset == ConfigProtos.Config.LoRaConfig.ModemPreset.UNRECOGNIZED) // Huh? didn't find it - keep same - // modemPreset = oldPrimary.settings.modemConfig -> TODO add from LoraConfig.ModemPreset? + var newModemPreset = getModemPreset(selectedModemPresetString) + if (newModemPreset == ConfigProtos.Config.LoRaConfig.ModemPreset.UNRECOGNIZED) // Huh? didn't find it - keep same + newModemPreset = oldPrimary.loraConfig.modemPreset // Generate a new AES256 key if the user changes channel name or the name is non-default and the settings changed - // if (newName != originalName || (newName.isNotEmpty() && modemConfig != oldPrimary.settings.modemConfig)) { - if (newName != originalName || newName.isNotEmpty()) { + if (newName != originalName || (newName.isNotEmpty() && newModemPreset != oldPrimary.loraConfig.modemPreset)) { // Install a new customized channel debug("ASSIGNING NEW AES256 KEY") @@ -325,9 +332,10 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } // No matter what apply the speed selection from the user - // newSettings.modemConfig = modemPreset -> TODO add from LoraConfig.ModemPreset? + val newLoRaConfig = ConfigProtos.Config.LoRaConfig.newBuilder() + .setModemPreset(newModemPreset) - installSettings(newSettings.build()) + installSettings(newSettings.build(),newLoRaConfig.build()) } } .show() 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 4984096e..c305363a 100644 --- a/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt +++ b/app/src/test/java/com/geeksville/mesh/model/ChannelSetTest.kt @@ -8,10 +8,10 @@ class ChannelSetTest { /** make sure we match the python and device code behavior */ @Test fun matchPython() { - val url = Uri.parse("https://www.meshtastic.org/d/#CgUYAiIBAQ") + val url = Uri.parse("https://www.meshtastic.org/e/#CgUiAQEYAg") val cs = ChannelSet(url) - // Assert.assertEquals("LongFast", cs.primaryChannel!!.name, ) - // Assert.assertEquals("#LongFast-I", cs.primaryChannel!!.humanName, ) - // Assert.assertEquals(url, cs.getChannelUrl(false)) + Assert.assertEquals("LongFast", cs.primaryChannel!!.name) + Assert.assertEquals("#LongFast-I", cs.primaryChannel!!.humanName) + Assert.assertEquals(url, cs.getChannelUrl(false)) } }