CBinary 是 OTX 代理插件中用于二进制数据缓冲区操作的轻量级工具类。它将原始字节数组 (uint8_t*) 和长度 (uint16_t) 封装为一个可拷贝、可比较、可拼接的值类型,简化了底层二进制数据(如诊断命令帧、安全访问种子/密钥、DTC 应答数据)的构建、传输与解析流程。CBinary 在刷写框架的 MCD 诊断通信层中承担核心角色——所有诊断请求与响应的载体均为 CBinary 对象,同时在 DTC 故障码管理模块中用于存储 ECU 返回的原始应答数据。
Sources: binary.h
设计动机与架构定位
在汽车诊断与 ECU 刷写场景中,数据交换的基本单位是可变长度的字节序列:UDS 诊断帧、安全访问的种子 (Seed) 与密钥 (Key)、TransferData (0x36) 的数据块、DTC 读取应答报文等。C++ 标准库虽然提供了 std::vector<uint8_t> 和 std::string,但它们各有局限——std::vector 内存开销较大、std::string 语义上不适合二进制数据且遇到 \0 会截断。
CBinary 的设计遵循 零开销抽象 原则:仅持有两个成员——指向堆内存的指针 m_pData 和 16 位长度 m_nSize。它牺牲了动态扩容的效率(每次 Append 都重新分配内存),换取了极简的内存模型和确定性的生命周期,适用于缓冲区最大不超过 65535 字节(64KB)的场景——这恰好是 UDS 协议中单帧最大长度的约束。
Sources: binary.h, binary.cpp
在项目中,CBinary 依赖于 algorithm.h 中定义的 MIN 宏(用于 operator < 的词法比较和 ReadBuffer 的边界裁剪),其自身位于底层工具层,被上层诊断通信模块和 DTC 管理模块引用:
graph TD
A[CBinary<br>binary.h / binary.cpp] --> B[algorithm.h<br>MIN 宏]
A --> C[标准库<br>stdlib.h, string.h]
D[MCDCmdTrans<br>MCD 诊断命令传输] --> A
E[MCDCmdBoardcast<br>MCD 广播命令] --> A
F[jidu_dtc.cpp<br>DTC 故障码管理] --> A
G[jidu_macro.h<br>公共类型定义] --> A
H[VBFBlock.h<br>VBF 数据块] --> A
style A fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style D fill:#fff3e0,stroke:#f57c00
style E fill:#fff3e0,stroke:#f57c00
style F fill:#f3e5f5,stroke:#7b1fa2Sources: binary.cpp, algorithm.h
核心数据结构与内存模型
CBinary 仅有两个公开成员变量:m_pData(指向堆上分配的 uint8_t 数组)和 m_nSize(uint16_t 类型,记录数组长度)。这种极简设计意味着:
- 默认构造后
m_pData = NULL,m_nSize = 0 - 所有构造路径最终汇集到
Init()方法 Init()在iLength == 0时保持 NULL 状态,否则new uint8_t[iLength]分配内存Empty()通过delete[]释放内存并将指针置 NULL- 析构函数直接调用
Empty()
Source: binary.cpp
内部缓冲区无容量预留机制(无 m_nCapacity 成员),这意味着每次 Append 操作都会触发完整的「分配新内存 → 拷贝旧数据 → 拷贝新数据 → 释放旧内存」流程。这一设计选择简化了内存管理逻辑,但使得频繁追加操作的复杂度为 O(n²)。在实际使用场景中,CBinary 对象通常在创建时就确定大小(通过构造函数传入完整数据),追加操作主要用于构建协议帧头后附加载荷,次数有限。
构造与初始化 API
CBinary 提供了五类构造函数和两种缓冲写入方式,覆盖了从零创建、从原始指针拷贝、值拷贝等场景:
| 构造函数 | 签名 | 用途 |
|---|---|---|
| 默认构造 | CBinary() | 创建空缓冲区,m_pData 为 NULL |
| 定长构造 | CBinary(uint16_t iLength) | 分配 iLength 字节空间,初始化为全零 |
| C 字符串构造 | CBinary(const char *pBuffer, uint16_t iLength) | 从 C 风格字符串(可含 \0)拷贝数据 |
| 字节数组构造 | CBinary(uint8_t *pBuffer, uint16_t iLength) | 从 uint8_t 指针拷贝数据 |
| 拷贝构造 | CBinary(const CBinary& binData) | 深拷贝另一个 CBinary 对象 |
Sources: binary.h, binary.cpp
实际项目中的应用模式非常丰富。在诊断通信层,常使用内联十六进制字面量快速构造命令帧:
// 进入编程会话 (0x10 0x02)
CBinary binReboot("\x10\x82", 2);
// 读取数据 (0x22 0xF1 0x86)
CBinary binReadData("\x22\xF1\x86", 3);
// 安全访问请求 Seed (0x27 0x01)
CBinary bin2701("\x27\x01", 2);Sources: MCDCmdTrans.cpp, MCDCmdTrans.cpp, MCDCmdTrans.cpp
在 DTC 管理模块中,CBinary 直接从 ECU 返回的原始字节流构造,并存入全局映射表:
// dtcBytefiled 来自诊断响应,inLen 为其长度
m_allEcuDtcAck[ecuName] = CBinary(dtcBytefiled, inLen);Sources: jidu_dtc.cpp
WriteBuffer 方法相当于「先清空再写入」,是 Empty() + Append() 的组合操作,适用于需要覆盖整个缓冲区内容的场景(如接收诊断响应后替换旧数据)。
Sources: binary.cpp
元素访问与修改
CBinary 提供了三种元素访问途径和两种修改方式,覆盖只读访问、可写引用访问、边界安全的 Get/Set 操作:
| 方法 | 签名 | 特性 |
|---|---|---|
operator[] (const) | uint8_t operator[](uint16_t nIndex) const | 只读访问,按值返回 |
operator[] (非 const) | uint8_t& operator[](uint16_t nIndex) | 可写引用,支持 bin[i] = 0xFF |
GetAt | uint8_t GetAt(uint16_t nIndex) | 带断言的只读访问 |
SetAt | void SetAt(uint16_t nIndex, uint8_t ucNewElement) | 带断言的单字节修改 |
Add | void Add(uint8_t ucNewElement) | 在末尾追加一个字节 |
Sources: binary.h, binary.h, binary.h, binary.cpp, binary.cpp
在刷写框架中,operator[] 的非 const 版本被大量用于按字节精确构建协议帧。例如 RequestDownload 方法中构建 11 字节请求帧的方式:
CBinary binRequestDownloadCmd("\x34\x00\x44\x00\x00\x00\x00\x00\x00\x00\x00", 11);
binRequestDownloadCmd[1] = formatIdentifer & 0xFF;
// ... 后续按位填充内存地址和数据长度Sources: MCDCmdTrans.cpp
类似的模式也出现在 EraseMemory 的地址与长度编码 SecurityAccess 的安全级别字节调整中,这种「模板帧 + 逐字节覆写」的模式是构建 UDS 诊断帧的惯用方法。
Sources: MCDCmdTrans.cpp, MCDCmdTrans.cpp
数据追加 (Append) 机制
Append 是 CBinary 的核心扩展方法,实现「分配新缓冲区 → 拷贝旧数据 → 追加新数据 → 释放旧缓冲区」的完整流程:
void CBinary::Append(uint8_t *pBuffer, uint16_t iLength) {
if (pBuffer == NULL || iLength <= 0) return;
uint16_t nTemp = m_nSize + iLength;
uint8_t *pTemp = new uint8_t[nTemp]; // 分配新空间
if (m_pData) memcpy(pTemp, m_pData, m_nSize); // 拷贝旧数据
memcpy(pTemp + m_nSize, pBuffer, iLength); // 追加新数据
Empty(); // 释放旧内存
m_pData = pTemp; // 指向新内存
m_nSize = nTemp;
}Sources: binary.cpp
Append 同时接受 uint8_t* 和 const char* 两种指针类型,后者内部转发到前者。在 SecurityAccess 方法中,Append 用于从诊断响应报文 (0x67 0x01 + 3字节种子) 中提取 3 字节种子值:
// rf 是 0x27 01 的响应帧,种子从第 3 字节开始
binSeed.Append(rf.m_pData + 2, 3);Sources: MCDCmdTrans.cpp
在 VerifyAuthenticity 中,Append 用于在命令头后拼接签名数据:
binSend = CBinary("\x31\x01\x02\x08", 4);
binSend.Append(strKeyInfo.c_str(), strKeyInfo.length());Sources: MCDCmdTrans.cpp
数据读取 (ReadBuffer) 方法
ReadBuffer 将内部缓冲区的数据拷贝到外部提供的缓冲区中。其关键设计在于 iLength = 0xFFFF 的哨兵值语义——当调用方传入 0xFFFF(即 65535,恰为 uint16_t 最大值)时,方法自动计算「从起始位置到缓冲区末尾」的长度:
if (iLength == 0xFFFF) iLength = m_nSize - iStart;
else if ((iStart + iLength) > m_nSize) iLength = m_nSize - iStart;这种设计允许调用方在不知道确切剩余长度的情况下,一次性读取所有剩余数据。数据通过逐字节循环拷贝到目标缓冲区中。
Sources: binary.cpp
插入与删除操作(条件编译)
InsertAt 和 RemoveAt 两个方法受到 #ifndef _BOOT 条件编译保护——在 Bootloader 场景下禁用。这两个方法用于在缓冲区的指定位置插入或删除指定数量的字节:
- InsertAt:在
nIndex位置插入nCount个值为ucNewElement的字节,通过「分配新内存 → 分段拷贝 → 释放旧内存」完成 - RemoveAt:从
nIndex位置删除nCount个字节,使用memmove将后续数据前移
Sources: binary.cpp
这种条件编译策略暗示了 _BOOT 宏标记的是存储空间严格受限的 Bootloader 环境——InsertAt 的临时分配 + 拷贝策略会在内存紧张时造成风险,因此在引导环境中被裁剪。
运算符重载体系
CBinary 实现了丰富的运算符重载,使其表现出值语义(Value Semantics),在诊断通信代码中可以像基本类型一样自然地组合和比较二进制数据。
拼接运算符 (+ 和 +=)
operator + 创建新对象(不修改原对象),operator += 在左操作数上原地追加。两者都支持 CBinary 与 CBinary 的拼接以及 CBinary 与单个 uint8_t 的拼接:
| 运算符 | 语义 | 是否修改原对象 |
|---|---|---|
bin1 + bin2 | 将 bin1 和 bin2 的内容拼接为新 CBinary | 否 |
bin1 + 0xFF | 在 bin1 末尾追加一个字节,返回新对象 | 否 |
bin1 += bin2 | 将 bin2 追加到 bin1 末尾 | 是 |
bin1 += 0xFF | 在 bin1 末尾追加一个字节 | 是 |
Sources: binary.cpp
在 SeedToKey 方法中,operator + 用于将种子和掩码拼接后进行密钥计算:
CBinary binData = binSeed + binMask;在 VerifyAuthenticity 中,operator += 用于将十六进制解码后的签名追加到命令头后:
binSend += CBinary(tempBuffer, rLen);Sources: MCDCmdTrans.cpp, MCDCmdTrans.cpp
比较运算符 (<, <=, >=, >, ==)
比较运算符实现了字典序 (lexicographic) 比较,与 std::string 的比较语义一致:从第一个字节开始逐字节比较,若所有公共字节相等则较短的缓冲区判定为较小值。
operator < 的实现清晰地展示了这一逻辑:
// NULL 处理:NULL 小于任何非 NULL
if (binData.m_pData == NULL) return false;
else if (this->m_pData == NULL) return true;
// 逐字节比较公共前缀
uint16_t nLength = MIN(this->m_nSize, binData.m_nSize);
for (int i = 0; i < nLength; i++) {
if (this->m_pData[i] < binData.m_pData[i]) return true;
if (this->m_pData[i] > binData.m_pData[i]) return false;
}
// 公共前缀相等,短者为小
return (this->m_nSize < binData.m_nSize) ? true : false;Sources: binary.cpp
operator == 采用短路求值策略:先比较指针(两 NULL 相等,一 NULL 一非 NULL 不等),再比较长度(不等则直接返回 false),最后逐字节比对。
operator <= 和 operator >= 同样受 #ifndef _BOOT 保护——这些复合比较运算符通过组合 < 和 == 实现,在 Bootloader 中通过直接使用 < 和 == 代替以减小代码体积。
Sources: binary.cpp, binary.cpp
赋值运算符 (=)
operator = 先清空当前对象再进行深拷贝,等价于 Empty() + Append() 的组合:
void CBinary::operator = (const CBinary& binData) {
Empty();
Append(binData.m_pData, binData.m_nSize);
}Sources: binary.cpp
查询与导出方法
CBinary 提供了简洁的查询接口用于获取内部状态和导出数据:
| 方法 | 返回类型 | 说明 |
|---|---|---|
GetSize() | uint16_t | 返回缓冲区字节数 |
GetBuffer() | uint8_t* | 返回指向内部缓冲区的裸指针 |
AsString() | const char* | 将内部数据强制解释为 C 字符串 |
IsEmpty() | bool | 判断缓冲区是否为空(NULL 或长度为 0) |
Sources: binary.h, binary.cpp, binary.cpp
GetBuffer() 返回的裸指针直接暴露内部内存,调用方可在知晓长度的情况下进行读取(如传给 Tracer::hex 进行十六进制日志输出、传给 MyStringUtils::BufferToString 进行字符串转换)。在 DTC 报告模块中,GetBuffer() 和 GetSize() 配合使用以获取 ECU 应答的完整原始数据:
jd_report_addNokItem(..., m_allEcuDtcAck[ecuName].GetBuffer(), m_allEcuDtcAck[ecuName].GetSize(), ...);Sources: jidu_dtc.cpp
在诊断通信层中的核心应用
CBinary 在 MCDCmdTrans 和 MCDCmdBoardcast 中承担了诊断命令帧的请求封装与响应解析的双重角色。
发送命令 (sendCommand)
sendCommand(CBinary binSend) 的执行流程展示了 CBinary 如何桥接 OTX 代理内部数据模型与 MCD3D 诊断协议库的 O_BYTEFIELD 类型:
CBinary MCDCmdTrans::sendCommand(CBinary binSend) {
CBinary rfData; // 空响应缓冲区
otx::O_BYTEFIELD dSend;
for (int i = 0; i < binSend.GetSize(); i++) // CBinary → O_BYTEFIELD
dSend.push_back(binSend.GetAt(i));
pdu.setBytefield(dSend); // 设置请求 PDU
request->enterPDU(pdu); // 注入请求
// ... 执行诊断通信 ...
otx::O_BYTEFIELD dRecv = response->getResponseMessage().getBytefield();
rfData.Init(dRecv.data(), dRecv.size()); // O_BYTEFIELD → CBinary
return rfData;
}Sources: MCDCmdTrans.cpp
这里的双向转换模式——CBinary → O_BYTEFIELD(发送)和 O_BYTEFIELD → CBinary(接收)——体现了 CBinary 作为「项目内部统一二进制数据表示」的设计意图:所有外部协议库的数据在边界处转换为 CBinary,内部代码仅操作 CBinary。
响应帧验证模式
诊断响应帧的验证通常遵循「检查非空 → 检查首字节(正响应 = 请求 SID + 0x40,负响应 = 0x7F)→ 检查后续字节」的链式模式:
CBinary binRecv = SendFrame(binSend, 1);
if (binRecv.IsEmpty() || binRecv[0] != binSend[0] + 0x40) {
return false; // 负响应或通信失败
}
// 正响应,继续解析...Sources: MCDCmdTrans.cpp
这一模式在 EraseMemory、RequestTransferExit、ActivateSbl、FinishProgram 等方法中反复出现,构成了刷写框架中诊断响应验证的通用范式。
在 DTC 管理模块中的应用
在 jidu_dtc.cpp 中,CBinary 用于存储 ECU 返回的 DTC 读取应答原始数据:
static map<std::string, CBinary> m_allEcuDtcAck;
// 存储 DTC 应答
m_allEcuDtcAck[ecuName] = CBinary(dtcBytefiled, inLen);
// 报告时导出
jd_report_addNokItem(..., m_allEcuDtcAck[ecuName].GetBuffer(), m_allEcuDtcAck[ecuName].GetSize(), ...);Sources: jidu_dtc.cpp, jidu_dtc.cpp, jidu_dtc.cpp
这里 CBinary 表现出值语义的优势:std::map 中存储 CBinary 对象而非指针,深拷贝确保数据生命周期独立于来源缓冲区,避免了悬空指针风险。当 ECU 多次读取 DTC 时,新 CBinary 赋值会自动释放旧数据。
条件编译与环境适配
CBinary 通过 _BOOT 和 _COCOS 两个预处理器宏适配不同的编译环境:
| 宏 | 影响范围 | 说明 |
|---|---|---|
#ifndef _BOOT | InsertAt、RemoveAt、operator <=、operator >= | Bootloader 环境裁剪复杂操作和冗余比较运算符 |
_COCOS | 定义后设为 1 | 标记 Cocos2d-x 游戏引擎环境(此项目中恒为 1) |
ASSERT_EX(x) | 多处 | 空宏定义,调试断言在生产构建中不产生代码 |
Sources: binary.h, binary.cpp, binary.cpp
局限性与设计取舍
CBinary 的直接性和简洁性也带来了以下局限:
- 64KB 上限:
uint16_t长度类型将缓冲区最大容量限制在 65535 字节,不适用于传输大型刷写文件(刷写文件通过分块传输绕过此限制,CBinary 仅承载单个传输帧) - 无容量预留:每次
Append都触发完整内存重新分配,频繁追加时效率低。可通过预分配定长缓冲区后使用operator[]逐字节填充来规避 - 直接暴露裸指针:
GetBuffer()返回的uint8_t*和AsString()返回的const char*不携带长度信息,调用方需自行通过GetSize()管理边界 - 无越界检查:
operator[]仅依赖ASSERT_EX(生产构建中为空宏),越界访问将导致未定义行为
这些取舍反映了 CBinary 的设计哲学:在受控的 ECU 诊断场景中,数据帧大小可预测、操作频率适中,优先保证代码简洁性和内存布局的确定性,而非通用性。
Sources: binary.h, binary.cpp
阅读下一步
CBinary 作为底层数据容器,在项目中主要被以下模块消费:
- MCDCmdTrans:基于 MCD3D 协议的 DoIP 文件下载与诊断通信 — CBinary 在此模块中作为诊断帧的载体,贯穿 sendCommand / SendFrame / SeedToKey 等核心方法
- DTC 故障码管理:读取、白名单过滤与掩码重载 — CBinary 用于存储 ECU 返回的 DTC 应答原始字节流
- 加密算法集:AES、DES-CBC、SHA256、ECDSA 签名与 Base64 编解码 — algorithm.h 中的 MIN 宏和加密函数与 CBinary 协同工作
- 刷写器架构总览:从 ECUFlasher 接口到多 ECU 并行刷写的继承体系 — 理解 CBinary 在刷写框架整体架构中的位置