Meshtastic-Android/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
geeksville ce14fde33b Fix #36 - allow users to select 'none' for the preferred radio
If none is selected, we do not leave our service running.  And we do
not start it on boot.
2020-06-07 18:55:08 -07:00

258 lines
8.6 KiB
Kotlin

package com.geeksville.mesh.service
import android.annotation.SuppressLint
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.IBinder
import androidx.core.content.edit
import com.geeksville.android.BinaryLogFile
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.IRadioInterfaceService
import com.geeksville.util.ignoreException
import com.geeksville.util.toRemoteExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
class RadioNotConnectedException(message: String = "Not connected to radio") :
BLEException(message)
/**
* Handles the bluetooth link with a mesh radio device. Does not cache any device state,
* just does bluetooth comms etc...
*
* This service is not exposed outside of this process.
*
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
*/
class RadioInterfaceService : Service(), Logging {
companion object : Logging {
/**
* The RECEIVED_FROMRADIO
* Payload will be the raw bytes which were contained within a MeshProtos.FromRadio protobuf
*/
const val RECEIVE_FROMRADIO_ACTION = "$prefix.RECEIVE_FROMRADIO"
/**
* This is broadcast when connection state changed
*/
const val RADIO_CONNECTED_ACTION = "$prefix.CONNECT_CHANGED"
const val DEVADDR_KEY_OLD = "devAddr"
const val DEVADDR_KEY = "devAddr2" // the new name for devaddr
/// This is public only so that SimRadio can bootstrap our message flow
fun broadcastReceivedFromRadio(context: Context, payload: ByteArray) {
val intent = Intent(RECEIVE_FROMRADIO_ACTION)
intent.putExtra(EXTRA_PAYLOAD, payload)
context.sendBroadcast(intent)
}
fun getPrefs(context: Context): SharedPreferences =
context.getSharedPreferences("radio-prefs", Context.MODE_PRIVATE)
/** Return the device we are configured to use, or null for none
* device address strings are of the form:
*
* at
*
* where a is either x for bluetooth or s for serial
* and t is an interface specific address (macaddr or a device path)
*/
@SuppressLint("NewApi")
fun getBondedDeviceAddress(context: Context): String? {
// If the user has unpaired our device, treat things as if we don't have one
val prefs = getPrefs(context)
var address = prefs.getString(DEVADDR_KEY, null)
if (address == null) { /// Check for the old preferences name we used to use
val rest = prefs.getString(DEVADDR_KEY_OLD, null)
if (rest != null)
address = "x$rest" // Add the bluetooth prefix
}
/// Interfaces can filter addresses to indicate that address is no longer acceptable
if (address != null) {
val c = address[0]
val rest = address.substring(1)
val isValid = when (c) {
'x' -> BluetoothInterface.addressValid(context, rest)
else -> true
}
if (!isValid)
return null
}
return address
}
/// If our service is currently running, this pointer can be used to reach it (in case setBondedDeviceAddress is called)
private var runningService: RadioInterfaceService? = null
}
private val logSends = false
private val logReceives = false
private lateinit var sentPacketsLog: BinaryLogFile // inited in onCreate
private lateinit var receivedPacketsLog: BinaryLogFile
private val serviceJob = Job()
val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private val nopIf = NopInterface()
private var radioIf: IRadioInterface = nopIf
/**
* If the user turns on bluetooth after we start, make sure to try and reconnected then
*/
private val bluetoothStateReceiver = BluetoothStateReceiver { enabled ->
if (enabled)
startInterface() // If bluetooth just got turned on, try to restart our ble link
}
fun broadcastConnectionChanged(isConnected: Boolean, isPermanent: Boolean) {
debug("Broadcasting connection=$isConnected")
val intent = Intent(RADIO_CONNECTED_ACTION)
intent.putExtra(EXTRA_CONNECTED, isConnected)
intent.putExtra(EXTRA_PERMANENT, isPermanent)
sendBroadcast(intent)
}
/// Send a packet/command out the radio link, this routine can block if it needs to
private fun handleSendToRadio(p: ByteArray) {
radioIf.handleSendToRadio(p)
}
// Handle an incoming packet from the radio, broadcasts it as an android intent
fun handleFromRadio(p: ByteArray) {
if (logReceives) {
receivedPacketsLog.write(p)
receivedPacketsLog.flush()
}
broadcastReceivedFromRadio(
this,
p
)
}
fun onDisconnect(isPermanent: Boolean) {
broadcastConnectionChanged(false, isPermanent)
}
override fun onCreate() {
runningService = this
super.onCreate()
registerReceiver(bluetoothStateReceiver, bluetoothStateReceiver.intent)
startInterface()
}
override fun onDestroy() {
unregisterReceiver(bluetoothStateReceiver)
stopInterface()
serviceJob.cancel()
runningService = null
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
return binder;
}
/** Start our configured interface (if it isn't already running) */
private fun startInterface() {
if (radioIf != nopIf)
warn("Can't start interface - $radioIf is already running")
else {
val address = getBondedDeviceAddress(this)
if (address == null)
warn("No bonded mesh radio, can't start interface")
else {
info("Starting radio $address")
if (logSends)
sentPacketsLog = BinaryLogFile(this, "sent_log.pb")
if (logReceives)
receivedPacketsLog = BinaryLogFile(this, "receive_log.pb")
val c = address[0]
val rest = address.substring(1)
radioIf = when (c) {
'x' -> BluetoothInterface(this, rest)
's' -> SerialInterface(this, rest)
'n' -> nopIf
else -> TODO("Unexpected radio interface type")
}
}
}
}
private fun stopInterface() {
val r = radioIf
info("stopping interface $r")
radioIf = nopIf
r.close()
if (logSends)
sentPacketsLog.close()
if (logReceives)
receivedPacketsLog.close()
// Don't broadcast disconnects if we were just using the nop device
if (radioIf != nopIf)
onDisconnect(isPermanent = true) // Tell any clients we are now offline
}
@SuppressLint("NewApi")
fun setBondedDeviceAddress(addressIn: String?) {
// Record that this use has configured a radio
GeeksvilleApplication.analytics.track(
"mesh_bond"
)
// Ignore any errors that happen while closing old device
ignoreException {
stopInterface()
}
// The device address "n" can be used to mean none
val address = if ("n" == addressIn) null else addressIn
debug("Setting bonded device to $address")
getPrefs(this).edit(commit = true) {
this.remove(DEVADDR_KEY_OLD) // remove any old version of the key
if (address == null)
this.remove(DEVADDR_KEY)
else
putString(DEVADDR_KEY, address)
}
// Force the service to reconnect
startInterface()
}
private val binder = object : IRadioInterfaceService.Stub() {
override fun setDeviceAddress(deviceAddr: String?) = toRemoteExceptions {
setBondedDeviceAddress(deviceAddr)
}
override fun sendToRadio(a: ByteArray) {
// Do this in the IO thread because it might take a while (and we don't care about the result code)
serviceScope.handledLaunch { handleSendToRadio(a) }
}
}
}