kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Making app aware of device sleep states, Fix #4
rodzic
83c1bfda69
commit
f2d43332f7
6
TODO.md
6
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue