Making app aware of device sleep states, Fix #4

1.2-legacy
geeksville 2020-04-04 15:29:16 -07:00
rodzic 83c1bfda69
commit f2d43332f7
7 zmienionych plików z 181 dodań i 79 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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