Skip to content

本文档深入剖析 otxReport 子系统中三条并行的数据模型层次——StationData(原始诊断数据采集)、PrintResult(打印结果视图)和 StationResult(工位结果摘要)——以及它们通过 cJSON 库实现的 JSON 序列化机制。该子系统是整个诊断报告管道的核心数据载体,直接决定了上报到服务器的数据结构和内容。

架构概览

otxReport 的数据模型遵循"一条采集主干 + 两条派生视图"的架构。诊断过程中,ReportRecordCollectorStationDataReportRecord 为主容器逐层积累原始 ECU 诊断数据;当采集结束时,系统自动将这份原始数据分别转化为 StationResultReportRecord(工位结果摘要,resultType="02")和 PrintResultReportRecord(打印结果报告,resultType="03")。三条报告线共享同一个基类 ReportRecord,并通过 JsonSerializable 接口实现统一的 JSON 序列化契约。

mermaid
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

注意:图中黄色节点(StationDataCategoryPrintResultModule)是两条层次中的"断点"——它们不继承 JsonSerializable,而是作为纯数据容器存在。StationDataCategory 不提供 toJsonString() 方法,仅在内存中组织 StationDataBlock 的层级关系;PrintResultModule 提供自定义的 toJsonString(std::string station) 方法,接受工位参数来控制输出策略。

Sources: JsonSerializable.hpp, StationDataCategory.hpp, PrintResultModule.hpp

JsonSerializable 接口:统一的序列化契约

整个报告数据模型的序列化以 JsonSerializable 抽象接口为基石。它定义了一个纯虚函数 toJsonString(),返回 std::string 类型的紧凑 JSON 字符串(无格式化空格)。该接口被 ReportRecordStationDataBlockStationDataItemStationDataECUPrintResultItemvirtual 继承方式实现——virtual 继承确保在菱形继承场景下(如 StationDataReportRecord 同时继承 ReportRecord 和聚合 StationDataBlock)不会产生重复的基类子对象。

cpp
// 位于 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 是所有三种报告类型(StationDataReportRecordStationResultReportRecordPrintResultReportRecord)的直接基类。它承载了所有报告共享的元数据字段,并在 toJsonString() 中将其序列化为 JSON:

字段JSON Key类型说明
mSerialNoserialNostring流水号
mMixNomixNostring混线号
mVehicleTypevehicleTypestring车型
mVinvinstring车辆 VIN 码
mStationstationstring工位编号(如 "01"、"02"、"03")
mReworkStationreworkStationstring返修工位
mTestBlockTotalNumtestBlockTotalNumsize_t→string测试模块总数
mTestItemTotalNumtestItemTotalNumsize_t→string测试项总数
mResultTyperesultTypestring报告类型:"01"(原始数据)、"02"(工位结果)、"03"(打印结果)
mCellcellstring电芯信息
mExecutionTimeUtcexecutionTimeUtcstring执行开始时间
mTestTimeUtctestTimeUtcstring测试结束时间

ReportRecord::toJsonString() 的序列化策略是 有条件添加:只有当字段的字符串长度大于 0(或数值大于 0)时,才会将对应键值对写入 JSON 对象。这一设计避免了向服务器发送大量为空的字段,减小了传输体积。所有数值字段(mTestBlockTotalNummTestItemTotalNum)在 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 自身的 testStatisticIdtestStatisticName 复制到新 Block 上,并设置反向引用(block->setCategory(this)),实现了父子节点之间的双向关联。

cpp
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来源字段说明
testStatisticNamemTestStatisticName所属大项名称
testStatisticIdmTestStatisticId所属大项 ID
testBlockNamemTestBlockName模块名称
testBlockIdmTestBlockId模块 ID(十六进制字符串)
testBlockResultmTestBlockResult模块结果(OK/NOK)
failReasonmFailReason[]失败原因数组
testItemListmTestItemList[]小项列表(仅包含有 ECU 数据的项)

Block 的 toJsonString() 中存在一个关键的过滤逻辑:对于每个 StationDataItem,只有当其 JSON 中包含 "testItem" 键(即存在 ECU 步骤数据)时,才会被添加到 testItemList 数组中;否则该 Item 的 JSON 对象会被立即删除(cJSON_Delete)。这确保了只有实际执行了诊断步骤的小项才会出现在报告中。

