具有 BLE GATT 特性的流数据方案

Schemes for streaming data with BLE GATT characteristics

BLE 的 GATT 架构适用于小的固定数据(每个特征最多 20 个字节)。但在某些情况下,您最终想要“流式传输”一些任意长度的数据,即大于 20 字节。比如固件升级,明知道慢

我很好奇其他人使用了什么方案来通过 BLE 特性“传输”数据(即使小而慢)。

迄今为止,我使用了两种不同的方案:

一种是使用控制特性,接收设备通知发送设备它收到了多少数据,然后发送设备使用它来触发下一次写入(我都做了with_response,和 without_response) 不同的特性。

我最近做的另一个方案基本上是将数据分成 19 个字节的段,其中第一个字节表示要跟随的数据包数量,当它达到 0 时,这会提示接收者所有最近的更新都可以连接并作为单个数据包处理。

我正在寻找的答案是概述有经验的人如何为此实现了一个体面的模式。并且可以证明为什么他们所做的是最好(或至少更好)的解决方案。

在对现有协议进行一些审查后,我最终设计了一个用于无线更新我的 BLE 外围设备的协议。

设计假设

  1. 我们无法预测堆栈行为(协议将与我们所有的产品一起使用,无论使用何种芯片和供应商堆栈,无论是在外围端还是在中央端,可能还未知),
  2. 使用标准 GATT 服务,
  3. 避免 L2CAP 碎片,
  4. 假设数据包在 TX 之前排队,
  5. 假设可能有一些丢包(即使堆栈不应该),
  6. 避免不必要的数据包往返,
  7. 将代码复杂性放在中央,
  8. 假设 4.2 增强功能不可用。

1 表示 2-5,6 是性能要求,7 是优化,8 是可移植性。

整体设计

发现服务并读取一些只读特征以检查设备与要上传图像的兼容性后,所有上传都在两个特征之间进行:

  • payload(只写,无响应),
  • 状态(应通知)。

整个固件映像通过有效负载特征分块发送。

Payload 是一个 20 字节的特征:4 字节的块偏移量,加上 16 字节的数据块。

状态通知告知是否存在错误情况,以及下一个预期的有效负载块偏移量。这样,上传者可以判断它是否可以继续推测,从它自己的偏移量发送它的块,或者它是否应该从状态通知中找到的偏移量恢复。

发送状态更新有两个主要原因:

  • 当一切顺利时(有效载荷按顺序飞行),以给定的速率(比如 4Hz,不是每个数据包),
  • 错误(无序,一段时间后没有收到有效负载等),具有相同的给定速率(也不是每个错误的数据包)。

接收方希望所有块按顺序排列,它不会重新排序。如果块乱序,它会被丢弃,并推送错误状态通知。

当状态出现时,它会隐式确认所有具有较小偏移量的块。

最后,发送方有一个传输window,其中许多成功的确认飞行允许发送方扩大其window(在匹配确认之前发送更多块)。 Window 如果发生错误则减少,丢弃的块可能是因为某处的队列溢出。

讨论

使用"one way" PDU(没有响应和通知的写入)是为了避免上面的6.,因为ATT协议明确告诉确认的PDU(写入,指示)不得流水线化(即您可能不会发送下一个PDU直到您收到回复)。

状态,包含最后接收到的数据块,缓解 5。

为了遵守2.和3.,payload是一个20字节的特征写。 4+16 有很多优点,一个是 16 字节块的偏移量验证只涉及移位,另一个是块在目标闪存中总是页面对齐的(7 更好)。

为了应对4.,在收到状态更新之前发送了多个块,推测它会被正确接收。

该协议具有以下特点:

  • 它适应无线电条件,
  • 它适应发送端的队列,
  • 没有来自目标的状态泛滥,
  • 队列保持填充状态,这允许整个中央堆栈使用每一个可能的 TX 机会。

一些参数超出了这个协议:

  • central 应该强制执行较短的连接间隔(尝试在更新应用程序中强制执行);
  • 从属 PHY 应该在从属延迟方面表现良好(YMMV,测试您供应商的堆栈);
  • 您可能应该压缩您的负载以减少传输时间。

数字

有:

  • 15% 压缩,
  • 一个连接的设备,connectionInterval = 10ms,
  • 主 PHY 将每个连接事件限制为 4-5 个 TX 数据包,
  • 平均无线电条件。

平均每个连接事件我得到 3.8 个数据包,即在数据包丢失、协议开销等之后约 6 kB/s 有用的有效负载

这样,60kB的图片上传不到10秒,整个过程(连接、发现、传输、图片验证、解压、刷机、重启)不到20秒。

这有点取决于你有什么样的中心设备。 通常,Write Without Response 是通过 BLE 传输数据的方式。 乱序接收的数据包不应该发生,因为 BLE 的 link 层永远不会在前一个数据包被确认之前发送下一个数据包。

对于Android,这非常简单:只需使用Write Without Response 一个接一个地发送所有数据包。一旦你得到 onCharacteristicWrite 你发送下一个数据包。这样 Android 自动将数据包排队,它也有自己的流量控制机制。当它的所有缓冲区都被填满时,onCharacteristicWrite 将在再次 space 时被调用。

然而

iOS 并不是那么聪明。如果您发送大量无响应写入数据包并且内部缓冲区已满,iOS 将静默丢弃新数据包。有两种方法可以解决这个问题,或者为外围设备实现一些(可能是复杂的)协议来通知传输状态,例如 Nipos answer。然而,更简单的方法是发送每 10 个数据包左右作为 Write With Response,其余的作为 Write Without Response。这样 iOS 将为您排队所有数据包并且不会丢弃 Write Without Response 数据包。唯一的缺点是 Write With Response 数据包需要一次往返。尽管如此,该方案仍应为您提供高吞吐量。