Skip to content

Commit

Permalink
Add echo screen (#62)
Browse files Browse the repository at this point in the history
**Background**

With newest firmware version you can use echo from/to flipper

**Changes**

- Add echo screen

**Test plan**

Open information screen
Type text and send
  • Loading branch information
LionZXY authored Sep 8, 2021
1 parent 6477df4 commit e28bb21
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 43 deletions.
2 changes: 1 addition & 1 deletion components/bridge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies {
implementation(Libs.APPCOMPAT)

implementation(Libs.NORDIC_BLE_SCAN)
implementation(Libs.NORDIC_BLE)
api(Libs.NORDIC_BLE)
implementation(Libs.NORDIC_BLE_KTX)
implementation(Libs.NORDIC_BLE_COMMON)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,72 +5,108 @@ import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import com.flipper.bridge.model.FlipperGATTInformation
import com.flipper.bridge.utils.Constants
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.BleManager
import timber.log.Timber
import java.util.UUID

class FlipperBleManager(context: Context) : BleManager(context) {
private val informationState = MutableStateFlow(FlipperGATTInformation())
private val echoText = MutableStateFlow(ByteArray(0))
private val infoCharacteristics = mutableMapOf<UUID, BluetoothGattCharacteristic>()
private var serialTxCharacteristic: BluetoothGattCharacteristic? = null
private var serialRxCharacteristic: BluetoothGattCharacteristic? = null

fun getInformationState(): StateFlow<FlipperGATTInformation> = informationState
fun getEchoState(): StateFlow<ByteArray> = echoText

override fun getGattCallback(): BleManagerGattCallback =
FlipperBleManagerGattCallback()

fun sendEcho(text: String) {
writeCharacteristic(serialTxCharacteristic, text.toByteArray()).enqueue()
}

private inner class FlipperBleManagerGattCallback :
BleManagerGattCallback() {
private val collectedCharacteristics = mutableMapOf<UUID, BluetoothGattCharacteristic>()

override fun initialize() {
readCharacteristic(
collectedCharacteristics[Constants.BLEInformationService.MANUFACTURER]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(manufacturerName = String(content))
}
}.enqueue()
readCharacteristic(
collectedCharacteristics[Constants.BLEInformationService.DEVICE_NAME]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(deviceName = String(content))
}
}.enqueue()
readCharacteristic(
collectedCharacteristics[Constants.BLEInformationService.HARDWARE_VERSION]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(hardwareRevision = String(content))
}
}.enqueue()
readCharacteristic(
collectedCharacteristics[Constants.BLEInformationService.SOFTWARE_VERSION]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(softwareVersion = String(content))
}
}.enqueue()
registerToInformationGATT()
registerToSerialGATT()
}

override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val informationService =
gatt.getService(Constants.BLEInformationService.SERVICE_UUID) ?: return false
gatt.getService(Constants.BLEInformationService.SERVICE_UUID)

informationService.characteristics.forEach {
collectedCharacteristics[it.uuid] = it
infoCharacteristics[it.uuid] = it
}

val serialService =
gatt.getService(Constants.BLESerialService.SERVICE_UUID)

serialTxCharacteristic = serialService.getCharacteristic(Constants.BLESerialService.TX)
serialRxCharacteristic = serialService.getCharacteristic(Constants.BLESerialService.RX)

return true
}

override fun onServicesInvalidated() {
// TODO reset state
}
}

@DelicateCoroutinesApi // TODO replace it
private fun registerToSerialGATT() {
setNotificationCallback(serialRxCharacteristic).with { _, data ->
Timber.i("Receive serial data")
val bytes = data.value ?: return@with
GlobalScope.launch {
echoText.emit(bytes)
}
}
enableNotifications(serialRxCharacteristic).enqueue()
enableIndications(serialRxCharacteristic).enqueue()
}

private fun registerToInformationGATT() {
readCharacteristic(
infoCharacteristics[Constants.BLEInformationService.MANUFACTURER]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(manufacturerName = String(content))
}
}.enqueue()
readCharacteristic(
infoCharacteristics[Constants.BLEInformationService.DEVICE_NAME]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(deviceName = String(content))
}
}.enqueue()
readCharacteristic(
infoCharacteristics[Constants.BLEInformationService.HARDWARE_VERSION]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(hardwareRevision = String(content))
}
}.enqueue()
readCharacteristic(
infoCharacteristics[Constants.BLEInformationService.SOFTWARE_VERSION]
).with { _, data ->
val content = data.value ?: return@with
informationState.update {
it.copy(softwareVersion = String(content))
}
}.enqueue()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.flipper.bridge.impl.scanner

import android.os.ParcelUuid
import com.flipper.bridge.api.scanner.FlipperScanner
import com.flipper.bridge.utils.Constants
import com.flipper.core.models.BLEDevice
Expand Down Expand Up @@ -38,10 +37,11 @@ class FlipperScannerImpl : FlipperScanner {
}

