ReportRecordCollector 是 otxReport 诊断报告子系统的核心采集引擎,负责在车辆产线工位(Station)上将 ECU 诊断测试数据按层级结构(大项→模块→小项→ECU 步骤)进行系统化采集,并在采集生命周期结束时自动汇总生成三种不同用途的报告。它通过事件回调机制将生成的报告推送给下游的 [ReportTransporter:基于队列的异步报告传输机制](20-reporttransporter-ji-yu-dui-lie-de-yi-bu-bao-gao-chuan-shu-ji-zhi) 进行异步上报。
数据模型:五层树形结构
ReportRecordCollector 管理一棵五层诊断数据树,各层级的包含关系与职责如下:
| 层级 | 类名 | 容器角色 | 唯一标识字段 | 关键属性 |
|---|---|---|---|---|
| 1 — 报告根 | StationDataReportRecord | 持有 Category 列表的所有权 | —(整体报告) | overallResult、testData(Block 引用) |
| 2 — 大项 | StationDataCategory | 持有 Block 列表的所有权 | testStatisticId | testStatisticResult(由 Block 结果推导) |
| 3 — 模块 | StationDataBlock | 持有 Item 列表的所有权 | testBlockId | testBlockResult、failReason[]、beginTime/endTime |
| 4 — 小项 | StationDataItem | 持有 ECU 列表的所有权 | testItemId | testItemResult(默认 "OK") |
| 5 — ECU 步骤 | StationDataECU | 叶子节点 | —(由 Item 管理) | ecuName、serverId、did、value、expectData、resultData |
Sources: StationDataReportRecord.hpp | StationDataCategory.hpp | StationDataBlock.hpp | StationDataItem.hpp | StationDataECU.hpp
整个树形结构遵循"OK 默认"设计原则:StationDataItem 构造时将 mTestItemResult 初始化为 "OK",StationDataBlock 构造时将 mState 初始化为 "OK",StationDataReportRecord 构造时将 mOverallResult 初始化为 "OK"。只有当 collectItemStepNOK 被调用时,才会将 Item、Block 乃至整个报告的标记翻转为 "NOK"。这种"无罪推定"模式简化了上层调用逻辑——调用方只需在失败时报告,无需在每个成功步骤显式标记。
Sources: StationDataItem.cpp | StationDataBlock.cpp | StationDataReportRecord.cpp
采集生命周期与 API 设计
ReportRecordCollector 的采集过程遵循显式的生命周期模型,由 OTX 运行时(或上层调用方)按阶段驱动:
sequenceDiagram
participant Caller as 上层调用方 (OTX)
participant RRC as ReportRecordCollector
participant SDRR as StationDataReportRecord
participant Block as StationDataBlock
participant Handler as ReportCollectorEventHandler
Caller->>RRC: collectBegin()
RRC->>SDRR: new + setExecutionTimeUtc()
Note over RRC: mCollectStartTime = now()
loop 每个诊断大项
Caller->>RRC: blockBegin(categoryId, categoryName, blockId, blockName)
RRC->>SDRR: getTestCategoryById() or newCategory()
RRC->>Block: getTestBlockById() or newTestBlock()
RRC->>Block: setBeginTime(datetimeNowString())
loop 每个诊断步骤
alt 步骤成功
Caller->>RRC: collectItemStepOK(blockId, itemId, ...)
RRC->>Block: getTestItemById() or newTestItem()
Note over RRC: 创建 StationDataECU 并填充诊断数据
else 步骤失败
Caller->>RRC: collectItemStepNOK(blockId, itemId, failReason, ...)
RRC->>Block: addFailReason() + setTestBlockResult("NOK")
RRC->>SDRR: setOverallResult("NOK")
end
end
Caller->>RRC: blockEnd(categoryId, blockId)
RRC->>Block: setEndTime() + updateState()
RRC->>RRC: 构建临时 StationDataReportRecord(Type 01)
RRC->>Handler: postBlockCompleted() → onBlockCompleted()
end
Caller->>RRC: collectEnd()
RRC->>SDRR: setTestTimeUtc()
Note over RRC: mCollectEndTime = now()
RRC->>RRC: generate02() → StationResultReportRecord
RRC->>Handler: postStationResultCompleted() → onStationResultCompleted()
RRC->>RRC: generate03() → PrintResultReportRecord
RRC->>Handler: postPrintResultCompleted() → onPrintResultCompleted()Sources: ReportRecordCollector.hpp | ReportRecordCollector.cpp
基本信息采集 API
在 collectBegin() 与 collectEnd() 之间,调用方通过一系列 setter 方法设置报告的元数据字段,这些方法直接透传至内部的 mStationDataReportRecord:
collectSerialNo(v) → 设置流水号(serialNo)
collectMixNo(v) → 设置混流号(mixNo)
collectVehicleType(v) → 设置车型(vehicleType)
collectVin(v) → 设置 VIN 码
collectStation(v) → 设置工位编号
collectReworkStation(v) → 设置返修工位编号
collectResultType(v) → 设置结果类型
collectCell(v) → 设置单元编号另有一组字段存储在 ReportRecordCollector 自身的私有成员中(mCarConfig、mTPMSId、mBncmFeedbackResult、mSshStatus、mChargingIsOpen、mGloveboxIsOpen、mElectricity),它们仅在 generate03() 生成 Type 03 报告时被写入 PrintResultReportRecord,因为这些信息属于工位级车辆状态描述,不属于诊断数据树的一部分。
Sources: ReportRecordCollector.hpp | ReportRecordCollector.cpp | ReportRecordCollector.cpp
Block 生命周期:分块采集的核心机制
blockBegin 和 blockEnd 是 ReportRecordCollector 最关键的设计决策——它们将采集过程切分为可独立上报的块(Block),而非等待整个工位测试结束后一次性上报。其设计动机是:产线工位的诊断数据量可能非常庞大,若集中上报,单次 HTTP 请求载荷过大且延迟不友好;以 Block 为粒度的分块上报则实现了"边测边报"的流式效果。
blockBegin 的逻辑采用"查找或创建"模式:先尝试在 StationDataReportRecord 中按 categoryId 查找已有的大项(Category),若不存在则新建;然后在该 Category 中按 blockId 查找已有的 Block,若不存在则新建;最后为该 Block 打上开始时间戳。
Sources: ReportRecordCollector.cpp
blockEnd 执行了三项核心操作。首先,为 Block 标记结束时间并调用 updateState() 遍历其下所有 Item:若任一 Item 的 testItemResult 为 "NOK",则将 Block 的 mState 翻转为 "NOK",同时将 mTestBlockResult 同步为该值。其次,构建一个临时的 StationDataReportRecord 副本,从当前报告根复制元数据字段(流水号、VIN、工位等),并仅将当前 Block 的引用放入 testData 数组,设置 resultType 为 "01"。最后,通过 postBlockCompleted 将该副本交给事件处理器。
Sources: ReportRecordCollector.cpp | StationDataBlock.cpp
值得注意的一个设计细节是 StationDataReportRecord 中的 mTestData 仅仅存储 Block 指针引用,真正的所有权属于 mTestCategories 中的 Category 对象。这意味着 blockEnd 构建的临时报告对象可以安全地在事件处理器中使用 Block 数据,而不会被析构函数释放。
Sources: StationDataReportRecord.hpp | StationDataReportRecord.cpp
Item 步骤采集:OK 与 NOK 两条路径
collectItemStepOK 和 collectItemStepNOK 共享相似的结构化流程。两者都在 Block 中按 itemId 查找或新建 Item,然后创建一个 StationDataECU 节点并填充诊断数据。关键的差异在于:
- OK 路径:ECU 的
resultData设为"OK",不修改任何上层状态标记。 - NOK 路径:ECU 的
resultData设为"NOK";将 Item 的testItemResult设为"NOK";将报告根的overallResult设为"NOK";按换行符拆分failReason字符串,逐条追加到 Block 的failReason列表中;同时将 Block 和它所属 Category 的结果标记为"NOK"。
Sources: ReportRecordCollector.cpp | ReportRecordCollector.cpp
在 NOK 路径中还有一个特殊的过滤逻辑:当请求的 Service ID(request[0] 的十六进制表示)为 "00" 时,不会创建 StationDataECU 节点。SID 0x00 表示无实际诊断请求(可能是超时或通信层错误),此时记录 ECU 级别的诊断步骤没有意义,因此仅更新状态标记和失败原因,不追加 ECU 数据行。这确保了报告中的 ECU 步骤列表只包含真实执行过的诊断交互。
Sources: ReportRecordCollector.cpp
ECU 节点的数据填充涉及请求和响应字节数组的解析:serverId 取自 request[0] 的一个字节(即诊断 Service ID),did 取自 request[1..sizeOfRequest-1](即 DID/子功能参数),value 取自 response[0..sizeOfResponse-1]。当请求体只有一个字节或响应为空时,分别用 "NA" 填充对应字段。数据均转换为空格分隔的大写十六进制字符串。
Sources: ReportRecordCollector.cpp | ReportRecordCollector.cpp
三种报告类型的设计意图
collectEnd() 中依次调用 generate02() 和 generate03(),分别产生两种汇总报告。加上 blockEnd() 中产生的 Type 01 报告,系统共输出三种报告类型:
| 类型 | 触发时机 | 报告类 | 核心数据 | 用途 |
|---|---|---|---|---|
| 01 | blockEnd() | StationDataReportRecord | 单个 Block 的完整诊断 Item/ECU 树 | 细粒度诊断数据上报,支持"边测边报" |
| 02 | collectEnd() → generate02() | StationResultReportRecord | 汇总统计(总 Block 数、总 Item 数、OK/NOK) | 工位级测试结论快速汇总 |
| 03 | collectEnd() → generate03() | PrintResultReportRecord | 工位摘要 + Category 级结果 + 车辆状态字段 | 打印输出/人工查看的格式化报告 |
Sources: ReportRecordCollector.cpp | ReportRecordCollector.hpp
generate02:工位结果汇总
generate02() 调用 getInfoOfBlocksAndItems() 遍历所有 Category 下的所有 Block 与 Item,统计总 Block 数、总 Item 数、失败 Block 数和失败 Item 数,然后构建一个精简的 StationResultReportRecord,仅包含统计数字和最终的 overallResult(OK/NOK),不包含任何诊断细节数据。
Sources: ReportRecordCollector.cpp
generate03:打印输出报告
generate03() 采用与 generate02() 相同的统计方法,但构建的是 PrintResultReportRecord——一个更适合人类阅读和打印输出的结构。它按 Category → Block 的层级组织摘要信息(不包含 Item 和 ECU 细节),并将 ReportRecordCollector 中缓存的车辆状态字段(mCarConfig、mTPMSId、mElectricity 等)纳入报告。同时计算工位耗时(formatCollectDuration()),格式化为 "Xmin Ys" 的可读字符串。
Sources: ReportRecordCollector.cpp | ReportRecordCollector.cpp
getInfoOfBlocksAndItems 的统计过滤
getInfoOfBlocksAndItems 方法在统计时对特定 Block ID 进行了显式排除:0x0601、0x0901、0x0082 这三个 Block 不会被计入总 Block 数和总 Item 数。这些 Block 对应的是系统内部管理性质的诊断步骤(如通信验证、环境检查),它们的成败不应影响工位级别的测试结论统计。被排除的 Block 的数据仍然存在,只是不被汇总数字反映。
此外,该方法在遍历时还同步推导了每个 Category 的 testStatisticResult:若 Category 下任一 Block 的 testBlockResult 不为 "OK",则该 Category 被标记为 "NOK"。
Sources: ReportRecordCollector.cpp
线程安全模型
ReportRecordCollector 使用两个独立的 std::mutex 进行并发控制:
mBlockLock:保护 Block 级别的操作(blockBegin、blockEnd)。由于 Block 操作涉及 Category 的查找/创建以及 Block 的时间戳和状态更新,需要保证同一时刻只有一个线程在操作 Block 边界。mItemLock:保护 Item/ECU 级别的操作(collectItemStepOK、collectItemStepNOK)。Item 操作在 Block 内部进行,粒度更细,使用独立的锁可以避免 Item 级别的快速操作被 Block 边界操作阻塞。
这种细粒度的锁分离设计允许在一个线程执行 blockEnd(串行化 JSON 序列化和事件分发)的同时,另一个线程可以在不同的 Block 内执行 Item 操作的准备(虽然在实际使用场景中,OTX 运行时通常是单线程顺序调用的)。
Sources: ReportRecordCollector.hpp | ReportRecordCollector.cpp | ReportRecordCollector.cpp
事件驱动与下游对接
ReportRecordCollector 通过 ReportCollectorEventHandler 抽象接口与下游解耦。该接口定义了三个纯虚方法:
onBlockCompleted(StationDataReportRecord *newBlock)
onStationResultCompleted(StationResultReportRecord *newResult)
onPrintResultCompleted(PrintResultReportRecord *newResult)在典型的部署中,ReportTransporter(或其内部的一个适配器)会注册为事件处理器:当 onBlockCompleted 被触发时,将 Type 01 报告序列化为 JSON 字符串并放入 ReportQueue,由后台线程异步发送至集度服务器。onStationResultCompleted 和 onPrintResultCompleted 同理处理 Type 02 和 Type 03 报告。
Sources: ReportCollectorEventHandler.hpp | ReportTransporter.hpp | ReportQueue.hpp
值得注意的是,当前实现中 postBlockCompleted 等三个事件分发方法的循环条件为 for (int i = 0; i < 1; i++)——这意味着仅第一个注册的事件处理器会被通知。这是一个有意为之的简化:实际部署中只需要一个处理器(即 ReportTransporter),多播没有必要。
Sources: ReportRecordCollector.cpp
数据流转全景
flowchart TD
subgraph 采集阶段
CB[collectBegin] --> BB[blockBegin × N]
BB --> CSI{collectItemStep}
CSI -->|OK| CIOK[collectItemStepOK]
CSI -->|NOK| CINOK[collectItemStepNOK]
CIOK --> BE[blockEnd]
CINOK --> BE
BE -->|"Type 01 报告"| EH[EventHandler.onBlockCompleted]
end
subgraph 汇总阶段
CE[collectEnd] --> G2[generate02]
CE --> G3[generate03]
G2 -->|"Type 02 报告"| EH2[EventHandler.onStationResultCompleted]
G3 -->|"Type 03 报告"| EH3[EventHandler.onPrintResultCompleted]
end
subgraph 传输阶段
EH --> RQ[ReportQueue.enque]
EH2 --> RQ
EH3 --> RQ
RQ --> RT[ReportTransporter 后台线程]
RT --> TA[ReportTransportAction.doTransport]
TA --> Server[集度服务器]
end
style CB fill:#4CAF50,color:#fff
style CE fill:#FF9800,color:#fff
style Server fill:#2196F3,color:#fff图中清晰展示了分块上报与汇总上报两条并行的数据通路:Type 01 报告在 blockEnd() 时即推入队列(流式),而 Type 02 和 Type 03 报告仅在 collectEnd() 时一次性生成并推入队列。
阅读建议
完成本文后,建议按以下顺序深入理解诊断报告系统的完整链路:
[报告数据模型:StationData、PrintResult 与 JSON 序列化](21-bao-gao-shu-ju-mo-xing-stationdata-printresult-yu-json-xu-lie-hua)— 深入各数据模型的 JSON 序列化实现(cJSON 构建细节)[ReportTransporter:基于队列的异步报告传输机制](20-reporttransporter-ji-yu-dui-lie-de-yi-bu-bao-gao-chuan-shu-ji-zhi)— 理解事件处理器如何将报告异步发送至服务器[事件驱动模型:刷写生命周期事件的发布与订阅机制](15-shi-jian-qu-dong-mo-xing-shua-xie-sheng-ming-zhou-qi-shi-jian-de-fa-bu-yu-ding-yue-ji-zhi)— 对比刷写事件与报告事件的设计异同