Block 还维护了三个不参与 JSON 序列化的运行时字段:mBeginTime(模块开始时间)、mEndTime(模块结束时间)、mState(运行时状态)。updateState() 方法遍历所有 Item 的 testItemResult,只要发现任一 "NOK",就将 Block 的 mStatemTestBlockResult 置为 "NOK",实现级联失败传播

Sources: StationDataBlock.hpp, StationDataBlock.cpp

StationDataItem:测试小项节点

StationDataItem 聚合了 StationDataECU 的列表,代表"对同一诊断目标发起的多次 ECU 交互"。其 JSON 输出结构如下:

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来源字段说明
ecuNamemEcuNameECU 名称(如 "BCM"、"IC")
ecuAddressmEcuAddressECU 诊断地址(如 "0x740")
serverIdmServerId诊断服务 ID(如 "22" 表示 ReadDataByIdentifier)
didmDid数据标识符(DID),如服务为单字节则标记为 "NA"
resultDatamResultData结果状态("OK" 或 "NOK")
valuemValue实际读取的响应数据(十六进制字符串,空格分隔)
expectDatamExpectData期望数据
numbermNumber同 ECU 名称的序号(用于区分同一 ECU 的多次交互)

mServerIdmDid 是从原始诊断请求字节中解析而来:首字节为服务 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() 方法中被创建:

cpp
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来源字段说明
carConfigmCarConfig车辆配置信息
tpmsIdmTPMSId胎压监测系统 ID(仅工位 "01"、"02")
elecDisplaymElectricity电量显示(仅工位 "02")
chargingIsOpenmChargingIsOpen充电口盖状态(仅工位 "00")
gloveboxIsOpenmGloveboxIsOpen手套箱状态(仅工位 "00")
testStatisticTotalNummTestStatisticTotalNum大项总数
testStatisticFailNummTestStatisticFailNum失败大项数
testBlockFailNummTestBlockFailNum失败模块数
stationTimemStationTime工位耗时(格式:"Xmin Ys")
testStatisticListmTestStatisticList[]大项列表

toJsonString() 方法中实现了按工位的条件输出:通过 getStation() 判断当前工位编号,仅在特定工位下才输出 tpmsIdelectricitychargingIsOpengloveboxIsOpen 等字段。此外,大项 ID 为 0x22(充电口盖打开)和 0x25(手套箱打开)的大项会被静默跳过cJSON_Deletecontinue),不会出现在 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() 的过滤逻辑较为复杂:

  1. Block ID 黑名单0x06010x09010x0082 的 Block 直接被跳过(continue),不参与输出。
  2. 按工位的结果过滤:当 station == "03" 时,所有 Block 不论结果如何都会输出;其他工位仅输出 testBlockResult != "OK" 的失败 Block。
  3. 空数组保护:只有 testBlockList 数组非空时才写入 JSON 对象,避免输出无意义的空数组。

这意味着打印报告在大多数工位下是"例外报告"——只显示异常项;而在工位 "03" 下则输出完整的全量报告。

Sources: PrintResultModule.hpp, PrintResultModule.cpp

PrintResultItem:打印小项节点

PrintResultItem 继承 JsonSerializable,结构简洁——仅包含 testBlockNametestBlockIdtestBlockResultfailReason[] 四个字段。其 toJsonString() 方法中包含一个特殊的内容过滤逻辑

cpp
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 为 0x0C0x330x75 的模块,其失败原因中如果包含中文关键词"请求"或"响应",则该条失败原因会被过滤掉——这是一种面向打印输出的脱敏处理,避免在纸质报告上暴露原始诊断请求/响应的详细错误信息。

此外,testBlockResult 字段即使为空字符串也会被强制设置为 "OK" 后再输出(使用 cJSON_AddStringToObject 无条件添加),确保该字段始终存在于 JSON 中。

Sources: PrintResultItem.hpp, PrintResultItem.cpp

数据流:从采集到三种报告的生成

整个报告数据模型的生命周期由 ReportRecordCollector 统一编排。以下是完整的数据流:

mermaid
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继承 ReportRecordPrintResultModule 自定义方法
用途MES 详细诊断追溯MES 工位状态判定打印输出 / 人工查看

相关页面