esp-nimble-cpp icon indicating copy to clipboard operation
esp-nimble-cpp copied to clipboard

Best approach for high bandwidth transfer?

Open cmorganBE opened this issue 1 year ago • 13 comments
trafficstars

I'd like to stream data as quickly as possible via a notify characteristic. In the past I've done this like the nrf example did, by just calling the characteristic update function as quickly as possible, https://github.com/NordicPlayground/nrf52-ble-image-transfer-demo/blob/b1338a7eb983aa5794fffa956c5928a0074f70c6/ble_image_transfer_service.c#L353.

For esp-nimble-cpp it wasn't clear how to know if the data was accepted into the stack:

while(data_to_send)
{
   pCharacteristic->setValue(xxx);
   pCharacteristic->notify();

  // how to know if the stack accepted the data?
}

cmorganBE avatar Feb 04 '24 01:02 cmorganBE

There is a callback that is called when the notification is sent. Also your connection interval parameter will determine the rate that you can send packets, minimum 7.5ms.

h2zero avatar Feb 06 '24 00:02 h2zero

@h2zero do you mean the NimBLECharacteristicCallbacks::onNotify() callback or is there a better location for this event that you were proposing?

tkx on the connection interval parameter, I'll adjust that down once I get the proof of concept in place.

cmorganBE avatar Feb 07 '24 13:02 cmorganBE

btw @h2zero, this is a very nice library. I was doing it with nimble directly and there is a TON of syntactic overhead, so much that I was looking at wrapping the api for easier use, searched and found this and it helps a ton to take care of the very verbose syntax necessary to set this stuff up.

cmorganBE avatar Feb 07 '24 13:02 cmorganBE

@h2zero, sorry to bug you but I'm close to getting back to this. Were you referring to the NimBLECharacteristicCallbacks::onNotify() callback to determine when I'd be good to send the next notify?

cmorganBE avatar Feb 10 '24 20:02 cmorganBE

Not wanting to derail your issue, but let me join in here, since I'm having a similar question. My goal is the same (low latency high bandwidth), although I'm only using GATT for the discovery part. I'm then using my L2CAP extensions for esp-nimble-cpp to open an L2CAP connection from iOS and transfer data in a stream oriented way.

I wonder how I could make sure that I'm a) using the LE 2M PHY for the L2CAP connection and b) using the "best" connection parameters to maximize throughput.

Where/How can I tweak all those parameters?

(@cmorganBE BTW., perhaps L2CAP would be an option for you as well, since it has way less overhead than transferring the data via GATT).

mickeyl avatar Feb 11 '24 12:02 mickeyl

Using PacketLogger on macOS, I now found out that the default setting seem to indeed enable the LE 2M PHY:

Bildschirmfoto 2024-02-11 um 17 52 01

I also found the proper place in the GAP service connection callback to adjust the connection parameters. That said, no changes there seem to have a measurable effect. I'm afraid I'm already at the maximum bandwidth using BLE. I'm getting roughly 13KB/sec (net payload) with an ESP32S3 talking to macOS. I would guess iOS devices being no different.

mickeyl avatar Feb 11 '24 17:02 mickeyl

@mickeyl would the l2cap approach work with ble services on ios and android devices? Support for easy mobile integration and minimal effort on the sw side would be preferred if the performance was 13KB/sec via GATT, that's fast enough for my use.

cmorganBE avatar Feb 11 '24 20:02 cmorganBE

@cmorganBE In fact, I developed the L2CAP support for esp-nimble-cpp exactly because it's faster (no GATT encapsulation) and way simpler to interact with in iOS and Android, since you don't need special care to fragment your packages. At least for iOS, I get a nice stream-based abstraction when I have opened an L2CAP connection channel ­– and from what I have seen in the Android API, it's the very same.

Why ­– despite all these measures ­– the bandwidth is still lower than I'd like, is a story for further debugging with BLE low level tools, I'm afraid.

mickeyl avatar Feb 12 '24 09:02 mickeyl

@mickeyl is your L2CAP support in mainline esp-nimble-cpp? Are there examples of how to make use of it to transfer data? Normally I'd dig in more but I have some schedule constraints on time here.

Oh, and can I mix and match L2CAP usage and gatt if I want characteristics with notify, indicate, read/write etc?

cmorganBE avatar Feb 12 '24 12:02 cmorganBE

@cmorganBE Not yet in mainline, I maintain a fork. Although I have been using it for some months now without any problems, it's not yet ready for inclusion before adding an example.

Yes, L2CAP usage is mix-and-match with GATT. In fact, you will have to use GATT in order to discover the device, before opening an L2CAP channel.

I'm pretty sure it would very well fit your example, but if you're short on time, it might pose a risk for you. I try to submit it within the next few weeks, but I'm right in the middle of ramping up production of my first ESP32S3-based hardware, so time is also tight for me ;-)

mickeyl avatar Feb 12 '24 16:02 mickeyl

@h2zero sorry to ping again, is NimBLECharacteristicCallbacks::onNotify() the callback where enough processing has been performed that the stack is ready for the next notify to be called?

cmorganBE avatar Feb 12 '24 18:02 cmorganBE

Sorry for the late response, yes that is the callback I was referring to.

h2zero avatar Feb 13 '24 13:02 h2zero

@h2zero if I call:

NimBLECharacteristic* pLogs;

                pLogs->setValue(logEntries[logIndex]);
                pLogs->notify();

from inside of the onNotify it looks like the notify() is triggering an onNotify() and this ends up in a recursive loop.

    void onNotify(NimBLECharacteristic* pCharacteristic) override
    {
        printf("%s : onNotify(), value: %s\n",
               pCharacteristic->getUUID().toString().c_str(),
               pCharacteristic->getValue().c_str());

                // this isn't exactly the code, i do bounds checking in the real case
                pLogs->setValue(logEntries[logIndex]);
                logIndex++;
                pLogs->notify();            
    }

Correct approach is to instead signal to an external thread from onNotify() and have that call setValue() and notify()?

cmorganBE avatar Feb 14 '24 22:02 cmorganBE

To close this one out I ended up doing:

ble processing thread:

while(true)
{
   if(readyForNextData)
   {
      readyForNextData = false;
      pTx->setValue(nextData);
   }

   // wait for events, sleep etc
}

and on the characteristic side:

class DataToTxCallbacks : public BLECharacteristicCallbacks
{
   ...

   void onNotify(NimBLECharacteristic* pCharacteristic) override
   {
      readForNextData = true;
   }
}

cmorganBE avatar Mar 04 '25 21:03 cmorganBE