Making app aware of device sleep states, Fix #4

This commit is contained in:
geeksville
2020-04-04 15:29:16 -07:00
parent 83c1bfda69
commit f2d43332f7
7 changed files with 181 additions and 79 deletions

View File

@@ -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}")
}

View File

@@ -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<MeshProtos.RadioConfig?>(null)

View File

@@ -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<String, String>()
@@ -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<MeshPacket>()
private val earlyReceivedPackets = mutableListOf<MeshPacket>()
/// If apps try to send packets when our radio is sleeping, we queue them here instead
private val offlineSentPackets = mutableListOf<MeshPacket>()
/// 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<NodeInfo> = 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()
}
}
}

View File

@@ -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()
}

View File

@@ -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")
}
}