本文档深入剖析 otxReport 子系统中三条并行的数据模型层次——StationData(原始诊断数据采集)、PrintResult(打印结果视图)和 StationResult(工位结果摘要)——以及它们通过 cJSON 库实现的 JSON 序列化机制。该子系统是整个诊断报告管道的核心数据载体,直接决定了上报到服务器的数据结构和内容。
架构概览
otxReport 的数据模型遵循"一条采集主干 + 两条派生视图"的架构。诊断过程中,ReportRecordCollector 以 StationDataReportRecord 为主容器逐层积累原始 ECU 诊断数据;当采集结束时,系统自动将这份原始数据分别转化为 StationResultReportRecord(工位结果摘要,resultType="02")和 PrintResultReportRecord(打印结果报告,resultType="03")。三条报告线共享同一个基类 ReportRecord,并通过 JsonSerializable 接口实现统一的 JSON 序列化契约。
graph TB
subgraph "序列化接口层"
JS[JsonSerializable<br/>+toJsonString = 0]
end
subgraph "报告基类"
RR[ReportRecord<br/>vin, station, serialNo...<br/>+toJsonString]
end
subgraph "采集主干 - StationData 层次"
SDRR[StationDataReportRecord<br/>overallResult<br/>+toJsonString]
SDC[StationDataCategory<br/>testStatisticName/Id/Result<br/>⚠️ 未继承 JsonSerializable]
SDB[StationDataBlock<br/>testBlockName/Id/Result<br/>failReason[], beginTime, endTime<br/>+toJsonString]
SDI[StationDataItem<br/>testItemName/Id/Result<br/>+toJsonString]
SDE[StationDataECU<br/>ecuName/Address, serverId, did<br/>resultData, value, expectData<br/>+toJsonString]
end
subgraph "派生视图 - PrintResult 层次"
PRRR[PrintResultReportRecord<br/>carConfig, tpmsId, electricity...<br/>+toJsonString]
PRM[PrintResultModule<br/>testStatisticName/Id/Result<br/>⚠️ 未继承 JsonSerializable<br/>+toJsonString station]
PRI[PrintResultItem<br/>testBlockName/Id/Result<br/>failReason[]<br/>+toJsonString]
end
subgraph "派生视图 - StationResult"
SRRR[StationResultReportRecord<br/>overallResult<br/>+toJsonString]
end
JS -.->|virtual继承| RR
JS -.->|virtual继承| SDB
JS -.->|virtual继承| SDI
JS -.->|virtual继承| SDE
JS -.->|virtual继承| PRI
RR -.->|virtual继承| SDRR
RR -.->|virtual继承| PRRR
RR -.->|virtual继承| SRRR
SDRR -->|聚合| SDC
SDC -->|聚合| SDB
SDB -->|聚合| SDI
SDI -->|聚合| SDE
PRRR -->|聚合| PRM
PRM -->|聚合| PRI
style SDC fill:#ffebcc,stroke:#e6a000
style PRM fill:#ffebcc,stroke:#e6a000注意:图中黄色节点(
StationDataCategory和PrintResultModule)是两条层次中的"断点"——它们不继承JsonSerializable,而是作为纯数据容器存在。StationDataCategory不提供toJsonString()方法,仅在内存中组织StationDataBlock的层级关系;PrintResultModule提供自定义的toJsonString(std::string station)方法,接受工位参数来控制输出策略。
Sources: JsonSerializable.hpp, StationDataCategory.hpp, PrintResultModule.hpp
JsonSerializable 接口:统一的序列化契约
整个报告数据模型的序列化以 JsonSerializable 抽象接口为基石。它定义了一个纯虚函数 toJsonString(),返回 std::string 类型的紧凑 JSON 字符串(无格式化空格)。该接口被 ReportRecord、StationDataBlock、StationDataItem、StationDataECU 和 PrintResultItem 以 virtual 继承方式实现——virtual 继承确保在菱形继承场景下(如 StationDataReportRecord 同时继承 ReportRecord 和聚合 StationDataBlock)不会产生重复的基类子对象。
// 位于 src/otxReport/JsonSerializable.hpp
class JsonSerializable {
protected:
JsonSerializable() = default; // 不允许外部直接实例化
public:
virtual ~JsonSerializable() = default;
virtual std::string toJsonString() = 0; // 纯虚函数,子类必须实现
};构造函数被保护(protected),确保 JsonSerializable 只能作为基类被继承,不能被外部直接实例化。这是一种典型的"接口类"设计模式,类似 Java 中的 interface 或 C# 中的 interface。
Sources: JsonSerializable.hpp
ReportRecord:所有报告的公共基类
ReportRecord 是所有三种报告类型(StationDataReportRecord、StationResultReportRecord、PrintResultReportRecord)的直接基类。它承载了所有报告共享的元数据字段,并在 toJsonString() 中将其序列化为 JSON:
| 字段 | JSON Key | 类型 | 说明 |
|---|---|---|---|
mSerialNo | serialNo | string | 流水号 |
mMixNo | mixNo | string | 混线号 |
mVehicleType | vehicleType | string | 车型 |
mVin | vin | string | 车辆 VIN 码 |
mStation | station | string | 工位编号(如 "01"、"02"、"03") |
mReworkStation | reworkStation | string | 返修工位 |
mTestBlockTotalNum | testBlockTotalNum | size_t→string | 测试模块总数 |
mTestItemTotalNum | testItemTotalNum | size_t→string | 测试项总数 |
mResultType | resultType | string | 报告类型:"01"(原始数据)、"02"(工位结果)、"03"(打印结果) |
mCell | cell | string | 电芯信息 |
mExecutionTimeUtc | executionTimeUtc | string | 执行开始时间 |
mTestTimeUtc | testTimeUtc | string | 测试结束时间 |
ReportRecord::toJsonString() 的序列化策略是 有条件添加:只有当字段的字符串长度大于 0(或数值大于 0)时,才会将对应键值对写入 JSON 对象。这一设计避免了向服务器发送大量为空的字段,减小了传输体积。所有数值字段(mTestBlockTotalNum、mTestItemTotalNum)在 JSON 中都被序列化为字符串类型(通过 std::to_string 转换),而非 JSON 数值类型——这是与集度服务端约定的格式。
Sources: ReportRecord.hpp, ReportRecord.cpp
采集主干:StationData 四层层次结构
StationData 层次是诊断数据采集过程中的"实时记录本"。它通过四层嵌套结构,从宏观的测试大项(Category)逐步深入到微观的单次 ECU 诊断步骤(ECU)。
层级关系概览
StationDataReportRecord (报告根节点:车辆级元数据 + overallResult)
└── StationDataCategory (测试大项:如"电检"、"刷写"等,按 categoryId 分组)
└── StationDataBlock (测试模块/Block:对应配置中的 function,如"读取VIN")
└── StationDataItem (测试小项:如某 ECU 的某 DID 读取)
└── StationDataECU (ECU 步骤:单次诊断请求/响应的完整记录)StationDataCategory:大项容器(非序列化节点)
StationDataCategory 是层次中唯一不继承 JsonSerializable 的容器类。它不直接参与 JSON 序列化,而是在内存中维护 StationDataBlock 的集合,按 testStatisticId 进行索引。其关键行为是 newTestBlock() 方法——创建新 Block 时自动将 Category 自身的 testStatisticId、testStatisticName 复制到新 Block 上,并设置反向引用(block->setCategory(this)),实现了父子节点之间的双向关联。
StationDataBlock* StationDataCategory::newTestBlock() {
StationDataBlock* result = new StationDataBlock();
result->setTestStatisticId(this->mTestStatisticId); // 自动继承父级 ID
result->setTestStatisticName(this->mTestStatisticName); // 自动继承父级名称
result->setTestBlockName(this->mTestStatisticName); // Block 名默认等于大项名
result->setCategory(this); // 建立反向引用
this->mTestBlockList.push_back(result);
return result;
}Category 还维护 mTestStatisticResult 状态,初始值为 "OK"。在 getInfoOfBlocksAndItems() 统计阶段,如果其下任一 Block 失败,该状态会被更新为 "NOK"。
Sources: StationDataCategory.hpp, StationDataCategory.cpp
StationDataBlock:测试模块节点
StationDataBlock 是诊断报告的核心粒度——每个 Block 对应配置文件中的一个 function,在结束时独立生成一份 resultType="01" 的报告并发送。它继承 JsonSerializable,其 JSON 序列化结构如下:
| JSON Key | 来源字段 | 说明 |
|---|---|---|
testStatisticName | mTestStatisticName | 所属大项名称 |
testStatisticId | mTestStatisticId | 所属大项 ID |
testBlockName | mTestBlockName | 模块名称 |
testBlockId | mTestBlockId | 模块 ID(十六进制字符串) |
testBlockResult | mTestBlockResult | 模块结果(OK/NOK) |
failReason | mFailReason[] | 失败原因数组 |
testItemList | mTestItemList[] | 小项列表(仅包含有 ECU 数据的项) |
Block 的 toJsonString() 中存在一个关键的过滤逻辑:对于每个 StationDataItem,只有当其 JSON 中包含 "testItem" 键(即存在 ECU 步骤数据)时,才会被添加到 testItemList 数组中;否则该 Item 的 JSON 对象会被立即删除(cJSON_Delete)。这确保了只有实际执行了诊断步骤的小项才会出现在报告中。
Block 还维护了三个不参与 JSON 序列化的运行时字段:mBeginTime(模块开始时间)、mEndTime(模块结束时间)、mState(运行时状态)。updateState() 方法遍历所有 Item 的 testItemResult,只要发现任一 "NOK",就将 Block 的 mState 和 mTestBlockResult 置为 "NOK",实现级联失败传播。
Sources: StationDataBlock.hpp, StationDataBlock.cpp
StationDataItem:测试小项节点
StationDataItem 聚合了 StationDataECU 的列表,代表"对同一诊断目标发起的多次 ECU 交互"。其 JSON 输出结构如下:
{
"testItemName": "读取VIN码",
"testItemId": "0x0011",
"testItemResult": "OK",
"testItem": [
{ "ecuName": "BCM", "ecuAddress": "0x740", ... },
{ "ecuName": "IC", "ecuAddress": "0x760", ... }
]
}值得注意的是,ECU 数组在 JSON 中的键名是 "testItem"(单数) 而非 "testItemList"——这与 StationDataBlock 中的 "testItemList" 命名不一致,反映了不同开发者或不同阶段的命名习惯差异。
StationDataItem 的默认 mTestItemResult 为 "OK",只有在 collectItemStepNOK 被调用时才会被设置为 "NOK"。析构函数负责遍历并释放所有子 StationDataECU 对象,体现了所有权管理的约定——父节点拥有子节点的生命周期。
Sources: StationDataItem.hpp, StationDataItem.cpp
StationDataECU:最小粒度——单次诊断步骤
StationDataECU 是诊断数据模型的最小原子单元,记录单次 ECU 诊断交互的完整信息:
| JSON Key | 来源字段 | 说明 |
|---|---|---|
ecuName | mEcuName | ECU 名称(如 "BCM"、"IC") |
ecuAddress | mEcuAddress | ECU 诊断地址(如 "0x740") |
serverId | mServerId | 诊断服务 ID(如 "22" 表示 ReadDataByIdentifier) |
did | mDid | 数据标识符(DID),如服务为单字节则标记为 "NA" |
resultData | mResultData | 结果状态("OK" 或 "NOK") |
value | mValue | 实际读取的响应数据(十六进制字符串,空格分隔) |
expectData | mExpectData | 期望数据 |
number | mNumber | 同 ECU 名称的序号(用于区分同一 ECU 的多次交互) |
mServerId 和 mDid 是从原始诊断请求字节中解析而来:首字节为服务 ID,剩余字节为 DID。当请求仅有一个字节时,mDid 被设为 "NA"。mValue 通过 toHex() 函数将二进制响应数据转换为可读的十六进制字符串(如 "4A 4D 43 42")。mNumber 字段用于在同一个小项中区分对同一 ECU 的多次操作——例如同一 Item 中对 BCM 的两次不同 DID 读取会分别标记为 number=0 和 number=1。
Sources: StationDataECU.hpp, StationDataECU.cpp
StationDataReportRecord:采集主报告根节点
StationDataReportRecord 继承 ReportRecord,在其 JSON 输出中通过 cJSON_Parse(ReportRecord::toJsonString()) 先获取基类的 JSON 对象,再追加 overallResult 字段和 testData 数组。这是一种**"解析-追加-重序列化"**模式——先让基类生成完整的 JSON 字符串,再解析回 cJSON 对象进行追加,最后再次序列化。虽然存在一次额外的解析开销,但避免了手动管理基类字段序列化逻辑的重复。
mTestData 是一个非拥有(non-owning)引用集合:其中的 StationDataBlock* 指针指向 mTestCategories 中各 Category 管理的 Block 对象。析构时先释放所有 Category(连带其中的 Block),再简单地 clear() mTestData(不 delete),注释中明确标注了"仅仅 clear 一次"。这是为了避免同一 Block 对象被 delete 两次。
mOverallResult 初始为 "OK",当任意 collectItemStepNOK 被调用时立即置为 "NOK"——这意味着一旦出现任何诊断失败,整个报告的总体结果就是失败,这是一种最严格的失败传播策略。
Sources: StationDataReportRecord.hpp, StationDataReportRecord.cpp
派生视图一:StationResultReportRecord(工位结果摘要)
StationResultReportRecord 是最简单的报告类型,仅在 ReportRecord 的基础上追加一个 overallResult 字段。它在 collectEnd() 的 generate02() 方法中被创建:
void ReportRecordCollector::generate02() {
// ...统计 blocks/items 数据...
StationResultReportRecord reportRecord;
// 从 StationDataReportRecord 复制所有元数据字段
reportRecord.setResultType("02"); // 标记为工位结果类型
reportRecord.setOverallResult(isFailed ? "NOK" : "OK");
postStationResultCompleted(&reportRecord); // 发送事件
}该报告类型的设计意图是为 MES(制造执行系统)提供一个轻量级的工位级别结果摘要,不包含详细的诊断步骤数据。resultType 固定为 "02"。
Sources: StationResultReportRecord.hpp, StationResultReportRecord.cpp, ReportRecordCollector.cpp
派生视图二:PrintResultReportRecord 与 PrintResult 两层层级
PrintResultReportRecord 是面向打印输出和人工查看的报告视图。与 StationData 的四层结构不同,PrintResult 层次仅有两层:PrintResultModule(对应大项)→ PrintResultItem(对应模块/Block)。ECU 级别的详细诊断数据被完全省略,只保留 Block 级别的结果和失败原因。
PrintResultReportRecord:打印报告根节点
除了从 ReportRecord 继承的公共字段外,PrintResultReportRecord 额外携带以下字段:
| JSON Key | 来源字段 | 说明 |
|---|---|---|
carConfig | mCarConfig | 车辆配置信息 |
tpmsId | mTPMSId | 胎压监测系统 ID(仅工位 "01"、"02") |
elecDisplay | mElectricity | 电量显示(仅工位 "02") |
chargingIsOpen | mChargingIsOpen | 充电口盖状态(仅工位 "00") |
gloveboxIsOpen | mGloveboxIsOpen | 手套箱状态(仅工位 "00") |
testStatisticTotalNum | mTestStatisticTotalNum | 大项总数 |
testStatisticFailNum | mTestStatisticFailNum | 失败大项数 |
testBlockFailNum | mTestBlockFailNum | 失败模块数 |
stationTime | mStationTime | 工位耗时(格式:"Xmin Ys") |
testStatisticList | mTestStatisticList[] | 大项列表 |
其 toJsonString() 方法中实现了按工位的条件输出:通过 getStation() 判断当前工位编号,仅在特定工位下才输出 tpmsId、electricity、chargingIsOpen、gloveboxIsOpen 等字段。此外,大项 ID 为 0x22(充电口盖打开)和 0x25(手套箱打开)的大项会被静默跳过(cJSON_Delete 后 continue),不会出现在 testStatisticList 中。
Sources: PrintResultReportRecord.hpp, PrintResultReportRecord.cpp
PrintResultModule:大项节点(非标准序列化)
PrintResultModule 是 PrintResult 层次中的"断点"——它不继承 JsonSerializable,而是提供自定义的 toJsonString(std::string station) 方法。testStatisticId 在此被序列化为 JSON Number 类型(通过 cJSON_AddNumberToObject,值由十六进制字符串转为十进制整数),这是整个报告系统中唯一使用 JSON Number 而非 String 的 ID 字段。
PrintResultModule::toJsonString() 的过滤逻辑较为复杂:
- Block ID 黑名单:
0x0601、0x0901、0x0082的 Block 直接被跳过(continue),不参与输出。 - 按工位的结果过滤:当
station == "03"时,所有 Block 不论结果如何都会输出;其他工位仅输出testBlockResult != "OK"的失败 Block。 - 空数组保护:只有
testBlockList数组非空时才写入 JSON 对象,避免输出无意义的空数组。
这意味着打印报告在大多数工位下是"例外报告"——只显示异常项;而在工位 "03" 下则输出完整的全量报告。
Sources: PrintResultModule.hpp, PrintResultModule.cpp
PrintResultItem:打印小项节点
PrintResultItem 继承 JsonSerializable,结构简洁——仅包含 testBlockName、testBlockId、testBlockResult 和 failReason[] 四个字段。其 toJsonString() 方法中包含一个特殊的内容过滤逻辑:
int blockId = MyStringUtils::toLong(this->mTestBlockId, 16);
if (blockId == 0x0C || blockId == 0x33 || blockId == 0x75) {
if (this->mFailReason[i].find("请求") == string::npos &&
this->mFailReason[i].find("响应") == string::npos) {
cJSON_AddItemToArray(arrObj, cJSON_CreateString(...));
}
}对于 Block ID 为 0x0C、0x33、0x75 的模块,其失败原因中如果包含中文关键词"请求"或"响应",则该条失败原因会被过滤掉——这是一种面向打印输出的脱敏处理,避免在纸质报告上暴露原始诊断请求/响应的详细错误信息。
此外,testBlockResult 字段即使为空字符串也会被强制设置为 "OK" 后再输出(使用 cJSON_AddStringToObject 无条件添加),确保该字段始终存在于 JSON 中。
Sources: PrintResultItem.hpp, PrintResultItem.cpp
数据流:从采集到三种报告的生成
整个报告数据模型的生命周期由 ReportRecordCollector 统一编排。以下是完整的数据流:
sequenceDiagram
participant Diag as 诊断过程
participant RC as ReportRecordCollector
participant SDRR as StationDataReportRecord
participant EH as EventHandler
participant Q as ReportQueue
RC->>RC: collectBegin()<br/>创建 StationDataReportRecord
loop 每个诊断模块
Diag->>RC: blockBegin(categoryId, blockId)
RC->>SDRR: getOrCreate Category → Block<br/>设置 beginTime
loop 每个诊断步骤
alt 步骤成功
Diag->>RC: collectItemStepOK(...)
RC->>SDRR: newECU() → 设置 resultData="OK"
else 步骤失败
Diag->>RC: collectItemStepNOK(failReason...)
RC->>SDRR: setTestItemResult("NOK")<br/>setOverallResult("NOK")<br/>addFailReason()
end
end
Diag->>RC: blockEnd(categoryId, blockId)
RC->>SDRR: updateState() → 级联检查失败
RC->>RC: 创建临时 StationDataReportRecord<br/>resultType="01"
RC->>EH: postBlockCompleted(block)
EH->>Q: enque(toJsonString())
end
Diag->>RC: collectEnd()
RC->>RC: generate02() → StationResultReportRecord
RC->>EH: postStationResultCompleted()
RC->>RC: generate03() → 遍历 StationData<br/>转换为 PrintResultReportRecord
RC->>EH: postPrintResultCompleted()关键转换发生在 generate03() 中:它遍历 StationDataReportRecord 中的所有 Category → Block,为每个 Category 创建一个 PrintResultModule,为每个 Block 创建一个 PrintResultItem,复制结果状态和失败原因,同时进行统计计数(getInfoOfBlocksAndItems 同时统计 Block 总数、失败 Block 数、Item 总数、失败 Item 数)。
Sources: ReportRecordCollector.cpp
JSON 序列化实现模式
整个子系统使用 cJSON(一个纯 C 语言的轻量级 JSON 解析/生成库)进行所有 JSON 操作。序列化遵循统一的"创建-填充-输出-释放"四步模式:
cJSON_CreateObject() // 1. 创建根对象
cJSON_AddStringToObject() // 2. 逐个添加字段(有条件判断)
cJSON_AddItemToArray() // 递归处理子对象数组
cJSON_PrintUnformatted() // 3. 序列化为紧凑 JSON 字符串
cJSON_Delete() // 4. 释放 cJSON 对象
cJSON_free() // 释放 PrintUnformatted 返回的 char*子类扩展父类 JSON 时采用**"解析-追加-重序列化"**策略(ReportRecord::toJsonString() 模式):父类的 toJsonString() 返回完整 JSON 字符串,子类通过 cJSON_Parse() 将其解析回 cJSON 对象,追加自有字段后再次 cJSON_PrintUnformatted()。这种模式的优点是父类字段序列化逻辑完全封装在父类中,子类无需重复;代价是额外的一次解析/序列化开销——对于报告数据量(通常几 KB 到几十 KB)来说,这个开销可以忽略。
在 PrintResultItem::toJsonString() 中还有一个微妙的细节:mTestBlockResult 即使为空也会被强制设为 "OK"(if (this->mTestBlockResult.empty()) { mTestBlockResult = "OK"; }),然后无条件写入 JSON——这与其他字段"仅非空时才添加"的策略不同,保证了结果的确定性。
Sources: StationDataBlock.cpp, PrintResultItem.cpp
三种报告类型对照总表
| 维度 | StationData (01) | StationResult (02) | PrintResult (03) |
|---|---|---|---|
| 数据深度 | 四层(Category→Block→Item→ECU) | 一层(仅 overallResult) | 两层(Module→Item) |
| ECU 详情 | 完整(request/response hex, DID, SID) | 无 | 无 |
| 生成时机 | 每个 blockEnd 时实时发送 | collectEnd 时一次性生成 | collectEnd 时一次性生成 |
| 失败原因 | 包含(Block 级别) | 不包含 | 包含(带脱敏过滤) |
| 过滤逻辑 | Item 无 ECU 数据时跳过 | 无过滤 | Block ID 黑名单 + 工位级过滤 + 失败原因关键词过滤 |
| 附加字段 | 无 | 无 | carConfig, tpmsId, electricity 等 |
| 序列化继承 | 完整实现 JsonSerializable | 继承 ReportRecord | PrintResultModule 自定义方法 |
| 用途 | MES 详细诊断追溯 | MES 工位状态判定 | 打印输出 / 人工查看 |
相关页面
- ReportRecordCollector:工位数据分块采集与结果汇总 — 了解 ReportRecordCollector 如何驱动整个数据采集流程
- ReportTransporter:基于队列的异步报告传输机制 — 了解 JSON 报告字符串如何通过队列异步传输到服务器
- 事件驱动模型:刷写生命周期事件的发布与订阅机制 — 了解报告中使用的 EventHandler 观察者模式
- MyStringUtils 与 DateFormat:字符串处理与时间格式化工具 — 了解
toLong()和split()等工具函数的实现