From 1e34e77fe9fe0d5dbc8ab656fec24268e9282108 Mon Sep 17 00:00:00 2001 From: geeksville Date: Sat, 4 Apr 2020 14:37:13 -0700 Subject: [PATCH 1/4] only force refresh of the BLE service table the first time we talk --- .../com/geeksville/mesh/service/RadioInterfaceService.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 f834c4a46..a88861b02 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -291,13 +291,19 @@ class RadioInterfaceService : Service(), Logging { } } + /// We only force service refresh the _first_ time we connect to the device. Thereafter it is assumed the firmware didn't change + private var hasForcedRefresh = false + private fun onConnect(connRes: Result) { // This callback is invoked after we are connected connRes.getOrThrow() // FIXME, instead just try to reconnect? info("Connected to radio!") - forceServiceRefresh() + if (!hasForcedRefresh) { + hasForcedRefresh = true + forceServiceRefresh() + } // FIXME - no need to discover services more than once - instead use lazy() to use them in future attempts safe!!.asyncDiscoverServices { discRes -> From 83c1bfda69cd6097051900746a62a99046ef997d Mon Sep 17 00:00:00 2001 From: geeksville Date: Sat, 4 Apr 2020 14:37:44 -0700 Subject: [PATCH 2/4] use coroutines in the mesh service to move processing out of the GUI thread --- TODO.md | 8 ++ app/build.gradle | 4 + app/proguard-rules.pro | 5 + .../geeksville/mesh/service/MeshService.kt | 92 ++++++++++++------- build.gradle | 2 + 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/TODO.md b/TODO.md index b7371a95b..856196ed5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,12 @@ # High priority Work items for soon alpha builds +* use states for meshservice: disconnected -> connected -> deviceasleep -> disconnected + +* use compose on each page, but not for the outer wrapper +* one view per page: https://developer.android.com/guide/navigation/navigation-swipe-view-2 +* use viewgroup with a unique ID https://developer.android.com/reference/kotlin/androidx/ui/core/package-summary#(android.view.ViewGroup).setContent(kotlin.Function0) + * let channel be editited * make link sharing work * finish map view @@ -19,6 +25,8 @@ Work items for soon alpha builds # Medium priority Features for future builds +* use coroutines in the services, to ensure low latency for both API calls and GUI operations https://developer.android.com/kotlin/coroutines & +https://medium.com/@kenkyee/android-kotlin-coroutine-best-practices-bc033fed62e7 & https://codelabs.developers.google.com/codelabs/kotlin-coroutines/#5 * fix notification setSmallIcon parameter - change it to use the meshtastic icon * ditch compose and use https://github.com/zsmb13/MaterialDrawerKt + https://github.com/Kotlin/anko/wiki/Anko-Layouts? * describe user experience: devices always point to each other and show distance, you can send texts between nodes diff --git a/app/build.gradle b/app/build.gradle index a5dc65527..650a633a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,6 +82,10 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + // Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + // You need to depend on the lite runtime library, not protobuf-java // For now I'm not using javalite, because I want JSON printing //implementation 'com.google.protobuf:protobuf-java:3.11.1' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b424510..960ab5d6c 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -19,3 +19,8 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile + +# per https://medium.com/@kenkyee/android-kotlin-coroutine-best-practices-bc033fed62e7 +-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} +-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} +-keepclassmembernames class kotlinx.** { volatile ; } \ 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 14c192903..c176e37e3 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -11,6 +11,7 @@ import android.os.Build import android.os.IBinder import android.os.RemoteException import androidx.annotation.RequiresApi +import androidx.annotation.UiThread import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.PRIORITY_MIN import com.geeksville.analytics.DataPair @@ -27,12 +28,26 @@ import com.geeksville.util.toRemoteExceptions import com.google.android.gms.common.api.ResolvableApiException import com.google.android.gms.location.* import com.google.protobuf.ByteString +import kotlinx.coroutines.* import java.nio.charset.Charset +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext class RadioNotConnectedException() : Exception("Not connected to radio") +private val errorHandler = CoroutineExceptionHandler { _, exception -> + Exceptions.report(exception, "MeshService-coroutine", "coroutine-exception") +} + +/// Wrap launch with an exception handler, FIXME, move into a utility lib +fun CoroutineScope.handledLaunch( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit +) = this.launch(context = context + errorHandler, start = start, block = block) + /** * Handles all the communication with android apps. Also keeps an internal model * of the network state. @@ -96,6 +111,9 @@ class MeshService : Service(), Logging { IRadioInterfaceService.Stub.asInterface(it) } + private val serviceJob = Job() + private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + /* see com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes @@ -117,7 +135,7 @@ class MeshService : Service(), Logging { private var lastSendMsec = 0L override fun onLocationResult(locationResult: LocationResult) { - exceptionReporter { + serviceScope.handledLaunch { super.onLocationResult(locationResult) var l = locationResult.lastLocation @@ -163,6 +181,7 @@ class MeshService : Service(), Logging { * per https://developer.android.com/training/location/change-location-settings */ @SuppressLint("MissingPermission") + @UiThread private fun startLocationRequests() { if (fusedLocationClient == null) { GeeksvilleApplication.analytics.track("location_start") // Figure out how many users needed to use the phone GPS @@ -375,6 +394,7 @@ class MeshService : Service(), Logging { radio.close() super.onDestroy() + serviceJob.cancel() } @@ -712,14 +732,17 @@ class MeshService : Service(), Logging { if (!myNodeInfo!!.hasGPS) { // If we have at least one other person in the mesh, send our GPS position otherwise stop listening to GPS - if (numOnlineNodes >= 2) - startLocationRequests() - else - stopLocationRequests() + serviceScope.handledLaunch(Dispatchers.Main) { + if (numOnlineNodes >= 2) + startLocationRequests() + else + stopLocationRequests() + } } else debug("Our radio has a built in GPS, so not reading GPS in phone") } + /// Called when we gain/lose connection to our radio private fun onConnectionChanged(c: Boolean) { debug("onConnectionChanged connected=$c") @@ -773,41 +796,42 @@ class MeshService : Service(), Logging { // Important to never throw exceptions out of onReceive override fun onReceive(context: Context, intent: Intent) = exceptionReporter { + serviceScope.handledLaunch { + debug("Received broadcast ${intent.action}") + when (intent.action) { + RadioInterfaceService.RADIO_CONNECTED_ACTION -> { + try { + onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false)) - debug("Received broadcast ${intent.action}") - when (intent.action) { - RadioInterfaceService.RADIO_CONNECTED_ACTION -> { - try { - onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false)) - - // forward the connection change message to anyone who is listening to us. but change the action - // to prevent an infinite loop from us receiving our own broadcast. ;-) - intent.action = ACTION_MESH_CONNECTED - explicitBroadcast(intent) - } catch (ex: RemoteException) { - // This can happen sometimes (especially if the device is slowly dying due to killing power, don't report to crashlytics - warn("Abandoning reconnect attempt, due to errors during init: ${ex.message}") + // forward the connection change message to anyone who is listening to us. but change the action + // to prevent an infinite loop from us receiving our own broadcast. ;-) + intent.action = ACTION_MESH_CONNECTED + explicitBroadcast(intent) + } catch (ex: RemoteException) { + // This can happen sometimes (especially if the device is slowly dying due to killing power, don't report to crashlytics + warn("Abandoning reconnect attempt, due to errors during init: ${ex.message}") + } } - } - RadioInterfaceService.RECEIVE_FROMRADIO_ACTION -> { - val proto = - MeshProtos.FromRadio.parseFrom( - intent.getByteArrayExtra( - EXTRA_PAYLOAD - )!! - ) - info("Received from radio service: ${proto.toOneLineString()}") - when (proto.variantCase.number) { - MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket( - proto.packet - ) + RadioInterfaceService.RECEIVE_FROMRADIO_ACTION -> { + val proto = + MeshProtos.FromRadio.parseFrom( + intent.getByteArrayExtra( + EXTRA_PAYLOAD + )!! + ) + info("Received from radio service: ${proto.toOneLineString()}") + when (proto.variantCase.number) { + MeshProtos.FromRadio.PACKET_FIELD_NUMBER -> handleReceivedMeshPacket( + proto.packet + ) - else -> TODO("Unexpected FromRadio variant") + else -> TODO("Unexpected FromRadio variant") + } } - } - else -> TODO("Unexpected radio interface broadcast") + else -> TODO("Unexpected radio interface broadcast") + } } } } diff --git a/build.gradle b/build.gradle index 1af1d03f6..ee34f2059 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,8 @@ buildscript { ext.kotlin_version = '1.3.61' ext.compose_version = '0.1.0-dev07' + ext.coroutines_version = "1.3.5" + repositories { google() jcenter() From f2d43332f7aceed0a6abc5309e649089012377a5 Mon Sep 17 00:00:00 2001 From: geeksville Date: Sat, 4 Apr 2020 15:29:16 -0700 Subject: [PATCH 3/4] Making app aware of device sleep states, Fix #4 --- TODO.md | 6 +- .../com/geeksville/mesh/IMeshService.aidl | 9 +- .../java/com/geeksville/mesh/MainActivity.kt | 10 +- .../java/com/geeksville/mesh/model/UIState.kt | 3 +- .../geeksville/mesh/service/MeshService.kt | 177 +++++++++++++----- .../mesh/service/RadioInterfaceService.kt | 3 +- .../java/com/geeksville/mesh/ui/MeshApp.kt | 52 ++--- 7 files changed, 181 insertions(+), 79 deletions(-) diff --git a/TODO.md b/TODO.md index 856196ed5..a83f6c0d0 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,11 @@ # High priority Work items for soon alpha builds -* use states for meshservice: disconnected -> connected -> deviceasleep -> disconnected +Document the following in application behavior +*change ls_secs is 1 hr normally, which is fine because if there are other nodes in the mesh and they send us a packet we will wake any time during ls_secs and update app state +* use states for meshservice: disconnected -> connected-> devsleep -> disconnected (3 states) +* when device enters LS state radiointerfaceservice publishes "Broadcasting connection=false", meshservice should then enter devicesleepstate for ls_secs + 30s (to allow for some margin) + * use compose on each page, but not for the outer wrapper * one view per page: https://developer.android.com/guide/navigation/navigation-swipe-view-2 diff --git a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl index ed2bca81c..a905e4478 100644 --- a/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl +++ b/app/src/main/aidl/com/geeksville/mesh/IMeshService.aidl @@ -30,8 +30,11 @@ interface IMeshService { typ is defined in mesh.proto Data.Type. For now juse use 0 to mean opaque bytes. destId can be null to indicate "broadcast message" + + Returns true if the packet has been sent into the mesh, or false if it was merely queued + inside the service - and will be delivered to mesh the next time we hear from our radio. */ - void sendData(String destId, in byte[] payload, int typ); + boolean sendData(String destId, in byte[] payload, int typ); /** Get the IDs of everyone on the mesh. You should also subscribe for NODE_CHANGE broadcasts. @@ -47,9 +50,9 @@ interface IMeshService { void setRadioConfig(in byte []payload); /** - Is the packet radio currently connected to the phone? + Is the packet radio currently connected to the phone? Returns a ConnectionState string. */ - boolean isConnected(); + String connectionState(); // see com.geeksville.com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index cf7a7c0b3..549f69de2 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -311,10 +311,10 @@ class MainActivity : AppCompatActivity(), Logging, } /// Called when we gain/lose a connection to our mesh radio - private fun onMeshConnectionChanged(connected: Boolean) { + private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) { UIState.isConnected.value = connected debug("connchange ${UIState.isConnected.value}") - if (connected) { + if (connected == MeshService.ConnectionState.CONNECTED) { // always get the current radio config when we connect readRadioConfig() @@ -383,7 +383,8 @@ class MainActivity : AppCompatActivity(), Logging, } } MeshService.ACTION_MESH_CONNECTED -> { - val connected = intent.getBooleanExtra(EXTRA_CONNECTED, false) + val connected = + MeshService.ConnectionState.valueOf(intent.getStringExtra(EXTRA_CONNECTED)!!) onMeshConnectionChanged(connected) } else -> TODO() @@ -402,7 +403,8 @@ class MainActivity : AppCompatActivity(), Logging, registerMeshReceiver() // We won't receive a notify for the initial state of connection, so we force an update here - onMeshConnectionChanged(service.isConnected) + val connectionState = MeshService.ConnectionState.valueOf(service.connectionState()) + onMeshConnectionChanged(connectionState) debug("connected to mesh service, isConnected=${UIState.isConnected.value}") } 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 9e509046e..a3fa1f6d8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIState.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIState.kt @@ -11,6 +11,7 @@ import com.geeksville.android.BuildUtils.isEmulator import com.geeksville.android.Logging import com.geeksville.mesh.IMeshService import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.ui.getInitials /// FIXME - figure out how to merge this staate with the AppStatus Model @@ -22,7 +23,7 @@ object UIState : Logging { var meshService: IMeshService? = null /// Are we connected to our radio device - val isConnected = mutableStateOf(false) + val isConnected = mutableStateOf(MeshService.ConnectionState.DISCONNECTED) /// various radio settings (including the channel) private val radioConfig = mutableStateOf(null) 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 c176e37e3..9f811f1ed 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt @@ -104,6 +104,12 @@ class MeshService : Service(), Logging { data class TextMessage(val fromId: String, val text: String) } + public enum class ConnectionState { + DISCONNECTED, + CONNECTED, + DEVICE_SLEEP // device is in LS sleep state, it will reconnected to us over bluetooth once it has data + } + /// A mapping of receiver class name to package name - used for explicit broadcasts private val clientPackages = mutableMapOf() @@ -114,6 +120,9 @@ class MeshService : Service(), Logging { private val serviceJob = Job() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) + /// The current state of our connection + private var connectionState = ConnectionState.DISCONNECTED + /* see com.geeksville.mesh broadcast intents // RECEIVED_OPAQUE for data received from other nodes @@ -165,7 +174,7 @@ class MeshService : Service(), Logging { ) } catch (ex: RadioNotConnectedException) { warn("Lost connection to radio, stopping location requests") - onConnectionChanged(false) + onConnectionChanged(ConnectionState.DEVICE_SLEEP) } } } @@ -262,7 +271,8 @@ class MeshService : Service(), Logging { /// Safely access the radio service, if not connected an exception will be thrown private val connectedRadio: IRadioInterfaceService - get() = (if (isConnected) radio.serviceP else null) ?: throw RadioNotConnectedException() + get() = (if (connectionState == ConnectionState.CONNECTED) radio.serviceP else null) + ?: throw RadioNotConnectedException() /// Send a command/packet to our radio. But cope with the possiblity that we might start up /// before we are fully bound to the RadioInterfaceService @@ -314,11 +324,13 @@ class MeshService : Service(), Logging { /// A text message that has a arrived since the last notification update private var recentReceivedText: TextMessage? = null - val summaryString - get() = if (!isConnected) - "No radio connected" - else - "Connected: $numOnlineNodes of $numNodes online" + private val summaryString + get() = when (connectionState) { + ConnectionState.CONNECTED -> "Connected: $numOnlineNodes of $numNodes online" + ConnectionState.DISCONNECTED -> "Disconnected" + ConnectionState.DEVICE_SLEEP -> "Device sleeping" + } + override fun toString() = summaryString @@ -416,8 +428,7 @@ class MeshService : Service(), Logging { var myNodeInfo: MyNodeInfo? = null - /// Is our radio connected to the phone? - private var isConnected = false + private var radioConfig: MeshProtos.RadioConfig? = null /// True after we've done our initial node db init private var haveNodeDB = false @@ -587,7 +598,10 @@ class MeshService : Service(), Logging { } /// If packets arrive before we have our node DB, we delay parsing them until the DB is ready - private val earlyPackets = mutableListOf() + private val earlyReceivedPackets = mutableListOf() + + /// If apps try to send packets when our radio is sleeping, we queue them here instead + private val offlineSentPackets = mutableListOf() /// Update our model and resend as needed for a MeshPacket we just received from the radio private fun handleReceivedMeshPacket(packet: MeshPacket) { @@ -595,17 +609,21 @@ class MeshService : Service(), Logging { processReceivedMeshPacket(packet) onNodeDBChanged() } else { - earlyPackets.add(packet) - logAssert(earlyPackets.size < 128) // The max should normally be about 32, but if the device is messed up it might try to send forever + earlyReceivedPackets.add(packet) + logAssert(earlyReceivedPackets.size < 128) // The max should normally be about 32, but if the device is messed up it might try to send forever } } /// Process any packets that showed up too early private fun processEarlyPackets() { - earlyPackets.forEach { processReceivedMeshPacket(it) } - earlyPackets.clear() + earlyReceivedPackets.forEach { processReceivedMeshPacket(it) } + earlyReceivedPackets.clear() + + offlineSentPackets.forEach { sendMeshPacket(it) } + offlineSentPackets.clear() } + /// Update our model and resend as needed for a MeshPacket we just received from the radio private fun processReceivedMeshPacket(packet: MeshPacket) { val fromNum = packet.from @@ -644,6 +662,7 @@ class MeshService : Service(), Logging { private fun currentSecond() = (System.currentTimeMillis() / 1000).toInt() + /// We are reconnecting to a radio, redownload the full state. This operation might take hundreds of milliseconds private fun reinitFromRadio() { // Read the MyNodeInfo object @@ -657,6 +676,8 @@ class MeshService : Service(), Logging { myNodeInfo = mi + radioConfig = MeshProtos.RadioConfig.parseFrom(connectedRadio.readRadioConfig()) + /// Track types of devices and firmware versions in use GeeksvilleApplication.analytics.setUserInfo( DataPair("region", mi.region), @@ -743,11 +764,42 @@ class MeshService : Service(), Logging { } + private var sleepTimeout: Job? = null + /// Called when we gain/lose connection to our radio - private fun onConnectionChanged(c: Boolean) { - debug("onConnectionChanged connected=$c") - isConnected = c - if (c) { + private fun onConnectionChanged(c: ConnectionState) { + debug("onConnectionChanged=$c") + + /// Perform all the steps needed once we start waiting for device sleep to complete + fun startDeviceSleep() { + // lost radio connection, therefore no need to keep listening to GPS + stopLocationRequests() + + // Have our timeout fire in the approprate number of seconds + sleepTimeout = serviceScope.handledLaunch { + try { + // If we have a valid timeout, wait that long (+30 seconds) otherwise, just wait 30 seconds + val timeout = (radioConfig?.preferences?.lsSecs ?: 0) + 30 + + debug("Waiting for sleeping device, timeout=$timeout secs") + delay(timeout * 1000L) + warn("Device timeout out, setting disconnected") + onConnectionChanged(ConnectionState.DISCONNECTED) + } catch (ex: CancellationException) { + debug("device sleep timeout cancelled") + } + } + } + + fun startDisconnect() { + GeeksvilleApplication.analytics.track( + "mesh_disconnect", + DataPair("num_nodes", numNodes), + DataPair("num_online", numOnlineNodes) + ) + } + + fun startConnect() { // Do our startup init try { reinitFromRadio() @@ -771,20 +823,37 @@ class MeshService : Service(), Logging { // It seems that when the ESP32 goes offline it can briefly come back for a 100ms ish which // causes the phone to try and reconnect. If we fail downloading our initial radio state we don't want to // claim we have a valid connection still - isConnected = false; + connectionState = ConnectionState.DEVICE_SLEEP + startDeviceSleep() throw ex; // Important to rethrow so that we don't tell the app all is well } - } else { - // lost radio connection, therefore no need to keep listening to GPS - stopLocationRequests() - - GeeksvilleApplication.analytics.track( - "mesh_disconnect", - DataPair("num_nodes", numNodes), - DataPair("num_online", numOnlineNodes) - ) } + // Cancel any existing timeouts + sleepTimeout?.let { + it.cancel() + sleepTimeout = null + } + + connectionState = c + when (c) { + ConnectionState.CONNECTED -> + startConnect() + ConnectionState.DEVICE_SLEEP -> + startDeviceSleep() + ConnectionState.DISCONNECTED -> + startDisconnect() + } + + // broadcast an intent with our new connection state + val intent = Intent(ACTION_MESH_CONNECTED) + intent.putExtra( + EXTRA_CONNECTED, + connectionState.toString() + ) + explicitBroadcast(intent) + + // Update the android notification in the status bar updateNotification() } @@ -801,12 +870,12 @@ class MeshService : Service(), Logging { when (intent.action) { RadioInterfaceService.RADIO_CONNECTED_ACTION -> { try { - onConnectionChanged(intent.getBooleanExtra(EXTRA_CONNECTED, false)) - - // forward the connection change message to anyone who is listening to us. but change the action - // to prevent an infinite loop from us receiving our own broadcast. ;-) - intent.action = ACTION_MESH_CONNECTED - explicitBroadcast(intent) + onConnectionChanged( + if (intent.getBooleanExtra(EXTRA_CONNECTED, false)) + ConnectionState.CONNECTED + else + ConnectionState.DEVICE_SLEEP + ) } catch (ex: RemoteException) { // This can happen sometimes (especially if the device is slowly dying due to killing power, don't report to crashlytics warn("Abandoning reconnect attempt, due to errors during init: ${ex.message}") @@ -870,6 +939,15 @@ class MeshService : Service(), Logging { }) } + /** + * Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException + */ + private fun sendMeshPacket(packet: MeshPacket) { + sendToRadio(ToRadio.newBuilder().apply { + this.packet = packet + }) + } + private val binder = object : IMeshService.Stub() { // Note: bound methods don't get properly exception caught/logged, so do that with a wrapper // per https://blog.classycode.com/dealing-with-exceptions-in-aidl-9ba904c6d63 @@ -900,9 +978,9 @@ class MeshService : Service(), Logging { connectedRadio.writeOwner(user.toByteArray()) } - override fun sendData(destId: String?, payloadIn: ByteArray, typ: Int) = + override fun sendData(destId: String?, payloadIn: ByteArray, typ: Int): Boolean = toRemoteExceptions { - info("sendData dest=$destId <- ${payloadIn.size} bytes") + info("sendData dest=$destId <- ${payloadIn.size} bytes (connectionState=$connectionState)") // encapsulate our payload in the proper protobufs and fire it off val packet = buildMeshPacket(destId) { @@ -911,24 +989,33 @@ class MeshService : Service(), Logging { it.payload = ByteString.copyFrom(payloadIn) }.build() } - - sendToRadio(ToRadio.newBuilder().apply { - this.packet = packet - }) + // If radio is sleeping, queue the packet + when (connectionState) { + ConnectionState.DEVICE_SLEEP -> + offlineSentPackets.add(packet) + else -> + sendMeshPacket(packet) + } GeeksvilleApplication.analytics.track( "data_send", DataPair("num_bytes", payloadIn.size), DataPair("type", typ) ) + + connectionState == ConnectionState.CONNECTED } override fun getRadioConfig(): ByteArray = toRemoteExceptions { - connectedRadio.readRadioConfig() + this@MeshService.radioConfig?.toByteArray() ?: throw RadioNotConnectedException() } override fun setRadioConfig(payload: ByteArray) = toRemoteExceptions { + // Update our device connectedRadio.writeRadioConfig(payload) + + // Update our cached copy + this@MeshService.radioConfig = MeshProtos.RadioConfig.parseFrom(payload) } override fun getNodes(): Array = toRemoteExceptions { @@ -938,10 +1025,10 @@ class MeshService : Service(), Logging { r } - override fun isConnected(): Boolean = toRemoteExceptions { - val r = this@MeshService.isConnected - info("in isConnected=$r") - r + override fun connectionState(): String = toRemoteExceptions { + val r = this@MeshService.connectionState + info("in connectionState=$r") + r.toString() } } } \ 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 a88861b02..c4d2ab131 100644 --- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt @@ -301,7 +301,8 @@ class RadioInterfaceService : Service(), Logging { info("Connected to radio!") if (!hasForcedRefresh) { - hasForcedRefresh = true + // FIXME - for some reason we need to refresh _everytime_. It is almost as if we've cached wrong descriptor fieldnums forever + // hasForcedRefresh = true forceServiceRefresh() } diff --git a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt index b0313b1e0..c87e3a21e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/MeshApp.kt @@ -4,10 +4,7 @@ import androidx.compose.Composable import androidx.compose.state import androidx.ui.core.ContextAmbient import androidx.ui.core.Text -import androidx.ui.layout.Column -import androidx.ui.layout.Container -import androidx.ui.layout.LayoutSize -import androidx.ui.layout.Row +import androidx.ui.layout.* import androidx.ui.material.* import androidx.ui.tooling.preview.Preview import androidx.ui.unit.dp @@ -15,6 +12,7 @@ import com.geeksville.android.Logging import com.geeksville.mesh.R import com.geeksville.mesh.model.NodeDB import com.geeksville.mesh.model.UIState +import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.RadioInterfaceService import com.geeksville.mesh.service.SoftwareUpdateService @@ -36,34 +34,40 @@ fun HomeContent() { Column { Row { + fun connected() = UIState.isConnected.value != MeshService.ConnectionState.DISCONNECTED VectorImage( - id = if (UIState.isConnected.value) R.drawable.cloud_on else R.drawable.cloud_off, - tint = palette.onBackground // , modifier = LayoutSize(40.dp, 40.dp) + id = if (connected()) R.drawable.cloud_on else R.drawable.cloud_off, + tint = palette.onBackground, + modifier = LayoutPadding(start = 8.dp) ) - if (UIState.isConnected.value) { - Column { - Text("Connected") + Column { - if (false) { // hide the firmware update button for now, it is kinda ugly and users don't need it yet - /// Create a software update button - val context = ContextAmbient.current - RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress -> - Button( - onClick = { - SoftwareUpdateService.enqueueWork( - context, - SoftwareUpdateService.startUpdateIntent(macAddress) - ) - } - ) { - Text(text = "Update firmware") + Text( + when (UIState.isConnected.value) { + MeshService.ConnectionState.CONNECTED -> "Connected" + MeshService.ConnectionState.DISCONNECTED -> "Disconnected" + MeshService.ConnectionState.DEVICE_SLEEP -> "Power Saving" + }, + modifier = LayoutPadding(start = 8.dp) + ) + + if (false) { // hide the firmware update button for now, it is kinda ugly and users don't need it yet + /// Create a software update button + val context = ContextAmbient.current + RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress -> + Button( + onClick = { + SoftwareUpdateService.enqueueWork( + context, + SoftwareUpdateService.startUpdateIntent(macAddress) + ) } + ) { + Text(text = "Update firmware") } } } - } else { - Text("Not Connected") } } From f1a0bf70015a7c734715002cc1ddafe600feba5a Mon Sep 17 00:00:00 2001 From: geeksville Date: Sat, 4 Apr 2020 17:00:52 -0700 Subject: [PATCH 4/4] release 0.2.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 650a633a3..828b773de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { applicationId "com.geeksville.mesh" minSdkVersion 22 // The oldest emulator image I have tried is 22 (though 21 probably works) targetSdkVersion 29 - versionCode 122 - versionName "0.2.2" + versionCode 123 + versionName "0.2.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes {