9

The GATT architecture of BLE lends itself to small fixed pieces of data (20 bytes max per characteristic). But in some cases, you end up wanting to “stream” some arbitrary length of data, that is greater than 20 bytes. For example, a firmware upgrade, even if you know its slow.

I’m curious what scheme others have used if any, to “stream” data (even if small and slow) over BLE characteristics.

I’ve used two different schemes to date:

One was to use a control characteristic, where the receiving device notify the sending device how much data it had received, and the sending device then used that to trigger the next write (I did both with_response, and without_response) on a different characteristic.

Another scheme I did recently, was to basically chunk the data into 19 byte segments, where the first byte indicates the number of packets to follow, when it hits 0, that clues the receiver that all of the recent updates can be concatenated and processed as a single packet.

The kind of answer I'm looking for, is an overview of how someone with experience has implemented a decent schema for doing this. And can justify why what they did is the best (or at least better) solution.

Travis Griggs
  • 21,522
  • 19
  • 91
  • 167

2 Answers2

20

After some review of existing protocols, I ended up designing a protocol for over-the-air update of my BLE peripherals.

Design assumptions

  1. we cannot predict stack behavior (protocol will be used with all our products, whatever the chip used and the vendor stack, either on peripheral side or on central side, potentially unknown yet),
  2. use standard GATT service,
  3. avoid L2CAP fragmentation,
  4. assume packets get queued before TX,
  5. assume there may be some dropped packets (even if stacks should not),
  6. avoid unnecessary packet round-trips,
  7. put code complexity on central side,
  8. assume 4.2 enhancements are unavailable.

1 implies 2-5, 6 is a performance requirement, 7 is optimization, 8 is portability.

Overall design

After discovery of service and reading a few read-only characteristics to check compatibility of device with image to be uploaded, all upload takes place between two characteristics:

  • payload (write only, without response),
  • status (notifiable).

The whole firmware image is sent in chunks through the payload characteristic.

Payload is a 20-byte characteristic: 4-byte chunk offset, plus 16-byte data chunk.

Status notifications tell whether there is an error condition or not, and next expected payload chunk offset. This way, uploader can tell whether it may go on speculatively, sending its chunks from its own offset, or if it should resume from offset found in status notification.

Status updates are sent for two main reasons:

  • when all goes well (payloads flying in, in order), at a given rate (like 4Hz, not on every packet),
  • on error (out of order, after some time without payload received, etc.), with the same given rate (not on every erroneous packet either).

Receiver expects all chunks in order, it does no reordering. If a chunk is out of order, it gets dropped, and an error status notification is pushed.

When a status comes in, it acknowledges all chunks with smaller offsets implicitly.

Lastly, there is a transmit window on the sender side, where many successful acknowledges flying allow sender to enlarge its window (send more chunks ahead of matching acknowledge). Window is reduced if errors happen, dropped chunks probably are because of a queue overflow somewhere.

Discussion

Using "one way" PDUs (write without response and notification) is to avoid 6. above, as ATT protocol explicitly tells acknowledged PDUs (write, indications) must not be pipelined (i.e. you may not send next PDU until you received response).

Status, containing the last received chunk, palliates 5.

To abide 2. and 3., payload is a 20-byte characteristic write. 4+16 has numerous advantages, one being the offset validation with a 16-byte chunk only involves shifts, another is that chunks are always page-aligned in target flash (better for 7.).

To cope with 4., more than one chunk is sent before receiving status update, speculating it will be correctly received.

This protocol has the following features:

  • it adapts to radio conditions,
  • it adapts to queues on sender side,
  • there is no status flooding from target,
  • queues are kept filled, this allows the whole central stack to use every possible TX opportunity.

