Skip to content

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 列表的所有权—(整体报告)overallResulttestData(Block 引用)
2 — 大项StationDataCategory持有 Block 列表的所有权testStatisticIdtestStatisticResult(由 Block 结果推导)
3 — 模块StationDataBlock持有 Item 列表的所有权testBlockIdtestBlockResultfailReason[]beginTime/endTime
4 — 小项StationDataItem持有 ECU 列表的所有权testItemIdtestItemResult(默认 "OK")
5 — ECU 步骤StationDataECU叶子节点—(由 Item 管理)ecuNameserverIddidvalueexpectDataresultData

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 运行时(或上层调用方)按阶段驱动:

mermaid
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 自身的私有成员中(mCarConfigmTPMSIdmBncmFeedbackResultmSshStatusmChargingIsOpenmGloveboxIsOpenmElectricity),它们仅在 generate03() 生成 Type 03 报告时被写入 PrintResultReportRecord,因为这些信息属于工位级车辆状态描述,不属于诊断数据树的一部分。

Sources: ReportRecordCollector.hpp | ReportRecordCollector.cpp | ReportRecordCollector.cpp

Block 生命周期:分块采集的核心机制

blockBeginblockEnd 是 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 两条路径

collectItemStepOKcollectItemStepNOK 共享相似的结构化流程。两者都在 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 报告,系统共输出三种报告类型:

类型触发时机报告类核心数据用途
01blockEnd()StationDataReportRecord单个 Block 的完整诊断 Item/ECU 树细粒度诊断数据上报,支持"边测边报"
02collectEnd()generate02()StationResultReportRecord汇总统计(总 Block 数、总 Item 数、OK/NOK)工位级测试结论快速汇总
03collectEnd()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 中缓存的车辆状态字段(mCarConfigmTPMSIdmElectricity 等)纳入报告。同时计算工位耗时(formatCollectDuration()),格式化为 "Xmin Ys" 的可读字符串。

Sources: ReportRecordCollector.cpp | ReportRecordCollector.cpp

getInfoOfBlocksAndItems 的统计过滤

getInfoOfBlocksAndItems 方法在统计时对特定 Block ID 进行了显式排除:0x06010x09010x0082 这三个 Block 不会被计入总 Block 数和总 Item 数。这些 Block 对应的是系统内部管理性质的诊断步骤(如通信验证、环境检查),它们的成败不应影响工位级别的测试结论统计。被排除的 Block 的数据仍然存在,只是不被汇总数字反映。

此外,该方法在遍历时还同步推导了每个 Category 的 testStatisticResult:若 Category 下任一 Block 的 testBlockResult 不为 "OK",则该 Category 被标记为 "NOK"

Sources: ReportRecordCollector.cpp

线程安全模型

ReportRecordCollector 使用两个独立的 std::mutex 进行并发控制:

  • mBlockLock:保护 Block 级别的操作(blockBeginblockEnd)。由于 Block 操作涉及 Category 的查找/创建以及 Block 的时间戳和状态更新,需要保证同一时刻只有一个线程在操作 Block 边界。
  • mItemLock:保护 Item/ECU 级别的操作(collectItemStepOKcollectItemStepNOK)。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,由后台线程异步发送至集度服务器。onStationResultCompletedonPrintResultCompleted 同理处理 Type 02 和 Type 03 报告。

Sources: ReportCollectorEventHandler.hpp | ReportTransporter.hpp | ReportQueue.hpp

值得注意的是,当前实现中 postBlockCompleted 等三个事件分发方法的循环条件为 for (int i = 0; i < 1; i++)——这意味着仅第一个注册的事件处理器会被通知。这是一个有意为之的简化:实际部署中只需要一个处理器(即 ReportTransporter),多播没有必要。

Sources: ReportRecordCollector.cpp

数据流转全景

mermaid
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) — 对比刷写事件与报告事件的设计异同