0

I'm writing an embedded system which doesn't have an internet connection, so the main interaction is using BLE from an Android device. (ESP32 is using the NimBLE-Arduino library)

I have some write characteristics and some read characteristics. Each one individually works well, but when I try to read immediately after write (or vice versa), only the first callback in the ESP32 is called

ESP32 code

// Setup function
void Bluetooth::setupCharacteristic()
{        
    NimBLEService *pService = m_pServer->createService(SERVICE_UUID);
    NimBLECharacteristic *getStationsChr = pService->createCharacteristic(GET_STATIONS_CHR_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::READ_AUTHEN);
    NimBLECharacteristic *setTimeChr = pService->createCharacteristic(SET_TIME_CHR_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_ENC | NIMBLE_PROPERTY::WRITE_AUTHEN);

    getStationsChr->setCallbacks(this);
    setTimeChr->setCallbacks(this);
    pService->start();
}

// Read/Write callbacks
void Bluetooth::onRead(NimBLECharacteristic* pCharacteristic){
    log_i("%s", pCharacteristic->getUUID().toString().c_str());
    log_i(": onRead(), value: %s",pCharacteristic->getValue().c_str());

    if (m_pCallback)
    {
        auto value = pCharacteristic->getValue();
        //m_pCallback->onMessageReceived(&value);
    }
    ...
}

void Bluetooth::onWrite(NimBLECharacteristic* pCharacteristic) {
    std::string characteristicStr = pCharacteristic->getUUID().toString();
    std::string value_str(pCharacteristic->getValue().c_str());
    log_i("%s: onWrite(), value: %s (%s)", characteristicStr.c_str(), pCharacteristic->getValue().c_str(), value_str.c_str());

    // Process incoming value and call the callback
    // ...
    
    if (m_pCallback)
    {
        auto value = pCharacteristic->getValue();
        //m_pCallback->onMessageReceived(&value);
    }
    ...
}

Android code

private fun updateTime() {
    val calendar = GregorianCalendar()
    val ts = Instant.now().epochSecond
    val timeZone = timezonesMap?.get(calendar.timeZone.id) ?: ""

    val timeMessage = TimeMessage(ts, 0, timeZone)
    val setTimeChar = bluetoothGatt?.getService(SERVICE_UUID)?.getCharacteristic(SET_TIME_UUID)
    if (setTimeChar?.isWritable() == true) {
        val timeStr = gson.toJson(timeMessage)
        Log.d("updateTime", "setting time to $timeStr")
        sendToCharacteristic(setTimeChar, timeStr)
    }

}

// This function is used to break long message to multiple messages, this is a simplified version
// for brevity
private fun sendToCharacteristic(characteristic: BluetoothGattCharacteristic?, value: String)
{
    Log.d(TAG, "Sending to write characteristic ${characteristic?.uuid} value ($value)")
    characteristic?.value = value.toByteArray()
    bluetoothGatt?.writeCharacteristic(characteristic)
    TimeUnit.MILLISECONDS.sleep(500)
}

private fun getStations() {
    Log.d("getStations", "getting stations")
    val getStationsChar = bluetoothGatt?.getService(SERVICE_UUID)?.getCharacteristic(GET_STATIONS_UUID)
    if (getStationsChar?.isReadable() == true) {
        Log.d("getStations", "reading")
        bluetoothGatt?.readCharacteristic(getStationsChar)
    }
}

The issue is when calling my sync() function

fun sync() {
    Log.d("sync", "syncing")
    bluetoothGatt?.apply {
        if (device.bondState != BluetoothDevice.BOND_BONDED) {
            device.createBond()
        }
        else {
            updateTime()
//                getStations()
        }
    }
}

If I uncomment the getStations, the read callback isn't called. If I comment the updateTime and uncomment the getStations, the read callback is called. switching the order doesn't make a difference. First called function works, second doesn't. Also adding a 1 second delay between the calls doesn't work

La bla bla
  • 8,558
  • 13
  • 60
  • 109

1 Answers1

1

Your problem lies on the Android side.

In GATT you may only have one outstanding request. This means you must wait for the previous callback before executing a new operation. In particular, you must in this case wait for onCharacteristicWrite on your BluetoothGattCallback object after performing a write operation, before you can execute a next operation.

Adding a sleep doesn't work since you block the thread and hence prevent the result callback from running, whenever a response is received from the remote device.

Emil
  • 16,784
  • 2
  • 41
  • 52
  • Thanks, what's the recommended way of implementing it? having the `onCharacteristicRead/Write` be aware of the entire flow? `if (read_1) do_read2() if(read_2) do_write()` etc? Coming from C++ it makes sense for me to have some queue with condition variable and in the callback do some notify and start processing the next item in the queue, but I'm unable to find a "proper" way in Kotlin – La bla bla Nov 07 '22 at 19:00
  • I found https://punchthrough.com/android-ble-guide/ which has a sample implementation to what you suggested and it also explains the issue as you explained it. Thanks! – La bla bla Nov 07 '22 at 19:53