mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-06 05:42:40 -05:00
Merge pull request #10 from geeksville/master
make app aware of device sleep states
This commit is contained in:
12
TODO.md
12
TODO.md
@@ -1,6 +1,16 @@
|
||||
# High priority
|
||||
Work items for soon alpha builds
|
||||
|
||||
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
|
||||
* 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 +29,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
|
||||
|
||||
@@ -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 {
|
||||
@@ -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'
|
||||
|
||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -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 <fields>; }
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
@@ -89,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>()
|
||||
|
||||
@@ -96,6 +117,12 @@ class MeshService : Service(), Logging {
|
||||
IRadioInterfaceService.Stub.asInterface(it)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -117,7 +144,7 @@ class MeshService : Service(), Logging {
|
||||
private var lastSendMsec = 0L
|
||||
|
||||
override fun onLocationResult(locationResult: LocationResult) {
|
||||
exceptionReporter {
|
||||
serviceScope.handledLaunch {
|
||||
super.onLocationResult(locationResult)
|
||||
var l = locationResult.lastLocation
|
||||
|
||||
@@ -147,7 +174,7 @@ class MeshService : Service(), Logging {
|
||||
)
|
||||
} catch (ex: RadioNotConnectedException) {
|
||||
warn("Lost connection to radio, stopping location requests")
|
||||
onConnectionChanged(false)
|
||||
onConnectionChanged(ConnectionState.DEVICE_SLEEP)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +190,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
|
||||
@@ -243,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
|
||||
@@ -295,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
|
||||
|
||||
@@ -375,6 +406,7 @@ class MeshService : Service(), Logging {
|
||||
radio.close()
|
||||
|
||||
super.onDestroy()
|
||||
serviceJob.cancel()
|
||||
}
|
||||
|
||||
|
||||
@@ -396,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
|
||||
@@ -567,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) {
|
||||
@@ -575,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
|
||||
@@ -624,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
|
||||
@@ -637,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),
|
||||
@@ -712,19 +753,53 @@ 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")
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
@@ -748,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()
|
||||
}
|
||||
|
||||
@@ -773,41 +865,42 @@ class MeshService : Service(), Logging {
|
||||
|
||||
// Important to never throw exceptions out of onReceive
|
||||
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
|
||||
|
||||
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}")
|
||||
serviceScope.handledLaunch {
|
||||
debug("Received broadcast ${intent.action}")
|
||||
when (intent.action) {
|
||||
RadioInterfaceService.RADIO_CONNECTED_ACTION -> {
|
||||
try {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -846,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
|
||||
@@ -876,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) {
|
||||
@@ -887,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 {
|
||||
@@ -914,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,13 +291,20 @@ 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<Unit>) {
|
||||
// This callback is invoked after we are connected
|
||||
|
||||
connRes.getOrThrow() // FIXME, instead just try to reconnect?
|
||||
info("Connected to radio!")
|
||||
|
||||
forceServiceRefresh()
|
||||
if (!hasForcedRefresh) {
|
||||
// FIXME - for some reason we need to refresh _everytime_. It is almost as if we've cached wrong descriptor fieldnums forever
|
||||
// hasForcedRefresh = true
|
||||
forceServiceRefresh()
|
||||
}
|
||||
|
||||
// FIXME - no need to discover services more than once - instead use lazy() to use them in future attempts
|
||||
safe!!.asyncDiscoverServices { discRes ->
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user