move to geeksville/mesh

This commit is contained in:
geeksville 2020-01-22 21:46:41 -08:00
parent 0903bfab6f
commit 3551eedb8a
15 changed files with 30 additions and 32 deletions

View file

@ -0,0 +1,14 @@
package com.geeksville.mesh
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootCompleteReceiver : BroadcastReceiver() {
override fun onReceive(mContext: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
// FIXME - start listening for bluetooth messages from our device
}
}
}

View file

@ -0,0 +1,135 @@
package com.geeksville.mesh
import android.Manifest
import android.bluetooth.*
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.compose.Composable
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.ui.core.Text
import androidx.ui.core.dp
import androidx.ui.core.setContent
import androidx.ui.layout.Column
import androidx.ui.layout.Spacing
import androidx.ui.material.Button
import androidx.ui.material.MaterialTheme
import androidx.ui.tooling.preview.Preview
import com.geeksville.android.Logging
class MainActivity : AppCompatActivity(), Logging {
companion object {
const val REQUEST_ENABLE_BT = 10
const val DID_REQUEST_PERM = 11
}
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
fun requestPermission() {
debug("Checking permissions")
val perms = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.WAKE_LOCK
)
val missingPerms = perms.filter {
ContextCompat.checkSelfPermission(
this,
it
) != PackageManager.PERMISSION_GRANTED
}
if (missingPerms.isNotEmpty()) {
missingPerms.forEach {
// Permission is not granted
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(this, it)) {
// FIXME
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
}
}
// Ask for all the missing perms
ActivityCompat.requestPermissions(this, missingPerms.toTypedArray(), DID_REQUEST_PERM)
// DID_REQUEST_PERM is an
// app-defined int constant. The callback method gets the
// result of the request.
} else {
// Permission has already been granted
}
}
@Preview
@Composable
fun composeView() {
MaterialTheme {
Column {
Text(text = "MeshUtil Ugly UI", modifier = Spacing(8.dp))
Button(text = "Start scan",
onClick = {
if (bluetoothAdapter != null) {
SoftwareUpdateService.enqueueWork(
this@MainActivity,
SoftwareUpdateService.scanDevicesIntent
)
}
})
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
composeView()
}
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (bluetoothAdapter != null) {
bluetoothAdapter!!.takeIf { !it.isEnabled }?.apply {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
} else {
Toast.makeText(this, "Error - this app requires bluetooth", Toast.LENGTH_LONG).show()
}
requestPermission()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
}

View file

@ -0,0 +1,31 @@
package com.geeksville.mesh
import android.app.Service
import android.content.Intent
import android.os.IBinder
class MeshService : Service() {
override fun onBind(intent: Intent): IBinder {
// Return the interface
return binder
}
private val binder = object : IMeshService.Stub() {
override fun setOwner(myId: String?, longName: String?, shortName: String?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun sendOpaque(destId: String?, payload: ByteArray?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getOnline(ids: Array<out String>?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun isConnected(): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}

View file

@ -0,0 +1,6 @@
package com.geeksville.mesh
import com.geeksville.android.GeeksvilleApplication
class MeshUtilApplication : GeeksvilleApplication(null, "58e72ccc361883ea502510baa46580e3") {
}

View file

@ -0,0 +1,294 @@
package com.geeksville.mesh
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.ParcelUuid
import android.os.SystemClock
import android.widget.Toast
import androidx.core.app.JobIntentService
import com.geeksville.android.Logging
import java.io.InputStream
import java.util.*
/**
* typical flow
*
* startScan
* startUpdate
* sendNextBlock
* finishUpdate
*
* stopScan
*
* FIXME - if we don't find a device stop our scan
* FIXME - broadcast when we found devices, made progress sending blocks or when the update is complete
* FIXME - make the user decide to start an update on a particular device
*/
class SoftwareUpdateService : JobIntentService(), Logging {
private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter!!
}
fun startUpdate() {
firmwareStream = assets.open("firmware.bin")
// Start the update by writing the # of bytes in the image
val numBytes = firmwareStream.available()
logAssert(totalSizeDesc.setValue(numBytes, BluetoothGattCharacteristic.FORMAT_UINT32, 0))
logAssert(updateGatt.writeCharacteristic(totalSizeDesc))
}
// Send the next block of our file to the device
fun sendNextBlock() {
if (firmwareStream.available() > 0) {
var blockSize = 512
if (blockSize > firmwareStream.available())
blockSize = firmwareStream.available()
val buffer = ByteArray(blockSize)
// slightly expensive to keep reallocing this buffer, but whatever
logAssert(firmwareStream.read(buffer) == blockSize)
// updateGatt.beginReliableWrite()
dataDesc.value = buffer
logAssert(updateGatt.writeCharacteristic(dataDesc))
} else {
logAssert(false) // fixme
}
}
private val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
throw NotImplementedError()
}
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
throw NotImplementedError()
}
// For each device that appears in our scan, ask for its GATT, when the gatt arrives,
// check if it is an eligable device and store it in our list of candidates
// if that device later disconnects remove it as a candidate
override fun onScanResult(callbackType: Int, result: ScanResult) {
// We don't need any more results now
bluetoothAdapter.bluetoothLeScanner.stopScan(this)
lateinit var bluetoothGatt: BluetoothGatt // late init so we can declare our callback and use this there
//var connectionState = STATE_DISCONNECTED
// Various callback methods defined by the BLE API.
val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
info("new bluetooth connection state $newState")
//val intentAction: String
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
//intentAction = ACTION_GATT_CONNECTED
//connectionState = STATE_CONNECTED
// broadcastUpdate(intentAction)
logAssert(bluetoothGatt.discoverServices())
}
BluetoothProfile.STATE_DISCONNECTED -> {
//intentAction = ACTION_GATT_DISCONNECTED
//connectionState = STATE_DISCONNECTED
// broadcastUpdate(intentAction)
}
}
}
// New services discovered
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
logAssert(status == BluetoothGatt.GATT_SUCCESS)
// broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED)
val service = gatt.services.find { it.uuid == SW_UPDATE_UUID }
if (service != null) {
// FIXME instead of slamming in the target device here, instead make it a param for startUpdate
updateService = service
totalSizeDesc = service.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)
dataDesc = service.getCharacteristic(SW_UPDATE_DATA_CHARACTER)
crc32Desc = service.getCharacteristic(SW_UPDATE_CRC32_CHARACTER)
updateResultDesc = service.getCharacteristic(SW_UPDATE_RESULT_CHARACTER)
// FIXME instead of keeping the connection open, make start update just reconnect (needed once user can choose devices)
updateGatt = bluetoothGatt
enqueueWork(this@SoftwareUpdateService, startUpdateIntent)
}
}
// Result of a characteristic read operation
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
logAssert(status == BluetoothGatt.GATT_SUCCESS)
if (characteristic == totalSizeDesc) {
// Our read of this has completed, either fail or continue updating
val readvalue =
characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0)
logAssert(readvalue != 0) // FIXME - handle this case
enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent)
}
// broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?,
status: Int
) {
logAssert(status == BluetoothGatt.GATT_SUCCESS)
if(characteristic == totalSizeDesc) {
// Our write completed, queue up a readback
logAssert(updateGatt.readCharacteristic(totalSizeDesc))
} else if (characteristic == dataDesc) {
enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent)
}
}
}
bluetoothGatt =
result.device.connectGatt(this@SoftwareUpdateService.applicationContext, true, gattCallback)!!
toast("FISH " + bluetoothGatt)
// too early to do this here
// logAssert(bluetoothGatt.discoverServices())
}
}
private fun scanLeDevice(enable: Boolean) {
when (enable) {
true -> {
// Stops scanning after a pre-defined scan period.
/* handler.postDelayed({
mScanning = false
bluetoothAdapter.stopLeScan(leScanCallback)
}, SCAN_PERIOD)
mScanning = true */
val scanner = bluetoothAdapter.bluetoothLeScanner
// filter and only accept devices that have a sw update service
val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SW_UPDATE_UUID)).build()
/* ScanSettings.CALLBACK_TYPE_FIRST_MATCH seems to trigger a bug returning an error of
SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES (error #5)
*/
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).
// setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT).
// setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH).
build()
scanner.startScan(listOf(filter), settings, scanCallback)
}
else -> {
// mScanning = false
// bluetoothAdapter.stopLeScan(leScanCallback)
}
}
}
override fun onHandleWork(intent: Intent) { // We have received work to do. The system or framework is already
// holding a wake lock for us at this point, so we can just go.
info("Executing work: $intent")
var label = intent.getStringExtra("label")
if (label == null) {
label = intent.toString()
}
toast("Executing: $label")
when (intent.action) {
scanDevicesIntent.action -> scanLeDevice(true)
startUpdateIntent.action -> startUpdate()
sendNextBlockIntent.action -> sendNextBlock()
else -> logAssert(false)
}
info(
"Completed service @ " + SystemClock.elapsedRealtime()
)
}
override fun onDestroy() {
super.onDestroy()
toast("All work complete")
}
val mHandler = Handler()
// Helper for showing tests
fun toast(text: CharSequence?) {
mHandler.post {
Toast.makeText(this@SoftwareUpdateService, text, Toast.LENGTH_SHORT).show()
}
}
companion object {
/**
* Unique job ID for this service. Must be the same for all work.
*/
const val JOB_ID = 1000
val scanDevicesIntent = Intent("com.geeksville.com.geeeksville.mesh.SCAN_DEVICES")
val startUpdateIntent = Intent("com.geeksville.com.geeeksville.mesh.START_UPDATE")
private val sendNextBlockIntent = Intent("com.geeksville.com.geeeksville.mesh.SEND_NEXT_BLOCK")
private val finishUpdateIntent = Intent("com.geeksville.com.geeeksville.mesh.FINISH_UPDATE")
private const val SCAN_PERIOD: Long = 10000
//const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED"
//const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"
private val TAG =
MainActivity::class.java.simpleName // FIXME - use my logging class instead
private val SW_UPDATE_UUID = UUID.fromString("cb0b9a0b-a84c-4c0d-bdbb-442e3144ee30")
private val SW_UPDATE_TOTALSIZE_CHARACTER =
UUID.fromString("e74dd9c0-a301-4a6f-95a1-f0e1dbea8e1e") // write|read total image size, 32 bit, write this first, then read read back to see if it was acceptable (0 mean not accepted)
private val SW_UPDATE_DATA_CHARACTER =
UUID.fromString("e272ebac-d463-4b98-bc84-5cc1a39ee517") // write data, variable sized, recommended 512 bytes, write one for each block of file
private val SW_UPDATE_CRC32_CHARACTER =
UUID.fromString("4826129c-c22a-43a3-b066-ce8f0d5bacc6") // write crc32, write last - writing this will complete the OTA operation, now you can read result
private val SW_UPDATE_RESULT_CHARACTER =
UUID.fromString("5e134862-7411-4424-ac4a-210937432c77") // read|notify result code, readable but will notify when the OTA operation completes
// FIXME - this is state that really more properly goes with the serice instance, but
// it can go away if our work queue gets empty. So we keep it here instead. Not sure
// if there is a better approach?
lateinit var updateGatt: BluetoothGatt // the gatt api used to talk to our device
lateinit var updateService: BluetoothGattService // The service we are currently talking to to do the update
lateinit var totalSizeDesc: BluetoothGattCharacteristic
lateinit var dataDesc: BluetoothGattCharacteristic
lateinit var crc32Desc: BluetoothGattCharacteristic
lateinit var updateResultDesc: BluetoothGattCharacteristic
lateinit var firmwareStream: InputStream
/**
* Convenience method for enqueuing work in to this service.
*/
fun enqueueWork(context: Context, work: Intent) {
enqueueWork(
context,
SoftwareUpdateService::class.java, JOB_ID, work
)
}
}
}