feat: display list of measurements
This commit is contained in:
@@ -46,5 +46,5 @@ dependencies {
|
|||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||||
}
|
}
|
||||||
@@ -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
|
package com.example.sensortestingapp
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothGatt
|
import android.bluetooth.BluetoothGatt
|
||||||
import android.bluetooth.BluetoothGattCallback
|
import android.bluetooth.BluetoothGattCallback
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
import android.bluetooth.BluetoothGattDescriptor
|
import android.bluetooth.BluetoothGattDescriptor
|
||||||
import android.bluetooth.BluetoothProfile
|
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.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -74,7 +63,19 @@ data class DiscoverServicesRequest(
|
|||||||
) : BleOperationType()
|
) : 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(
|
open fun onSuccessfulCharRead(
|
||||||
gatt: BluetoothGatt,
|
gatt: BluetoothGatt,
|
||||||
characteristic: BluetoothGattCharacteristic
|
characteristic: BluetoothGattCharacteristic
|
||||||
@@ -86,6 +87,27 @@ open class BleListener {
|
|||||||
characteristic: BluetoothGattCharacteristic
|
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) }
|
joinToString(separator = "", prefix = "0x") { String.format("%02X", it) }
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
object ConnectionManager {
|
class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||||
|
|
||||||
private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
|
private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
|
||||||
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
|
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
|
||||||
private var pendingOperation: BleOperationType? = null
|
private var pendingOperation: BleOperationType? = null
|
||||||
private var listeners = ArrayList<BleListener>()
|
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) {
|
fun register(listener: BleListener) {
|
||||||
listeners.add(listener)
|
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()) {
|
if (device.isConnected()) {
|
||||||
Log.e("ConnectionManager", "Already connected to ${device.address}!")
|
Log.e("ConnectionManager", "Already connected to ${device.address}!")
|
||||||
} else {
|
} else {
|
||||||
enqueueOperation(Connect(device, context.applicationContext))
|
enqueueOperation(Connect(device, context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,15 +235,13 @@ object ConnectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestMtu(device: BluetoothDevice, mtu: Int = Int.MAX_VALUE) {
|
||||||
fun requestMtu(device: BluetoothDevice, mtu: Int) {
|
|
||||||
enqueueOperation(MtuRequest(device, mtu.coerceIn(GATT_MIN_MTU_SIZE, GATT_MAX_MTU_SIZE)))
|
enqueueOperation(MtuRequest(device, mtu.coerceIn(GATT_MIN_MTU_SIZE, GATT_MAX_MTU_SIZE)))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun discoverServices(device: BluetoothDevice) {
|
fun discoverServices(device: BluetoothDevice) {
|
||||||
enqueueOperation(DiscoverServicesRequest(device))
|
enqueueOperation(DiscoverServicesRequest(device))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun readChar(device: BluetoothDevice, service: UUID, char: UUID) {
|
fun readChar(device: BluetoothDevice, service: UUID, char: UUID) {
|
||||||
@@ -200,8 +259,16 @@ object ConnectionManager {
|
|||||||
|
|
||||||
// - Beginning of PRIVATE functions
|
// - Beginning of PRIVATE functions
|
||||||
|
|
||||||
|
private fun notifyListeners(address: String?, notifier: (listener: BleListener) -> Unit) {
|
||||||
|
listeners.filter { n -> n.isRelevantMessage(address) }.forEach(notifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun enqueueOperation(operation: BleOperationType) {
|
private fun enqueueOperation(operation: BleOperationType) {
|
||||||
|
if (isScanning) {
|
||||||
|
stopScan()
|
||||||
|
}
|
||||||
operationQueue.add(operation)
|
operationQueue.add(operation)
|
||||||
if (pendingOperation == null) {
|
if (pendingOperation == null) {
|
||||||
doNextOperation()
|
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
|
@Synchronized
|
||||||
private fun doNextOperation() {
|
private fun doNextOperation() {
|
||||||
if (pendingOperation != null) {
|
if (pendingOperation != null) {
|
||||||
@@ -266,6 +328,9 @@ object ConnectionManager {
|
|||||||
Log.w("ConnectionManager", "Disconnecting from ${device.address}")
|
Log.w("ConnectionManager", "Disconnecting from ${device.address}")
|
||||||
gatt.close()
|
gatt.close()
|
||||||
deviceGattMap.remove(device)
|
deviceGattMap.remove(device)
|
||||||
|
notifyListeners(gatt.device.address) {
|
||||||
|
it.onDisconnect(gatt)
|
||||||
|
}
|
||||||
signalEndOfOperation()
|
signalEndOfOperation()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,11 +420,14 @@ object ConnectionManager {
|
|||||||
|
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
if (newState == BluetoothProfile.STATE_CONNECTED) {
|
||||||
Log.w(
|
Log.i(
|
||||||
"ConnectionManager",
|
"ConnectionManager",
|
||||||
"onConnectionStateChange: connected to $deviceAddress"
|
"onConnectionStateChange: connected to $deviceAddress"
|
||||||
)
|
)
|
||||||
deviceGattMap[gatt.device] = gatt
|
deviceGattMap[gatt.device] = gatt
|
||||||
|
notifyListeners(gatt.device.address) {
|
||||||
|
it.onConnect(gatt)
|
||||||
|
}
|
||||||
if (pendingOperation is Connect) {
|
if (pendingOperation is Connect) {
|
||||||
signalEndOfOperation()
|
signalEndOfOperation()
|
||||||
}
|
}
|
||||||
@@ -368,6 +436,9 @@ object ConnectionManager {
|
|||||||
"ConnectionManager",
|
"ConnectionManager",
|
||||||
"onConnectionStateChange: disconnected from $deviceAddress"
|
"onConnectionStateChange: disconnected from $deviceAddress"
|
||||||
)
|
)
|
||||||
|
notifyListeners(gatt.device.address) {
|
||||||
|
it.onDisconnect(gatt)
|
||||||
|
}
|
||||||
teardownConnection(gatt.device)
|
teardownConnection(gatt.device)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -388,6 +459,9 @@ object ConnectionManager {
|
|||||||
"Discovered ${services.size} services for ${device.address}."
|
"Discovered ${services.size} services for ${device.address}."
|
||||||
)
|
)
|
||||||
printGattTable()
|
printGattTable()
|
||||||
|
notifyListeners(gatt.device.address) {
|
||||||
|
it.onConnect(gatt)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e("ConnectionManager", "Service discovery failed due to status $status")
|
Log.e("ConnectionManager", "Service discovery failed due to status $status")
|
||||||
teardownConnection(gatt.device)
|
teardownConnection(gatt.device)
|
||||||
@@ -422,7 +496,7 @@ object ConnectionManager {
|
|||||||
"ConnectionManager",
|
"ConnectionManager",
|
||||||
"Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}"
|
"Read characteristic $uuid (service: ${service.uuid}): ${value.toHexString()}"
|
||||||
)
|
)
|
||||||
notifyListeners { listener ->
|
notifyListeners(gatt.device.address) { listener ->
|
||||||
listener.onSuccessfulCharRead(
|
listener.onSuccessfulCharRead(
|
||||||
gatt,
|
gatt,
|
||||||
characteristic
|
characteristic
|
||||||
@@ -462,6 +536,16 @@ object ConnectionManager {
|
|||||||
descriptor.uuid == CCC_DESCRIPTOR_UUID
|
descriptor.uuid == CCC_DESCRIPTOR_UUID
|
||||||
&& op.charId == descriptor.characteristic.uuid && op.serviceId == descriptor.characteristic.service.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()
|
signalEndOfOperation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,7 +554,12 @@ object ConnectionManager {
|
|||||||
gatt: BluetoothGatt,
|
gatt: BluetoothGatt,
|
||||||
characteristic: BluetoothGattCharacteristic
|
characteristic: BluetoothGattCharacteristic
|
||||||
) {
|
) {
|
||||||
notifyListeners { listener -> listener.onCharChange(gatt, characteristic) }
|
notifyListeners(gatt.device.address) { listener ->
|
||||||
|
listener.onCharChange(
|
||||||
|
gatt,
|
||||||
|
characteristic
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
262
app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt
Normal file
262
app/src/main/java/com/example/sensortestingapp/KirbyDevice.kt
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,8 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothGatt
|
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
|
||||||
import android.bluetooth.BluetoothManager
|
import android.bluetooth.BluetoothManager
|
||||||
import android.bluetooth.le.ScanCallback
|
|
||||||
import android.bluetooth.le.ScanResult
|
import android.bluetooth.le.ScanResult
|
||||||
import android.bluetooth.le.ScanSettings
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -28,26 +24,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.example.sensortestingapp.databinding.ActivityMainBinding
|
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 ENABLE_BLUETOOTH_REQUEST_CODE = 1
|
||||||
|
|
||||||
private const val RUNTIME_PERMISSION_REQUEST_CODE = 2
|
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 {
|
fun Context.hasPermission(permissionType: String): Boolean {
|
||||||
return ContextCompat.checkSelfPermission(this, permissionType) ==
|
return ContextCompat.checkSelfPermission(this, permissionType) ==
|
||||||
PackageManager.PERMISSION_GRANTED
|
PackageManager.PERMISSION_GRANTED
|
||||||
@@ -62,113 +42,26 @@ 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")
|
@SuppressLint("MissingPermission")
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
|
|
||||||
private val scanSettings = ScanSettings.Builder()
|
|
||||||
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
private val bluetoothAdapter: BluetoothAdapter by lazy {
|
||||||
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
bluetoothManager.adapter
|
bluetoothManager.adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val bleScanner by lazy {
|
|
||||||
bluetoothAdapter.bluetoothLeScanner
|
private val connectionManager: ConnectionManager by lazy {
|
||||||
|
newConnectionManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isScanning = false
|
private val kirbyDevices = mutableListOf<DeviceListEntry>();
|
||||||
set(value) {
|
|
||||||
field = value
|
private val deviceListAdapter: DeviceListAdapter by lazy {
|
||||||
runOnUiThread {
|
DeviceListAdapter(kirbyDevices)
|
||||||
binding.fab.setText(if (value) "Stop Scan" else "Start Scan")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
@@ -182,37 +75,97 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
|
|
||||||
binding.fab.setOnClickListener { view ->
|
binding.fab.setOnClickListener { view ->
|
||||||
if (!isScanning) {
|
if (!hasRequiredRuntimePermissions()) {
|
||||||
startBleScan()
|
requestRelevantRuntimePermissions()
|
||||||
|
}
|
||||||
|
if (connectionManager.isScanning) {
|
||||||
|
connectionManager.stopScan()
|
||||||
} else {
|
} else {
|
||||||
stopBleScan()
|
startBleScan()
|
||||||
}
|
}
|
||||||
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
|
|
||||||
// .setAnchorView(R.id.fab)
|
|
||||||
// .setAction("Action", null).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupRecyclerView()
|
setupDevicesList()
|
||||||
|
|
||||||
ConnectionManager.register(bleListener)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupDevicesList() {
|
||||||
binding.scanResultsRecyclerView.apply {
|
binding.devicesList.apply {
|
||||||
adapter = scanResultAdapter
|
adapter = deviceListAdapter
|
||||||
layoutManager = LinearLayoutManager(
|
layoutManager = LinearLayoutManager(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
RecyclerView.VERTICAL,
|
RecyclerView.VERTICAL,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
isNestedScrollingEnabled = false
|
//isNestedScrollingEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
val animator = binding.scanResultsRecyclerView.itemAnimator
|
val animator = binding.devicesList.itemAnimator
|
||||||
if (animator is SimpleItemAnimator) {
|
if (animator is SimpleItemAnimator) {
|
||||||
animator.supportsChangeAnimations = false
|
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 {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
@@ -231,7 +184,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (!bluetoothAdapter.isEnabled) {
|
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() {
|
private fun Activity.requestRelevantRuntimePermissions() {
|
||||||
if (hasRequiredRuntimePermissions()) {
|
if (hasRequiredRuntimePermissions()) {
|
||||||
return
|
return
|
||||||
@@ -357,8 +264,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun requestLocationPermission() {
|
private fun requestLocationPermission() {
|
||||||
|
|
||||||
|
|
||||||
val onClick = { dialog: DialogInterface, which: Int ->
|
val onClick = { dialog: DialogInterface, which: Int ->
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
this,
|
this,
|
||||||
@@ -366,8 +271,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
RUNTIME_PERMISSION_REQUEST_CODE
|
RUNTIME_PERMISSION_REQUEST_CODE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val builder = AlertDialog.Builder(this)
|
val builder = AlertDialog.Builder(this)
|
||||||
|
|
||||||
@@ -389,7 +292,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun requestBluetoothPermissions() {
|
private fun requestBluetoothPermissions() {
|
||||||
|
|
||||||
val onClick = { dialog: DialogInterface, which: Int ->
|
val onClick = { dialog: DialogInterface, which: Int ->
|
||||||
ActivityCompat.requestPermissions(
|
ActivityCompat.requestPermissions(
|
||||||
this,
|
this,
|
||||||
@@ -400,8 +302,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
RUNTIME_PERMISSION_REQUEST_CODE
|
RUNTIME_PERMISSION_REQUEST_CODE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
val builder = AlertDialog.Builder(this)
|
val builder = AlertDialog.Builder(this)
|
||||||
|
|
||||||
@@ -420,7 +320,63 @@ class MainActivity : AppCompatActivity() {
|
|||||||
show()
|
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"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
5
app/src/main/res/drawable/baseline_more_vert_24.xml
Normal file
5
app/src/main/res/drawable/baseline_more_vert_24.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||||
|
</vector>
|
||||||
6
app/src/main/res/drawable/layout_bg.xml
Normal file
6
app/src/main/res/drawable/layout_bg.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#FFFFFF"/>
|
||||||
|
<corners android:radius="10dp"/>
|
||||||
|
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||||
|
</shape>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
|
android:background="@color/grey"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
@@ -15,22 +16,20 @@
|
|||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize" />
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
style="@style/Widget.MaterialComponents.Toolbar.Primary" />
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/scan_results_recycler_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
tools:listitem="@layout/row_scan_result" />
|
|
||||||
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/devices_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
app:layoutManager="LinearLayoutManager"
|
||||||
|
android:layout_marginBottom="?attr/actionBarSize"
|
||||||
|
android:padding="10dp"
|
||||||
|
tools:listitem="@layout/row_device_list" />
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
android:id="@+id/fab"
|
android:id="@+id/fab"
|
||||||
@@ -42,5 +41,4 @@
|
|||||||
android:text='Scan'
|
android:text='Scan'
|
||||||
app:srcCompat="@android:drawable/stat_sys_data_bluetooth" />
|
app:srcCompat="@android:drawable/stat_sys_data_bluetooth" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
103
app/src/main/res/layout/row_device_list.xml
Normal file
103
app/src/main/res/layout/row_device_list.xml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/device_fields"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:background="@drawable/layout_bg"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/device_meta"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/name_and_actions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/device_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
tools:text="Device Name" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/device_actions"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.Icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:icon="@drawable/baseline_more_vert_24"
|
||||||
|
app:iconGravity="textStart"
|
||||||
|
app:iconPadding="-5dp"
|
||||||
|
app:iconSize="25dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/address_and_rssi"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/name_and_actions">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/mac_address"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="MAC: XX:XX:XX:XX:XX" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/signal_strength"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:text="-100 dBm" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/status_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="25dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/address_and_rssi">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/device_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="Device Status" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:id="@+id/measurement_fields"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/device_meta" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -3,37 +3,26 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:padding="8dp"
|
|
||||||
android:background="?android:attr/selectableItemBackground">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/device_name"
|
android:id="@+id/measurement_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:text="Device Name" />
|
tools:text="Temperature" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/mac_address"
|
android:id="@+id/measurement_value"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="14sp"
|
android:textSize="18sp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/device_name"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
tools:text="MAC: XX:XX:XX:XX:XX" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/signal_strength"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="14sp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:text="-100 dBm" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="20 °C" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
3
app/src/main/res/menu/device_menu.xml
Normal file
3
app/src/main/res/menu/device_menu.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context="com.example.sensortestingapp.MainActivity" />
|
||||||
@@ -2,4 +2,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="grey">#ECECEC</color>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user