本文档深入剖析集度 OTX 代理插件中的 DTC(Diagnostic Trouble Code)故障码管理子系统。该模块负责从 ECU 原始诊断响应中解析 SAE J2012 标准格式的故障码,支持工厂版与售后版两套白名单配置路径,并在报告生成阶段自动过滤被掩码的故障码项。
架构总览
DTC 模块由三个静态全局容器与六个对外 API 构成,所有对容器数据的访问均受 std::mutex mItemLock 保护,保证多线程环境下的操作原子性。模块不直接发起诊断通信,而是接收外部传入的原始 UDS 响应字节流,内部完成解析、白名单过滤、上报三道工序。
flowchart TD
subgraph 外部调用者
A[OTX 诊断序列] -->|原始响应字节流| B[jd_dtc_readAll]
C[工厂端配置] -->|分号分隔字符串| D[jd_dtc_setWhiteList]
E[售后端配置] -->|触发重载| F[jd_dtc_reloadDtcMask]
end
subgraph DTC模块 jidu_dtc.cpp
B -->|dtc_calc 解析| G[vecDtcNo]
G -->|填充| H["m_allEcuDtcInfo<br/>map<ecuName, vector<JIDU_DTC_INFO_t>>"]
B -->|保存原始响应| I["m_allEcuDtcAck<br/>map<ecuName, CBinary>"]
D -->|解析 ECUN:dtc1,dtc2| J["m_mapEcuDtcWhiteList<br/>map<ecuName, vector<string>>"]
F -->|加载配置文件 JSON| J
H --> K[jd_dtc_parsedByIndex]
J --> L[jd_dtc_isInWhiteList]
end
subgraph 上报阶段
M[jd_dtc_report] -->|遍历 m_allEcuDtcInfo| N{在白名单中?}
N -->|是| O[跳过]
N -->|否| P[jd_report_addNokItem]
end
J -.-> N
style B fill:#e1f5fe
style D fill:#fff3e0
style F fill:#fff3e0
style M fill:#e8f5e9核心设计原则:读取与过滤分离 —— jd_dtc_readAll 只负责解析与缓存原始数据,白名单过滤延迟到 jd_dtc_report 阶段执行。这意味着即使白名单后续发生变化(如通过 reloadDtcMask 重载),已缓存的 DTC 数据仍可按新规则重新过滤上报。
Sources: jidu_dtc.cpp
DTC 数据结构
JIDU_DTC_INFO_t
定义于 jidu_macro.h,是 DTC 信息的最小存储单元:
| 字段 | 类型 | 说明 |
|---|---|---|
strDTCNo | std::string | SAE J2012 标准 7 字符故障码,如 P0301、U0073 |
strContent | std::string | 故障码描述文本,当前固定为 "未定义" |
typedef struct JIDU_DTC_INFO
{
std::string strDTCNo;
std::string strContent;
}JIDU_DTC_INFO_t;Sources: jidu_macro.h
模块级全局容器
| 容器 | 类型 | 用途 | 生命周期 |
|---|---|---|---|
m_allEcuDtcInfo | map<string, vector<JIDU_DTC_INFO_t>> | 按 ECU 名称存储已解析的 DTC 列表 | 每次 jd_dtc_readAll 覆盖对应 ECU 的条目 |
m_allEcuDtcAck | map<string, CBinary> | 按 ECU 名称保存原始 UDS 响应字节流 | 与 m_allEcuDtcInfo 同步更新 |
m_mapEcuDtcWhiteList | map<string, vector<string>> | 按 ECU 名称存储白名单 DTC 编号 | jd_dtc_setWhiteList 全量替换;jd_dtc_reloadDtcMask 追加 |
这三个容器通过 std::mutex mItemLock 实现互斥访问。m_allEcuDtcAck 的存在是为了在 jd_dtc_report 上报时将原始诊断请求响应字节流一并写入报告记录,满足可追溯性要求。
Sources: jidu_dtc.cpp
故障码解析:从原始字节到 SAE 标准格式
SAE J2012 编码规则
UDS 诊断响应中,每个故障码占用 4 个字节,其中第 3~5 字节包含 DTC 编号的编码信息。第 3 个字节的高 2 位(bit7-bit6)决定了故障码的前缀字母:
| 高 2 位值 | 前缀字母 | 故障域 |
|---|---|---|
00 (0x00) | P (Powertrain) | 动力总成 |
01 (0x40) | C (Chassis) | 底盘 |
10 (0x80) | B (Body) | 车身 |
11 (0xC0) | U (Network) | 网络通信 |
jd_PCBU_4BYTE — 前缀判定与格式化
int jd_PCBU_4BYTE(uint8_t* binId, int inLen, char* outBuffer, int outBufferSize)该函数接收 6 字节输入(0x8B 0x01 0x01 XX XX XX),取第 3 字节的高 2 位判断前缀字符,然后将该字节的高 2 位清零,与第 4、第 5 字节拼接为 {P|C|B|U}XX XX XX 格式的 7 字符故障码。
示例:若输入 binId[3]=0x03,由于 0x03 & 0xC0 = 0x00,前缀为 P,清零后 binId[3]=0x03,结合 binId[4]=0x01、binId[5]=0x00,最终输出 P030100。实际调用中此函数仅输出 7 字符(sprintf 格式化 %c%02X%02X%02X),返回值固定为 7。
Sources: jidu_dtc.cpp
dtc_calc — 批量解析
inline bool dtc_calc(uint8_t* rf, int rfLen, vector<string>& vecDtcNo)该内联函数负责将 UDS ReadDTCInformation(0x19 服务)响应的原始字节流解析为 DTC 编号列表。响应格式遵循 ISO 14229-1:
- 前 3 字节为响应头(SID + 子功能 + 状态掩码)
- 之后每 4 字节为一个 DTC 记录(HighByte + MiddleByte + LowByte + StatusByte)
函数计算 iDtcNo = (rfLen - 3) / 4 确定 DTC 数量,迭代提取每个 DTC 的 3 字节编号部分,过滤全零记录(表示无故障),调用 jd_PCBU_4BYTE 进行格式化。
uint8_t binDtcId[8] = { 0x8B, 0x01, 0x01, 0x00, 0x00, 0x00 };
int iDtcNo = (rfLen - 3) / 4;
for (int i = 0; i < iDtcNo; i++)
{
binDtcId[3] = rf[3 + i * 4]; // DTC HighByte
binDtcId[4] = rf[4 + i * 4]; // DTC MiddleByte
binDtcId[5] = rf[5 + i * 4]; // DTC LowByte
// 过滤全零记录
if ((binDtcId[3] == 0) && (binDtcId[4] == 0) && (binDtcId[5] == 0))
continue;
// 格式化为 SAE 标准字符串
jd_PCBU_4BYTE(binDtcId, 6, dtcIdBuffer, 128);
vecDtcNo.push_back(dtcIdBuffer);
}Sources: jidu_dtc.cpp
API 详解
jd_dtc_readAll — 读取全部故障码
OTX_API int jd_dtc_readAll(const char* ecuName, uint8_t* dtcBytefiled, uint32_t inLen);这是 DTC 模块的数据入口。工作流程:
sequenceDiagram
participant Caller as OTX 诊断序列
participant ReadAll as jd_dtc_readAll
participant Calc as dtc_calc
participant Map as 模块全局容器
Caller->>ReadAll: ecuName, 原始响应字节流, inLen
ReadAll->>ReadAll: lock(mItemLock)
ReadAll->>ReadAll: Tracer::hex 记录原始字节
ReadAll->>Calc: dtc_calc(dtcBytefiled, inLen, vecDtcNo)
Calc-->>ReadAll: 返回 DTC 编号列表
ReadAll->>Map: 清空 m_allEcuDtcInfo[ecuName]
ReadAll->>Map: m_allEcuDtcAck[ecuName] = CBinary(原始字节)
loop 每个 DTC 编号
ReadAll->>Map: m_allEcuDtcInfo[ecuName].push_back({编号, "未定义"})
end
ReadAll-->>Caller: 返回 DTC 数量每次调用会覆盖该 ECU 之前缓存的全部 DTC 数据——先执行 m_allEcuDtcInfo[ecuName].clear() 再重新填充。返回值为解析出的 DTC 总数。
关键观察:strContent 字段始终设为 "未定义",表明当前版本不查询 DTC 描述数据库。jd_dtc_queryByDTCNo 函数体直接返回 -1(空实现),佐证了这一点。
Sources: jidu_dtc.cpp
jd_dtc_parsedByIndex — 按索引查询
OTX_API int jd_dtc_parsedByIndex(const char* ecuName, int index, char* outBuffer, int outBufferSize);从已缓存的 DTC 列表中按索引取出 DTC 编号字符串。返回值为写入 outBuffer 的字节数,错误时返回 -1(ECU 无数据)或 false(缓冲区过小)。
flowchart LR
A[输入: ecuName, index] --> B{ecuName 存在于 m_allEcuDtcInfo?}
B -->|否| C[返回 -1]
B -->|是| D{index < vecDtcInfo.size?}
D -->|否| E[返回 -1]
D -->|是| F[memcpy strDTCNo 到 outBuffer]
F --> G[返回 true]Sources: jidu_dtc.cpp
jd_dtc_isInWhiteList — 白名单判定
OTX_API int jd_dtc_isInWhiteList(const char* ecuName, const char* dtcNo);判定逻辑的核心是 ECU 名称的域控后缀剥离。代码注释明确指出:"白名单中的 Ecu 名称不包含域控后缀"。对于以 _SOC 或 _MCU 结尾的 ECU 名称(如 BGM_SOC、BGM_MCU),函数会截取下划线之前的部分(BGM)用于白名单匹配。
string newName = ecuName;
if (MyStringUtils::endWith(newName, "_SOC") || MyStringUtils::endWith(newName, "_MCU")) {
newName = newName.substr(0, newName.find('_'));
}返回值:1 表示在名单中(应被过滤),0 表示不在(有效故障码,需要上报)。
Sources: jidu_dtc.cpp
白名单配置的双路径设计
系统支持两种白名单配置路径,分别服务于工厂端和售后端场景:
| 维度 | jd_dtc_setWhiteList(工厂版) | jd_dtc_reloadDtcMask(售后版) |
|---|---|---|
| 输入格式 | 分号+冒号+逗号分隔字符串 | 配置文件中的 JSON 数组 |
| ECU 标识方式 | 直接使用 ECU 名称字符串 | 通过 DoIP 地址反查 ECU 名称 |
| 车型过滤 | 无 | 按 vehicleModelCode 匹配当前车辆类型 |
| 容器操作 | clear() 后全量替换 | clear() 后重新加载 |
| 触发方式 | 诊断序列主动调用 | OTX 运行时调用 callServer 后自动触发 |
工厂版:jd_dtc_setWhiteList
输入字符串格式为 "ECU1:DTC1,DTC2;ECU2:DTC3,DTC4"。解析过程使用三级分割:
flowchart TD
A["输入: ECU1:DTC1,DTC2;ECU2:DTC3"] -->|";" 分割| B["ECU1:DTC1,DTC2", "ECU2:DTC3"]
B -->|每个元素 ":" 分割| C{vecTemp.size() == 2?}
C -->|是| D["ecuName=vecTemp[0]<br/>dtcList=vecTemp[1]"]
D -->|"," 分割| E["vecDtc = [DTC1, DTC2]"]
E --> F["m_mapEcuDtcWhiteList[ecuName].push_back(每个DTC)"]
C -->|否| G[丢弃]每次调用先 m_mapEcuDtcWhiteList.clear(),执行全量替换而非增量追加。
Sources: jidu_dtc.cpp
售后版:jd_dtc_reloadDtcMask
售后版的配置加载链路更为复杂,涉及两级配置文件读取:
flowchart TD
A["jd_dtc_reloadDtcMask()"] --> B["获取 appPath + Diagnosis/config/{env}/config.json"]
B --> C{文件存在?}
C -->|否| D["Tracer::error, 返回 -1"]
C -->|是| E["逐项遍历 JSON 数组"]
E --> F{vehicleModelCode 匹配?}
F -->|不匹配| E
F -->|匹配或无此字段| G{configOptionName == 'DtcMask'?}
G -->|否| E
G -->|是| H["取出 resultFile 路径"]
H --> I["调用 reloadDtcMask(resultFile)"]第一级 config.json 的结构为 JSON 数组,每项包含可选字段 vehicleModelCode(车型代码,如 "MarsOne"、"Venus")、configOptionName(配置项名称)、resultFile(实际掩码文件路径)。程序会匹配 DataCenter::m_vehicleInfo.vehicleType 与车型代码,然后找到 configOptionName 为 "DtcMask" 的条目,将其 resultFile 路径传给 reloadDtcMask。
Sources: jidu_dtc.cpp
reloadDtcMask — 底层掩码文件加载器
int reloadDtcMask(string filePath)掩码文件的 JSON 结构是一个数组,每项包含:
doipAddr:ECU 的 DoIP 逻辑地址(16 进制字符串)dtcList:该 ECU 的 DTC 白名单数组
[
{
"doipAddr": "1001",
"dtcList": ["U0073", "U0100", "P0606"]
},
{
"doipAddr": "1201",
"dtcList": ["U0073"]
}
]函数通过 get_jd_ecuName_byIpAddr(MyStringUtils::toLong(doipAddr, 16), ...) 将 DoIP 地址转换为 ECU 名称,然后将白名单条目通过 emplace 插入 m_mapEcuDtcWhiteList。如果 DoIP 地址无法解析为已知 ECU,则记录错误并返回 -1。
文件读取有两个容错路径:优先用 fopen 直接读取;若 cJSON_Parse 失败(文件可能经 FlashFileUtils 编码),则回退到 CFlashFileUtils::LoadConfig 重新尝试解析。
Sources: jidu_dtc.cpp
上报阶段:jd_dtc_report
OTX_API int jd_dtc_report(char* statisticId, char* statisticName, char* blockId, char* blockName);此函数遍历 m_allEcuDtcInfo 中每个 ECU 的每条 DTC,在白名单过滤后,将未命中白名单的故障码通过 jd_report_addNokItem 写入诊断报告。
flowchart TD
A["遍历 m_allEcuDtcInfo"] --> B["构建 itemInfo:<br/>blockId, ECU_DTC_Read, 01, ecuName, 0x1EFF, 0x00"]
B --> C{vecDtcInfo.size() > 0?}
C -->|否| A
C -->|是| D["遍历每条 DTC"]
D --> E{ecuName 在白名单中<br/>且当前 DTCNo 命中?}
E -->|是| F["continue 跳过"]
E -->|否| G["jd_report_addNokItem<br/>itemInfo, cmdReq=0x190209, cmdAck=原始响应, failReason=ecuName: DTCNo,内容"]
G --> D上报时 itemInfo 参数固定为 "{blockId},ECU_DTC_Read,01,{ecuName},0x1EFF,0x00":
itemName="ECU_DTC_Read"(测试项名称)itemId="01"(测试项 ID)ecuAddr="0x1EFF"(固定逻辑地址)expectedData="0x00"(期望无故障码)
cmdReq 固定为 \x19\x02\x09(UDS ReadDTCInformation 按状态掩码 0x09 请求),cmdAck 取 m_allEcuDtcAck[ecuName] 中保存的原始响应字节。failReason 采用 "{ecuName}: {dtcNo},{strContent}" 格式。
设计考量:每条未过滤的 DTC 都作为独立的 NOK(不合格)项上报,而非聚合成单条。这意味着一个有 20 个故障码的 ECU 会产生 20 条上报记录,每条都携带完整的请求/响应诊断数据。
Sources: jidu_dtc.cpp
完整数据流
sequenceDiagram
participant Diag as 诊断序列
participant DTC as DTC 模块
participant WL as 白名单管理
participant Rpt as 报告系统
Note over Diag,Rpt: 阶段一:配置白名单(二选一)
Diag->>WL: jd_dtc_setWhiteList("BGM:U0073;CDC:U0100")
WL->>WL: 解析 → m_mapEcuDtcWhiteList
Note over Diag,Rpt: 阶段二:读取故障码
Diag->>DTC: jd_dtc_readAll("BGM_SOC", rawBytes, len)
DTC->>DTC: dtc_calc 解析 → vecDtcNo
DTC->>DTC: m_allEcuDtcInfo["BGM_SOC"] = DTC列表
DTC->>DTC: m_allEcuDtcAck["BGM_SOC"] = 原始字节
Note over Diag,Rpt: 阶段三:按需查询
Diag->>DTC: jd_dtc_isInWhiteList("BGM_SOC", "U0073")
DTC->>DTC: 剥离后缀 → "BGM"
DTC->>DTC: 在 m_mapEcuDtcWhiteList["BGM"] 中查找
DTC-->>Diag: 1(在名单中)
Note over Diag,Rpt: 阶段四:上报
Diag->>DTC: jd_dtc_report(statId, statName, blockId, blockName)
DTC->>DTC: 遍历每 ECU 每条 DTC
DTC->>WL: 白名单过滤
DTC->>Rpt: jd_report_addNokItem(未过滤项)线程安全设计
模块中 mItemLock(std::mutex)在以下函数中加锁:
| 函数 | 锁策略 |
|---|---|
jd_dtc_readAll | lock_guard 覆盖整个函数 |
jd_dtc_setWhiteList | 无显式加锁(仅操作 m_mapEcuDtcWhiteList,调用方应保证串行) |
jd_dtc_reloadDtcMask | lock_guard 覆盖整个函数 |
jd_dtc_isInWhiteList | 无显式加锁(只读访问,但存在 TOCTOU 风险) |
jd_dtc_parsedByIndex | 无显式加锁(只读访问) |
jd_dtc_report | 无显式加锁(遍历期间数据可能被修改) |
jd_dtc_setWhiteList 与 jd_dtc_isInWhiteList、jd_dtc_report 之间缺少同步机制,理论上存在数据竞争。实际使用中,白名单配置通常在诊断序列启动阶段完成,DTC 读取与上报在后续阶段执行,时间上的自然隔离降低了并发冲突概率。
与其他模块的关系
graph TD
DTC[jidu_dtc.cpp<br/>DTC 模块] --> Macro[jidu_macro.h<br/>JIDU_DTC_INFO_t 定义]
DTC --> Tracer[tracer.h<br/>分级日志输出]
DTC --> DC[jidu_dataCenter.h<br/>m_vehicleInfo.vehicleType<br/>车型匹配]
DTC --> EC[jidu_envConfig.h<br/>getAppPath / getEnvPath<br/>配置文件路径]
DTC --> Str[MyStringUtils.h<br/>split / endWith / toLong]
DTC --> Bin[binary.h<br/>CBinary 字节缓冲]
DTC --> Flash[FlashFileUtils.h<br/>加密文件读取容错]
DTC --> cJSON[cjson.h<br/>JSON 解析]
DTC --> Rpt[jidu_report.cpp<br/>jd_report_addNokItem<br/>NOK 项上报]
DTC --> Func[jidu.cpp<br/>get_jd_ecuName_byIpAddr<br/>DoIP→ECU 名称映射]
style DTC fill:#1565c0,color:#fff
style Rpt fill:#2e7d32,color:#fff
style DC fill:#e65100,color:#fff扩展阅读
- 白名单配置中的 DoIP 地址匹配依赖 ECUFlasherManager:ECU 注册、刷写文件绑定与优先级调度 中维护的 ECU 注册表
- DTC 上报的 NOK 项通过 报告数据模型:StationData、PrintResult 与 JSON 序列化 序列化后经由 ReportTransporter:基于队列的异步报告传输机制 发送至集度服务器
- 配置文件的环境感知路径构建依赖于 EnvConfig:多环境(Dev/Test/Staging/Prod)配置切换机制
- 原始诊断字节操作基于 CBinary:高效二进制数据缓冲区操作类