feat: display list of measurements

This commit is contained in:
Fabian Christoffel
2023-06-25 11:16:12 +02:00
parent 2be1a5d6e9
commit f3d56ba59c
13 changed files with 807 additions and 312 deletions

View File

@@ -1,27 +1,16 @@
/*
* Copyright 2019 Punch Through Design LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.sensortestingapp
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCallback
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.util.Log
import java.util.UUID
@@ -74,7 +63,19 @@ data class DiscoverServicesRequest(
) : BleOperationType()
open class BleListener {
open class BleListener(private val deviceAddress: String?) {
open fun isRelevantMessage(address: String?): Boolean {
if (deviceAddress != null && deviceAddress == address) {
return true;
}
return deviceAddress == null
}
open fun onConnect(gatt: BluetoothGatt) {}
open fun onDisconnect(gatt: BluetoothGatt) {}
open fun onSuccessfulCharRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
@@ -86,6 +87,27 @@ open class BleListener {
characteristic: BluetoothGattCharacteristic
) {
}
open fun onSubscribe(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
) {
}
open fun onUnsubscribe(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
) {
}
open fun onScanningStateChange(isScanning: Boolean) {
}
open fun onScanResult(callbackType: Int, result: ScanResult) {
}
}
@@ -138,28 +160,67 @@ fun ByteArray.toHexString(): String =
joinToString(separator = "", prefix = "0x") { String.format("%02X", it) }
@SuppressLint("MissingPermission")
object ConnectionManager {
class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
private var pendingOperation: BleOperationType? = null
private var listeners = ArrayList<BleListener>()
private val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
private val bleScanner by lazy {
bleAdapter.bluetoothLeScanner
}
var isScanning = false
set(value) {
field = value
notifyListeners(null) {
it.onScanningStateChange(value)
}
}
@SuppressLint("MissingPermission")
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
Log.d(
"ScanCallback",
"Found BLE device with address ${result.device.address} (name: ${result.device.name}, rssi: ${result.rssi})"
)
notifyListeners(result.device.address) { it.onScanResult(callbackType, result) }
}
override fun onScanFailed(errorCode: Int) {
Log.e("ScanCallback", "onScanFailed: code $errorCode")
}
}
fun register(listener: BleListener) {
listeners.add(listener)
}
private fun notifyListeners(notifier: (listener: BleListener) -> Unit) {
listeners.forEach(notifier)
fun startScan() {
if (!isScanning) {
isScanning = true
bleScanner.startScan(null, scanSettings, scanCallback)
}
}
fun stopScan() {
bleScanner.stopScan(scanCallback)
isScanning = false
}
fun connect(device: BluetoothDevice, context: Context) {
fun connect(device: BluetoothDevice) {
if (device.isConnected()) {
Log.e("ConnectionManager", "Already connected to ${device.address}!")
} else {
enqueueOperation(Connect(device, context.applicationContext))
enqueueOperation(Connect(device, context))
}
}
@@ -174,15 +235,13 @@ object ConnectionManager {
}
}
fun requestMtu(device: BluetoothDevice, mtu: Int) {
fun requestMtu(device: BluetoothDevice, mtu: Int = Int.MAX_VALUE) {
enqueueOperation(MtuRequest(device, mtu.coerceIn(GATT_MIN_MTU_SIZE, GATT_MAX_MTU_SIZE)))
}
fun discoverServices(device: BluetoothDevice) {
enqueueOperation(DiscoverServicesRequest(device))
}
fun readChar(device: BluetoothDevice, service: UUID, char: UUID) {
@@ -200,8 +259,16 @@ object ConnectionManager {
// - Beginning of PRIVATE functions
private fun notifyListeners(address: String?, notifier: (listener: BleListener) -> Unit) {
listeners.filter { n -> n.isRelevantMessage(address) }.forEach(notifier)
}
@Synchronized
private fun enqueueOperation(operation: BleOperationType) {
if (isScanning) {
stopScan()
}
operationQueue.add(operation)
if (pendingOperation == null) {
doNextOperation()
@@ -217,11 +284,6 @@ object ConnectionManager {
}
}
/**
* Perform a given [BleOperationType]. All permission checks are performed before an operation
* can be enqueued by [enqueueOperation].
*/
@Synchronized
private fun doNextOperation() {
if (pendingOperation != null) {
@@ -266,6 +328,9 @@ object ConnectionManager {
Log.w("ConnectionManager", "Disconnecting from ${device.address}")
gatt.close()
deviceGattMap.remove(device)
notifyListeners(gatt.device.address) {
it.onDisconnect(gatt)
}
signalEndOfOperation()
}
@@ -355,11 +420,14 @@ object ConnectionManager {
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.w(
Log.i(
"ConnectionManager",
"onConnectionStateChange: connected to $deviceAddress"
)
deviceGattMap[gatt.device] = gatt
notifyListeners(gatt.device.address) {
it.onConnect(gatt)
}
if (pendingOperation is Connect) {
signalEndOfOperation()
}
@@ -368,6 +436,9 @@ object ConnectionManager {
"ConnectionManager",
"onConnectionStateChange: disconnected from $deviceAddress"
)
notifyListeners(gatt.device.address) {
it.onDisconnect(gatt)
}
teardownConnection(gatt.device)
}
} else {
@@ -388,6 +459,9 @@ object ConnectionManager {
"Discovered ${services.size} services for ${device.address}."
)
printGattTable()
notifyListeners(gatt.device.address) {
it.onConnect(gatt)
}
} else {
Log.e("ConnectionManager", "Service discovery failed due to status $status")
teardownConnection(gatt.device)
@@ -422,7 +496,7 @@ object ConnectionManager {
"ConnectionManager",
"Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}"
)
notifyListeners { listener ->
notifyListeners(gatt.device.address) { listener ->
listener.onSuccessfulCharRead(
gatt,
characteristic
@@ -462,6 +536,16 @@ object ConnectionManager {
descriptor.uuid == CCC_DESCRIPTOR_UUID
&& op.charId == descriptor.characteristic.uuid && op.serviceId == descriptor.characteristic.service.uuid
) {
if (!descriptor.value.contentEquals(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)) {
notifyListeners(gatt.device.address) {
it.onSubscribe(gatt, descriptor)
}
} else {
notifyListeners(gatt.device.address) {
it.onUnsubscribe(gatt, descriptor)
}
}
signalEndOfOperation()
}
}
@@ -470,7 +554,12 @@ object ConnectionManager {
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
notifyListeners { listener -> listener.onCharChange(gatt, characteristic) }
notifyListeners(gatt.device.address) { listener ->
listener.onCharChange(
gatt,
characteristic
)
}
}

View File

@@ -0,0 +1,147 @@
package com.example.sensortestingapp
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.ListView
import android.widget.PopupMenu
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
interface Action {
fun getLabel(): String
fun getIcon(): String
fun execute(): Unit
}
interface Measurement {
fun getLabel(): String
fun getFormattedValue(): String
}
interface DeviceListEntry {
val address: String
var rssi: Int
val name: String?
val status: String?
fun getActions(): List<Action>
fun getMeasurements(): List<Measurement>
}
@SuppressLint("MissingPermission")
class DeviceListAdapter(
private val items: List<DeviceListEntry>,
) : RecyclerView.Adapter<DeviceListAdapter.ViewHolder>() {
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder)
*/
class ViewHolder(
private val view: View,
private val context: Context
) : RecyclerView.ViewHolder(view) {
val deviceNameView: TextView
val macAddressView: TextView
val signalStrengthView: TextView
val statusView: TextView
val measurementsListView: ListView
val deviceActions: Button
init {
deviceNameView = view.findViewById(R.id.device_name)
macAddressView = view.findViewById(R.id.mac_address)
signalStrengthView = view.findViewById(R.id.signal_strength)
statusView = view.findViewById(R.id.device_status)
measurementsListView = view.findViewById(R.id.measurement_fields)
deviceActions = view.findViewById(R.id.device_actions)
}
fun bind(result: DeviceListEntry) {
deviceNameView.text = result.name ?: "<N/A>"
macAddressView.text = result.address
signalStrengthView.text = "${result.rssi ?: "-"} dBm"
statusView.text = result.status
deviceActions.setOnClickListener {
val popup = PopupMenu(context, deviceActions)
result.getActions().forEach { action ->
popup.menu.add(action.getLabel())
.setOnMenuItemClickListener { menuItem ->
action.execute()
true
}
}
val inflater = popup.menuInflater
inflater.inflate(R.menu.device_menu, popup.menu)
popup.show()
}
val measurements = result.getMeasurements()
val measurementsRowHeight = 100
val measurementsAdapter = object :
ArrayAdapter<Measurement>(
context,
R.layout.row_measurements_list,
measurements
) {
override fun getView(
position: Int,
convertView: View?,
parent: ViewGroup
): View {
val measurementView = convertView ?: LayoutInflater.from(parent.context)
.inflate(R.layout.row_measurements_list, parent, false)
measurementView.findViewById<TextView>(R.id.measurement_label).text =
measurements[position].getLabel()
measurementView.findViewById<TextView>(R.id.measurement_value).text =
measurements[position].getFormattedValue()
measurementView.layoutParams.height = measurementsRowHeight
return measurementView
}
override fun isEnabled(position: Int): Boolean {
// make measurement rows unclickable
return false
}
}
measurementsListView.adapter = measurementsAdapter
measurementsListView.divider = null
measurementsListView.layoutParams.height = measurementsRowHeight * measurements.size
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.row_device_list, viewGroup, false)
return ViewHolder(view, viewGroup.context)
}
override fun getItemCount() = items.size
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Get element from your dataset at this position and replace the
// contents of the view with that element
val item = items[position]
holder.bind(item)
}
}

View File

@@ -0,0 +1,262 @@
package com.example.sensortestingapp
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.le.ScanResult
import android.util.Log
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.EnumSet
import java.util.UUID
import java.util.stream.Collectors
private val DEMO_SERVICE_UUID = UUID.fromString("00000000-0001-11E1-9AB4-0002A5D5C51B")
private val DEMO_CHAR_UUID = UUID.fromString("00140000-0001-11E1-AC36-0002A5D5C51B")
data class DemoPayload(
val ts: Int,
val pressure: Float,
val temperature: Float,
val sysTs: String = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("HH:mm:ss"))
)
fun bytesToUInt16(arr: ByteArray, start: Int): Int {
return ByteBuffer.wrap(arr, start, 2)
.order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF
}
fun bytesToInt16(arr: ByteArray, start: Int): Short {
return ByteBuffer.wrap(arr, start, 2)
.order(ByteOrder.LITTLE_ENDIAN).short
}
fun bytesToInt32(arr: ByteArray, start: Int): Int {
return ByteBuffer.wrap(arr, start, 4)
.order(ByteOrder.LITTLE_ENDIAN).int
}
fun decodeDemoPayload(bytes: ByteArray): DemoPayload {
val ts = bytesToUInt16(bytes, 0)
val pressure = bytesToInt32(bytes, 2) / 100.0f
val temp = bytesToInt16(bytes, 6) / 10.0f;
return DemoPayload(ts, pressure, temp)
}
enum class DeviceStatus {
DISCOVERED, CONNECTED, BONDED, SUBSCRIBED
}
@SuppressLint("MissingPermission")
class KirbyDevice(
private val connectionManager: ConnectionManager,
private val bleDevice: BluetoothDevice,
initialRssi: Int,
private val onStateChange: (device: KirbyDevice) -> Unit
) : BleListener(bleDevice.address), DeviceListEntry {
override fun onScanResult(callbackType: Int, result: ScanResult) {
statuses.add(DeviceStatus.DISCOVERED)
onStateChange(this)
}
override fun onConnect(gatt: BluetoothGatt) {
statuses.add(DeviceStatus.CONNECTED)
onStateChange(this)
}
override fun onDisconnect(gatt: BluetoothGatt) {
statuses.remove(DeviceStatus.CONNECTED)
statuses.remove(DeviceStatus.SUBSCRIBED)
onStateChange(this)
}
override fun onSuccessfulCharRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
addMeasurement(characteristic)
onStateChange(this)
}
override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
addMeasurement(characteristic)
//statuses.add(DeviceStatus.SUBSCRIBED)
onStateChange(this)
}
override fun onSubscribe(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
) {
statuses.add(DeviceStatus.SUBSCRIBED)
onStateChange(this)
}
override fun onUnsubscribe(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor) {
statuses.remove(DeviceStatus.SUBSCRIBED)
onStateChange(this)
}
override var rssi = initialRssi
set(value) {
field = value
onStateChange(this)
}
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
if (characteristic.service.uuid == DEMO_SERVICE_UUID && characteristic.uuid == DEMO_CHAR_UUID) {
val payload = decodeDemoPayload(characteristic.value)
Log.i("BleListener", "Demo char received: $payload")
measurements.add(payload)
}
}
private val measurements = ArrayList<DemoPayload>()
private val statuses = EnumSet.noneOf(DeviceStatus::class.java)
override val address: String
get() = bleDevice.address
override val name: String?
get() = bleDevice.name
override val status: String?
get() = statuses.stream().map { it.name }.collect(Collectors.joining(", "))
// override fun getRssi(): Int? {
// return rssi
// }
override fun getMeasurements(): List<Measurement> {
if (measurements.isEmpty()) {
return emptyList()
}
val latest = measurements.last()
return listOf(object : Measurement {
override fun getLabel(): String {
return "TS"
}
override fun getFormattedValue(): String {
return "${latest.sysTs} (${latest.ts})"
}
}, object : Measurement {
override fun getLabel(): String {
return "Temperature"
}
override fun getFormattedValue(): String {
return "${latest.temperature} °C"
}
}, object : Measurement {
override fun getLabel(): String {
return "Pressure"
}
override fun getFormattedValue(): String {
return "${latest.pressure} hPa"
}
})
}
override fun getActions(): List<Action> {
val actions = mutableListOf<Action>()
if (statuses.contains(DeviceStatus.DISCOVERED) && !statuses.contains(DeviceStatus.CONNECTED)
) {
actions.add(object : Action {
override fun getLabel(): String {
return "Connect"
}
override fun getIcon(): String {
return ""
}
override fun execute() {
connectionManager.connect(bleDevice)
connectionManager.discoverServices(bleDevice)
}
})
}
if (statuses.contains(DeviceStatus.CONNECTED)) {
actions.add(object : Action {
override fun getLabel(): String {
return "Disconnect"
}
override fun getIcon(): String {
return ""
}
override fun execute() {
connectionManager.teardownConnection(bleDevice)
}
})
actions.add(object : Action {
override fun getLabel(): String {
return "Fetch Measurement"
}
override fun getIcon(): String {
TODO("Not yet implemented")
}
override fun execute() {
connectionManager.readChar(bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID)
}
})
if (!statuses.contains(DeviceStatus.SUBSCRIBED)) {
actions.add(object : Action {
override fun getLabel(): String {
return "Subscribe"
}
override fun getIcon(): String {
return ""
}
override fun execute() {
connectionManager.enableNotification(
bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID
)
}
})
}
}
if (statuses.contains(DeviceStatus.SUBSCRIBED)) {
actions.add(object : Action {
override fun getLabel(): String {
return "Unsubscribe"
}
override fun getIcon(): String {
return ""
}
override fun execute() {
connectionManager.disableNotification(
bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID
)
}
})
}
return actions;
}
}

