AWS IoT MQTT File Streams 性能优化分析
目录
概述
aws-iot-core-mqtt-file-streams-embedded-c 是 AWS IoT 提供的专门用于通过 MQTT 协议高效传输大文件的库。它通过分块传输(Block-based Transfer) 、**按需请求(On-demand Request)和流式处理(Streaming)**等机制,显著改善了传统 MQTT 文件传输的效率和性能。
主要特点
- ✅ 分块传输:将大文件分割成小块,避免单次传输过大消息
- ✅ 按需请求:设备可以控制请求的块大小和数量
- ✅ 双格式支持:支持 JSON 和 CBOR 两种编码格式(CBOR 更紧凑)
- ✅ 流式处理:支持边接收边处理,无需等待整个文件
- ✅ 内存友好:适合资源受限的嵌入式设备
核心优化机制
1. 分块传输(Block-based Transfer)
传统方式的问题
传统 MQTT 文件传输:
文件 (10MB) → 单个 MQTT 消息 → 传输失败/超时
问题:
- MQTT 消息大小限制(通常 128KB-256KB)
- 大文件无法一次性传输
- 内存占用大(需要完整文件缓冲区)
- 传输失败需要重传整个文件
MQTT File Streams 的解决方案
分块传输流程:
文件 (10MB) → 分成多个块 (4KB/块) → 逐个请求和接收
关键参数:
c
// 可配置的块大小(默认 4KB)
#define mqttFileDownloader_CONFIG_BLOCK_SIZE 4096U
// 每个请求可以请求多个块
uint32_t numberOfBlocksRequested; // 例如:一次请求 10 个块
优势:
- 突破消息大小限制:每个块都在 MQTT 消息大小限制内
- 降低内存占用:只需为单个块分配内存(4KB),而不是整个文件(10MB)
- 部分失败恢复:只需重传失败的块,而不是整个文件
- 流式处理:可以边接收边处理,无需等待整个文件
2. 按需请求机制(On-demand Request)
设备控制传输节奏
设备可以根据自身情况控制传输:
c
// 创建请求
size_t mqttDownloader_createGetDataBlockRequest(
DataType_t dataType, // JSON 或 CBOR
uint16_t fileId, // 文件 ID
uint32_t blockSize, // 块大小(可调整)
uint16_t blockOffset, // 起始块偏移
uint32_t numberOfBlocksRequested, // 请求的块数量(可调整)
char * getStreamRequest,
size_t getStreamRequestLength
);
灵活的参数调整
块大小调整:
- 小内存设备:使用较小的块(如 1KB-2KB)
- 大内存设备:使用较大的块(如 8KB-16KB)以提高效率
- 网络条件差:使用较小的块以减少重传成本
块数量调整:
- 网络稳定:一次请求多个块(如 10-20 个)减少往返次数
- 网络不稳定:一次请求少量块(如 1-3 个)提高可靠性
- 内存受限:一次请求少量块以降低内存峰值
请求格式示例
JSON 格式:
json
{
"s": 1, // stream ID
"f": 1, // file ID
"l": 4096, // block size
"o": 0, // block offset
"n": 10 // number of blocks requested
}
CBOR 格式(更紧凑):
c
// CBOR 编码后的消息更小,节省带宽
CBOR_Encode_GetStreamRequestMessage(...)
3. 双格式支持(JSON vs CBOR)
JSON 格式
特点:
- 人类可读
- 易于调试
- 消息较大(文本格式)
适用场景:
- 开发和调试阶段
- 需要人工查看消息内容
CBOR 格式(推荐用于生产)
特点:
- 二进制编码:比 JSON 小 20-40%
- 解析速度快:二进制解析比文本解析快
- 带宽效率高:减少网络传输量
性能对比:
| 格式 | 消息大小 | 解析速度 | 带宽效率 |
|---|---|---|---|
| JSON | 100% | 基准 | 基准 |
| CBOR | 60-80% | 快 2-3x | 高 20-40% |
代码示例:
c
// 使用 CBOR 格式(推荐)
mqttDownloader_init(&context, streamName, streamNameLen,
thingName, thingNameLen, DATA_TYPE_CBOR);
// 使用 JSON 格式(调试用)
mqttDownloader_init(&context, streamName, streamNameLen,
thingName, thingNameLen, DATA_TYPE_JSON);
CBOR 格式详细介绍
什么是 CBOR?
CBOR(Concise Binary Object Representation,简洁二进制对象表示) 是一种轻量级的数据交换格式,由 IETF 在 RFC 7049(2013年)中标准化,后来被 RFC 8949(2020年)更新。
设计目标
CBOR 的设计目标包括:
- 极小的代码体积:编码/解码器实现简单,适合嵌入式设备
- 紧凑的消息大小:比 JSON 等文本格式更小
- 无需版本协商的扩展性:可以添加新特性而不破坏兼容性
- 确定性编码:相同数据总是产生相同的字节序列
CBOR 基本概念
1. 数据类型(Major Types)
CBOR 定义了 8 种主要数据类型:
| Major Type | 值 | 数据类型 | 说明 |
|---|---|---|---|
| 0 | 0-23 | 无符号整数 | 直接编码小整数(0-23) |
| 1 | 0-23 | 负整数 | -1 到 -24 |
| 2 | - | 字节串(Byte String) | 二进制数据 |
| 3 | - | 文本串(Text String) | UTF-8 字符串 |
| 4 | - | 数组(Array) | 有序元素列表 |
| 5 | - | 映射(Map) | 键值对集合 |
| 6 | - | 标签(Tag) | 语义标签 |
| 7 | - | 浮点数/简单值 | float、double、true、false、null 等 |
2. 编码格式
CBOR 使用自描述的二进制格式:
[初始字节] [附加信息] [数据内容]
初始字节结构:
7 6 5 4 3 2 1 0
| | |
| | +-- 附加信息(0-23 或长度指示)
| +-------- 主要类型(0-7)
+-------------- 保留位
示例:
0x18= 主要类型 0(无符号整数),附加信息 24(表示后面有 1 字节数据)0x61= 主要类型 3(文本串),附加信息 1(字符串长度 1)0xA1= 主要类型 5(映射),附加信息 1(1 个键值对)
CBOR vs JSON 对比示例
示例 1:简单的 Get Stream 请求
JSON 格式:
json
{
"s": 1,
"f": 1,
"l": 4096,
"o": 0,
"n": 10
}
大小: 约 45 字节(包含空格和换行)
CBOR 格式(十六进制):
A5 61 73 01 61 66 01 61 6C 19 10 00 61 6F 00 61 6E 0A
大小: 约 18 字节
节省: 60% 的空间
CBOR 解码说明:
A5 - 映射(Map),5 个键值对
61 73 - 文本串 "s"(键)
01 - 整数 1(值)
61 66 - 文本串 "f"(键)
01 - 整数 1(值)
61 6C - 文本串 "l"(键)
19 10 00 - 整数 4096(值,2 字节)
61 6F - 文本串 "o"(键)
00 - 整数 0(值)
61 6E - 文本串 "n"(键)
0A - 整数 10(值)
示例 2:包含 Base64 编码数据的响应
JSON 格式:
json
{
"f": 1,
"i": 0,
"l": 4096,
"p": "SGVsbG8gV29ybGQh..."
}
大小: 约 60+ 字节(Base64 字符串本身)
CBOR 格式:
A4 61 66 01 61 69 00 61 6C 19 10 00 61 70 58 10 48 65 6C 6C 6F 20 57 6F 72 6C 64 21
大小: 约 30+ 字节
关键优势:
- CBOR 可以直接编码二进制数据(字节串),无需 Base64 编码
- 如果使用字节串而不是 Base64 文本串,可以节省 33% 的空间
CBOR 在 MQTT File Streams 中的使用
1. Get Stream Request 编码
函数签名:
c
bool CBOR_Encode_GetStreamRequestMessage(
uint8_t *messageBuffer,
size_t messageBufferSize,
size_t *encodedMessageSize,
const char *clientToken, // "c" 键
uint32_t fileId, // "f" 键
uint32_t blockSize, // "l" 键
uint32_t blockOffset, // "o" 键
const uint8_t *blockBitmap, // "b" 键(字节串)
size_t blockBitmapSize,
uint32_t numOfBlocksRequested // "n" 键
);
编码结构:
c
// CBOR 映射结构
{
"c": "client_token", // 客户端令牌(文本串)
"f": 1, // 文件 ID(整数)
"l": 4096, // 块大小(整数)
"o": 0, // 块偏移(整数)
"b": <bytes>, // 块位图(字节串)
"n": 10 // 请求的块数量(整数)
}
编码过程:
c
// 1. 初始化编码器
cbor_encoder_init(&encoder, messageBuffer, messageBufferSize, 0);
// 2. 创建映射容器(6 个键值对)
cbor_encoder_create_map(&encoder, &cborMapEncoder, 6);
// 3. 编码每个键值对
cbor_encode_text_stringz(&cborMapEncoder, "c"); // 键
cbor_encode_text_stringz(&cborMapEncoder, clientToken); // 值
cbor_encode_text_stringz(&cborMapEncoder, "f"); // 键
cbor_encode_int(&cborMapEncoder, fileId); // 值
// ... 其他键值对
// 4. 关闭映射容器
cbor_encoder_close_container_checked(&encoder, &cborMapEncoder);
2. Get Stream Response 解码
函数签名:
c
bool CBOR_Decode_GetStreamResponseMessage(
const uint8_t *messageBuffer,
size_t messageSize,
int32_t *fileId, // "f" 键
int32_t *blockId, // "i" 键
int32_t *blockSize, // "l" 键
uint8_t *const *payload, // "p" 键(字节串)
size_t *payloadSize
);
解码结构:
c
// CBOR 映射结构
{
"f": 1, // 文件 ID(整数)
"i": 0, // 块 ID(整数)
"l": 4096, // 块大小(整数)
"p": <bytes> // 块数据(字节串,Base64 编码)
}
解码过程:
c
// 1. 初始化解析器
cbor_parser_init(messageBuffer, messageSize, 0, &parser, &cborMap);
// 2. 验证是映射类型
cbor_value_is_map(&cborMap);
// 3. 查找并解码每个字段
cbor_value_map_find_value(&cborMap, "f", &value); // 查找文件 ID
cbor_value_get_int(&value, fileId); // 解码整数
cbor_value_map_find_value(&cborMap, "i", &value); // 查找块 ID
cbor_value_get_int(&value, blockId); // 解码整数
// ... 其他字段
cbor_value_map_find_value(&cborMap, "p", &value); // 查找负载
cbor_value_get_byte_string(&value, payload, payloadSize, NULL); // 解码字节串
CBOR 的优势详解
1. 空间效率
整数编码优化:
- 小整数(0-23)直接编码在初始字节中,无需额外字节
- JSON 需要至少 1 字节(数字字符)+ 可能的引号和逗号
字符串优化:
- 短字符串(< 24 字符)长度直接编码在初始字节中
- JSON 需要引号、转义字符等额外开销
二进制数据:
- CBOR 原生支持字节串,无需 Base64 编码(节省 33% 空间)
- JSON 必须使用 Base64 文本串
2. 解析性能
二进制解析 vs 文本解析:
- CBOR:直接读取字节,类型已知,无需字符解析
- JSON:需要字符解析、类型推断、字符串转义处理
性能对比(典型值):
- 编码速度:CBOR 快 2-3 倍
- 解码速度:CBOR 快 2-4 倍
- 内存占用:CBOR 解析器通常更小
3. 确定性编码
相同输入 → 相同输出:
- CBOR 编码是确定性的(在相同实现下)
- 适合需要数据签名的场景
- JSON 可能有多种有效表示(空格、键顺序等)
4. 扩展性
标签(Tags)机制:
- 可以使用标签为数据添加语义信息
- 例如:日期时间、大整数、URI 等
- 不影响基本解析器
CBOR 数据类型映射
| JSON 类型 | CBOR 类型 | CBOR Major Type | 说明 |
|---|---|---|---|
number (整数) |
无符号/负整数 | 0/1 | 直接编码 |
number (浮点) |
浮点数 | 7 | float/double |
string |
文本串 | 3 | UTF-8 编码 |
array |
数组 | 4 | 有序列表 |
object |
映射 | 5 | 键值对 |
true |
简单值 | 7 | true |
false |
简单值 | 7 | false |
null |
简单值 | 7 | null |
| N/A | 字节串 | 2 | JSON 不支持 |
实际使用建议
1. 何时使用 CBOR
✅ 推荐使用 CBOR:
- 生产环境
- 带宽受限的场景
- 需要高性能解析
- 需要传输二进制数据
- 嵌入式设备
❌ 不推荐使用 CBOR:
- 开发和调试阶段(难以人工阅读)
- 需要人工查看消息内容
- 简单的配置数据(JSON 更直观)
2. CBOR 库选择
MQTT File Streams 使用 tinyCBOR:
- 轻量级(适合嵌入式)
- 无动态内存分配
- 符合 MISRA C 标准
- 代码体积小
其他常用 CBOR 库:
- C: tinyCBOR(本项目使用)、libcbor
- Python: cbor2、cbor
- JavaScript: cbor-js、cbor-web
3. 调试 CBOR 数据
在线工具:
- CBOR Playground - 在线 CBOR 编码/解码
- CBOR Inspector - CBOR 数据查看器
命令行工具:
bash
# 使用 cbor2(Python)
pip install cbor2
python -m cbor2.tool < file.cbor
# 使用 cbor-cli(Node.js)
npm install -g cbor-cli
cbor decode < file.cbor
CBOR 编码示例(完整)
Get Stream Request(CBOR)
C 代码:
c
uint8_t requestBuffer[256];
size_t requestLen = 0;
// 创建请求
mqttDownloader_createGetDataBlockRequest(
DATA_TYPE_CBOR,
1, // fileId
4096, // blockSize
0, // blockOffset
10, // numberOfBlocksRequested
(char *)requestBuffer,
sizeof(requestBuffer)
);
生成的 CBOR 数据(十六进制):
A6 # 映射,6 个键值对
61 63 # 文本串 "c"(键)
63 72 64 79 # 文本串 "rdy"(值,客户端令牌)
61 66 # 文本串 "f"(键)
01 # 整数 1(值,文件 ID)
61 6C # 文本串 "l"(键)
19 10 00 # 整数 4096(值,块大小)
61 6F # 文本串 "o"(键)
00 # 整数 0(值,块偏移)
61 62 # 文本串 "b"(键)
42 4D 51 # 字节串,长度 2,内容 "MQ"(Base64 编码的位图)
61 6E # 文本串 "n"(键)
0A # 整数 10(值,请求的块数量)
总大小: 约 25 字节
对应的 JSON:
json
{
"c": "rdy",
"f": 1,
"l": 4096,
"o": 0,
"b": "MQ==",
"n": 10
}
总大小: 约 50 字节
节省: 50% 的空间
CBOR 格式总结
| 特性 | CBOR | JSON |
|---|---|---|
| 编码方式 | 二进制 | 文本 |
| 人类可读 | ❌ | ✅ |
| 消息大小 | 小(60-80% of JSON) | 大 |
| 解析速度 | 快(2-4x) | 慢 |
| 代码体积 | 小 | 中等 |
| 二进制数据 | ✅ 原生支持 | ❌ 需 Base64 |
| 确定性 | ✅ | ❌(多种表示) |
| 扩展性 | ✅(标签机制) | ✅ |
| 调试难度 | 高 | 低 |
结论: CBOR 是嵌入式 IoT 应用的理想选择,特别是在带宽受限、需要高性能的场景下。MQTT File Streams 库通过支持 CBOR 格式,显著提升了文件传输的效率和性能。
4. 流式处理(Streaming Processing)
边接收边处理
传统方式:
接收完整文件 → 存储到文件系统 → 处理文件
(需要完整文件的内存/存储空间)
流式处理:
接收块 1 → 处理块 1 → 接收块 2 → 处理块 2 → ...
(只需块大小的内存)
实现示例
c
// 伪代码示例
while (!fileComplete) {
// 1. 请求数据块
size_t requestLen = mqttDownloader_createGetDataBlockRequest(
DATA_TYPE_CBOR, fileId, blockSize, blockOffset,
numberOfBlocks, requestBuffer, sizeof(requestBuffer));
// 2. 发布请求
MQTT_Publish(&mqttContext, getTopic, requestBuffer, requestLen, ...);
// 3. 等待并接收数据块
MQTT_ProcessLoop(&mqttContext, timeout);
// 4. 检查是否收到数据块
if (mqttDownloader_isDataBlockReceived(&context, topic, topicLen)) {
// 5. 处理接收到的数据块
mqttDownloader_processReceivedDataBlock(
&context, message, messageLen,
&fileId, &blockId, &blockSize, dataBuffer, &dataLen);
// 6. 立即处理数据(写入文件、验证等)
processBlock(dataBuffer, dataLen, blockId);
// 7. 更新偏移,请求下一批块
blockOffset += numberOfBlocks;
}
}
优势:
- 内存占用低:只需一个块大小的缓冲区
- 实时处理:可以边下载边验证、边写入
- 快速响应:不需要等待整个文件下载完成
5. Base64 解码优化
高效的 Base64 解码实现
库中实现了优化的 Base64 解码算法:
c
// 使用缓冲区累积,减少函数调用开销
static void updateBase64DecodingBuffer(
const uint8_t base64Index,
uint32_t *base64IndexBufferPtr,
uint32_t *numDataIndexBuffer);
// 批量解码(2、3、4 个字符一组)
static Base64Status_t decodeBase64IndexBuffer(
uint32_t *base64IndexBufferPtr,
uint32_t *numDataIndexBuffer,
uint8_t *dest,
size_t destLen);
优化点:
- 批量处理:累积 4 个 Base64 字符后一次性解码
- 减少函数调用:降低调用开销
- 内存高效:使用 32 位缓冲区临时存储
与传统 MQTT 文件传输的对比
传统方式
c
// 传统方式:直接发布整个文件(如果文件小)
MQTT_Publish(&mqttContext, topic, entireFile, fileSize, ...);
// 问题:
// 1. 文件大小受限于 MQTT 消息大小(通常 128KB)
// 2. 需要完整文件的内存
// 3. 传输失败需要重传整个文件
// 4. 无法流式处理
MQTT File Streams 方式
c
// 分块请求和接收
mqttDownloader_createGetDataBlockRequest(...);
MQTT_Publish(&mqttContext, getTopic, request, requestLen, ...);
// 接收并处理块
mqttDownloader_processReceivedDataBlock(...);
// 优势:
// 1. 支持任意大小的文件
// 2. 只需块大小的内存
// 3. 部分失败只需重传失败的块
// 4. 支持流式处理
性能对比表
| 特性 | 传统方式 | MQTT File Streams |
|---|---|---|
| 最大文件大小 | 受限于 MQTT 消息大小(~128KB) | 无限制 |
| 内存占用 | 完整文件大小 | 块大小(可配置,默认 4KB) |
| 失败恢复 | 重传整个文件 | 只需重传失败的块 |
| 传输效率 | 低(单次传输) | 高(可并行请求多个块) |
| 流式处理 | 不支持 | 支持 |
| 带宽效率 | 基准 | CBOR 格式节省 20-40% |
| 设备控制 | 无 | 完全控制(块大小、数量) |
性能优化策略
1. 选择合适的块大小
块大小选择原则
c
// 小内存设备(RAM < 64KB)
#define BLOCK_SIZE 2048U // 2KB
// 中等内存设备(RAM 64KB-256KB)
#define BLOCK_SIZE 4096U // 4KB(默认)
// 大内存设备(RAM > 256KB)
#define BLOCK_SIZE 8192U // 8KB 或更大
考虑因素:
- 可用内存:块大小不应超过可用内存的 20-30%
- 网络条件:网络不稳定时使用较小的块
- 处理速度:如果处理速度快,可以使用较大的块减少请求次数
2. 优化块请求数量
c
// 网络稳定 + 内存充足
uint32_t numberOfBlocks = 20; // 一次请求 20 个块
// 网络不稳定
uint32_t numberOfBlocks = 3; // 一次请求 3 个块
// 内存受限
uint32_t numberOfBlocks = 1; // 一次请求 1 个块
权衡:
- 更多块:减少往返次数,提高吞吐量,但需要更多内存
- 更少块:降低内存占用,提高可靠性,但增加往返次数
3. 使用 CBOR 格式
c
// 生产环境推荐使用 CBOR
mqttDownloader_init(&context, ..., DATA_TYPE_CBOR);
收益:
- 带宽节省 20-40%
- 解析速度提升 2-3 倍
- 特别适合大文件传输
4. 实现并行请求(如果支持)
c
// 伪代码:并行请求多个块范围
for (int i = 0; i < PARALLEL_REQUESTS; i++) {
uint16_t offset = currentOffset + i * blocksPerRequest;
createAndSendRequest(fileId, blockSize, offset, blocksPerRequest);
}
// 并行接收和处理
while (pendingBlocks > 0) {
processReceivedBlocks();
}
注意: 需要确保 MQTT 客户端支持并行处理多个响应。
5. 实现块验证和重传机制
c
// 维护已接收块的位图
bool receivedBlocks[MAX_BLOCKS];
// 接收块后标记
receivedBlocks[blockId] = true;
// 定期检查缺失的块并重传
for (uint16_t i = 0; i < totalBlocks; i++) {
if (!receivedBlocks[i]) {
// 请求缺失的块
requestBlock(fileId, i);
}
}
6. 优化 Base64 解码
库已经实现了优化的 Base64 解码,但可以进一步优化:
c
// 如果可能,使用硬件加速的 Base64 解码
// 或者使用 SIMD 指令(如果平台支持)
实际应用建议
1. 内存受限设备(RAM < 64KB)
c
// 配置
#define BLOCK_SIZE 2048U // 2KB 块
#define NUMBER_OF_BLOCKS 1U // 一次请求 1 个块
#define DATA_TYPE DATA_TYPE_CBOR // 使用 CBOR
// 实现
- 使用最小的块大小
- 一次只请求一个块
- 立即处理接收到的块,释放内存
- 使用 CBOR 格式减少带宽
2. 中等资源设备(RAM 64KB-256KB)
c
// 配置
#define BLOCK_SIZE 4096U // 4KB 块(默认)
#define NUMBER_OF_BLOCKS 5U // 一次请求 5 个块
#define DATA_TYPE DATA_TYPE_CBOR
// 实现
- 使用默认块大小
- 一次请求多个块以提高效率
- 可以实现简单的块缓存
3. 资源充足设备(RAM > 256KB)
c
// 配置
#define BLOCK_SIZE 8192U // 8KB 块
#define NUMBER_OF_BLOCKS 10U // 一次请求 10 个块
#define DATA_TYPE DATA_TYPE_CBOR
// 实现
- 使用较大的块大小
- 一次请求更多块
- 可以实现块缓存和预取
- 可以考虑并行请求
4. 网络条件差的环境
c
// 配置
#define BLOCK_SIZE 2048U // 较小的块
#define NUMBER_OF_BLOCKS 2U // 少量块
#define RETRY_COUNT 5U // 增加重试次数
// 实现
- 使用较小的块以减少重传成本
- 实现更积极的重传机制
- 增加超时时间
5. 大文件传输(> 10MB)
重要: 需要增加位图大小以跟踪所有块
c
// 在配置文件中增加位图大小
// 默认 128 字节(1024 位)只能跟踪 1024 个块
// 对于 10MB 文件,4KB 块 = 2560 个块
// 需要至少 320 字节的位图
#define OTA_MAX_BLOCK_BITMAP_SIZE 512U // 支持 4096 个块
计算位图大小:
所需位图大小(字节)= (总块数 + 7) / 8
例如:2560 个块 = (2560 + 7) / 8 = 320 字节
性能优化检查清单
配置优化
- 根据设备内存选择合适的块大小
- 根据网络条件调整块请求数量
- 使用 CBOR 格式(生产环境)
- 大文件时增加位图大小
实现优化
- 实现流式处理(边接收边处理)
- 实现块验证机制
- 实现重传机制
- 优化内存使用(及时释放已处理的块)
网络优化
- 使用 QoS 1 确保消息送达
- 实现适当的超时机制
- 处理网络中断和重连
- 监控传输进度和性能
总结
aws-iot-core-mqtt-file-streams-embedded-c 通过以下机制显著改善了 MQTT 文件传输的效率和性能:
- 分块传输:突破消息大小限制,支持任意大小文件
- 按需请求:设备完全控制传输节奏
- 双格式支持:CBOR 格式节省 20-40% 带宽
- 流式处理:边接收边处理,降低内存占用
- 优化的解码:高效的 Base64 解码实现
关键优化建议:
- 根据设备资源选择合适的块大小和请求数量
- 生产环境使用 CBOR 格式
- 大文件传输时增加位图大小
- 实现流式处理和块验证机制
通过这些优化,MQTT File Streams 可以在资源受限的嵌入式设备上高效传输大文件,特别适合 OTA 更新等场景。
最后更新:基于 AWS IoT MQTT File Streams v1.1.0