Some parameters are out of this protocol:

  • central should enforce short connection interval (try to enforce it in the updater app);
  • slave PHY should be well-behaved with slave latency (YMMV, test your vendor's stack);
  • you should probably compress your payload to reduce transfer time.

Numbers

With:

  • 15% compression,
  • a device connected with connectionInterval = 10ms,
  • a master PHY limiting every connection event to 4-5 TX packets,
  • average radio conditions.

I get 3.8 packets per connection event on average, i.e. ~6 kB/s of useful payload after packet loss, protocol overhead, etc.

This way, upload of a 60 kB image is done in less than 10 seconds, the whole process (connection, discovery, transfer, image verification, decompression, flashing, reboot) under 20 seconds.

Nipo
  • 2,617
  • 13
  • 20
  • Great answer, thank you. I wonder about 5. Is it really needed to assume dropped packets (and/or bad order). In practice, is this assumption required? – Frans Lundberg May 18 '16 at 11:04
  • Nipo, you don't happen to have some sharable Java/Android code for this? – Frans Lundberg May 18 '16 at 11:40
  • @FransLundberg: Some BLE peripheral vendor stacks will begin to act strangely when they are used in streaming conditions. I've seen some drop packets silently (including packets containing acknowledged requests at the ATT level, deadlocking the protocol totally). I've never seen reordering, but [others have](http://stackoverflow.com/questions/37140959), even if it's a bug, it will happen again in another stack some day... – Nipo May 18 '16 at 12:39
  • The ordering is kept just by the time the sender sends/dequeues the write, right (which is why occasionally the packets can be received out of order)? Confused because the other answer says that BLE will not send a new package until the previous was acknowledged, which seems to contradict "write without response". – User May 23 '19 at 06:05
  • @lxx All queues should be in-order, failing to do so is a bug. Using Write Without Response is the whole point: devices are allowed to send many without waiting for packets in the other direction (there aren't any anyway...). – Nipo May 23 '19 at 13:18
  • @Nipo that's clear, what I mean is that there's no guarantee that the packets will arrive at this order except what's given by the time they're sent by the client. The client will send them almost at the same time as it's not waiting for an ACK. Thus you need (as described in your post) to handle possible out of order packages and a transmit window for possible flooding. – User May 23 '19 at 20:32
  • @Nipo realize this is an old post but could you explain the transmit window? Would this require time stamping of the packets or at least of the transmissions, and only writing the next packet if we've surpassed the time window since the previous Tx? I'm implementing something similar for an embedded platform i'll probably implement this with a tick counter of some kind. – codebender Aug 05 '19 at 22:12
  • For optimal performance, yes. Ack packets should be sent periodically, and data packets should be sent, as ack allows, either in bursts, or timely scattered if there is a risk of some overflow in tx queue. – Nipo Aug 07 '19 at 04:54
2

It depends a bit on what kind of central device you have. Generally, Write Without Response is the way to stream data over BLE. Packets being received out-of-order should not happen since BLE's link layer never sends the next packet before it the previous one has been acknowledged.

For Android it's very easy: just use Write Without Response to send all packets, one after another. Once you get the onCharacteristicWrite you send the next packet. That way Android automatically queues up the packets and it also has its own mechanism for flow control. When all its buffers are filled up, the onCharacteristicWrite will be called when there is space again.

iOS is not that smart however. If you send a lot of Write Without Response packets and the internal buffers are full, iOS will silently drop new packets. There are two ways around this, either implement some (maybe complex) protocol for the peripheral notifying the status of the transmission, like Nipos answer. An easier way however is to send each 10th packet or so as a Write With Response, the rest as Write Without Response. That way iOS will queue up all packets for you and not drop the Write Without Response packets. The only downside is that the Write With Response packets require one round-trip. This scheme should nevertheless give you high throughput.

Emil
  • 16,784
  • 2
  • 41
  • 52
  • If BLE waits for acknowledgment of the previous package to send a new one, how is it "write without response"? Where is the performance gain? – User May 23 '19 at 05:59
  • The acknowledgement is on the Link Layer, not the ATT layer. That makes it fast because multiple packets can then be sent during each connection interval, compared to Write With Response. – Emil May 23 '19 at 07:13
  • interesting, I thought that "write without response" didn't have a response at all. So basically the client is still receiving acknowledgments for each packet, but it doesn't need to wait for them before sending the next packet? What's the purpose of these ACKs handled by link layer - verify data integrity, maybe? Would be link layer resend failed packets? – User May 23 '19 at 20:36
  • The only purpose of the acknowledgement system in the Link Layer is flow control. Only one packet can be outstanding, which must be acknowledged before a new packet can be sent. A packet is resent until acknowledged, or the link times out (disconnection). – Emil May 23 '19 at 20:44
  • An ACK is just a handshake (no response payload). Very fast, just to ensure packets are are ordered. – Dominic Cerisano Mar 16 '23 at 18:26