[+] Scan activity

This commit is contained in:
Azalea Gui
2023-01-23 14:56:17 -05:00
parent 032423fa4d
commit e867a080b5
4 changed files with 160 additions and 69 deletions
@@ -1,32 +1,40 @@
package org.hydev.wearsync.bles
package com.welie.blessedexample
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.View
import android.widget.TextView
import android.widget.ArrayAdapter
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.welie.blessed.BluetoothPeripheral
import org.hydev.wearsync.bles.BluetoothHandler.Companion.getInstance
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.consumeAsFlow
import org.hydev.wearsync.R
import org.hydev.wearsync.bles.BluetoothHandler
import org.hydev.wearsync.bles.ObservationUnit
import org.hydev.wearsync.databinding.ActivityScanBinding
import org.hydev.wearsync.snack
import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
class MainActivity : AppCompatActivity() {
class ActivityScan : AppCompatActivity() {
lateinit var binding: ActivityScanBinding
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var measurementValue: TextView? = null
private val dateFormat: DateFormat = SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.ENGLISH)
private val enableBluetoothRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
@@ -40,21 +48,22 @@ class MainActivity : AppCompatActivity() {
}
private val bluetoothManager by lazy {
applicationContext
.getSystemService(BLUETOOTH_SERVICE)
as BluetoothManager
applicationContext.getSystemService(BLUETOOTH_SERVICE) as BluetoothManager
}
private lateinit var bluetoothHandler: BluetoothHandler
private fun askToEnableBluetooth() {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
enableBluetoothRequest.launch(enableBtIntent)
}
@SuppressLint("MissingPermission")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
measurementValue = findViewById<View>(R.id.bloodPressureValue) as TextView
binding = ActivityScanBinding.inflate(layoutInflater)
setContentView(binding.root)
registerReceiver(
locationServiceStateReceiver,
IntentFilter(LocationManager.MODE_CHANGED_ACTION)
@@ -64,74 +73,86 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (bluetoothManager.adapter != null) {
if (!isBluetoothEnabled) {
askToEnableBluetooth()
} else {
checkPermissions()
}
if (!isBluetoothEnabled) askToEnableBluetooth() else checkPermissions()
} else {
Timber.e("This device has no Bluetooth hardware")
}
}
private val isBluetoothEnabled: Boolean
get() {
val bluetoothAdapter = bluetoothManager.adapter ?: return false
return bluetoothAdapter.isEnabled
get() = bluetoothManager.adapter?.isEnabled ?: false
private val central get() = bluetoothHandler.central
@SuppressLint("MissingPermission")
private fun initBluetoothHandler() {
if (this::bluetoothHandler.isInitialized) return
bluetoothHandler = BluetoothHandler.getInstance(applicationContext)
println("OnCreate called, Initializing...")
// List bonded device addresses
val pairedDevices = bluetoothManager.adapter.bondedDevices.toList()
val pairedAddresses = pairedDevices.map { it.address }.toSet()
// Scan devices
val scannedDevices = ArrayList<BluetoothPeripheral>()
central.scanForPeripherals({ peripheral, scanResult ->
if (peripheral.name.isBlank() || scannedDevices.contains(peripheral)) return@scanForPeripherals
// Add to scanned devices
scannedDevices.add(peripheral)
Handler(Looper.getMainLooper()).post {
binding.lvScanned.adapter = ArrayAdapter(applicationContext, android.R.layout.simple_list_item_1,
scannedDevices.map {
"${it.name + if (it.address in pairedAddresses) " (Paired)" else ""}\n" +
"MAC Address: ${it.address}"
})
}
}, {})
// Click scanned device
binding.lvScanned.setOnItemClickListener { parent, view, position, id ->
central.stopScan()
bluetoothHandler.connectPeripheral(scannedDevices[position])
}
// Format and show bounded device list
binding.lvPaired.adapter = ArrayAdapter(applicationContext, android.R.layout.simple_list_item_1,
pairedDevices.map { "Name: ${it.name}\nMAC Address: ${it.address}" })
private fun initBluetoothHandler() {
val bluetoothHandler = getInstance(applicationContext)
// On click handler
binding.lvPaired.setOnItemClickListener { parent, view, position, id ->
// Extract MAC address
val dev = pairedDevices[position]
println("Clicked: ${dev.address}")
view.snack("Connecting...")
// Scan for the device with the MAC address
central.stopScan()
central.scanForPeripheralsWithAddresses(arrayOf(dev.address), { peripheral, scanResult ->
if (peripheral.address != dev.address) return@scanForPeripheralsWithAddresses
view.snack("✅ Connected.")
central.stopScan()
bluetoothHandler.connectPeripheral(peripheral)
}, {})
}
collectBloodPressure(bluetoothHandler)
collectHeartRate(bluetoothHandler)
collectGlucose(bluetoothHandler)
collectPulseOxContinuous(bluetoothHandler)
collectPulseOxSpot(bluetoothHandler)
collectTemperature(bluetoothHandler)
collectWeight(bluetoothHandler)
}
private fun collectBloodPressure(bluetoothHandler: BluetoothHandler) {
scope.launch {
bluetoothHandler.bloodpressureChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue!!.text = String.format(
Locale.ENGLISH,
"%.0f/%.0f %s, %.0f bpm\n%s\n",
it.systolic,
it.diastolic,
if (it.unit == ObservationUnit.MMHG) "mmHg" else "kpa",
it.pulseRate,
dateFormat.format(it.timestamp ?: Calendar.getInstance())
)
}
}
}
}
private fun collectGlucose(bluetoothHandler: BluetoothHandler) {
scope.launch {
bluetoothHandler.glucoseChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue!!.text = String.format(
Locale.ENGLISH,
"%.1f %s\n%s\n",
it.value,
if (it.unit === ObservationUnit.MmolPerLiter) "mmol/L" else "mg/dL",
dateFormat.format(it.timestamp ?: Calendar.getInstance()),
)
}
}
}
}
private fun collectHeartRate(bluetoothHandler: BluetoothHandler) {
scope.launch {
bluetoothHandler.heartRateChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue?.text = String.format(Locale.ENGLISH, "%d bpm", it.pulse)
binding.mainText.text = String.format(Locale.ENGLISH, "%d bpm", it.pulse)
}
}
}
@@ -141,7 +162,7 @@ class MainActivity : AppCompatActivity() {
scope.launch {
bluetoothHandler.pulseOxContinuousChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue!!.text = String.format(
binding.mainText.text = String.format(
Locale.ENGLISH,
"SpO2 %d%%, Pulse %d bpm\n%s\n\nfrom %s",
it.spO2,
@@ -157,7 +178,7 @@ class MainActivity : AppCompatActivity() {
scope.launch {
bluetoothHandler.pulseOxSpotChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue!!.text = String.format(
binding.mainText.text = String.format(
Locale.ENGLISH,
"SpO2 %d%%, Pulse %d bpm\n",
it.spO2,
@@ -172,7 +193,7 @@ class MainActivity : AppCompatActivity() {
scope.launch {
bluetoothHandler.temperatureChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue?.text = String.format(
binding.mainText.text = String.format(
Locale.ENGLISH,
"%.1f %s (%s)\n%s\n",
it.temperatureValue,
@@ -189,7 +210,7 @@ class MainActivity : AppCompatActivity() {
scope.launch {
bluetoothHandler.weightChannel.consumeAsFlow().collect {
withContext(Dispatchers.Main) {
measurementValue!!.text = String.format(
binding.mainText.text = String.format(
Locale.ENGLISH,
"%.1f %s\n%s\n",
it.weight, it.unit.toString(),
@@ -217,8 +238,7 @@ class MainActivity : AppCompatActivity() {
}
private fun getPeripheral(peripheralAddress: String): BluetoothPeripheral {
val central = getInstance(applicationContext).central
return central.getPeripheral(peripheralAddress)
return bluetoothHandler.central.getPeripheral(peripheralAddress)
}
private fun checkPermissions() {
@@ -277,7 +297,7 @@ class MainActivity : AppCompatActivity() {
private fun checkLocationServices(): Boolean {
return if (!areLocationServicesEnabled()) {
AlertDialog.Builder(this@MainActivity)
AlertDialog.Builder(this)
.setTitle("Location services are not enabled")
.setMessage("Scanning for Bluetooth peripherals requires locations services to be enabled.") // Want to enable?
.setPositiveButton("Enable") { dialogInterface, _ ->
@@ -315,7 +335,7 @@ class MainActivity : AppCompatActivity() {
if (allGranted) {
checkIfLocationIsNeeded()
} else {
AlertDialog.Builder(this@MainActivity)
AlertDialog.Builder(this)
.setTitle("Location permission is required for scanning Bluetooth peripherals")
.setMessage("Please grant permissions")
.setPositiveButton("Retry") { dialogInterface, _ ->
@@ -0,0 +1,7 @@
package org.hydev.wearsync
import android.view.View
import com.google.android.material.snackbar.Snackbar
fun View.snack(msg: String) = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
.setAction("Action", null).show()
@@ -218,7 +218,7 @@ internal class BluetoothHandler private constructor(context: Context) {
{ scanFailure -> Timber.e("scan failed with reason $scanFailure") })
}
private fun connectPeripheral(peripheral: BluetoothPeripheral) {
fun connectPeripheral(peripheral: BluetoothPeripheral) {
peripheral.observeBondState {
Timber.i("Bond state is $it")
}
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/mainText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Waiting for measurement..."
android:textAlignment="center"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvPaired"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Paired Devices"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvScanned"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Scanned Devices"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/lvPaired" />
<ListView
android:id="@+id/lvPaired"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvPaired" />
<ListView
android:id="@+id/lvScanned"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvScanned" />
</androidx.constraintlayout.widget.ConstraintLayout>