AndroidX ←→Bluetooth, Light Up a LED Part 2: Connect
Let’s continue from Part 1. Part two will focus on Bluetooth connection. I promise that you will turn on the LED light asynchronously on Android. Let the fun time begin.
— === Menu === —
🐰1. Update: Fix No Input UI after Disappeared Password Input
👨❤️👨2. Brothers:Handler & Thread
→ → → 【 ➰ 】 bondThread
→ → → 【 ➰ 】ConnectedThread
→ → → 👨💼 Handler
🖐🏻3. Step 4:Bonding, Connect Test
🌟4. Light up the LED
🔀5. Asynchronous Method to Light Up LED
🔄6. Use UUID as Switch
🐰1. Update: Fix No Input UI after Disappeared Password Input
Some of my friends may found that Bonding Password input will be automatically disappeared after 35 seconds.
What happened?
BleHc05Observer:
ACTION_BOND_STATE_CHANGED -> {
lgd(tag + "Checking Bonded State...")
val device: BluetoothDevice =
intent.getParcelableExtra<Parcelable>(
EXTRA_DEVICE
) as BluetoothDevicewhen (device.bondState) {
BOND_BONDED -> {
lgd("$tag Bonded to device")
devStatus.postValue(BONDED)
}
BOND_BONDING -> {
lgd("$tag Bonding to device")
// Nothing happend here.
}
BOND_NONE -> {
lge("$tag Nothing has bonded.")
devStatus.postValue(FAIL)
}
}
}
🤔: Don’t worry! I’ll fix it: Redirect the code to Bonding of DeviceStatuse.
BOND_BONDING -> {
lgd("$tag Bonding to device")
devStatus.postValue(BONDING)
}
Next, I’ll add a countdown ⏱️ for the user to enter the password, otherwise, the code will redirect to FAIL👎🏻.
New UI of activity_main.xml:
MainActivity, onCreate():
//region vars:
...
private val counterTV: TextView by
lazy { findViewById(R.id.counterTV) }
...private var newDevice = false
private var counter = 0
private var bonded = false
//endregion
...
override fun onCreate(savedInstanceState: Bundle?) {
...
mainVM.deviceStatus.observe(
this,
{ status ->
when (status) {
...
PAIRING -> {
val info = "Searching Bonded List..."
infoTV.text = info
progressBar.visibility = View.VISIBLE
mainVM.checkBondedList()
}
DISCOVERING -> {
val info = "Discovering device in range..."
infoTV.text = info
progressBar.visibility = View.VISIBLE
newDevice = true
mainVM.discovering()
}
BONDING -> {
val info = "Device Found in Record!\nBonding..."
infoTV.text = info
if (newDevice)
mainVM.checkNewDevice()
else
mainVM.bonding()
}
DISCOVERED -> {
val pass = "Your Pass: ${ConfigHelper.getPass()}"
discoveryTV.text = pass
discoveryTV.visibility = View.VISIBLE
}
BONDED -> {
lgd("MainAct: Bonded successful!")
counterTV.visibility = View.GONE
counter = 0
bonded = true
discoveryTV.text = ""
discoveryTV.visibility = View.GONE
progressBar.visibility = View.GONE val info = "Bonded to $DEVICE_NAME."
infoTV.text = info
msg(this, info, 1)
mainVM.connected()
}
CONNECTED -> {
val info = "Device connected..."
infoTV.text = info
onBT.visibility = View.VISIBLE
progressBar.visibility = View.GONE
}
FAIL -> {
counter = 0
counterTV.visibility = View.GONE
val info = "FAIL to create connection!" +
"\nOr\nPassword is incorrect!"
infoTV.text = info
discoveryTV.visibility = View.GONE
progressBar.visibility = View.GONE
tryAgainBT.visibility = View.VISIBLE
}
NOT_FOUND -> {
lgd("MainAct: Device Not Found.")
progressBar.visibility = View.GONE
val info = "Device NOT Found!"
infoTV.setTextColor(Color.RED)
infoTV.text = info
msg(this, info, 1)
}
COUNT_DOWN -> {
if (!bonded) {
counterTV.visibility = View.VISIBLE
val down = 35 - counter
counter++
val timer = "35s to Enter Password: $down"
counterTV.text = timer
}
}
...
➕ COUNT_DOWN 进 DeviceStatus:
enum class DeviceStatus {
PAIRING, BONDING, DISCOVERING, SWITCHING,
DISCOVERED, BONDED, CONNECTED,
FAIL, DISCONNECT, NOT_FOUND, COUNT_DOWN
}
MainViewModel:
fun bonding() {
val result = bleHelper.checkDeviceBonding()
lgd("$tag Bonding...$result")
if (result) {
deviceStatus.postValue(CONNECTED)
} else {
deviceStatus.postValue(FAIL)
}
}
...
fun checkNewDevice() {
// new password entry timeout is 40s
viewModelScope.launch {
repeat(40) {
deviceStatus.postValue(COUNT_DOWN)
delay(SEC)
}
if (!bleHelper.checkBondedList()) {
lgd("Check Bonded List.")
deviceStatus.postValue(FAIL)
}
}
}
The SEC = 1000L. My setting is 40 seconds. During this period, the UI🕵 jumps to COUNT_DOWN to update. After 40 seconds, the UI🕵 will jump to FAIL if Bluetooth is not in the bonded list。💃 Let’s rock. Ctrl+K,commit. Let’s begin to test.
Good👍, it works!
👨❤️👨2. Brothers:Handler & Thread
Thread --> Handler --(Message) --> MessageQueue ==> Looper
|| ||
\\ ____ //
- Thread: It’s a major force to handle the work beside the Main thread.
- Handler: handleMessage, sendMessage, and obtainMessage
- Message: sendTarget() from Thread to Handler’s getTarget()
Bluetooth Threads
【 ➰ 】 bondThread
1️⃣. Apply socket:
private val bondThread = object : Thread() {
override fun run() {
var fail = false
try {
// new socket
mBTSocket = createBluetoothSocket(serviceUuid)
} catch (e: IOException) {
fail = true
lge("Socket creation failed")
}
> createBluetoothSocket() :
@Throws(IOException::class)
private fun createBluetoothSocket(uuid: UUID): BluetoothSocket? {
return mBleDevice.createRfcommSocketToServiceRecord(uuid)
}
2️⃣. Let’s continue with the run():
try {
mBTSocket?.connect() // init socket
} catch (e: IOException) {
try {
fail = true
mBTSocket?.close()
// package: option 3, arg1=NONE, arg2=NONE
mHandler?.obtainMessage(CONNECTING_STATUS, -1, -1)
?.sendToTarget() // deliver
} catch (e2: IOException) {
lge("Socket creation failed")
}
}
You can find CONNECTING_STATUS at,
companion object {
..
private const val MESSAGE_READ = 4
private const val CONNECTING_STATUS = 3
3️⃣. When bonding is done, we need to connect to Bluetooth with ConnectedThread.
if (!fail) {
mConnectedThread = mBTSocket?.let{ ConnectedThread(it) }
mConnectedThread?.setHandler(mHandler!!) // prov Hander
// Once start() per thread
if (mConnectedThread?.state != State.NEW)
mConnectedThread?.start()
// send connection message
mHandler?.obtainMessage(CONNECTING_STATUS, 1, -1, name)
?.sendToTarget() // send message
}
}
}
…
【 ➰ 】ConnectedThread
This is the inner class of BluetoothHelper.
private class ConnectedThread(socket: BluetoothSocket) : Thread() {
private var mmSocket: BluetoothSocket = socket
private val mmInStream: InputStream? = mmSocket.inputStream
private val mmOutStream: OutputStream? = mmSocket.outputStream
private lateinit var mHandler: Handler
🔍Who’s the boss?
fun setHandler(handler: Handler) { mHandler = handler }
💌 Take the message:
override fun run() {
val buffer = ByteArray(1024) // buffer store for the stream
var bytes = 0 // bytes returned from read()
while (true) {
if (mmInStream != null) {
try {
// read InputStream
bytes = mmInStream.read(buffer)
lgd("+++++ ConnectedThread: bytes size = $bytes")
// notify #4, arg1=size, arg2=NONE, data
mHandler.obtainMessage(
MESSAGE_READ, bytes, -1, buffer)
.sendToTarget()
} catch (e: IOException) {
lge("+++++ ConnectedThread: Error on read: ${e.message}")
break
}
}
}
}
→💙Bluetooth:
fun write(input: String) {
val bytes = input.toByteArray()
try {
mmOutStream?.write(bytes)
} catch (e: IOException) {
lge("+++++ ConnectedThread: Error on write: ${e.message}")
}
}
And 🚫cancel:
fun cancel() {
try {
mmSocket.close()
} catch (e: IOException) {
}
}
}
…
👨💼 Handler
private val newHandler = object : Handler() {
override fun handleMessage(msg: Message) {
lgd ("Handler: what = ${msg.what}")
// choices
when (msg.what) {
MESSAGE_READ -> {
lgd("msg = MESSAGE READ")
var readMessage: String? = null
try {
readMessage = String(
(msg.obj as ByteArray),
StandardCharsets.UTF_8)
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
}
}
CONNECTING_STATUS -> {
lgd("msg = CONNECTION STATUS")
if (msg.arg1 == CONNECTED)
lgd("Connected to Device: " + msg.obj as String)
else
lge("Connection Failed")
}
}
}
}
Like a menu, the handler dispatches the tasks.
🖐🏻3. Step 4:Bonding, Connect Test
Can Step4-Bonding always connect to Bluetooth?
In the beginning, the new device has successfully bonded and connected to the cell phone. Now, the HC05 is on the bonded list. When we restart the process, we hope the app can connect to the device again.
MainActivity:Let’s go over the process
PAIRING -> {
val info = "Searching Bonded List..."
infoTV.text = info
progressBar.visibility = View.VISIBLE
mainVM.checkBondedList()
}
BONDING -> {
val info = "Device Found in Record!\nBonding..."
infoTV.text = info
if (newDevice)
mainVM.checkNewDevice()
else
mainVM.bonding()
}
MainViewModel:
fun checkBondedList() {
if (bleHelper.checkBondedList()) {
lgd("$tag Found. Move to Step 4, Bonding.")
deviceStatus.postValue(BONDING)
} else {
lgd("$tag Not Found. Move to Step 3, Discovering.")
deviceStatus.postValue(DISCOVERING)
}
}fun bonding() {
val result = bleHelper.checkDeviceBonding()
lgd("$tag Bonding...$result") if (result) {
deviceStatus.postValue(CONNECTED)
} else {
deviceStatus.postValue(FAIL)
}
}
BluetoothHelper:
// Step 4
fun checkDeviceBonding(): Boolean {
mBleDevice.createBond()
val uuids = mBleDevice.uuids
serviceUuid = uuids[0].uuid // ???? UUID ??
val bonded = mBleDevice.bondState == BOND_BONDED
if (bonded) {
lgd("$tag state: bonded")
} else {
lge("$tag state: bonded fail")
}
return bonded
}
🤓: Turn off HC05 to check how the App responds.
Logcat:
D: MainVM: Found. Move to Step 4, Bonding.
D: BlueHelper: Service UUID: 00001101-0000-1000-8000-00805f9b34fb
D: BlueHelper: Characteristic UUID: 00000000-0000-1000-8000-00805f9b34fb
D: BlueHelper: state: bonded
D: MainVM: Bonding...true
D: Handler: what = 3
D: msg = CONNECTION STATUS
E: Connection Failed
This proves that Bonding has no relationship with the connection.
😎: I’ll connect Bluetooth by myself.
Connect Bluetooth with Thread
BluetoothHelper:Add a new variable
// check connection
private var connected = false
Get an answer from the handler:
private val newHandler = object : Handler() {
override fun handleMessage(msg: Message) {
...
CONNECTING_STATUS -> {
lgd("msg = CONNECTION STATUS")
if (msg.arg1 == CONNECTED) {
lgd("Connected to Device: " + msg.obj as String)
connected = true
}
else {
lge("Connection Failed")
connected = false
}
}
checkDeviceBonding() — bond and connect:
suspend fun checkDeviceBonding(): Boolean {
...
// handler:
if (mHandler == null) mHandler = newHandler
if (bondThread.state == State.NEW) bondThread.start() delay(COM_DELAY)
lgd("$tag connected? $connected")
return connected
}
This is Coroutines, so we need to add the suspend keyword.
➕ a constant, COM_DELAY:
companion object {
private const val COM_DELAY = 500L
}
MainViewModel: patch a scope
fun bonding() {
viewModelScope.launch {
val result = bleHelper.checkDeviceBonding()
lgd("$tag Bonding...$result")
if (result) {
deviceStatus.postValue(CONNECTED)
} else {
deviceStatus.postValue(FAIL)
}
}
}
Turn on the HC05 and run:
The timing is too short. I have to run twice. Let’s increase COM_DELAY = 3000L.
🤩:Pass!
🌟4. Light up the LED
VS Code
main.cpp — [ 🐣 ] head
#include <Arduino.h>
#include <SoftwareSerial.h>// LED switch
#define ledPin 13
#define rxPin 2 // => HC05: rx
#define txPin 3 // => HC05: tx#define LED_ON "101"
#define LED_OFF "011"
int codeLen = 3;// Bluetooth port
SoftwareSerial BTserial(rxPin, txPin); // RX | TX of Arduinoconst long interval = 50; // interval at which to delay
char reading = ' ';
String pass = ""; // received from Bluetoothvoid decodePass();
> decodePass(): convert signal to message
main.cpp — [ 🐥 ] setup()
void setup() {
// set led pin
pinMode( ledPin, OUTPUT );
digitalWrite( ledPin, HIGH ); Serial.begin(9600);
Serial.println("Arduino to Host is 9600 Baud");
BTserial.begin(9600);
Serial.println("Bluetooth started at 9600 Baud");
}
Uno and HC05 communicate at 9600 baud.
[ 🐍 ] loop() :
void loop() {
if( BTserial.available() )
{
reading = BTserial.read();
Serial.print("Bluetooth Received: ");
Serial.println(reading);
switch (reading) {
case '1':
pass.concat('1');
break;
case '2':
pass.concat('2');
break;
case '3':
pass.concat('3');
break;
case '4':
pass.concat('4');
break;
case '5':
pass.concat('5');
break;
case '6':
pass.concat('6');
break;
case '7':
pass.concat('7');
break;
case '8':
pass.concat('8');
break;
case '9':
pass.concat('9');
break;
case '0':
pass.concat('0');
break;
case 'a':
pass.concat('a');
break;
case 'b':
pass.concat('b');
break;
case 'c':
pass.concat('c');
break;
case 'd':
pass.concat('d');
break;
case 'e':
pass.concat('e');
break;
case 'f':
pass.concat('f');
break;
case '-':
pass.concat('-');
break;
default:
Serial.println("Junk...");
} // read input
if (pass.length() >= codeLen) {
Serial.print("pass =");
Serial.println(pass);
decodePass();
pass = "";
delay(interval);
}
}
}
I program the phrase to accept Hex code, such as UUID.
main.cpp — [ 🎭 ] decodePass() :
void decodePass()
{
// connect?
if (pass.equals(LED_ON))
{
Serial.print("LED ON Code: ");
Serial.println(pass);
}
else if (pass.equals(LED_ON))
{
Serial.print("LED OFF Code: ");
Serial.println(pass);
}
}
It will print the result on Serial Monitor.
…
Android Studio
【👆🏻】ON Button &. OFF Button
> MainActivity:
override fun onCreate(savedInstanceState: Bundle?) {
...
onBT.visibility = View.GONE
onBT.setOnClickListener {
offBT.visibility = View.VISIBLE
onBT.visibility = View.GONE
mainVM.turnOn()
} offBT.visibility = View.GONE
offBT.setOnClickListener {
offBT.visibility = View.GONE
onBT.visibility = View.VISIBLE
mainVM.turnOff()
}
> MainViewModel:
fun turnOn() {
bleHelper.ledSwitch("101")
}fun turnOff() {
bleHelper.ledSwitch("011")
}
> BluetoothHelper:
fun ledSwitch(signal: String) {
mConnectedThread?.write(signal)
}
> Run and Serial Monitor on VS Code:
Arduino to Host is 9600 Baud
Bluetooth started at 9600 Baud
Bluetooth Received: 1
Bluetooth Received: 0
Bluetooth Received: 1
pass =101
Bluetooth Received: 0
Bluetooth Received: 1
Bluetooth Received: 1
pass =011
Good, the On and Off code are equal to predefined.
【👆🏻】Success and Fail
> main.cpp — [ 🎭 ] decodePass() :
void decodePass()
{
if (pass.equals(LED_ON))
{
Serial.print("LED ON Code: ");
Serial.println(pass);
digitalWrite(ledPin, LOW); // ON
delay(interval); // wait
} else if (pass.equals(LED_ON))
{
Serial.print("LED OFF Code: ");
Serial.println(pass);
digitalWrite(ledPin, HIGH); // OFF
delay(interval);
}
}
LED ON:
LED OFF:
…
> Test: Turn off HC05 and turn it back on.
Fail: App lost control of the LED.
Let’s check Logcat:
E: +++++ ConnectedThread: Error on write: Broken pipe
We lost the socket connection.
🔀5. Asynchronous Method to Light Up LED
This is the experiment of remote control. We always control hardware in an asynchronous situation.
We need to increase the constants of StatusDevice to control the LED.
- [ ✔️ ] DeviceStatus
enum class DeviceStatus {
PAIRING, BONDING, DISCOVERING, SWITCHING,
DISCOVERED, BONDED, CONNECTED,
FAIL, DISCONNECTED, NOT_FOUND, COUNT_DOWN,
LED_ON, LED_OFF, LED_FAIL_ON, LED_FAIL_OFF,
RESTART
}
- [ ✔️ ] MainActivity: onCreate()
mainVM.deviceStatus.observe(
this,
{ status ->
when (status) {
...
LED_ON -> {
offBT.visibility = View.VISIBLE
onBT.visibility = View.GONE
val info = "LED is ON."
infoTV.setTextColor(Color.BLUE)
infoTV.text = info
}
LED_FAIL_ON -> {
val info = "LED: Fail to turn ON!\n" +
"Please check your distance!"
infoTV.setTextColor(Color.DKGRAY)
infoTV.text = info
}
LED_OFF -> {
offBT.visibility = View.GONE
onBT.visibility = View.VISIBLE
val info = "LED is OFF."
infoTV.setTextColor(Color.BLACK)
infoTV.text = info
}
LED_FAIL_OFF -> {
val info = "LED: Fail to turn OFF!\n" +
"Please check your distance!"
infoTV.setTextColor(Color.DKGRAY)
infoTV.text = info
}
RESTART -> {
lgd("MainAct: Restart the App")
val info = "Broken Connection\n" +
"Restarting the App!"
infoTV.setTextColor(Color.RED)
infoTV.text = info
progressBar.visibility = View.VISIBLE
val packageManager: PackageManager =
this.packageManager
val intent =
packageManager.getLaunchIntentForPackage(
this.packageName)
val componentName = intent!!.component
val mainIntent = Intent.makeRestartActivityTask(
componentName)
this.startActivity(mainIntent)
Runtime.getRuntime().exit(0)
}
else -> {
val info = "Illegal Process Error..."
infoTV.text = info
}
}
}
Why do we restart the app?
😑: The broken socket and thread are not safe. I restart the app to re-establish the connection.
- [ ✔️ ] ConfigHelper
const val ERR_BROKEN_PIPE = "Socket: Broken pipe"
- [ ✔️ ] MainViewMode
fun turnOn() {
viewModelScope.launch {
if (bleHelper.bleMsg != ERR_BROKEN_PIPE) {
var bleMsg = ""
for (i in 1..3) {
bleMsg = bleHelper.ledSwitch("101")
lgd("$tag Retry $i; received Bluetooth message: $bleMsg")
if (bleMsg.contains(ON)) break
}
if (bleMsg.contains(ON)) {
deviceStatus.postValue(LED_ON)
} else {
deviceStatus.postValue(LED_FAIL_ON)
}
} else {
lgd("Call Restart!")
deviceStatus.postValue(RESTART)
}
}
}fun turnOff() {
viewModelScope.launch {
if (bleHelper.bleMsg != ERR_BROKEN_PIPE) {
var bleMsg = ""
for (i in 1..3) {
bleMsg = bleHelper.ledSwitch("011")
lgd("$tag Retry $i; received Bluetooth message: $bleMsg")
if (bleMsg.contains(OFF)) break
}
if (bleMsg.contains(OFF)) {
deviceStatus.postValue(LED_OFF)
} else {
deviceStatus.postValue(LED_FAIL_OFF)
}
} else {
lgd("Call Restart!")
deviceStatus.postValue(RESTART)
}
}
}
The turnOn() and turnOff() are converted to Coroutines.
- [ ✔️ ] BluetoothHelper:
var bleMsg = ""
suspend fun ledSwitch(signal: String): String {
bleMsg = ""
mConnectedThread?.write(signal)
delay(1000)
return bleMsg
}
I check the response each second.
Modify the error message at ConnectedThread class.
override fun run() {
...
while (true) {
if (mmInStream != null) {
try {
...
} catch (e: IOException) {
lge("+++++ ConnectedThread: Error on read: ${e.message}")
mHandler.obtainMessage(BROKEN_PIPE, -1, -1)
.sendToTarget()
break
}
}
}
}/* Call this from the main activity to send data to the remote device */
fun write(input: String) {
...
try {
...
} catch (e: IOException) {
lge("+++++ ConnectedThread: Error on write: ${e.message}")
mHandler.obtainMessage(BROKEN_PIPE, -1, -1)
.sendToTarget()
}
}
> Handler:
private val newHandler = object : Handler() {
override fun handleMessage(msg: Message) {
...
when (msg.what) {
MESSAGE_READ -> {
try {
val readBuffer = msg.obj as ByteArray
bleMsg += String(readBuffer, 0, msg.arg1)
} catch (e: UnsupportedEncodingException) {
e.printStackTrace()
}
}
...
BROKEN_PIPE -> {
bleMsg = ERR_BROKEN_PIPE
}
}
}
}
➕ BROKEN_PIPE :
companion object {
private const val tag = "BlueHelper: "
private const val MESSAGE_READ = 4
private const val CONNECTING_STATUS = 3
private const val BROKEN_PIPE = 5
VS Code — main.cpp
- [ 🎭 ] decodePass:
void decodePass()
{
// connect
Serial.print("Decode Pass = ");
if (pass.equals(LED_ON))
{
Serial.println("LED ON");
digitalWrite(ledPin, LOW);
BTserial.println("ON");
delay(interval);
}
else if (pass.equals(LED_OFF))
{
Serial.println("LED OFF");
digitalWrite(ledPin, HIGH);
BTserial.println("OFF");
delay(interval);
}
}
Test Result
- Test 1: Restart HC05 at the beginning.
- Test 2: Turn on LED and restart HC05
🔄6. Use UUID as Switch
We have tried 3 digits as the switch. Let’s try to use UUID as a switch.
ConfigHelper:
// BLE code
const val UUID_LED_ON = "538688b3-54ee-36ed-a830-f7ec4b0f24bb"
const val UUID_LED_OFF = "031048cc-2fad-3d80-bc69-a35119a12f49"
Each UUID has 36 characters.
MainViewModel:
fun turnOn() {
viewModelScope.launch {
if (bleHelper.bleMsg != ERR_BROKEN_PIPE) {
var bleMsg = ""
for (i in 1..3) {
bleMsg = bleHelper.ledSwitch(UUID_LED_ON)
...
fun turnOff() {
viewModelScope.launch {
if (bleHelper.bleMsg != ERR_BROKEN_PIPE) {
var bleMsg = ""
for (i in 1..3) {
bleMsg = bleHelper.ledSwitch(UUID_LED_OFF)
UNO — main.cpp:
// LED 123456789a123456789b123456789c123456
#define LED_ON "538688b3-54ee-36ed-a830-f7ec4b0f24bb"
#define LED_OFF "031048cc-2fad-3d80-bc69-a35119a12f49"
unsigned int codeLen = 36;
Test and Run
The LED has turned on, but the App reports fail. Let’s check Logcat.
D: MainVM: Bonding...true
D: MainVM: Retry 1; received Bluetooth message:
D: MainVM: Retry 2; received Bluetooth message: NN
D: MainVM: Retry 3; received Bluetooth message: NN
ON is not NN. Let’s double the answer code in main.cpp.
if (pass.equals(LED_ON))
{
...
BTserial.println("ONON");
delay(interval);
}
else if (pass.equals(LED_OFF))
{
...
BTserial.println("OFFOFF");
delay(interval);
}
Tryout#1: Pass on 2nd second
D: MainVM: Retry 1; received Bluetooth message:
D: MainVM: Retry 2; received Bluetooth message: ONON
Tryout#2: Pass on 2nd second
D: MainVM: Retry 1; received Bluetooth message:
D: MainVM: Retry 2; received Bluetooth message: OFFOFF
Tryout#3: Pass on 1st second
D: MainVM: Retry 1; received Bluetooth message: NNON
…
Test Result
We may lose some of the signals from HC05. A double phrase may ensure the answer.