private fun provideFilter(): List<ScanFilter> {
return listOf(
return emptyList()
/*return listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString(Constants.HEARTRATE_SERVICE_UUID))
.build()
)
)*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ object Constants {
val SOFTWARE_VERSION: UUID = UUID.fromString("00002a28-0000-1000-8000-00805f9b34fb")
}

// BLE serial service uuids: service uuid and characteristics uuids
object BLESerialService {
val SERVICE_UUID: UUID = UUID.fromString("8fe5b3d5-2e7f-4a98-2a48-7acc60fe0000")

val TX: UUID = UUID.fromString("19ed82ae-ed21-4c9d-4145-228e62fe0000")
val RX: UUID = UUID.fromString("19ed82ae-ed21-4c9d-4145-228e61fe0000")
}

object BLE {
const val RECONNECT_COUNT = 3
const val RECONNECT_TIME = 100
Expand Down
4 changes: 4 additions & 0 deletions components/info/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ dependencies {
implementation(Libs.COMPOSE_TOOLING)
implementation(Libs.COMPOSE_FOUNDATION)

implementation(Libs.KOTLIN_COROUTINES)
implementation(Libs.LIFECYCLE_RUNTIME_KTX)
implementation(Libs.LIFECYCLE_VIEWMODEL_KTX)

implementation(Libs.FRAGMENT_KTX)

kapt(Libs.DAGGER_COMPILER)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.fragment.app.activityViewModels
import com.flipper.bridge.impl.viewmodel.FlipperViewModel
import com.flipper.core.models.BLEDevice
import com.flipper.core.view.ComposeFragment
import com.flipper.info.main.compose.ComposeInfoScreen
import com.flipper.info.main.service.FlipperViewModel

class InfoFragment : ComposeFragment() {
private val bleViewModel by activityViewModels<FlipperViewModel>()

@Composable
override fun renderView() {
val information by bleViewModel.getDeviceInformation().collectAsState()
ComposeInfoScreen(information)
val echoList by bleViewModel.getEchoAnswers().collectAsState()
ComposeInfoScreen(information, echoList) {
bleViewModel.sendEcho(it)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,99 @@
package com.flipper.info.main.compose

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.flipper.bridge.model.FlipperGATTInformation
import com.flipper.info.R

@Preview(
showBackground = true,
showSystemUi = true
)
@Composable
fun ComposeInfoScreen(
flipperGATTInformation: FlipperGATTInformation = FlipperGATTInformation()
flipperGATTInformation: FlipperGATTInformation = FlipperGATTInformation(),
echoAnswers: List<ByteArray> = listOf("Test", "Test2").map { it.toByteArray() },
echoListener: (String) -> Unit = {},
) {
var text by rememberSaveable { mutableStateOf("") }

Column {
Text(text = "Device name: ${flipperGATTInformation.deviceName ?: "Unavailable"}")
Text(text = "Manufacturer: ${flipperGATTInformation.manufacturerName ?: "Unavailable"}")
Text(text = "Hardware: ${flipperGATTInformation.hardwareRevision ?: "Unavailable"}")
Text(text = "Firmware: ${flipperGATTInformation.softwareVersion ?: "Unavailable"}")
Row(
modifier = Modifier.fillMaxWidth()
) {
TextField(
value = text,
onValueChange = {
text = it
},
label = { Text("Type text") }
)
Button(
onClick = {
echoListener.invoke(text)
}
) {
Image(
painter = painterResource(id = R.drawable.ic_baseline_send_24),
contentDescription = "Send"
)
}
}
if (echoAnswers.isEmpty()) {
Text(
text = "No echo answers yet",
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
items(echoAnswers) { bytes ->
EchoAnswer(String(bytes), bytes.toHex())
}
}
}
}
}

@Composable
private fun EchoAnswer(echoAnswer: String, hexRepresentation: String) {
Column(
modifier = Modifier.padding(all = 16.dp)
) {
Text(text = echoAnswer)
Text(text = "HEX: $hexRepresentation")
}
Divider(color = Color.Black, thickness = 1.dp)
}

private fun ByteArray.toHex(): String =
joinToString(separator = " ") { eachByte -> "%02x".format(eachByte) }
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
package com.flipper.bridge.impl.viewmodel
package com.flipper.info.main.service

import android.app.Application
import android.bluetooth.BluetoothDevice
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.flipper.bridge.impl.manager.FlipperBleManager
import com.flipper.bridge.model.FlipperGATTInformation
import com.flipper.bridge.utils.Constants
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class FlipperViewModel(application: Application) : AndroidViewModel(application) {
private val bleManager = FlipperBleManager(application)
private var currentDevice: BluetoothDevice? = null
private val echoAnswers = MutableStateFlow(emptyList<ByteArray>())
private val allEchoAnswers = mutableListOf<ByteArray>()

init {
viewModelScope.launch {
bleManager.getEchoState().collect {
if (it.isEmpty()) {
return@collect
}
allEchoAnswers.add(it)
echoAnswers.emit(ArrayList(allEchoAnswers))
}
}
}

fun getEchoAnswers(): StateFlow<List<ByteArray>> {
return echoAnswers
}

fun sendEcho(text: String) {
bleManager.sendEcho(text)
}

fun getDeviceInformation(): StateFlow<FlipperGATTInformation> {
return bleManager.getInformationState()
Expand Down
11 changes: 11 additions & 0 deletions components/info/src/main/res/drawable/ic_baseline_send_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:autoMirrored="true"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="@android:color/white"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z" />
</vector>

0 comments on commit e28bb21

Please sign in to comment.