From 85a0ea72867d029f9b2479179215884b2640fa3d Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 27 Feb 2021 10:18:00 +0800 Subject: [PATCH 01/21] 1.2 wip --- .../java/com/geeksville/mesh/MyNodeInfo.kt | 6 - .../java/com/geeksville/mesh/model/UIState.kt | 2 - .../geeksville/mesh/service/MeshService.kt | 104 ++++++++---------- app/src/main/proto | 2 +- 4 files changed, 46 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt index e5a447b1c..76dffcb6e 100644 --- a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt @@ -15,8 +15,6 @@ data class MyNodeInfo( val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want val shouldUpdate: Boolean, // this device has old firmware val currentPacketId: Long, - val nodeNumBits: Int, - val packetIdBits: Int, val messageTimeoutMsec: Int, val minAppVersion: Int ) : Parcelable { @@ -33,8 +31,6 @@ data class MyNodeInfo( parcel.readByte() != 0.toByte(), parcel.readLong(), parcel.readInt(), - parcel.readInt(), - parcel.readInt(), parcel.readInt() ) { } @@ -48,8 +44,6 @@ data class MyNodeInfo( parcel.writeByte(if (couldUpdate) 1 else 0) parcel.writeByte(if (shouldUpdate) 1 else 0) parcel.writeLong(currentPacketId) - parcel.writeInt(nodeNumBits) - parcel.writeInt(packetIdBits) parcel.writeInt(messageTimeoutMsec) parcel.writeInt(minAppVersion) } diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index cdfb805bc..34ff5992b 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -71,8 +71,6 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging companion object { /** * Return the current channel info - * FIXME, we should sim channels at the MeshService level if we are running on an emulator, - * for now I just fake it by returning a canned channel. */ fun getChannel(c: MeshProtos.RadioConfig?): Channel? { val channel = c?.channelSettings?.let { Channel(it) } 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 817fdd95e..4eb3f4594 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -512,15 +512,12 @@ class MeshService : Service(), Logging { /// Generate a new mesh packet builder with our node as the sender, and the specified node num private fun newMeshPacketTo(idNum: Int) = MeshPacket.newBuilder().apply { - val useShortAddresses = (myNodeInfo?.nodeNumBits ?: 8) != 32 - if (myNodeInfo == null) throw RadioNotConnectedException() from = myNodeNum - // We might need to change broadcast addresses to work with old device loads - to = if (useShortAddresses && idNum == DataPacket.NODENUM_BROADCAST) 255 else idNum + to = idNum } /** @@ -540,11 +537,11 @@ class MeshService : Service(), Logging { destId: String, wantAck: Boolean = false, id: Int = 0, - initFn: MeshProtos.SubPacket.Builder.() -> Unit + initFn: MeshProtos.Data.Builder.() -> Unit ): MeshPacket = newMeshPacketTo(destId).apply { this.wantAck = wantAck this.id = id - decoded = MeshProtos.SubPacket.newBuilder().also { + decoded = MeshProtos.Data.newBuilder().also { initFn(it) }.build() }.build() @@ -556,11 +553,11 @@ class MeshService : Service(), Logging { /// Generate a DataPacket from a MeshPacket, or null if we didn't have enough data to do so private fun toDataPacket(packet: MeshPacket): DataPacket? { - return if (!packet.hasDecoded() || !packet.decoded.hasData()) { + return if (!packet.hasDecoded()) { // We never convert packets that are not DataPackets null } else { - val data = packet.decoded.data + val data = packet.decoded val bytes = data.payload.toByteArray() val fromId = toNodeID(packet.from) val toId = toNodeID(packet.to) @@ -591,15 +588,10 @@ class MeshService : Service(), Logging { } } - /// Syntactic sugar to create data subpackets - private fun makeData(portNum: Int, bytes: ByteString) = MeshProtos.Data.newBuilder().also { - it.portnumValue = portNum - it.payload = bytes - }.build() - private fun toMeshPacket(p: DataPacket): MeshPacket { return buildMeshPacket(p.to!!, id = p.id, wantAck = true) { - data = makeData(p.dataType, ByteString.copyFrom(p.bytes)) + portnumValue = p.dataType + payload = ByteString.copyFrom(p.bytes) } } @@ -624,7 +616,7 @@ class MeshService : Service(), Logging { /// Update our model and resend as needed for a MeshPacket we just received from the radio private fun handleReceivedData(packet: MeshPacket) { myNodeInfo?.let { myInfo -> - val data = packet.decoded.data + val data = packet.decoded val bytes = data.payload.toByteArray() val fromId = toNodeID(packet.from) val dataPacket = toDataPacket(packet) @@ -648,6 +640,8 @@ class MeshService : Service(), Logging { dataPacket.status = MessageStatus.RECEIVED rememberDataPacket(dataPacket) + // if (p.hasUser()) handleReceivedUser(fromNum, p.user) + when (data.portnumValue) { Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> { debug("Received CLEAR_TEXT from $fromId") @@ -666,6 +660,15 @@ class MeshService : Service(), Logging { val u = MeshProtos.User.parseFrom(data.payload) handleReceivedUser(packet.from, u) } + + // Handle new style routing info + Portnums.PortNum.ROUTING_APP_VALUE -> { + val u = MeshProtos.Routing.parseFrom(data.payload) + if (u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE) + handleAckNak(true, data.requestId) + else + handleAckNak(false, data.requestId) + } } // We always tell other apps when new data packets arrive @@ -778,44 +781,35 @@ class MeshService : Service(), Logging { //val toNum = packet.to // debug("Recieved: $packet") - val p = packet.decoded + if (packet.hasDecoded()) { + val p = packet.decoded - val packetToSave = Packet( - UUID.randomUUID().toString(), - "packet", - System.currentTimeMillis(), - packet.toString() - ) - insertPacket(packetToSave) - // If the rxTime was not set by the device (because device software was old), guess at a time - val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond() + val packetToSave = Packet( + UUID.randomUUID().toString(), + "packet", + System.currentTimeMillis(), + packet.toString() + ) + insertPacket(packetToSave) + // If the rxTime was not set by the device (because device software was old), guess at a time + val rxTime = if (packet.rxTime != 0) packet.rxTime else currentSecond() - // Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes - // through our node on the way to the phone that means that local node is also alive in the mesh + // Update last seen for the node that sent the packet, but also for _our node_ because anytime a packet passes + // through our node on the way to the phone that means that local node is also alive in the mesh - updateNodeInfo(myNodeNum) { - it.position = it.position?.copy(time = currentSecond()) - } + updateNodeInfo(myNodeNum) { + it.position = it.position?.copy(time = currentSecond()) + } + + // if (p.hasPosition()) handleReceivedPosition(fromNum, p.position, rxTime) - if (p.hasPosition()) - handleReceivedPosition(fromNum, p.position, rxTime) - else updateNodeInfo(fromNum) { // Update our last seen based on any valid timestamps. If the device didn't provide a timestamp make one updateNodeInfoTime(it, rxTime) } - if (p.hasData()) handleReceivedData(packet) - - if (p.hasUser()) - handleReceivedUser(fromNum, p.user) - - if (p.successId != 0) - handleAckNak(true, p.successId) - - if (p.failId != 0) - handleAckNak(false, p.failId) + } } private fun insertPacket(packetToSave: Packet) { @@ -1138,8 +1132,6 @@ class MeshService : Service(), Logging { DeviceVersion(firmwareVersion) ), currentPacketId.toLong() and 0xffffffffL, - if (nodeNumBits == 0) 8 else nodeNumBits, - if (packetIdBits == 0) 8 else packetIdBits, if (messageTimeoutMsec == 0) 5 * 60 * 1000 else messageTimeoutMsec, // constants from current device code minAppVersion ) @@ -1294,16 +1286,12 @@ class MeshService : Service(), Logging { // encapsulate our payload in the proper protobufs and fire it off val packet = newMeshPacketTo(destNum) - packet.decoded = MeshProtos.SubPacket.newBuilder().also { - val isNewPositionAPI = - deviceVersion >= DeviceVersion("1.20.0") // We changed position APIs with this version - if (isNewPositionAPI) { - // Use the new position as data format - it.data = makeData(Portnums.PortNum.POSITION_APP_VALUE, position.toByteString()) - } else { - // Send the old dedicated position subpacket - it.position = position - } + packet.decoded = MeshProtos.Data.newBuilder().also { + + // Use the new position as data format + it.portnumValue = Portnums.PortNum.POSITION_APP_VALUE + it.payload = position.toByteString() + it.wantResponse = wantResponse }.build() @@ -1395,11 +1383,9 @@ class MeshService : Service(), Logging { myNodeInfo?.let { val numPacketIds = - ((1L shl it.packetIdBits) - 1).toLong() // A mask for only the valid packet ID bits, either 255 or maxint + ((1L shl 32) - 1).toLong() // A mask for only the valid packet ID bits, either 255 or maxint if (currentPacketId == 0L) { - logAssert(it.packetIdBits == 8 || it.packetIdBits == 32) // Only values I'm expecting (though we don't require this) - // We now always pick a random initial packet id (odds of collision with the device is insanely low with 32 bit ids) val random = Random(System.currentTimeMillis()) val devicePacketId = random.nextLong().absoluteValue diff --git a/app/src/main/proto b/app/src/main/proto index b1aed0644..83e00e564 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit b1aed06442025624841b2288fac273d9bc41c438 +Subproject commit 83e00e564d3973b594a46e786b62eed2823e02ed From 31a106039b27ae33881319f912ac73106264979c Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 27 Feb 2021 11:13:30 +0800 Subject: [PATCH 02/21] impl set owner/set radio --- .../geeksville/mesh/service/MeshService.kt | 38 ++++++++++++++++--- app/src/main/proto | 2 +- 2 files changed, 33 insertions(+), 7 deletions(-) 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 4eb3f4594..501f6cc77 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1323,9 +1323,22 @@ class MeshService : Service(), Logging { */ private fun sendRadioConfig(c: MeshProtos.RadioConfig) { // Update our device - sendToRadio(ToRadio.newBuilder().apply { - this.setRadio = c - }) + val payload = AdminProtos.AdminMessage.newBuilder().also { + it.setRadio = c + }.build() + + // encapsulate our payload in the proper protobufs and fire it off + val packet = newMeshPacketTo(myNodeNum) + + packet.decoded = MeshProtos.Data.newBuilder().also { + + // Use the new position as data format + it.portnumValue = Portnums.PortNum.ADMIN_APP_VALUE + it.payload = payload.toByteString() + }.build() + + // send the packet into the mesh + sendToRadio(packet.build()) // Update our cached copy this@MeshService.radioConfig = c @@ -1365,9 +1378,22 @@ class MeshService : Service(), Logging { handleReceivedUser(myNode.myNodeNum, user) // set my owner info - sendToRadio(ToRadio.newBuilder().apply { - this.setOwner = user - }) + val payload = AdminProtos.AdminMessage.newBuilder().also { + it.setOwner = user + }.build() + + // encapsulate our payload in the proper protobufs and fire it off + val packet = newMeshPacketTo(myNodeNum) + + packet.decoded = MeshProtos.Data.newBuilder().also { + + // Use the new position as data format + it.portnumValue = Portnums.PortNum.ADMIN_APP_VALUE + it.payload = payload.toByteString() + }.build() + + // send the packet into the mesh + sendToRadio(packet.build()) } } else throw Exception("Can't set user without a node info") // this shouldn't happen diff --git a/app/src/main/proto b/app/src/main/proto index 83e00e564..564b48869 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 83e00e564d3973b594a46e786b62eed2823e02ed +Subproject commit 564b488695da6bee7bb9c4553872268595a0f77d From 0743feadc496e8963b47a45c1122d98589eb564a Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 27 Feb 2021 11:44:05 +0800 Subject: [PATCH 03/21] wip adding channelset --- .../com/geeksville/mesh/IMeshService.aidl | 16 ++++++--- .../java/com/geeksville/mesh/model/UIState.kt | 33 ++++++++++++------- .../geeksville/mesh/service/MeshService.kt | 4 +++ 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index fcb44874a..03580804d 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -71,18 +71,26 @@ interface IMeshService { */ List getNodes(); - /// This method is only intended for use in our GUI, so the user can set radio options - /// It returns a RadioConfig protobuf. - byte []getRadioConfig(); - /// Return an list of MeshPacket protobuf (byte arrays) which were received while your client app was offline (recent messages only). /// Also includes any messages we have sent recently (useful for finding current message status) List getOldMessages(); + /// This method is only intended for use in our GUI, so the user can set radio options + /// It returns a RadioConfig protobuf. + byte []getRadioConfig(); + /// This method is only intended for use in our GUI, so the user can set radio options /// It sets a RadioConfig protobuf void setRadioConfig(in byte []payload); + /// This method is only intended for use in our GUI, so the user can set radio options + /// It returns a ChannelSet protobuf. + byte []getChannels(); + + /// This method is only intended for use in our GUI, so the user can set radio options + /// It sets a ChannelSet protobuf + void setChannels(in byte []payload); + /** Is the packet radio currently connected to the phone? Returns a ConnectionState string. */ diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index 34ff5992b..bc4f3a0b5 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.geeksville.android.Logging +import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.IMeshService import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.MyNodeInfo @@ -69,15 +70,6 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging } companion object { - /** - * Return the current channel info - */ - fun getChannel(c: MeshProtos.RadioConfig?): Channel? { - val channel = c?.channelSettings?.let { Channel(it) } - - return channel - } - fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) } @@ -101,6 +93,9 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging val radioConfig = object : MutableLiveData(null) { } + val channels = object : MutableLiveData(null) { + } + var positionBroadcastSecs: Int? get() { radioConfig.value?.preferences?.let { @@ -155,15 +150,31 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging debug("ViewModel cleared") } + /** + * Return the primary channel info + */ + val primaryChannel: ChannelSet? get() { + return channels.value?.let { it -> + Channel(it.getSettings(0)) + } + } /// Set the radio config (also updates our saved copy in preferences) - fun setRadioConfig(c: MeshProtos.RadioConfig) { + private fun setRadioConfig(c: MeshProtos.RadioConfig) { debug("Setting new radio config!") meshService?.radioConfig = c.toByteArray() radioConfig.value = c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings) + } + + /// Set the radio config (also updates our saved copy in preferences) + private fun setChannels(c: AppOnlyProtos.ChannelSet) { + debug("Setting new channels!") + meshService?.channels = c.toByteArray() + channels.value = + c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings) getPreferences(context).edit(commit = true) { - this.putString("channel-url", getChannel(c)!!.getChannelUrl().toString()) + this.putString("channel-url", primaryChannel!!.getChannelUrl().toString()) } } 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 501f6cc77..861a2f295 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1604,6 +1604,10 @@ class MeshService : Service(), Logging { this@MeshService.setRadioConfig(payload) } + override fun getChannels(): ByteArray { + TODO("Not yet implemented") + } + override fun getNodes(): MutableList = toRemoteExceptions { val r = nodeDBbyID.values.toMutableList() info("in getOnline, count=${r.size}") From bd796524b95bf5523ac24ca1c76dd7bc47e89b76 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 27 Feb 2021 12:04:44 +0800 Subject: [PATCH 04/21] channelset wip --- .idea/dictionaries/kevinh.xml | 1 + app/src/main/AndroidManifest.xml | 6 +- .../java/com/geeksville/mesh/model/Channel.kt | 49 +----------- .../com/geeksville/mesh/model/ChannelSet.kt | 75 +++++++++++++++++++ 4 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt diff --git a/.idea/dictionaries/kevinh.xml b/.idea/dictionaries/kevinh.xml index 9de6060e7..56d3af4f2 100644 --- a/.idea/dictionaries/kevinh.xml +++ b/.idea/dictionaries/kevinh.xml @@ -5,6 +5,7 @@ errormsg geeksville meshtastic + protobuf \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 421034192..997db7e0f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -133,7 +133,7 @@ @@ -143,11 +143,11 @@ + android:pathPrefix="/d/" /> + android:pathPrefix="/D/" /> 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 54886c7e5..d19faed8e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -20,7 +20,7 @@ data class Channel( companion object { // Note: this string _SHOULD NOT BE LOCALIZED_ because it directly hashes to values used on the device for the default channel name. // FIXME - make this work with new channel name system - val defaultChannelName = "Default" + const val defaultChannelName = "Default" // These bytes must match the well known and not secret bytes used the default channel AES128 key device code val channelDefaultKey = byteArrayOfInts( @@ -33,27 +33,8 @@ data class Channel( MeshProtos.ChannelSettings.newBuilder().setName(defaultChannelName) .setModemConfig(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build() ) - - const val prefix = "https://www.meshtastic.org/c/#" - - private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING - - private fun urlToSettings(url: Uri): MeshProtos.ChannelSettings { - val urlStr = url.toString() - - // We no longer support the super old (about 0.8ish? verison of the URLs that don't use the # separator - I doubt - // anyone is still using that old format - val pathRegex = Regex("$prefix(.*)", RegexOption.IGNORE_CASE) - val (base64) = pathRegex.find(urlStr)?.destructured - ?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.take(40)}") - val bytes = Base64.decode(base64, base64Flags) - - return MeshProtos.ChannelSettings.parseFrom(bytes) - } } - constructor(url: Uri) : this(urlToSettings(url)) - /// Return the name of our channel as a human readable string. If empty string, assume "Default" per mesh.proto spec val name: String get() = if (settings.name.isEmpty()) { @@ -106,32 +87,4 @@ data class Channel( return "#${name}-${suffix}" } - - /// Can this channel be changed right now? - var editable = false - - /// Return an URL that represents the current channel values - /// @param upperCasePrefix - portions of the URL can be upper case to make for more efficient QR codes - fun getChannelUrl(upperCasePrefix: Boolean = false): Uri { - // If we have a valid radio config use it, othterwise use whatever we have saved in the prefs - - val channelBytes = settings.toByteArray() ?: ByteArray(0) // if unset just use empty - val enc = Base64.encodeToString(channelBytes, base64Flags) - - val p = if(upperCasePrefix) - prefix.toUpperCase() - else - prefix - return Uri.parse("$p$enc") - } - - fun getChannelQR(): Bitmap { - val multiFormatWriter = MultiFormatWriter() - - // We encode as UPPER case for the QR code URL because QR codes are more efficient for that special case - val bitMatrix = - multiFormatWriter.encode(getChannelUrl(true).toString(), BarcodeFormat.QR_CODE, 192, 192); - val barcodeEncoder = BarcodeEncoder() - return barcodeEncoder.createBitmap(bitMatrix) - } } diff --git a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt new file mode 100644 index 000000000..1206af6e7 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt @@ -0,0 +1,75 @@ +package com.geeksville.mesh.model + +import android.graphics.Bitmap +import android.net.Uri +import android.util.Base64 +import com.geeksville.mesh.AppOnlyProtos +import com.geeksville.mesh.MeshProtos +import com.google.protobuf.ByteString +import com.google.zxing.BarcodeFormat +import com.google.zxing.MultiFormatWriter +import com.journeyapps.barcodescanner.BarcodeEncoder +import java.net.MalformedURLException + +/** 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() } + + +data class ChannelSet( + val protobuf: AppOnlyProtos.ChannelSet = AppOnlyProtos.ChannelSet.getDefaultInstance() +) { + companion object { + + // Placeholder when emulating + val emulated = ChannelSet( + AppOnlyProtos.ChannelSet.newBuilder().addSettings(Channel.emulated.settings).build() + ) + + const val prefix = "https://www.meshtastic.org/d/#" + + private const val base64Flags = Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING + + private fun urlToChannels(url: Uri): AppOnlyProtos.ChannelSet { + val urlStr = url.toString() + + // We no longer support the super old (about 0.8ish? verison of the URLs that don't use the # separator - I doubt + // anyone is still using that old format + val pathRegex = Regex("$prefix(.*)", RegexOption.IGNORE_CASE) + val (base64) = pathRegex.find(urlStr)?.destructured + ?: throw MalformedURLException("Not a meshtastic URL: ${urlStr.take(40)}") + val bytes = Base64.decode(base64, base64Flags) + + return AppOnlyProtos.ChannelSet.parseFrom(bytes) + } + } + + constructor(url: Uri) : this(urlToChannels(url)) + + /// Can this channel be changed right now? + var editable = false + + /// Return an URL that represents the current channel values + /// @param upperCasePrefix - portions of the URL can be upper case to make for more efficient QR codes + fun getChannelUrl(upperCasePrefix: Boolean = false): Uri { + // If we have a valid radio config use it, othterwise use whatever we have saved in the prefs + + val channelBytes = protobuf.toByteArray() ?: ByteArray(0) // if unset just use empty + val enc = Base64.encodeToString(channelBytes, base64Flags) + + val p = if(upperCasePrefix) + prefix.toUpperCase() + else + prefix + return Uri.parse("$p$enc") + } + + fun getChannelQR(): Bitmap { + val multiFormatWriter = MultiFormatWriter() + + // We encode as UPPER case for the QR code URL because QR codes are more efficient for that special case + val bitMatrix = + multiFormatWriter.encode(getChannelUrl(true).toString(), BarcodeFormat.QR_CODE, 192, 192); + val barcodeEncoder = BarcodeEncoder() + return barcodeEncoder.createBitmap(bitMatrix) + } +} From 1eaabfc216c5492f982d4819448a3b6927031cd0 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 27 Feb 2021 13:43:55 +0800 Subject: [PATCH 05/21] fix java paths for protos --- .../java/com/geeksville/mesh/MainActivity.kt | 10 +++-- .../java/com/geeksville/mesh/model/Channel.kt | 17 ++++---- .../geeksville/mesh/model/ChannelOption.kt | 43 ++++++++++--------- .../com/geeksville/mesh/model/ChannelSet.kt | 11 +++-- .../java/com/geeksville/mesh/model/UIState.kt | 42 ++++++------------ .../geeksville/mesh/service/MeshService.kt | 26 ++++++----- .../mesh/service/MeshServiceSettingsData.kt | 7 +-- .../geeksville/mesh/service/MockInterface.kt | 41 ++++++++---------- .../mesh/ui/AdvancedSettingsFragment.kt | 2 +- .../com/geeksville/mesh/ui/ChannelFragment.kt | 30 +++++++------ app/src/main/proto | 2 +- 11 files changed, 113 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index b79a26156..f87fb87cb 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -41,6 +41,7 @@ import com.geeksville.android.ServiceClient import com.geeksville.concurrent.handledLaunch import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.model.Channel +import com.geeksville.mesh.model.ChannelSet import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.* import com.geeksville.mesh.ui.* @@ -620,7 +621,7 @@ class MainActivity : AppCompatActivity(), Logging, debug("Getting latest radioconfig from service") try { model.radioConfig.value = - MeshProtos.RadioConfig.parseFrom(service.radioConfig) + RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig) val info = service.myNodeInfo model.myNodeInfo.value = info @@ -654,19 +655,20 @@ class MainActivity : AppCompatActivity(), Logging, // If the is opening a channel URL, handle it now requestedChannelUrl?.let { url -> try { - val channel = Channel(url) + val channels = ChannelSet(url) + val primary = channels.primaryChannel requestedChannelUrl = null MaterialAlertDialogBuilder(this) .setTitle(R.string.new_channel_rcvd) - .setMessage(getString(R.string.do_you_want_switch).format(channel.name)) + .setMessage(getString(R.string.do_you_want_switch).format(primary.name)) .setNeutralButton(R.string.cancel) { _, _ -> // Do nothing } .setPositiveButton(R.string.accept) { _, _ -> debug("Setting channel from URL") try { - model.setChannel(channel.settings) + model.setChannels(channels) } catch (ex: RemoteException) { errormsg("Couldn't change channel ${ex.message}") Toast.makeText( 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 d19faed8e..5686bb209 100644 --- a/app/src/main/java/com/geeksville/mesh/model/Channel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/Channel.kt @@ -3,6 +3,7 @@ package com.geeksville.mesh.model import android.graphics.Bitmap import android.net.Uri import android.util.Base64 +import com.geeksville.mesh.ChannelProtos import com.geeksville.mesh.MeshProtos import com.google.protobuf.ByteString import com.google.zxing.BarcodeFormat @@ -15,7 +16,7 @@ fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos]. data class Channel( - val settings: MeshProtos.ChannelSettings = MeshProtos.ChannelSettings.getDefaultInstance() + val settings: ChannelProtos.ChannelSettings = ChannelProtos.ChannelSettings.getDefaultInstance() ) { companion object { // Note: this string _SHOULD NOT BE LOCALIZED_ because it directly hashes to values used on the device for the default channel name. @@ -30,8 +31,8 @@ data class Channel( // Placeholder when emulating val emulated = Channel( - MeshProtos.ChannelSettings.newBuilder().setName(defaultChannelName) - .setModemConfig(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build() + ChannelProtos.ChannelSettings.newBuilder().setName(defaultChannelName) + .setModemConfig(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128).build() ) } @@ -42,16 +43,16 @@ data class Channel( if (settings.bandwidth != 0) "Unset" else when (settings.modemConfig) { - MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128 -> "Medium" - MeshProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128 -> "ShortFast" - MeshProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512 -> "LongAlt" - MeshProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096 -> "LongSlow" + ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128 -> "Medium" + ChannelProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128 -> "ShortFast" + ChannelProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512 -> "LongAlt" + ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096 -> "LongSlow" else -> "Invalid" } } else settings.name - val modemConfig: MeshProtos.ChannelSettings.ModemConfig get() = settings.modemConfig + val modemConfig: ChannelProtos.ChannelSettings.ModemConfig get() = settings.modemConfig val psk get() = if (settings.psk.size() != 1) 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 827624cd5..62409f4ff 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelOption.kt @@ -1,22 +1,23 @@ -package com.geeksville.mesh.model - -import com.geeksville.mesh.MeshProtos -import com.geeksville.mesh.R - -enum class ChannelOption(val modemConfig: MeshProtos.ChannelSettings.ModemConfig, val configRes: Int, val minBroadcastPeriodSecs: Int) { - SHORT(MeshProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128, R.string.modem_config_short, 3), - MEDIUM(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128, R.string.modem_config_medium, 12), - LONG(MeshProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512, R.string.modem_config_long, 240), - VERY_LONG(MeshProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096, R.string.modem_config_very_long, 375); - - companion object { - fun fromConfig(modemConfig: MeshProtos.ChannelSettings.ModemConfig?): ChannelOption? { - for (option in values()) { - if (option.modemConfig == modemConfig) - return option - } - return null - } - val defaultMinBroadcastPeriod = VERY_LONG.minBroadcastPeriodSecs - } +package com.geeksville.mesh.model + +import com.geeksville.mesh.ChannelProtos +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.R + +enum class ChannelOption(val modemConfig: ChannelProtos.ChannelSettings.ModemConfig, val configRes: Int, val minBroadcastPeriodSecs: Int) { + SHORT(ChannelProtos.ChannelSettings.ModemConfig.Bw500Cr45Sf128, R.string.modem_config_short, 3), + MEDIUM(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr45Sf128, R.string.modem_config_medium, 12), + LONG(ChannelProtos.ChannelSettings.ModemConfig.Bw31_25Cr48Sf512, R.string.modem_config_long, 240), + VERY_LONG(ChannelProtos.ChannelSettings.ModemConfig.Bw125Cr48Sf4096, R.string.modem_config_very_long, 375); + + companion object { + fun fromConfig(modemConfig: ChannelProtos.ChannelSettings.ModemConfig?): ChannelOption? { + for (option in values()) { + if (option.modemConfig == modemConfig) + return option + } + return null + } + val defaultMinBroadcastPeriod = VERY_LONG.minBroadcastPeriodSecs + } } \ No newline at end of file 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 1206af6e7..d6b0bcf5e 100644 --- a/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt +++ b/app/src/main/java/com/geeksville/mesh/model/ChannelSet.kt @@ -11,10 +11,6 @@ import com.google.zxing.MultiFormatWriter import com.journeyapps.barcodescanner.BarcodeEncoder import java.net.MalformedURLException -/** 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() } - - data class ChannelSet( val protobuf: AppOnlyProtos.ChannelSet = AppOnlyProtos.ChannelSet.getDefaultInstance() ) { @@ -48,6 +44,13 @@ data class ChannelSet( /// Can this channel be changed right now? var editable = false + /** + * Return the primary channel info + */ + val primaryChannel: Channel get() { + return Channel(protobuf.getSettings(0)) + } + /// Return an URL that represents the current channel values /// @param upperCasePrefix - portions of the URL can be upper case to make for more efficient QR codes fun getChannelUrl(upperCasePrefix: Boolean = false): Uri { diff --git a/app/src/main/java/com/geeksville/mesh/model/UIState.kt b/app/src/main/java/com/geeksville/mesh/model/UIState.kt index bc4f3a0b5..91d78a4bd 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -12,10 +12,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.geeksville.android.Logging -import com.geeksville.mesh.AppOnlyProtos -import com.geeksville.mesh.IMeshService -import com.geeksville.mesh.MeshProtos -import com.geeksville.mesh.MyNodeInfo +import com.geeksville.mesh.* import com.geeksville.mesh.database.MeshtasticDatabase import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.entity.Packet @@ -90,16 +87,16 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging } /// various radio settings (including the channel) - val radioConfig = object : MutableLiveData(null) { + val radioConfig = object : MutableLiveData(null) { } - val channels = object : MutableLiveData(null) { + val channels = object : MutableLiveData(null) { } var positionBroadcastSecs: Int? get() { radioConfig.value?.preferences?.let { - if (it.locationShare == MeshProtos.LocationSharing.LocDisabled) return 0 + if (it.locationShare == RadioConfigProtos.LocationSharing.LocDisabled) return 0 if (it.positionBroadcastSecs > 0) return it.positionBroadcastSecs // These default values are borrowed from the device code. if (it.isRouter) return 60 * 60 @@ -116,11 +113,11 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging builder.preferencesBuilder.gpsUpdateInterval = value builder.preferencesBuilder.sendOwnerInterval = max(1, 3600 / value).toInt() builder.preferencesBuilder.locationShare = - MeshProtos.LocationSharing.LocEnabled + RadioConfigProtos.LocationSharing.LocEnabled } else { builder.preferencesBuilder.positionBroadcastSecs = Int.MAX_VALUE builder.preferencesBuilder.locationShare = - MeshProtos.LocationSharing.LocDisabled + RadioConfigProtos.LocationSharing.LocDisabled } setRadioConfig(builder.build()) } @@ -153,13 +150,11 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging /** * Return the primary channel info */ - val primaryChannel: ChannelSet? get() { - return channels.value?.let { it -> - Channel(it.getSettings(0)) - } - } - /// Set the radio config (also updates our saved copy in preferences) - private fun setRadioConfig(c: MeshProtos.RadioConfig) { + val primaryChannel: Channel? get() = channels.value?.primaryChannel + + /// + // Set the radio config (also updates our saved copy in preferences) + private fun setRadioConfig(c: RadioConfigProtos.RadioConfig) { debug("Setting new radio config!") meshService?.radioConfig = c.toByteArray() radioConfig.value = @@ -167,23 +162,14 @@ class UIViewModel(private val app: Application) : AndroidViewModel(app), Logging } /// Set the radio config (also updates our saved copy in preferences) - private fun setChannels(c: AppOnlyProtos.ChannelSet) { + fun setChannels(c: ChannelSet) { debug("Setting new channels!") - meshService?.channels = c.toByteArray() + meshService?.channels = c.protobuf.toByteArray() channels.value = c // Must be done after calling the service, so we will will properly throw if the service failed (and therefore not cache invalid new settings) getPreferences(context).edit(commit = true) { - this.putString("channel-url", primaryChannel!!.getChannelUrl().toString()) - } - } - - /** Update just the channel settings portion of our config (both in the device and in saved preferences) */ - fun setChannel(c: MeshProtos.ChannelSettings) { - // When running on the emulator, radio config might not really be available, in that case, just ignore attempts to change the config - radioConfig.value?.toBuilder()?.let { config -> - config.channelSettings = c - setRadioConfig(config.build()) + this.putString("channel-url", c.getChannelUrl().toString()) } } 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 861a2f295..8aef6d463 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -111,7 +111,7 @@ class MeshService : Service(), Logging { private var fusedLocationClient: FusedLocationProviderClient? = null // If we've ever read a valid region code from our device it will be here - var curRegionValue = MeshProtos.RegionCode.Unset_VALUE + var curRegionValue = RadioConfigProtos.RegionCode.Unset_VALUE val radio = ServiceClient { IRadioInterfaceService.Stub.asInterface(it).apply { @@ -417,7 +417,7 @@ class MeshService : Service(), Logging { var myNodeInfo: MyNodeInfo? = null - private var radioConfig: MeshProtos.RadioConfig? = null + private var radioConfig: RadioConfigProtos.RadioConfig? = null /// True after we've done our initial node db init @Volatile @@ -1029,7 +1029,7 @@ class MeshService : Service(), Logging { MeshProtos.FromRadio.NODE_INFO_FIELD_NUMBER -> handleNodeInfo(proto.nodeInfo) - MeshProtos.FromRadio.RADIO_FIELD_NUMBER -> handleRadioConfig(proto.radio) + // MeshProtos.FromRadio.RADIO_FIELD_NUMBER -> handleRadioConfig(proto.radio) else -> errormsg("Unexpected FromRadio variant") } @@ -1053,7 +1053,7 @@ class MeshService : Service(), Logging { private var configNonce = 1 - private fun handleRadioConfig(radio: MeshProtos.RadioConfig) { + private fun handleRadioConfig(radio: RadioConfigProtos.RadioConfig) { val packetToSave = Packet( UUID.randomUUID().toString(), "RadioConfig", @@ -1174,20 +1174,20 @@ class MeshService : Service(), Logging { ignoreException { // Try to pull our region code from the new preferences field // FIXME - do not check net - figuring out why board is rebooting - val curConfigRegion = radioConfig?.preferences?.region ?: MeshProtos.RegionCode.Unset - if (curConfigRegion != MeshProtos.RegionCode.Unset) { + val curConfigRegion = radioConfig?.preferences?.region ?: RadioConfigProtos.RegionCode.Unset + if (curConfigRegion != RadioConfigProtos.RegionCode.Unset) { info("Using device region $curConfigRegion (code ${curConfigRegion.number})") curRegionValue = curConfigRegion.number } - if (curRegionValue == MeshProtos.RegionCode.Unset_VALUE) { + if (curRegionValue == RadioConfigProtos.RegionCode.Unset_VALUE) { // look for a legacy region val legacyRegex = Regex(".+-(.+)") myNodeInfo?.region?.let { legacyRegion -> val matches = legacyRegex.find(legacyRegion) if (matches != null) { val (region) = matches.destructured - val newRegion = MeshProtos.RegionCode.valueOf(region) + val newRegion = RadioConfigProtos.RegionCode.valueOf(region) info("Upgrading legacy region $newRegion (code ${newRegion.number})") curRegionValue = newRegion.number } @@ -1195,7 +1195,7 @@ class MeshService : Service(), Logging { } // If nothing was set in our (new style radio preferences, but we now have a valid setting - slam it in) - if (curConfigRegion == MeshProtos.RegionCode.Unset && curRegionValue != MeshProtos.RegionCode.Unset_VALUE) { + if (curConfigRegion == RadioConfigProtos.RegionCode.Unset && curRegionValue != RadioConfigProtos.RegionCode.Unset_VALUE) { info("Telling device to upgrade region") // Tell the device to set the new region field (old devices will simply ignore this) @@ -1321,7 +1321,7 @@ class MeshService : Service(), Logging { /** Send our current radio config to the device */ - private fun sendRadioConfig(c: MeshProtos.RadioConfig) { + private fun sendRadioConfig(c: RadioConfigProtos.RadioConfig) { // Update our device val payload = AdminProtos.AdminMessage.newBuilder().also { it.setRadio = c @@ -1347,7 +1347,7 @@ class MeshService : Service(), Logging { /** Set our radio config */ private fun setRadioConfig(payload: ByteArray) { - val parsed = MeshProtos.RadioConfig.parseFrom(payload) + val parsed = RadioConfigProtos.RadioConfig.parseFrom(payload) sendRadioConfig(parsed) } @@ -1608,6 +1608,10 @@ class MeshService : Service(), Logging { TODO("Not yet implemented") } + override fun setChannels(payload: ByteArray?) { + TODO("Not yet implemented") + } + override fun getNodes(): MutableList = toRemoteExceptions { val r = nodeDBbyID.values.toMutableList() info("in getOnline, count=${r.size}") diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt index 2b6e455a4..c1477b718 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceSettingsData.kt @@ -1,9 +1,6 @@ package com.geeksville.mesh.service -import com.geeksville.mesh.DataPacket -import com.geeksville.mesh.MeshProtos -import com.geeksville.mesh.MyNodeInfo -import com.geeksville.mesh.NodeInfo +import com.geeksville.mesh.* import kotlinx.serialization.Serializable /// Our saved preferences as stored on disk @@ -12,7 +9,7 @@ data class MeshServiceSettingsData( val nodeDB: Array, val myInfo: MyNodeInfo, val messages: Array, - val regionCode: Int = MeshProtos.RegionCode.Unset_VALUE + val regionCode: Int = RadioConfigProtos.RegionCode.Unset_VALUE ) { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt index 5eaf012c6..06ed39603 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt @@ -46,11 +46,9 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi to = 0xffffffff.toInt() // ugly way of saying broadcast rxTime = (System.currentTimeMillis() / 1000).toInt() rxSnr = 1.5f - decoded = MeshProtos.SubPacket.newBuilder().apply { - data = MeshProtos.Data.newBuilder().apply { - portnum = Portnums.PortNum.TEXT_MESSAGE_APP - payload = ByteString.copyFromUtf8("This simulated node sends Hi!") - }.build() + decoded = MeshProtos.Data.newBuilder().apply { + portnum = Portnums.PortNum.TEXT_MESSAGE_APP + payload = ByteString.copyFromUtf8("This simulated node sends Hi!") }.build() }.build() } @@ -64,17 +62,20 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi to = toIn rxTime = (System.currentTimeMillis() / 1000).toInt() rxSnr = 1.5f - decoded = MeshProtos.SubPacket.newBuilder().apply { - data = MeshProtos.Data.newBuilder().apply { - successId = msgId - }.build() + decoded = MeshProtos.Data.newBuilder().apply { + portnum = Portnums.PortNum.ROUTING_APP + payload = MeshProtos.Routing.newBuilder().apply { + }.build().toByteString() + requestId = msgId }.build() }.build() } /// Send a fake ack packet back if the sender asked for want_ack private fun sendFakeAck(pr: MeshProtos.ToRadio) { - service.handleFromRadio(makeAck(pr.packet.to, pr.packet.from, pr.packet.id).build().toByteArray()) + service.handleFromRadio( + makeAck(pr.packet.to, pr.packet.from, pr.packet.id).build().toByteArray() + ) } private fun sendConfigResponse(configId: Int) { @@ -101,37 +102,33 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi } // Simulated network data to feed to our app - val MY_NODE = 0x42424242 + val MY_NODE = 0x42424242 val packets = arrayOf( // MyNodeInfo MeshProtos.FromRadio.newBuilder().apply { myInfo = MeshProtos.MyNodeInfo.newBuilder().apply { myNodeNum = MY_NODE region = "TW" - numChannels = 7 hwModel = "Sim" - packetIdBits = 32 - nodeNumBits = 32 - currentPacketId = 1 messageTimeoutMsec = 5 * 60 * 1000 firmwareVersion = service.getString(R.string.cur_firmware_version) }.build() }, // RadioConfig - MeshProtos.FromRadio.newBuilder().apply { - radio = MeshProtos.RadioConfig.newBuilder().apply { + /* MeshProtos.FromRadio.newBuilder().apply { + radio = RadioConfigProtos.RadioConfig.newBuilder().apply { - preferences = MeshProtos.RadioConfig.UserPreferences.newBuilder().apply { - region = MeshProtos.RegionCode.TW + preferences = RadioConfigProtos.RadioConfig.UserPreferences.newBuilder().apply { + region = RadioConfigProtos.RegionCode.TW // FIXME set critical times? }.build() - channel = MeshProtos.ChannelSettings.newBuilder().apply { + /* channel = ChannelProtos.ChannelSettings.newBuilder().apply { // we just have an empty listing so that the default channel works - }.build() + }.build() */ }.build() - }, + }, */ // Fake NodeDB makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas diff --git a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt index 75175a14e..6b22b7e6a 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/AdvancedSettingsFragment.kt @@ -51,7 +51,7 @@ class AdvancedSettingsFragment : ScreenFragment("Advanced Settings"), Logging { val textEdit = binding.positionBroadcastPeriodEditText val n = textEdit.text.toString().toIntOrNull() val minBroadcastPeriodSecs = - ChannelOption.fromConfig(model.radioConfig.value?.channelSettings?.modemConfig)?.minBroadcastPeriodSecs + ChannelOption.fromConfig(model.primaryChannel?.modemConfig)?.minBroadcastPeriodSecs ?: ChannelOption.defaultMinBroadcastPeriod if (n != null && n < MAX_INT_DEVICE && (n == 0 || n >= minBroadcastPeriodSecs)) { 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 ecc3cfe76..d7e1743ea 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -16,6 +16,7 @@ import com.geeksville.analytics.DataPair import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.hideKeyboard +import com.geeksville.mesh.ChannelProtos import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.R import com.geeksville.mesh.databinding.ChannelFragmentBinding @@ -76,10 +77,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { /// Pull the latest data from the model (discarding any user edits) private fun setGUIfromModel() { val radioConfig = model.radioConfig.value - val channel = UIViewModel.getChannel(radioConfig) + val channels = model.channels.value binding.editableCheckbox.isChecked = false // start locked - if (channel != null) { + if (channels != null) { + val channel = channels.primaryChannel + binding.qrView.visibility = View.VISIBLE binding.channelNameEdit.visibility = View.VISIBLE binding.channelNameEdit.setText(channel.humanName) @@ -89,9 +92,9 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { val connected = model.isConnected.value == MeshService.ConnectionState.CONNECTED binding.editableCheckbox.isEnabled = connected - binding.qrView.setImageBitmap(channel.getChannelQR()) + binding.qrView.setImageBitmap(channels.getChannelQR()) - val modemConfig = radioConfig?.channelSettings?.modemConfig + val modemConfig = channel.modemConfig val channelOption = ChannelOption.fromConfig(modemConfig) binding.filledExposedDropdown.setText( getString( @@ -118,7 +121,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } private fun shareChannel() { - UIViewModel.getChannel(model.radioConfig.value)?.let { channel -> + model.channels.value?.let { channels -> GeeksvilleApplication.analytics.track( "share", @@ -127,7 +130,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, channel.getChannelUrl().toString()) + putExtra(Intent.EXTRA_TEXT, channels.getChannelUrl().toString()) putExtra( Intent.EXTRA_TITLE, getString(R.string.url_for_join) @@ -152,8 +155,8 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { val checked = binding.editableCheckbox.isChecked if (checked) { // User just unlocked for editing - remove the # goo around the channel name - UIViewModel.getChannel(model.radioConfig.value)?.let { channel -> - binding.channelNameEdit.setText(channel.name) + model.channels.value?.let { channels -> + binding.channelNameEdit.setText(channels.primaryChannel.name) } } else { // User just locked it, we should warn and then apply changes to radio @@ -165,8 +168,9 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } .setPositiveButton(getString(R.string.accept)) { _, _ -> // Generate a new channel with only the changes the user can change in the GUI - UIViewModel.getChannel(model.radioConfig.value)?.let { old -> - val newSettings = old.settings.toBuilder() + model.channels.value?.let { old -> + val oldPrimary = old.primaryChannel + val newSettings = oldPrimary.settings.toBuilder() newSettings.name = binding.channelNameEdit.text.toString().trim() // Generate a new AES256 key (for any channel not named Default) @@ -191,7 +195,7 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { binding.filledExposedDropdown.editableText.toString() val modemConfig = getModemConfig(selectedChannelOptionString) - if (modemConfig != MeshProtos.ChannelSettings.ModemConfig.UNRECOGNIZED) + if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED) newSettings.modemConfig = modemConfig // Try to change the radio, if it fails, tell the user why and throw away their redits try { @@ -232,12 +236,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { }) } - private fun getModemConfig(selectedChannelOptionString: String): MeshProtos.ChannelSettings.ModemConfig { + private fun getModemConfig(selectedChannelOptionString: String): ChannelProtos.ChannelSettings.ModemConfig { for (item in ChannelOption.values()) { if (getString(item.configRes) == selectedChannelOptionString) return item.modemConfig } - return MeshProtos.ChannelSettings.ModemConfig.UNRECOGNIZED + return ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED } } diff --git a/app/src/main/proto b/app/src/main/proto index 564b48869..39c41e0ef 160000 --- a/app/src/main/proto +++ b/app/src/main/proto @@ -1 +1 @@ -Subproject commit 564b488695da6bee7bb9c4553872268595a0f77d +Subproject commit 39c41e0ef130e7239eed916f7609aad1aa7f6db8 From 850358e103c6c00bd6b56876f0361b4f197893af Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sat, 27 Feb 2021 14:31:52 +0800 Subject: [PATCH 06/21] channel setting is healthier --- .../geeksville/mesh/service/MeshService.kt | 36 +++++++++++++++++-- .../com/geeksville/mesh/ui/ChannelFragment.kt | 7 +++- 2 files changed, 39 insertions(+), 4 deletions(-) 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 8aef6d463..bd97cf976 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -419,6 +419,8 @@ class MeshService : Service(), Logging { private var radioConfig: RadioConfigProtos.RadioConfig? = null + private var channels = listOf() + /// True after we've done our initial node db init @Volatile private var haveNodeDB = false @@ -510,6 +512,33 @@ class MeshService : Service(), Logging { /// My node ID string private val myNodeID get() = toNodeID(myNodeNum) + /// Convert the channels array into a ChannelSet + private var channelSet: AppOnlyProtos.ChannelSet + get() { + val cs = channels.filter { + it.role != ChannelProtos.Channel.Role.DISABLED + }.map { + it.settings + } + + return AppOnlyProtos.ChannelSet.newBuilder().apply { + addAllSettings(cs) + }.build() + } + set(value) { + val asChannels = value.settingsList.mapIndexed { i, c -> + ChannelProtos.Channel.newBuilder().apply { + role = if(i == 0) ChannelProtos.Channel.Role.PRIMARY else ChannelProtos.Channel.Role.SECONDARY + index = i + settings = c + }.build() + } + + // FIXME, send channels to device! + + channels = asChannels + } + /// Generate a new mesh packet builder with our node as the sender, and the specified node num private fun newMeshPacketTo(idNum: Int) = MeshPacket.newBuilder().apply { if (myNodeInfo == null) @@ -1604,12 +1633,13 @@ class MeshService : Service(), Logging { this@MeshService.setRadioConfig(payload) } - override fun getChannels(): ByteArray { - TODO("Not yet implemented") + override fun getChannels(): ByteArray = toRemoteExceptions { + channelSet.toByteArray() } override fun setChannels(payload: ByteArray?) { - TODO("Not yet implemented") + val parsed = AppOnlyProtos.ChannelSet.parseFrom(payload) + channelSet = parsed } override fun getNodes(): MutableList = toRemoteExceptions { 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 d7e1743ea..ad7dbcc31 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -16,12 +16,14 @@ import com.geeksville.analytics.DataPair import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.android.hideKeyboard +import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.ChannelProtos import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.R import com.geeksville.mesh.databinding.ChannelFragmentBinding import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.ChannelOption +import com.geeksville.mesh.model.ChannelSet import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.MeshService import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -197,9 +199,12 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { if (modemConfig != ChannelProtos.ChannelSettings.ModemConfig.UNRECOGNIZED) newSettings.modemConfig = modemConfig + + val newChannel = newSettings.build() + val newSet = ChannelSet(AppOnlyProtos.ChannelSet.newBuilder().addSettings(newChannel).build()) // Try to change the radio, if it fails, tell the user why and throw away their redits try { - model.setChannel(newSettings.build()) + model.setChannels(newSet) // Since we are writing to radioconfig, that will trigger the rest of the GUI update (QR code etc) } catch (ex: RemoteException) { errormsg("ignoring channel problem", ex) From 2cf39096a756dda5d801e101e2db98cdc90f5fd6 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 2 Mar 2021 10:36:37 +0800 Subject: [PATCH 07/21] lib update --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 08e0929fb..e40be1072 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,7 +130,7 @@ dependencies { // optional - Test helpers testImplementation "androidx.room:room-testing:$room_version" - testImplementation 'junit:junit:4.13.1' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' From e27a3d937d76d358d9810c8254956e55c6bf5230 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 2 Mar 2021 15:12:57 +0800 Subject: [PATCH 08/21] show alert for old firmware --- .../java/com/geeksville/mesh/MainActivity.kt | 62 ++++++++++++------- .../mesh/{service => model}/DeviceVersion.kt | 2 +- .../geeksville/mesh/service/MeshService.kt | 1 + .../mesh/service/SoftwareUpdateService.kt | 1 + .../geeksville/mesh/ui/MessagesFragment.kt | 21 ++++--- app/src/main/res/values/strings.xml | 2 + 6 files changed, 57 insertions(+), 32 deletions(-) rename app/src/main/java/com/geeksville/mesh/{service => model}/DeviceVersion.kt (96%) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 2edcbb917..c3abeabe5 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -46,6 +46,7 @@ import com.geeksville.concurrent.handledLaunch import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.ChannelSet +import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.service.* import com.geeksville.mesh.ui.* @@ -613,6 +614,29 @@ class MainActivity : AppCompatActivity(), Logging, } } + /** Show an alert that may contain HTML */ + private fun showAlert(titleText: Int, messageText: Int) { + // make links clickable per https://stackoverflow.com/a/62642807 + // val messageStr = getText(messageText) + + val builder = MaterialAlertDialogBuilder(this) + .setTitle(titleText) + .setMessage(messageText) + .setPositiveButton("Okay") { _, _ -> + info("User acknowledged") + } + + val dialog = builder.show() + + // Make the textview clickable. Must be called after show() + val view = (dialog.findViewById(android.R.id.message) as TextView?)!! + // Linkify.addLinks(view, Linkify.ALL) // not needed with this method + view.movementMethod = LinkMovementMethod.getInstance() + + showSettingsPage() // Default to the settings page in this case + } + + /// Called when we gain/lose a connection to our mesh radio private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) { debug("connchange ${model.isConnected.value} -> $connected") @@ -629,33 +653,25 @@ class MainActivity : AppCompatActivity(), Logging, model.myNodeInfo.value = info val isOld = info.minAppVersion > BuildConfig.VERSION_CODE - if (isOld) { - // make links clickable per https://stackoverflow.com/a/62642807 - val messageStr = getText(R.string.must_update) + if (isOld) + showAlert(R.string.app_too_old, R.string.must_update) + else { - val builder = MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.app_too_old)) - .setMessage(messageStr) - .setPositiveButton("Okay") { _, _ -> - info("User acknowledged app is old") - } + val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0") + val minVer = DeviceVersion("1.2.0") + if(curVer < minVer) + showAlert(R.string.app_too_old, R.string.firmware_old) + else { + // If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here - val dialog = builder.show() + model.radioConfig.value = + RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig) - // Make the textview clickable. Must be called after show() - val view = (dialog.findViewById(android.R.id.message) as TextView?)!! - // Linkify.addLinks(view, Linkify.ALL) // not needed with this method - view.movementMethod = LinkMovementMethod.getInstance() - } else { - // If our app is too old, we probably don't understand the new radioconfig messages + updateNodesFromDevice() - model.radioConfig.value = - RadioConfigProtos.RadioConfig.parseFrom(service.radioConfig) - - updateNodesFromDevice() - - // we have a connection to our device now, do the channel change - perhapsChangeChannel() + // we have a connection to our device now, do the channel change + perhapsChangeChannel() + } } } catch (ex: RemoteException) { warn("Abandoning connect $ex, because we probably just lost device connection") diff --git a/app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt b/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt similarity index 96% rename from app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt rename to app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt index 13ef461e0..961524b66 100644 --- a/app/src/main/java/com/geeksville/mesh/service/DeviceVersion.kt +++ b/app/src/main/java/com/geeksville/mesh/model/DeviceVersion.kt @@ -1,4 +1,4 @@ -package com.geeksville.mesh.service +package com.geeksville.mesh.model import com.geeksville.android.Logging 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 9b40d025c..15b21f8e6 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -23,6 +23,7 @@ import com.geeksville.mesh.MeshProtos.ToRadio import com.geeksville.mesh.database.MeshtasticDatabase import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.entity.Packet +import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.service.SoftwareUpdateService.Companion.ProgressNotStarted import com.geeksville.util.* import com.google.android.gms.common.api.ApiException diff --git a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt index d7b3cc2dd..b9050105e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/SoftwareUpdateService.kt @@ -9,6 +9,7 @@ import androidx.core.app.JobIntentService import com.geeksville.android.Logging import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R +import com.geeksville.mesh.model.DeviceVersion import com.geeksville.util.exceptionReporter import java.util.* import java.util.zip.CRC32 diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt index 92c1d0d73..668f789d3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -237,17 +237,22 @@ class MessagesFragment : ScreenFragment("Messages"), Logging { }) // If connection state _OR_ myID changes we have to fix our ability to edit outgoing messages - model.isConnected.observe(viewLifecycleOwner, Observer { connected -> - // If we don't know our node ID and we are offline don't let user try to send + fun updateTextEnabled() { binding.textInputLayout.isEnabled = - connected != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null - }) + model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && model.nodeDB.myId.value != null && model.radioConfig.value != null + } - model.nodeDB.myId.observe(viewLifecycleOwner, Observer { myId -> + model.isConnected.observe(viewLifecycleOwner, Observer { _ -> // If we don't know our node ID and we are offline don't let user try to send - binding.textInputLayout.isEnabled = - model.isConnected.value != MeshService.ConnectionState.DISCONNECTED && myId != null - }) + updateTextEnabled() }) + + model.nodeDB.myId.observe(viewLifecycleOwner, Observer { _ -> + // If we don't know our node ID and we are offline don't let user try to send + updateTextEnabled() }) + + model.radioConfig.observe(viewLifecycleOwner, Observer { _ -> + // If we don't know our node ID and we are offline don't let user try to send + updateTextEnabled() }) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33c6f745f..6a45820ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,4 +89,6 @@ Minimum broadcast period for this channel is %d Protocol stress test Advanced settings + Firmware update required + The radio firmware is too old to talk to this application, please go to the settings pane and choose "Update Firmware". For more information on this see our wiki. From ba86d3e88fea06ce56782fb7b1801b1e29217e19 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 2 Mar 2021 16:27:34 +0800 Subject: [PATCH 09/21] localization --- app/src/main/java/com/geeksville/mesh/MainActivity.kt | 4 ++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index c3abeabe5..9171095d6 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -622,7 +622,7 @@ class MainActivity : AppCompatActivity(), Logging, val builder = MaterialAlertDialogBuilder(this) .setTitle(titleText) .setMessage(messageText) - .setPositiveButton("Okay") { _, _ -> + .setPositiveButton(R.string.okay) { _, _ -> info("User acknowledged") } @@ -660,7 +660,7 @@ class MainActivity : AppCompatActivity(), Logging, val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0") val minVer = DeviceVersion("1.2.0") if(curVer < minVer) - showAlert(R.string.app_too_old, R.string.firmware_old) + showAlert(R.string.firmware_too_old, R.string.firmware_old) else { // If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a45820ed..0f564d172 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,4 +91,5 @@ Advanced settings Firmware update required The radio firmware is too old to talk to this application, please go to the settings pane and choose "Update Firmware". For more information on this see our wiki. + Okay From a800bd1319c14d6338d9bd459dbfbadfc3042e5c Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 2 Mar 2021 16:27:43 +0800 Subject: [PATCH 10/21] cleanup admin packet generation --- .../geeksville/mesh/service/MeshService.kt | 102 ++++++++---------- 1 file changed, 47 insertions(+), 55 deletions(-) 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 15b21f8e6..7d8cecef8 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -564,23 +564,42 @@ class MeshService : Service(), Logging { /** * Helper to make it easy to build a subpacket in the proper protobufs - * - * If destId is null we assume a broadcast message */ - private fun buildMeshPacket( - destId: String, + private fun MeshProtos.MeshPacket.Builder.buildMeshPacket( wantAck: Boolean = false, id: Int = 0, hopLimit: Int = 0, + priority: MeshPacket.Priority = MeshPacket.Priority.UNSET, initFn: MeshProtos.Data.Builder.() -> Unit - ): MeshPacket = newMeshPacketTo(destId).apply { + ): MeshPacket { this.wantAck = wantAck this.id = id this.hopLimit = hopLimit + this.priority = priority decoded = MeshProtos.Data.newBuilder().also { initFn(it) }.build() - }.build() + + return build() + } + + + /** + * Helper to make it easy to build a subpacket in the proper protobufs + */ + private fun MeshProtos.MeshPacket.Builder.buildAdminPacket( + initFn: AdminProtos.AdminMessage.Builder.() -> Unit + ): MeshPacket = buildMeshPacket( + wantAck = true, + priority = MeshPacket.Priority.RELIABLE + ) + { + portnumValue = Portnums.PortNum.ADMIN_APP_VALUE + payload = AdminProtos.AdminMessage.newBuilder().also { + initFn(it) + }.build().toByteString() + } + // FIXME - possible kotlin bug in 1.3.72 - it seems that if we start with the (globally shared) emptyList, // then adding items are affecting that shared list rather than a copy. This was causing aliasing of @@ -627,7 +646,11 @@ class MeshService : Service(), Logging { } private fun toMeshPacket(p: DataPacket): MeshPacket { - return buildMeshPacket(p.to!!, id = p.id, wantAck = true, hopLimit = p.hopLimit) { + return newMeshPacketTo(p.to!!).buildMeshPacket( + id = p.id, + wantAck = true, + hopLimit = p.hopLimit + ) { portnumValue = p.dataType payload = ByteString.copyFrom(p.bytes) } @@ -820,8 +843,6 @@ class MeshService : Service(), Logging { // debug("Recieved: $packet") if (packet.hasDecoded()) { - val p = packet.decoded - val packetToSave = Packet( UUID.randomUUID().toString(), "packet", @@ -1322,26 +1343,20 @@ class MeshService : Service(), Logging { it.time = currentSecond() // Include our current timestamp }.build() - // encapsulate our payload in the proper protobufs and fire it off - val packet = newMeshPacketTo(destNum) - - packet.decoded = MeshProtos.Data.newBuilder().also { - - // Use the new position as data format - it.portnumValue = Portnums.PortNum.POSITION_APP_VALUE - it.payload = position.toByteString() - - it.wantResponse = wantResponse - }.build() - - // Assume our position packets are not critical - packet.priority = MeshProtos.MeshPacket.Priority.BACKGROUND - // Also update our own map for our nodenum, by handling the packet just like packets from other users handleReceivedPosition(myNodeInfo!!.myNodeNum, position) + val fullPacket = + newMeshPacketTo(destNum).buildMeshPacket(priority = MeshProtos.MeshPacket.Priority.BACKGROUND) { + // Use the new position as data format + portnumValue = Portnums.PortNum.POSITION_APP_VALUE + payload = position.toByteString() + + this.wantResponse = wantResponse + } + // send the packet into the mesh - sendToRadio(packet.build()) + sendToRadio(fullPacket) } private fun sendPositionScoped( @@ -1361,23 +1376,10 @@ class MeshService : Service(), Logging { /** Send our current radio config to the device */ private fun sendRadioConfig(c: RadioConfigProtos.RadioConfig) { - // Update our device - val payload = AdminProtos.AdminMessage.newBuilder().also { - it.setRadio = c - }.build() - - // encapsulate our payload in the proper protobufs and fire it off - val packet = newMeshPacketTo(myNodeNum) - - packet.decoded = MeshProtos.Data.newBuilder().also { - - // Use the new position as data format - it.portnumValue = Portnums.PortNum.ADMIN_APP_VALUE - it.payload = payload.toByteString() - }.build() - // send the packet into the mesh - sendToRadio(packet.build()) + sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { + setRadio = c + }) // Update our cached copy this@MeshService.radioConfig = c @@ -1416,23 +1418,13 @@ class MeshService : Service(), Logging { handleReceivedUser(myNode.myNodeNum, user) - // set my owner info - val payload = AdminProtos.AdminMessage.newBuilder().also { - it.setOwner = user - }.build() - // encapsulate our payload in the proper protobufs and fire it off - val packet = newMeshPacketTo(myNodeNum) - - packet.decoded = MeshProtos.Data.newBuilder().also { - - // Use the new position as data format - it.portnumValue = Portnums.PortNum.ADMIN_APP_VALUE - it.payload = payload.toByteString() - }.build() + val packet = newMeshPacketTo(myNodeNum).buildAdminPacket { + setOwner = user + } // send the packet into the mesh - sendToRadio(packet.build()) + sendToRadio(packet) } } else throw Exception("Can't set user without a node info") // this shouldn't happen From 02198864c51fc05c17a40796e533aba21645932a Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Tue, 2 Mar 2021 22:12:42 +0800 Subject: [PATCH 11/21] WIP on getting new style settings/channels --- app/build.gradle | 4 +- .../java/com/geeksville/mesh/MyNodeInfo.kt | 5 +- .../geeksville/mesh/service/MeshService.kt | 129 +++++++++++++----- 3 files changed, 100 insertions(+), 38 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index ffb8e7599..eac4907dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { applicationId "com.geeksville.mesh" minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 29 - versionCode 20150 // format is Mmmss (where M is 1+the numeric major number - versionName "1.1.50" + versionCode 20200 // format is Mmmss (where M is 1+the numeric major number + versionName "1.2.00" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // per https://developer.android.com/studio/write/vector-asset-studio diff --git a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt index 76dffcb6e..05a106cfb 100644 --- a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt @@ -16,7 +16,8 @@ data class MyNodeInfo( val shouldUpdate: Boolean, // this device has old firmware val currentPacketId: Long, val messageTimeoutMsec: Int, - val minAppVersion: Int + val minAppVersion: Int, + val maxChannels: Int ) : Parcelable { /** A human readable description of the software/hardware version */ val firmwareString: String get() = "$model $region/$firmwareVersion" @@ -31,6 +32,7 @@ data class MyNodeInfo( parcel.readByte() != 0.toByte(), parcel.readLong(), parcel.readInt(), + parcel.readInt(), parcel.readInt() ) { } @@ -46,6 +48,7 @@ data class MyNodeInfo( parcel.writeLong(currentPacketId) parcel.writeInt(messageTimeoutMsec) parcel.writeInt(minAppVersion) + parcel.writeInt(maxChannels) } override fun describeContents(): Int { 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 7d8cecef8..4271b6389 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -423,7 +423,7 @@ class MeshService : Service(), Logging { private var radioConfig: RadioConfigProtos.RadioConfig? = null - private var channels = listOf() + private var channels = arrayOf() /// True after we've done our initial node db init @Volatile @@ -539,9 +539,9 @@ class MeshService : Service(), Logging { }.build() } - // FIXME, send channels to device! + TODO("Need to send channels to device") - channels = asChannels + channels = asChannels.toTypedArray() } /// Generate a new mesh packet builder with our node as the sender, and the specified node num @@ -567,7 +567,7 @@ class MeshService : Service(), Logging { */ private fun MeshProtos.MeshPacket.Builder.buildMeshPacket( wantAck: Boolean = false, - id: Int = 0, + id: Int = generatePacketId(), // always assign a packet ID if we didn't already have one hopLimit: Int = 0, priority: MeshPacket.Priority = MeshPacket.Priority.UNSET, initFn: MeshProtos.Data.Builder.() -> Unit @@ -702,6 +702,9 @@ class MeshService : Service(), Logging { // if (p.hasUser()) handleReceivedUser(fromNum, p.user) + /// We tell other apps about most message types, but some may have sensitve data, so that is not shared' + var shouldBroadcast = true + when (data.portnumValue) { Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> { debug("Received CLEAR_TEXT from $fromId") @@ -728,10 +731,20 @@ class MeshService : Service(), Logging { else handleAckNak(false, data.requestId) } + + Portnums.PortNum.ADMIN_APP_VALUE -> { + val u = AdminProtos.AdminMessage.parseFrom(data.payload) + handleReceivedAdmin(packet.from, u) + shouldBroadcast = false + } + + else -> + debug("No custom processing needed for ${data.portnumValue}") } // We always tell other apps when new data packets arrive - serviceBroadcasts.broadcastReceivedData(dataPacket) + if (shouldBroadcast) + serviceBroadcasts.broadcastReceivedData(dataPacket) GeeksvilleApplication.analytics.track( "num_data_receive", @@ -748,6 +761,35 @@ class MeshService : Service(), Logging { } } + private fun handleReceivedAdmin(fromNodeNum: Int, a: AdminProtos.AdminMessage) { + // For the time being we only care about admin messages from our local node + if (fromNodeNum == myNodeNum) { + when (a.variantCase) { + AdminProtos.AdminMessage.VariantCase.GET_RADIO_RESPONSE -> { + radioConfig = a.getRadioResponse + requestChannel(0) // Now start reading channels + } + + AdminProtos.AdminMessage.VariantCase.GET_CHANNEL_RESPONSE -> { + val mi = myNodeInfo + if (mi != null) { + val ch = a.getChannelResponse + channels[ch.index] = ch + if (ch.index + 1 < mi.maxChannels) { + // Not done yet, request next channel + requestChannel(ch.index + 1) + } else { + onHasSettings() + } + } + } + else -> + warn("No special processing needed for ${a.variantCase}") + + } + } + } + /// Update our DB of users based on someone sending out a User subpacket private fun handleReceivedUser(fromNum: Int, p: MeshProtos.User) { updateNodeInfo(fromNum) { @@ -1192,12 +1234,23 @@ class MeshService : Service(), Logging { ), currentPacketId.toLong() and 0xffffffffL, if (messageTimeoutMsec == 0) 5 * 60 * 1000 else messageTimeoutMsec, // constants from current device code - minAppVersion + minAppVersion, + maxChannels ) } newMyNodeInfo = mi + // We'll need to get a new set of channels and settings now + radioConfig = null + + // prefill the channel array with null channels + channels = Array(mi.maxChannels) { + val b = ChannelProtos.Channel.newBuilder() + b.index = it + b.build() + } + /// Track types of devices and firmware versions in use GeeksvilleApplication.analytics.setUserInfo( DataPair("region", mi.region), @@ -1272,6 +1325,18 @@ class MeshService : Service(), Logging { } } + /// If we've received our initial config, our radio settings and all of our channels, send any queueed packets and broadcast connected to clients + private fun onHasSettings() { + processEarlyPackets() // send receive any packets that were queued up + + // broadcast an intent with our new connection state + serviceBroadcasts.broadcastConnection() + onNodeDBChanged() + reportConnection() + + updateRegion() + } + private fun handleConfigComplete(configCompleteId: Int) { if (configCompleteId == configNonce) { @@ -1295,19 +1360,25 @@ class MeshService : Service(), Logging { newNodes.clear() // Just to save RAM ;-) haveNodeDB = true // we now have nodes from real hardware - processEarlyPackets() // send receive any packets that were queued up - - // broadcast an intent with our new connection state - serviceBroadcasts.broadcastConnection() - onNodeDBChanged() - reportConnection() - - updateRegion() + requestRadioConfig() + requestChannel(0) } } else warn("Ignoring stale config complete") } + private fun requestRadioConfig() { + sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { + getRadioRequest = true + }) + } + + private fun requestChannel(channelIndex: Int) { + sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { + getChannelRequest = channelIndex + 1 + }) + } + /** * Start the modern (REV2) API configuration flow */ @@ -1430,36 +1501,24 @@ class MeshService : Service(), Logging { throw Exception("Can't set user without a node info") // this shouldn't happen } + /// Do not use directly, instead call generatePacketId() - private var currentPacketId = 0L + private var currentPacketId = Random(System.currentTimeMillis()).nextLong().absoluteValue /** * Generate a unique packet ID (if we know enough to do so - otherwise return 0 so the device will do it) */ + @Synchronized private fun generatePacketId(): Int { + val numPacketIds = + ((1L shl 32) - 1).toLong() // A mask for only the valid packet ID bits, either 255 or maxint - myNodeInfo?.let { - val numPacketIds = - ((1L shl 32) - 1).toLong() // A mask for only the valid packet ID bits, either 255 or maxint + currentPacketId++ - if (currentPacketId == 0L) { - // We now always pick a random initial packet id (odds of collision with the device is insanely low with 32 bit ids) - val random = Random(System.currentTimeMillis()) - val devicePacketId = random.nextLong().absoluteValue + currentPacketId = currentPacketId and 0xffffffff // keep from exceeding 32 bits - // Not inited - pick a number on the opposite side of what the device is using - currentPacketId = devicePacketId + numPacketIds / 2 - } else { - currentPacketId++ - } - - currentPacketId = currentPacketId and 0xffffffff // keep from exceeding 32 bits - - // Use modulus and +1 to ensure we skip 0 on any values we return - return ((currentPacketId % numPacketIds) + 1L).toInt() - } - - return 0 // We don't have mynodeinfo yet, so just let the radio eventually assign an ID + // Use modulus and +1 to ensure we skip 0 on any values we return + return ((currentPacketId % numPacketIds) + 1L).toInt() } var firmwareUpdateFilename: UpdateFilenames? = null From da95b4f0c2307e4d2cd77ce9032744cca4c3059a Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Wed, 3 Mar 2021 07:30:05 +0800 Subject: [PATCH 12/21] basic settings and channel stuff works for android --- .../geeksville/mesh/service/MeshService.kt | 104 +++++++++--------- 1 file changed, 51 insertions(+), 53 deletions(-) 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 4271b6389..fa4008e0e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -111,7 +111,10 @@ class MeshService : Service(), Logging { private val serviceJob = Job() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) private var connectionState = ConnectionState.DISCONNECTED + + /// A database of received packets - used only for debug log private var packetRepo: PacketRepository? = null + private var fusedLocationClient: FusedLocationProviderClient? = null // If we've ever read a valid region code from our device it will be here @@ -588,12 +591,14 @@ class MeshService : Service(), Logging { * Helper to make it easy to build a subpacket in the proper protobufs */ private fun MeshProtos.MeshPacket.Builder.buildAdminPacket( + wantResponse: Boolean = false, initFn: AdminProtos.AdminMessage.Builder.() -> Unit ): MeshPacket = buildMeshPacket( wantAck = true, priority = MeshPacket.Priority.RELIABLE ) { + this.wantResponse = wantResponse portnumValue = Portnums.PortNum.ADMIN_APP_VALUE payload = AdminProtos.AdminMessage.newBuilder().also { initFn(it) @@ -684,47 +689,42 @@ class MeshService : Service(), Logging { if (dataPacket != null) { - if (myInfo.myNodeNum == packet.from) { - // Handle position updates from the device - if (data.portnumValue == Portnums.PortNum.POSITION_APP_VALUE) { - handleReceivedPosition( - packet.from, - MeshProtos.Position.parseFrom(data.payload), - dataPacket.time - ) - } else - debug("Ignoring packet sent from our node, portnum=${data.portnumValue} ${bytes.size} bytes") - } else { - debug("Received data from $fromId, portnum=${data.portnumValue} ${bytes.size} bytes") + // We ignore most messages that we sent + val fromUs = myInfo.myNodeNum == packet.from - dataPacket.status = MessageStatus.RECEIVED - rememberDataPacket(dataPacket) + debug("Received data from $fromId, portnum=${data.portnum} ${bytes.size} bytes") - // if (p.hasUser()) handleReceivedUser(fromNum, p.user) + dataPacket.status = MessageStatus.RECEIVED + rememberDataPacket(dataPacket) - /// We tell other apps about most message types, but some may have sensitve data, so that is not shared' - var shouldBroadcast = true + // if (p.hasUser()) handleReceivedUser(fromNum, p.user) - when (data.portnumValue) { - Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> { + /// We tell other apps about most message types, but some may have sensitve data, so that is not shared' + var shouldBroadcast = !fromUs + + when (data.portnumValue) { + Portnums.PortNum.TEXT_MESSAGE_APP_VALUE -> + if (!fromUs) { debug("Received CLEAR_TEXT from $fromId") updateMessageNotification(dataPacket) } - // Handle new style position info - Portnums.PortNum.POSITION_APP_VALUE -> { - val u = MeshProtos.Position.parseFrom(data.payload) - handleReceivedPosition(packet.from, u, dataPacket.time) - } + // Handle new style position info + Portnums.PortNum.POSITION_APP_VALUE -> { + val u = MeshProtos.Position.parseFrom(data.payload) + handleReceivedPosition(packet.from, u, dataPacket.time) + } - // Handle new style user info - Portnums.PortNum.NODEINFO_APP_VALUE -> { + // Handle new style user info + Portnums.PortNum.NODEINFO_APP_VALUE -> + if (!fromUs) { val u = MeshProtos.User.parseFrom(data.payload) handleReceivedUser(packet.from, u) } - // Handle new style routing info - Portnums.PortNum.ROUTING_APP_VALUE -> { + // Handle new style routing info + Portnums.PortNum.ROUTING_APP_VALUE -> + if (!fromUs) { val u = MeshProtos.Routing.parseFrom(data.payload) if (u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE) handleAckNak(true, data.requestId) @@ -732,31 +732,30 @@ class MeshService : Service(), Logging { handleAckNak(false, data.requestId) } - Portnums.PortNum.ADMIN_APP_VALUE -> { - val u = AdminProtos.AdminMessage.parseFrom(data.payload) - handleReceivedAdmin(packet.from, u) - shouldBroadcast = false - } - - else -> - debug("No custom processing needed for ${data.portnumValue}") + Portnums.PortNum.ADMIN_APP_VALUE -> { + val u = AdminProtos.AdminMessage.parseFrom(data.payload) + handleReceivedAdmin(packet.from, u) + shouldBroadcast = false } - // We always tell other apps when new data packets arrive - if (shouldBroadcast) - serviceBroadcasts.broadcastReceivedData(dataPacket) - - GeeksvilleApplication.analytics.track( - "num_data_receive", - DataPair(1) - ) - - GeeksvilleApplication.analytics.track( - "data_receive", - DataPair("num_bytes", bytes.size), - DataPair("type", data.portnumValue) - ) + else -> + debug("No custom processing needed for ${data.portnumValue}") } + + // We always tell other apps when new data packets arrive + if (shouldBroadcast) + serviceBroadcasts.broadcastReceivedData(dataPacket) + + GeeksvilleApplication.analytics.track( + "num_data_receive", + DataPair(1) + ) + + GeeksvilleApplication.analytics.track( + "data_receive", + DataPair("num_bytes", bytes.size), + DataPair("type", data.portnumValue) + ) } } } @@ -1361,20 +1360,19 @@ class MeshService : Service(), Logging { haveNodeDB = true // we now have nodes from real hardware requestRadioConfig() - requestChannel(0) } } else warn("Ignoring stale config complete") } private fun requestRadioConfig() { - sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { + sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) { getRadioRequest = true }) } private fun requestChannel(channelIndex: Int) { - sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket { + sendToRadio(newMeshPacketTo(myNodeNum).buildAdminPacket(wantResponse = true) { getChannelRequest = channelIndex + 1 }) } From 55d0110ff26048e574172223b992bd9a91204f9e Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Wed, 3 Mar 2021 07:49:23 +0800 Subject: [PATCH 13/21] optimize channel rx --- .../geeksville/mesh/service/MeshService.kt | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) 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 fa4008e0e..40fe6b394 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -723,14 +723,13 @@ class MeshService : Service(), Logging { } // Handle new style routing info - Portnums.PortNum.ROUTING_APP_VALUE -> - if (!fromUs) { - val u = MeshProtos.Routing.parseFrom(data.payload) - if (u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE) - handleAckNak(true, data.requestId) - else - handleAckNak(false, data.requestId) - } + Portnums.PortNum.ROUTING_APP_VALUE -> { + val u = MeshProtos.Routing.parseFrom(data.payload) + if (u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE) + handleAckNak(true, data.requestId) + else + handleAckNak(false, data.requestId) + } Portnums.PortNum.ADMIN_APP_VALUE -> { val u = AdminProtos.AdminMessage.parseFrom(data.payload) @@ -774,10 +773,19 @@ class MeshService : Service(), Logging { if (mi != null) { val ch = a.getChannelResponse channels[ch.index] = ch + debug("Received channel ${ch.index}") if (ch.index + 1 < mi.maxChannels) { - // Not done yet, request next channel - requestChannel(ch.index + 1) + if(ch.hasSettings()) { + // Not done yet, request next channel + requestChannel(ch.index + 1) + } + /* if(ch.index == 0) { + // We allow the app to start as soon as we've received the primary channel, we'll keep fetching other channels in the background + debug("We've received the primary channel, allowing rest of app to start...") + onHasSettings() + } */ } else { + debug("Received all channels") onHasSettings() } } @@ -1326,6 +1334,7 @@ class MeshService : Service(), Logging { /// If we've received our initial config, our radio settings and all of our channels, send any queueed packets and broadcast connected to clients private fun onHasSettings() { + processEarlyPackets() // send receive any packets that were queued up // broadcast an intent with our new connection state From dbc91e4ac5cd31586c77bdf027ba8a7bc22190f7 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Wed, 3 Mar 2021 08:14:40 +0800 Subject: [PATCH 14/21] remove deprecations --- .../main/java/com/geeksville/mesh/MyNodeInfo.kt | 5 +---- .../com/geeksville/mesh/service/MeshService.kt | 17 +++++++++-------- .../mesh/service/MeshServiceBroadcasts.kt | 2 ++ .../geeksville/mesh/service/MockInterface.kt | 1 - .../com/geeksville/mesh/ui/MessagesFragment.kt | 5 +++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt index 05a106cfb..da76ccfaa 100644 --- a/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/MyNodeInfo.kt @@ -9,7 +9,6 @@ import kotlinx.serialization.Serializable data class MyNodeInfo( val myNodeNum: Int, val hasGPS: Boolean, - val region: String?, val model: String?, val firmwareVersion: String?, val couldUpdate: Boolean, // this application contains a software load we _could_ install if you want @@ -20,14 +19,13 @@ data class MyNodeInfo( val maxChannels: Int ) : Parcelable { /** A human readable description of the software/hardware version */ - val firmwareString: String get() = "$model $region/$firmwareVersion" + val firmwareString: String get() = "$model $firmwareVersion" constructor(parcel: Parcel) : this( parcel.readInt(), parcel.readByte() != 0.toByte(), parcel.readString(), parcel.readString(), - parcel.readString(), parcel.readByte() != 0.toByte(), parcel.readByte() != 0.toByte(), parcel.readLong(), @@ -40,7 +38,6 @@ data class MyNodeInfo( override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeInt(myNodeNum) parcel.writeByte(if (hasGPS) 1 else 0) - parcel.writeString(region) parcel.writeString(model) parcel.writeString(firmwareVersion) parcel.writeByte(if (couldUpdate) 1 else 0) 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 40fe6b394..93aa32cec 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -52,8 +52,8 @@ class MeshService : Service(), Logging { /// Intents broadcast by MeshService - @Deprecated(message = "Does not filter by port number. For legacy reasons only broadcast for UNKNOWN_APP, switch to ACTION_RECEIVED") - const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA" + /* @Deprecated(message = "Does not filter by port number. For legacy reasons only broadcast for UNKNOWN_APP, switch to ACTION_RECEIVED") + const val ACTION_RECEIVED_DATA = "$prefix.RECEIVED_DATA" */ fun actionReceived(portNum: String) = "$prefix.RECEIVED.$portNum" @@ -724,6 +724,7 @@ class MeshService : Service(), Logging { // Handle new style routing info Portnums.PortNum.ROUTING_APP_VALUE -> { + shouldBroadcast = true // We always send acks to other apps, because they might care about the messages they sent val u = MeshProtos.Routing.parseFrom(data.payload) if (u.errorReasonValue == MeshProtos.Routing.Error.NONE_VALUE) handleAckNak(true, data.requestId) @@ -1231,7 +1232,6 @@ class MeshService : Service(), Logging { MyNodeInfo( myNodeNum, hasGps, - region, hwModel, firmwareVersion, firmwareUpdateFilename != null, @@ -1260,7 +1260,7 @@ class MeshService : Service(), Logging { /// Track types of devices and firmware versions in use GeeksvilleApplication.analytics.setUserInfo( - DataPair("region", mi.region), + // DataPair("region", mi.region), DataPair("firmware", mi.firmwareVersion), DataPair("has_gps", mi.hasGPS), DataPair("hw_model", mi.model), @@ -1275,8 +1275,8 @@ class MeshService : Service(), Logging { // We also include this info, because it is required to correctly decode address from the map file DataPair("firmware", mi.firmwareVersion), - DataPair("hw_model", mi.model), - DataPair("region", mi.region) + DataPair("hw_model", mi.model) + // DataPair("region", mi.region) ) } } @@ -1301,7 +1301,8 @@ class MeshService : Service(), Logging { } if (curRegionValue == RadioConfigProtos.RegionCode.Unset_VALUE) { - // look for a legacy region + TODO("Need gui for setting region") + /* // look for a legacy region val legacyRegex = Regex(".+-(.+)") myNodeInfo?.region?.let { legacyRegion -> val matches = legacyRegex.find(legacyRegion) @@ -1311,7 +1312,7 @@ class MeshService : Service(), Logging { info("Upgrading legacy region $newRegion (code ${newRegion.number})") curRegionValue = newRegion.number } - } + } */ } // If nothing was set in our (new style radio preferences, but we now have a valid setting - slam it in) diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt index 831ab1bd2..6d782269e 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceBroadcasts.kt @@ -20,10 +20,12 @@ class MeshServiceBroadcasts( explicitBroadcast(Intent(MeshService.actionReceived(payload.dataType)).putExtra(EXTRA_PAYLOAD, payload)) + /* // For the time being we ALSO broadcast using old ACTION_RECEIVED_DATA field for any oldschool opaque packets // newer packets (that have a non zero portnum) are only broadcast using the standard mechanism. if(payload.dataType == Portnums.PortNum.UNKNOWN_APP_VALUE) explicitBroadcast(Intent(MeshService.ACTION_RECEIVED_DATA).putExtra(EXTRA_PAYLOAD, payload)) + */ } fun broadcastNodeChange(info: NodeInfo) { diff --git a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt index 06ed39603..84d3aab53 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt @@ -108,7 +108,6 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi MeshProtos.FromRadio.newBuilder().apply { myInfo = MeshProtos.MyNodeInfo.newBuilder().apply { myNodeNum = MY_NODE - region = "TW" hwModel = "Sim" messageTimeoutMsec = 5 * 60 * 1000 firmwareVersion = service.getString(R.string.cur_firmware_version) diff --git a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt index 668f789d3..43553a20d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MessagesFragment.kt @@ -9,6 +9,7 @@ import android.widget.EditText import android.widget.ImageView import android.widget.TextView import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat import androidx.fragment.app.activityViewModels import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager @@ -151,11 +152,11 @@ class MessagesFragment : ScreenFragment("Messages"), Logging { if (isMe) { marginParams.leftMargin = messageOffset marginParams.rightMargin = 0 - holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMyMsg)) + context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMyMsg)) } } else { marginParams.rightMargin = messageOffset marginParams.leftMargin = 0 - holder.card.setCardBackgroundColor(resources.getColor(R.color.colorMsg)) + context?.let{ holder.card.setCardBackgroundColor(ContextCompat.getColor(it, R.color.colorMsg)) } } // Hide the username chip for my messages if (isMe) { From b53acd206b093b8187903371409fbbfc6b042acf Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Wed, 3 Mar 2021 13:51:33 +0800 Subject: [PATCH 15/21] region GUI wip --- .../geeksville/mesh/ui/SettingsFragment.kt | 9 +++++- app/src/main/res/layout/settings_fragment.xml | 32 ++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 06eb73658..ae77be60c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -19,6 +19,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo +import android.widget.ArrayAdapter import android.widget.RadioButton import android.widget.Toast import androidx.fragment.app.activityViewModels @@ -585,6 +586,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { /// Setup the ui widgets unrelated to BLE scanning private fun initCommonUI() { + val regions = arrayOf("US", "CN", "EU488") + val spinner = binding.regionSpinner + val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions) + regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + spinner.adapter = regionAdapter + model.ownerName.observe(viewLifecycleOwner, { name -> binding.usernameEditText.setText(name) }) @@ -593,7 +600,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { // Only let user edit their name or set software update while connected to a radio model.isConnected.observe(viewLifecycleOwner, Observer { connectionState -> val connected = connectionState == MeshService.ConnectionState.CONNECTED - binding.usernameView.isEnabled = connected + binding.nodeSettings.visibility = if(connected) View.VISIBLE else View.GONE if (connectionState == MeshService.ConnectionState.DISCONNECTED) model.ownerName.value = "" diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index d5f5103d9..80c7a6714 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -10,14 +10,14 @@ + android:layout_height="0dp" + android:layout_marginTop="16dp"> + + + + app:layout_constraintTop_toBottomOf="@+id/nodeSettings" /> - - \ No newline at end of file From aa79ee4335aa65d619ff3838b0fec9f257ce17d8 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Wed, 3 Mar 2021 14:43:29 +0800 Subject: [PATCH 16/21] make our simulator work like 1.2 --- .../geeksville/mesh/service/MockInterface.kt | 61 ++++++++++++++++--- .../com/geeksville/mesh/service/SimRadio.kt | 51 ---------------- .../geeksville/mesh/ui/SettingsFragment.kt | 2 +- app/src/main/res/layout/settings_fragment.xml | 2 + app/src/main/res/values/protobufs.xml | 15 +++++ app/src/main/res/values/styles.xml | 14 +++++ 6 files changed, 85 insertions(+), 60 deletions(-) delete mode 100644 app/src/main/java/com/geeksville/mesh/service/SimRadio.kt create mode 100644 app/src/main/res/values/protobufs.xml diff --git a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt index 84d3aab53..c83b0b00d 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt @@ -26,13 +26,40 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi override fun handleSendToRadio(p: ByteArray) { val pr = MeshProtos.ToRadio.parseFrom(p) + val data = if (pr.hasPacket()) pr.packet.decoded else null + when { pr.wantConfigId != 0 -> sendConfigResponse(pr.wantConfigId) + data != null && data.portnum == Portnums.PortNum.ADMIN_APP -> handleAdminPacket( + pr, + AdminProtos.AdminMessage.parseFrom(data.payload) + ) pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr) else -> info("Ignoring data sent to mock interface $pr") } } + private fun handleAdminPacket(pr: MeshProtos.ToRadio, d: AdminProtos.AdminMessage) { + if (d.getRadioRequest) + sendAdmin(pr.packet.to, pr.packet.from, pr.packet.id) { + getRadioResponse = RadioConfigProtos.RadioConfig.newBuilder().apply { + + preferences = RadioConfigProtos.RadioConfig.UserPreferences.newBuilder().apply { + region = RadioConfigProtos.RegionCode.TW + // FIXME set critical times? + }.build() + }.build() + } + + if (d.getChannelRequest != 0) + sendAdmin(pr.packet.to, pr.packet.from, pr.packet.id) { + getChannelResponse = ChannelProtos.Channel.newBuilder().apply { + index = d.getChannelRequest - 1 // 0 based on the response + role = if(d.getChannelRequest == 1) ChannelProtos.Channel.Role.PRIMARY else ChannelProtos.Channel.Role.DISABLED + }.build() + } + } + override fun close() { info("Closing the mock interface") } @@ -53,8 +80,7 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi }.build() } - - private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) = + private fun makeDataPacket(fromIn: Int, toIn: Int, data: MeshProtos.Data.Builder) = MeshProtos.FromRadio.newBuilder().apply { packet = MeshProtos.MeshPacket.newBuilder().apply { id = messageNumSequence.next() @@ -62,15 +88,34 @@ class MockInterface(private val service: RadioInterfaceService) : Logging, IRadi to = toIn rxTime = (System.currentTimeMillis() / 1000).toInt() rxSnr = 1.5f - decoded = MeshProtos.Data.newBuilder().apply { - portnum = Portnums.PortNum.ROUTING_APP - payload = MeshProtos.Routing.newBuilder().apply { - }.build().toByteString() - requestId = msgId - }.build() + decoded = data.build() }.build() } + private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) = + makeDataPacket(fromIn, toIn, MeshProtos.Data.newBuilder().apply { + portnum = Portnums.PortNum.ROUTING_APP + payload = MeshProtos.Routing.newBuilder().apply { + }.build().toByteString() + requestId = msgId + }) + + private fun sendAdmin( + fromIn: Int, + toIn: Int, + reqId: Int, + initFn: AdminProtos.AdminMessage.Builder.() -> Unit + ) { + val p = makeDataPacket(fromIn, toIn, MeshProtos.Data.newBuilder().apply { + portnum = Portnums.PortNum.ADMIN_APP + payload = AdminProtos.AdminMessage.newBuilder().also { + initFn(it) + }.build().toByteString() + requestId = reqId + }) + service.handleFromRadio(p.build().toByteArray()) + } + /// Send a fake ack packet back if the sender asked for want_ack private fun sendFakeAck(pr: MeshProtos.ToRadio) { service.handleFromRadio( diff --git a/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt b/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt deleted file mode 100644 index 36b87b40d..000000000 --- a/app/src/main/java/com/geeksville/mesh/service/SimRadio.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.geeksville.mesh - -import android.content.Context -import com.geeksville.mesh.service.RadioInterfaceService - -class SimRadio(private val context: Context) { - - /** - * When simulating we parse these MeshPackets as if they arrived at startup - * Send broadcast them after we receive a ToRadio.WantNodes message. - * - * Our fake net has three nodes - * - * +16508675309, nodenum 9 - our node - * +16508675310, nodenum 10 - some other node, name Bob One/BO - * (eventually) +16508675311, nodenum 11 - some other node - */ - private val simInitPackets = - arrayOf( - """ { "from": 10, "to": 9, "payload": { "user": { "id": "+16508675310", "longName": "Bob One", "shortName": "BO" }}} """, - """ { "from": 10, "to": 9, "payload": { "data": { "payload": "aGVsbG8gd29ybGQ=", "typ": 0 }}} """, // SIGNAL_OPAQUE - """ { "from": 10, "to": 9, "payload": { "data": { "payload": "aGVsbG8gd29ybGQ=", "typ": 1 }}} """, // CLEAR_TEXT - """ { "from": 10, "to": 9, "payload": { "data": { "payload": "", "typ": 2 }}} """ // CLEAR_READACK - ) - - fun start() { - // FIXME, do this sim startup elsewhere, because waiting for a packet from MeshService - // isn't right, because that service can't successfully send radio packets until it knows - // our node num - // Instead a separate sim radio thing can come in at startup and force these broadcasts to happen - // at the right time - // Send a fake my_node_num response - /* FIXME - change to use new radio info message - RadioInterfaceService.broadcastReceivedFromRadio( - context, - MeshProtos.FromRadio.newBuilder().apply { - myNodeNum = 9 - }.build().toByteArray() - ) */ - - simInitPackets.forEach { _ -> - val fromRadio = MeshProtos.FromRadio.newBuilder().apply { - packet = MeshProtos.MeshPacket.newBuilder().apply { - // jsonParser.merge(json, this) - }.build() - }.build() - - RadioInterfaceService.broadcastReceivedFromRadio(context, fromRadio.toByteArray()) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index ae77be60c..86c20fc2c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -590,7 +590,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { val spinner = binding.regionSpinner val regionAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, regions) regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - spinner.adapter = regionAdapter + // spinner.adapter = regionAdapter model.ownerName.observe(viewLifecycleOwner, { name -> binding.usernameEditText.setText(name) diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index 80c7a6714..e34fb6518 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -62,8 +62,10 @@ android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="16dp" + android:theme="@style/AppTheme.Spinner" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/usernameView" + android:entries="@array/regions" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/values/protobufs.xml b/app/src/main/res/values/protobufs.xml new file mode 100644 index 000000000..190471fc4 --- /dev/null +++ b/app/src/main/res/values/protobufs.xml @@ -0,0 +1,15 @@ + + + + + Unset + US + EU433 + EU865 + CN + JP + ANZ + KR + TW + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 691680617..e36ac68c6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -13,6 +13,20 @@ @style/menu_item_color + + + + +