View File

@@ -5,12 +5,8 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothManager
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@@ -28,26 +24,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.example.sensortestingapp.databinding.ActivityMainBinding
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID
private const val ENABLE_BLUETOOTH_REQUEST_CODE = 1
private const val RUNTIME_PERMISSION_REQUEST_CODE = 2
// Top level declaration
private const val GATT_MAX_MTU_SIZE = 517
// Top level declaration
private const val GATT_REQUESTED_MTU_SIZE = GATT_MAX_MTU_SIZE
private val DEMO_SERVICE_UUID = UUID.fromString("00000000-0001-11E1-9AB4-0002A5D5C51B")
private val DEMO_CHAR_UUID = UUID.fromString("00140000-0001-11E1-AC36-0002A5D5C51B")
fun Context.hasPermission(permissionType: String): Boolean {
return ContextCompat.checkSelfPermission(this, permissionType) ==
PackageManager.PERMISSION_GRANTED
@@ -62,114 +42,27 @@ fun Context.hasRequiredRuntimePermissions(): Boolean {
}
}
data class DemoPayload(val ts: Int, val pressure: Float, val temperature: Float)
fun bytesToUInt16(arr: ByteArray, start: Int): Int {
return ByteBuffer.wrap(arr, start, 2)
.order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF
}
fun bytesToInt16(arr: ByteArray, start: Int): Short {
return ByteBuffer.wrap(arr, start, 2)
.order(ByteOrder.LITTLE_ENDIAN).short
}
fun bytesToInt32(arr: ByteArray, start: Int): Int {
return ByteBuffer.wrap(arr, start, 4)
.order(ByteOrder.LITTLE_ENDIAN).int
}
fun decodeDemoPayload(bytes: ByteArray): DemoPayload {
val ts = bytesToUInt16(bytes, 0)
val pressure = bytesToInt32(bytes, 2) / 100.0f
val temp = bytesToInt16(bytes, 6) / 10.0f;
return DemoPayload(ts, pressure, temp)
}
@SuppressLint("MissingPermission")
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.build()
private val bluetoothAdapter: BluetoothAdapter by lazy {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
private val bleScanner by lazy {
bluetoothAdapter.bluetoothLeScanner
private val connectionManager: ConnectionManager by lazy {
newConnectionManager()
}
private var isScanning = false
set(value) {
field = value
runOnUiThread {
binding.fab.setText(if (value) "Stop Scan" else "Start Scan")
}
}
private val kirbyDevices = mutableListOf<DeviceListEntry>();
private val kirbyScanResults = ArrayList<ScanResult>();
private var mtuSizeInBytes = 23
private val scanResultAdapter: ScanResultAdapter by lazy {
ScanResultAdapter(kirbyScanResults) { scanResult ->
// User tapped on a scan result
if (isScanning) {
stopBleScan()
}
Log.i(
"ConnectCallback",
"Start connecting to ${scanResult.device} (${scanResult.device.name})"
)
ConnectionManager.connect(scanResult.device, applicationContext)
ConnectionManager.requestMtu(scanResult.device, Int.MAX_VALUE)
ConnectionManager.discoverServices(scanResult.device)
//ConnectionManager.readChar(scanResult.device, DEMO_SERVICE_UUID, DEMO_CHAR_UUID)
ConnectionManager.enableNotification(
scanResult.device,
DEMO_SERVICE_UUID,
DEMO_CHAR_UUID
)
}
private val deviceListAdapter: DeviceListAdapter by lazy {
DeviceListAdapter(kirbyDevices)
}
private val bleListener = object : BleListener() {
fun logDemoPayload(characteristic: BluetoothGattCharacteristic) {
if (characteristic.service.uuid == DEMO_SERVICE_UUID && characteristic.uuid == DEMO_CHAR_UUID) {
val payload = decodeDemoPayload(characteristic.value)
Log.i("BleListener", "Demo char received: $payload")
}
}
override fun onSuccessfulCharRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
logDemoPayload(characteristic)
}
override fun onCharChange(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
logDemoPayload(characteristic)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
@@ -182,37 +75,97 @@ class MainActivity : AppCompatActivity() {
binding.fab.setOnClickListener { view ->
if (!isScanning) {
startBleScan()
} else {
stopBleScan()
if (!hasRequiredRuntimePermissions()) {
requestRelevantRuntimePermissions()
}
if (connectionManager.isScanning) {
connectionManager.stopScan()
} else {
startBleScan()
}
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
// .setAnchorView(R.id.fab)
// .setAction("Action", null).show()
}
setupRecyclerView()
ConnectionManager.register(bleListener)
setupDevicesList()
}
private fun setupRecyclerView() {
binding.scanResultsRecyclerView.apply {
adapter = scanResultAdapter
private fun setupDevicesList() {
binding.devicesList.apply {
adapter = deviceListAdapter
layoutManager = LinearLayoutManager(
this@MainActivity,
RecyclerView.VERTICAL,
false
)
isNestedScrollingEnabled = false
//isNestedScrollingEnabled = false
}
val animator = binding.scanResultsRecyclerView.itemAnimator
val animator = binding.devicesList.itemAnimator
if (animator is SimpleItemAnimator) {
animator.supportsChangeAnimations = false
}
//addDummyDevices()
}
@SuppressLint("NotifyDataSetChanged")
private fun addDummyDevices() {
for (i in 0..14) {
kirbyDevices.add(DummyListEntry("$i"))
}
deviceListAdapter.notifyDataSetChanged()
}
private fun newConnectionManager(): ConnectionManager {
val mngr = ConnectionManager(applicationContext, bluetoothAdapter)
mngr.register(object : BleListener(null) {
override fun onScanningStateChange(isScanning: Boolean) {
runOnUiThread {
binding.fab.setText(if (isScanning) "Stop Scan" else "Start Scan")
}
}
override fun onScanResult(callbackType: Int, result: ScanResult) {
if ((result.device.name ?: "").lowercase().contains("kirby")) {
Log.i(
"ScanCallback",
"Found Kirby device with name ${result.device.name} (address: ${result.device.address}, rssi: ${result.rssi})"
)
val kirbyDevice = kirbyDevices.find { it.address == result.device.address }
if (kirbyDevice == null) {
newKirbyDevice(callbackType, result)
} else {
kirbyDevice.rssi = result.rssi
}
}
}
})
return mngr
}
private fun newKirbyDevice(callbackType: Int, scanResult: ScanResult): KirbyDevice {
val device = KirbyDevice(connectionManager, scanResult.device, scanResult.rssi) {
val i = kirbyDevices.indexOfFirst { d -> d === it }
runOnUiThread {
deviceListAdapter.notifyItemChanged(i)
}
}
connectionManager.register(device)
kirbyDevices.add(device)
device.onScanResult(callbackType, scanResult)
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
return device
}
private fun startBleScan() {
if (!hasRequiredRuntimePermissions()) {
requestRelevantRuntimePermissions()
} else {
connectionManager.startScan()
// kirbyDevices.clear()
//deviceListAdapter.notifyDataSetChanged()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -231,7 +184,6 @@ class MainActivity : AppCompatActivity() {
}
}
override fun onResume() {
super.onResume()
if (!bluetoothAdapter.isEnabled) {
@@ -296,51 +248,6 @@ class MainActivity : AppCompatActivity() {
}
}
@SuppressLint("MissingPermission")
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
Log.d(
"ScanCallback",
"Found BLE device with address ${result.device.address} (name: ${result.device.name}, rssi: ${result.rssi})"
)
if ((result.device.name ?: "").lowercase().contains("kirby")) {
Log.i(
"ScanCallback",
"Found Kirby device with name ${result.device.name} (address: ${result.device.address}, rssi: ${result.rssi})"
)
val index =
kirbyScanResults.indexOfFirst { r -> r.device.address == result.device.address }
if (index == -1) {
kirbyScanResults.add(result);
scanResultAdapter.notifyItemInserted(index)
} else {
kirbyScanResults[index] = result
scanResultAdapter.notifyItemChanged(index)
}
}
}
override fun onScanFailed(errorCode: Int) {
Log.e("ScanCallback", "onScanFailed: code $errorCode")
}
}
private fun startBleScan() {
if (!hasRequiredRuntimePermissions()) {
requestRelevantRuntimePermissions()
} else {
kirbyScanResults.clear()
scanResultAdapter.notifyDataSetChanged()
bleScanner.startScan(null, scanSettings, scanCallback)
isScanning = true
}
}
private fun stopBleScan() {
bleScanner.stopScan(scanCallback)
isScanning = false
}
private fun Activity.requestRelevantRuntimePermissions() {
if (hasRequiredRuntimePermissions()) {
return
@@ -357,8 +264,6 @@ class MainActivity : AppCompatActivity() {
}
private fun requestLocationPermission() {
val onClick = { dialog: DialogInterface, which: Int ->
ActivityCompat.requestPermissions(
this,
@@ -366,8 +271,6 @@ class MainActivity : AppCompatActivity() {
RUNTIME_PERMISSION_REQUEST_CODE
)
}
runOnUiThread {
val builder = AlertDialog.Builder(this)
@@ -389,7 +292,6 @@ class MainActivity : AppCompatActivity() {
}
private fun requestBluetoothPermissions() {
val onClick = { dialog: DialogInterface, which: Int ->
ActivityCompat.requestPermissions(
this,
@@ -400,8 +302,6 @@ class MainActivity : AppCompatActivity() {
RUNTIME_PERMISSION_REQUEST_CODE
)
}
runOnUiThread {
val builder = AlertDialog.Builder(this)
@@ -420,7 +320,63 @@ class MainActivity : AppCompatActivity() {
show()
}
}
}
}
@SuppressLint("MissingPermission")
class DummyListEntry(override val address: String) : DeviceListEntry {
override var rssi: Int = 99
override val name: String = "Device 123"
override val status: String
get() = "statusA, statusB"
override fun getActions(): List<Action> {
return listOf(object : Action {
override fun getLabel(): String {
return "Test action 1"
}
override fun getIcon(): String {
return ""
}
override fun execute() {
}
}, object : Action {
override fun getLabel(): String {
return "Test action 2"
}
override fun getIcon(): String {
return ""
}
override fun execute() {
}
})
}
override fun getMeasurements(): List<Measurement> {
return listOf(object : Measurement {
override fun getLabel(): String {
return "Temperature"
}
override fun getFormattedValue(): String {
return "21.2 °C"
}
}, object : Measurement {
override fun getLabel(): String {
return "Pressure"
}
override fun getFormattedValue(): String {
return "232 bar"
}
})
}
}

View File

@@ -1,64 +0,0 @@
package com.example.sensortestingapp
import android.annotation.SuppressLint
import android.bluetooth.le.ScanResult
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
@SuppressLint("MissingPermission")
class ScanResultAdapter(
private val items: List<ScanResult>,
private val onClickListener: ((device: ScanResult) -> Unit)
) : RecyclerView.Adapter<ScanResultAdapter.ViewHolder>() {
/**
* Provide a reference to the type of views that you are using
* (custom ViewHolder)
*/
class ViewHolder(
private val view: View,
private val onClickListener: ((device: ScanResult) -> Unit)
) : RecyclerView.ViewHolder(view) {
val deviceNameView: TextView
val macAddressView: TextView
val signalStrengthView: TextView
init {
deviceNameView = view.findViewById(R.id.device_name)
macAddressView = view.findViewById(R.id.mac_address)
signalStrengthView = view.findViewById(R.id.signal_strength)
}
fun bind(result: ScanResult) {
deviceNameView.text = result.device.name ?: "Unnamed"
macAddressView.text = result.device.address
signalStrengthView.text = "${result.rssi} dBm"
view.setOnClickListener { onClickListener.invoke(result) }
}
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
// Create a new view, which defines the UI of the list item
val view = LayoutInflater.from(viewGroup.context)
.inflate(R.layout.row_scan_result, viewGroup, false)
return ViewHolder(view, onClickListener)
}
override fun getItemCount() = items.size
// Replace the contents of a view (invoked by the layout manager)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// Get element from your dataset at this position and replace the
// contents of the view with that element
val item = items[position]
holder.bind(item)
}
}