diff --git a/app/build.gradle b/app/build.gradle index ecc0c98b4..7d1bfea3d 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 20137 // format is Mmmss (where M is 1+the numeric major number - versionName "1.1.37" + versionCode 20138 // format is Mmmss (where M is 1+the numeric major number + versionName "1.1.38" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // per https://developer.android.com/studio/write/vector-asset-studio diff --git a/app/src/main/ic_launcher2-playstore.png b/app/src/main/ic_launcher2-playstore.png index 5e8f9939e..e3e10fb55 100644 Binary files a/app/src/main/ic_launcher2-playstore.png and b/app/src/main/ic_launcher2-playstore.png differ diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index ebd378a05..8f6f23f3a 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -606,7 +606,7 @@ class MainActivity : AppCompatActivity(), Logging, /// Called when we gain/lose a connection to our mesh radio private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) { - debug("connchange ${model.isConnected.value}") + debug("connchange ${model.isConnected.value} -> $connected") if (connected == MeshService.ConnectionState.CONNECTED) { model.meshService?.let { service -> diff --git a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt index 5400937c0..dd8828804 100644 --- a/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/MessagesState.kt @@ -2,13 +2,13 @@ package com.geeksville.mesh.model import android.os.RemoteException import androidx.lifecycle.MutableLiveData -import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.android.Logging import com.geeksville.mesh.DataPacket import com.geeksville.mesh.MessageStatus class MessagesState(private val ui: UIViewModel) : Logging { + /* We now provide fake messages a via MockInterface private val testTexts = listOf( DataPacket( "+16508765310", @@ -18,10 +18,10 @@ class MessagesState(private val ui: UIViewModel) : Logging { "+16508765311", "Help! I've fallen and I can't get up." ) - ) + ) */ /// This is the inner storage for messages - private val messagesList = (if (isEmulator) testTexts else emptyList()).toMutableList() + private val messagesList = emptyList().toMutableList() // If the following (unused otherwise) line is commented out, the IDE preview window works. // if left in the preview always renders as empty. diff --git a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt index f3e31eaf3..d2f35f5ff 100644 --- a/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt +++ b/app/src/main/java/com/geeksville/mesh/model/NodeDB.kt @@ -1,7 +1,6 @@ package com.geeksville.mesh.model import androidx.lifecycle.MutableLiveData -import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.mesh.MeshUser import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.Position @@ -43,15 +42,15 @@ class NodeDB(private val ui: UIViewModel) { ) } - private val seedWithTestNodes = isEmulator + private val seedWithTestNodes = false /// The unique ID of our node - val myId = object : MutableLiveData(if (isEmulator) "+16508765309" else null) {} + val myId = object : MutableLiveData(if (seedWithTestNodes) "+16508765309" else null) {} /// A map from nodeid to to nodeinfo val nodes = object : - MutableLiveData>(mapOf(*(if (isEmulator) testNodes else listOf()).map { it.user!!.id to it } + MutableLiveData>(mapOf(*(if (seedWithTestNodes) testNodes else listOf()).map { it.user!!.id to it } .toTypedArray())) {} /// Could be null if we haven't received our node DB yet 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 eb7ee53bc..40ee1f4ed 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -11,7 +11,6 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.android.Logging import com.geeksville.mesh.IMeshService import com.geeksville.mesh.MeshProtos @@ -77,10 +76,7 @@ class UIViewModel(app: Application) : AndroidViewModel(app), Logging { fun getChannel(c: MeshProtos.RadioConfig?): Channel? { val channel = c?.channelSettings?.let { Channel(it) } - return if (channel == null && isEmulator) - Channel.emulated - else - channel + return channel } fun getPreferences(context: Context): SharedPreferences = diff --git a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt index a1e1eda66..a424f8640 100644 --- a/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/BluetoothInterface.kt @@ -97,6 +97,8 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String return bluetoothManager.adapter } + fun toInterfaceName(deviceName: String) = "x$deviceName" + /** Return true if this address is still acceptable. For BLE that means, still bonded */ fun addressValid(context: Context, address: String): Boolean { val allPaired = diff --git a/app/src/main/java/com/geeksville/mesh/service/IRadioInterface.kt b/app/src/main/java/com/geeksville/mesh/service/IRadioInterface.kt index c8d5f1b7a..a45d0aa99 100644 --- a/app/src/main/java/com/geeksville/mesh/service/IRadioInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/service/IRadioInterface.kt @@ -6,11 +6,3 @@ interface IRadioInterface : Closeable { fun handleSendToRadio(p: ByteArray) } -class NopInterface : IRadioInterface { - override fun handleSendToRadio(p: ByteArray) { - } - - override fun close() { - } - -} \ No newline at end of file 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 c8ba1f98c..47e05df48 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -1196,8 +1196,8 @@ class MeshService : Service(), Logging { if (newMyNodeInfo == null || newNodes.isEmpty()) errormsg("Did not receive a valid config") else { - debug("Installing new node DB") discardNodeDB() + debug("Installing new node DB") myNodeInfo = newMyNodeInfo newNodes.forEach(::installNodeInfo) diff --git a/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt new file mode 100644 index 000000000..ade70bf6c --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/service/MockInterface.kt @@ -0,0 +1,156 @@ +package com.geeksville.mesh.service + +import com.geeksville.android.Logging +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.Portnums +import com.geeksville.mesh.Position +import com.geeksville.mesh.R +import com.geeksville.mesh.model.getInitials +import com.google.protobuf.ByteString +import okhttp3.internal.toHexString + +/** A simulated interface that is used for testing in the simulator */ +class MockInterface(private val service: RadioInterfaceService) : Logging, IRadioInterface { + companion object : Logging { + + const val interfaceName = "m" + } + + private var messageCount = 50 + + // an infinite sequence of ints + private val messageNumSequence = generateSequence { messageCount++ }.iterator() + + init { + info("Starting the mock interface") + service.onConnect() // Tell clients they can use the API + } + + override fun handleSendToRadio(p: ByteArray) { + val pr = MeshProtos.ToRadio.parseFrom(p) + + when { + pr.wantConfigId != 0 -> sendConfigResponse(pr.wantConfigId) + pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr) + else -> info("Ignoring data sent to mock interface $pr") + } + } + + override fun close() { + info("Closing the mock interface") + } + + /// Generate a fake text message from a node + private fun makeTextMessage(numIn: Int) = + MeshProtos.FromRadio.newBuilder().apply { + packet = MeshProtos.MeshPacket.newBuilder().apply { + id = messageNumSequence.next() + from = numIn + 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() + }.build() + }.build() + } + + + private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) = + MeshProtos.FromRadio.newBuilder().apply { + packet = MeshProtos.MeshPacket.newBuilder().apply { + id = messageNumSequence.next() + from = fromIn + to = toIn + rxTime = (System.currentTimeMillis() / 1000).toInt() + rxSnr = 1.5f + decoded = MeshProtos.SubPacket.newBuilder().apply { + data = MeshProtos.Data.newBuilder().apply { + successId = msgId + }.build() + }.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()) + } + + private fun sendConfigResponse(configId: Int) { + debug("Sending mock config response") + + /// Generate a fake node info entry + fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = + MeshProtos.FromRadio.newBuilder().apply { + nodeInfo = MeshProtos.NodeInfo.newBuilder().apply { + num = numIn + user = MeshProtos.User.newBuilder().apply { + id = "!0x" + num.toHexString() + longName = "Sim " + num.toHexString() + shortName = getInitials(longName) + }.build() + position = MeshProtos.Position.newBuilder().apply { + latitudeI = Position.degI(lat) + longitudeI = Position.degI(lon) + batteryLevel = 42 + altitude = 35 + time = (System.currentTimeMillis() / 1000).toInt() + }.build() + }.build() + } + + // Simulated network data to feed to our app + 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 { + + preferences = MeshProtos.RadioConfig.UserPreferences.newBuilder().apply { + region = MeshProtos.RegionCode.TW + // FIXME set critical times? + }.build() + + channel = MeshProtos.ChannelSettings.newBuilder().apply { + // we just have an empty listing so that the default channel works + }.build() + }.build() + }, + + // Fake NodeDB + makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas + makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson + + MeshProtos.FromRadio.newBuilder().apply { + configCompleteId = configId + }, + + // Done with config response, now pretend to receive some text messages + + makeTextMessage(MY_NODE + 1) + ) + + packets.forEach { p -> + service.handleFromRadio(p.build().toByteArray()) + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/service/NopInterface.kt b/app/src/main/java/com/geeksville/mesh/service/NopInterface.kt new file mode 100644 index 000000000..f4e462349 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/service/NopInterface.kt @@ -0,0 +1,10 @@ +package com.geeksville.mesh.service + +class NopInterface : IRadioInterface { + override fun handleSendToRadio(p: ByteArray) { + } + + override fun close() { + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt index ece6f66e4..c24e5aa87 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -8,6 +8,7 @@ import android.content.SharedPreferences import android.os.IBinder import androidx.core.content.edit import com.geeksville.android.BinaryLogFile +import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.android.GeeksvilleApplication import com.geeksville.android.Logging import com.geeksville.concurrent.handledLaunch @@ -81,12 +82,20 @@ class RadioInterfaceService : Service(), Logging { rest = null if (rest != null) - address = "x$rest" // Add the bluetooth prefix + address = BluetoothInterface.toInterfaceName(rest) // Add the bluetooth prefix } + // If we are running on the emulator we default to the mock interface, so we can have some data to show to the user + if(address == null && isMockInterfaceAvailable(context)) + address = MockInterface.interfaceName + return address } + /** return true if we should show the mock interface on this device + * (ie are we in an emulator or in testlab + */ + fun isMockInterfaceAvailable(context: Context) = isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab) /** Like getDeviceAddress, but filtered to return only devices we are currently bonded with * @@ -108,6 +117,7 @@ class RadioInterfaceService : Service(), Logging { 'x' -> BluetoothInterface.addressValid(context, rest) 's' -> SerialInterface.addressValid(context, rest) 'n' -> true + 'm' -> true else -> TODO("Unexpected interface type $c") } if (!isValid) @@ -236,6 +246,7 @@ class RadioInterfaceService : Service(), Logging { radioIf = when (c) { 'x' -> BluetoothInterface(this, rest) 's' -> SerialInterface(this, rest) + 'm' -> MockInterface(this) 'n' -> nopIf else -> { errormsg("Unexpected radio interface type") 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 ec8f2ac53..26cb3263c 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -265,10 +265,11 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging { debug("BTScan component active") selectedAddress = RadioInterfaceService.getDeviceAddress(context) - return if (bluetoothAdapter == null) { + return if (bluetoothAdapter == null || RadioInterfaceService.isMockInterfaceAvailable(context)) { warn("No bluetooth adapter. Running under emulation?") val testnodes = listOf( + DeviceListEntry("Simulated interface", "m", true), DeviceListEntry("Meshtastic_ab12", "xaa", false), DeviceListEntry("Meshtastic_32ac", "xbb", true) ) diff --git a/app/src/main/res/drawable/ic_launcher2_foreground.xml b/app/src/main/res/drawable/ic_launcher2_foreground.xml index c115779cb..d2393a0d6 100644 --- a/app/src/main/res/drawable/ic_launcher2_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher2_foreground.xml @@ -8,12 +8,12 @@ android:translateX="18.36" android:translateY="18.36"> diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher2.png b/app/src/main/res/mipmap-hdpi/ic_launcher2.png index b64d9280b..6bca632e5 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher2.png and b/app/src/main/res/mipmap-hdpi/ic_launcher2.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher2_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher2_round.png index 56c520a9c..6915cc50a 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher2_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher2_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher2.png b/app/src/main/res/mipmap-mdpi/ic_launcher2.png index 2b79c00fa..cb6cad39b 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher2.png and b/app/src/main/res/mipmap-mdpi/ic_launcher2.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher2_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher2_round.png index 85fb0a2e2..917810c59 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher2_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher2_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher2.png b/app/src/main/res/mipmap-xhdpi/ic_launcher2.png index 2de915abc..d42ae3ccc 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher2.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher2.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher2_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher2_round.png index 341130c04..9a608c578 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher2_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher2_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher2.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher2.png index a4dca3aa8..618049278 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher2.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher2.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher2_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher2_round.png index 1a1f6ba53..767a4d7e6 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher2_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher2_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher2.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher2.png index 6b068589d..70b025009 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher2.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher2.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher2_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher2_round.png index 2daa1eeed..a66d98b03 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher2_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher2_round.png differ diff --git a/images/play-store-feature-graphic.jpg b/deprecated/play-store-feature-graphic.jpg similarity index 100% rename from images/play-store-feature-graphic.jpg rename to deprecated/play-store-feature-graphic.jpg diff --git a/design b/design index 9f340a534..a81074152 160000 --- a/design +++ b/design @@ -1 +1 @@ -Subproject commit 9f340a53463ad38516c372adac80e966ccf7a21a +Subproject commit a81074152157fa54b0d02ccbbd6a6357cc3cedcf