4、测试系统软件架构:MES对接、测试执行框架、数据库设计、日志系统

好,咱们接着聊。前面几章我们把硬件平台、射频链路、自动化夹具都讲透了。这一章,我打算聊聊软件层面的事。

很多人觉得,CPE产测嘛,硬件调通了就完事了。其实不然。我见过太多项目,硬件指标测出来漂漂亮亮,一到量产就崩盘——为什么?软件架构没撑住。说白了,产测软件就是整条产线的大脑和神经系统。它要管指令下发、数据回传、跟MES握手、把结果存进数据库,还得把每台设备的“病历”记清楚。

这一章,我就把这几块掰开揉碎了讲。你想想看,如果连软件架构都搭不牢,产线跑起来就是一团乱麻。

4.1 MES对接:产线的“中央情报局”

MES,制造执行系统。它不直接测产品,但它管着所有产品的“身份”和“命运”。

我个人习惯,把MES对接分成三层:

  • 工单层:MES下发工单,告诉产线“今天要测哪批货、测什么项目”。
  • 设备层:测试工位向MES上报“我在测哪台设备、当前状态如何”。
  • 结果层:每台设备测完,把PASS/FAIL、关键指标、不良代码一股脑推给MES。

我在项目中遇到过一种情况:MES接口文档写得像天书,字段定义模棱两可。结果产线跑了两小时,发现MES那边根本没收到数据。排查了一整天,最后发现是报文里的时间戳格式没对齐——他们用yyyy-MM-dd HH:mm:ss,我们用了yyyy/MM/dd HH:mm:ss。就一个斜杠的区别,整条线停摆。

⚠️ 我曾经踩过的坑: 对接MES时,一定要先做“空跑测试”。就是让产线模拟跑一遍,但不接真实设备,只验证报文格式和字段映射。别等到真上线了才发现握手失败。

这里给出一段典型的MES上报接口示例(HTTP JSON):

POST /api/mes/testResult
Content-Type: application/json

{
  "workOrder": "WO20240521001",
  "sn": "CPE2405210001",
  "stationId": "STN_05",
  "testResult": "PASS",
  "testTime": "2024-05-21 14:30:25",
  "items": [
    {"name": "WIFI_2G_TX_POWER", "value": "18.5", "unit": "dBm", "limit": "16.0~20.0", "status": "PASS"},
    {"name": "LTE_RSRP", "value": "-85", "unit": "dBm", "limit": "-110~-70", "status": "PASS"}
  ],
  "failCode": ""
}

注意看failCode字段。我建议你设计成:PASS时传空字符串,FAIL时传一个预定义的错误码。比如E001代表“Wi-Fi功率超标”,E002代表“LTE灵敏度不足”。这样MES那边做统计分析时,一眼就能看出产线的瓶颈在哪。

4.2 测试执行框架:别让代码变成意大利面

测试执行框架,说白了就是“怎么把测试流程跑起来”。

我见过最糟糕的做法:把所有测试步骤写在一个巨大的main()函数里,几百行代码,if-else嵌套七八层。一旦要加一个新测试项,就得改核心逻辑。这种代码,我称之为“产线定时炸弹”。

我个人推荐用插件化+状态机的架构。每个测试项是一个独立的插件,框架只负责调度和状态流转。

举个例子,一个典型的CPE产测流程包含:

  1. 上电自检(Power On Self Test)
  2. 读取SN/IMEI/MAC
  3. Wi-Fi 2.4G/5G 射频测试
  4. LTE 4G/5G 射频测试
  5. 以太网吞吐测试
  6. 整机功耗测试
  7. 结果上传MES

每个步骤都是一个独立的类,继承自同一个基类:

class TestPlugin:
    def __init__(self, name):
        self.name = name
        self.status = "IDLE"

    def execute(self, context):
        # context 是全局上下文,存放设备信息、配置参数
        raise NotImplementedError

    def report(self):
        # 返回测试结果字典
        pass

框架里维护一个状态机,每个插件执行完后,根据结果决定下一步:

  • PASS → 进入下一个插件
  • FAIL → 记录不良代码,进入“重测”或“报废”分支
  • ERROR(设备断连、仪器超时)→ 进入“异常处理”分支

为什么要用状态机?因为产线不是实验室。实验室里你可以慢慢调,产线上每秒钟都在烧钱。状态机能让你清晰地定义“异常时怎么办”——是重试3次?还是直接跳过?还是整条线急停?

💡 我的一个小技巧: 在框架里加一个“调试模式”开关。调试模式下,每个插件执行完都暂停,等操作员按回车再继续。这样产线工程师排查问题时,可以一步步看数据,不用猜。

4.3 数据库设计:别把数据存成“一锅粥”

产测数据,每天少则几千条,多则几十万条。怎么存?

我建议用关系型数据库(比如MySQL/PostgreSQL)来存结构化数据,用时序数据库(比如InfluxDB)来存波形、频谱这类连续数据。但考虑到产线环境往往比较简陋,很多工厂连DBA都没有,我一般推荐直接用SQLite或MySQL搞定一切。

核心表设计,我习惯分成三张:

表名 用途 关键字段
test_records 每台设备的主记录 id, sn, work_order, station_id, test_time, result, fail_code
test_items 每台设备下各测试项的明细 id, record_id, item_name, value, unit, limit_low, limit_high, status
test_logs 详细的日志信息 id, record_id, log_level, message, timestamp

为什么要分开?因为查询场景不同。MES那边只关心“这台设备PASS了没有”,查test_records就够了。产线工程师要分析“为什么这批设备Wi-Fi功率偏低”,那就得查test_items,按item_name分组看趋势。

嗯,这里要注意一个细节:索引设计。我见过有人把test_items表建了上百万条数据,查询一次要十几秒。为什么?因为没有给record_iditem_name建联合索引。加上索引后,查询时间降到几十毫秒。

CREATE INDEX idx_record_item ON test_items(record_id, item_name);

就这么一行,效果天差地别。

4.4 日志系统:产线的“黑匣子”

日志这东西,平时没人看,一出问题就抓瞎。我自己的经验是:日志不是写给机器看的,是写给3个月后的自己看的

产测日志,我建议分三个级别:

  • INFO:正常流程记录。比如“开始测试Wi-Fi 2.4G TX Power”、“仪器已复位”。
  • WARNING:非致命异常。比如“设备响应超时,重试第1次”、“信号强度接近下限阈值”。
  • ERROR:致命错误。比如“仪器连接断开”、“数据库写入失败”。

我曾经遇到过一个案例:产线半夜突然大批量FAIL,白班工程师来了查了半天,发现日志里只有“PASS”和“FAIL”,没有任何上下文。根本不知道是设备问题、仪器问题还是软件问题。后来我强制要求:每个测试项开始和结束时,必须打印一条带时间戳的日志。这样一旦出问题,可以精确到毫秒级定位。

日志格式,我推荐用结构化的方式,方便后续用工具分析:

2024-05-21 14:30:25.123 | INFO | STN_05 | SN:CPE2405210001 | 开始测试: WIFI_2G_TX_POWER
2024-05-21 14:30:25.456 | INFO | STN_05 | SN:CPE2405210001 | 仪器: 频谱仪 N9020A, 频率: 2412MHz
2024-05-21 14:30:26.789 | INFO | STN_05 | SN:CPE2405210001 | 测量值: 18.5 dBm, 限值: 16.0~20.0 dBm
2024-05-21 14:30:26.790 | INFO | STN_05 | SN:CPE2405210001 | 结果: PASS

你看,每一行都包含了时间、工位、设备SN、操作内容。就算产线同时跑20个工位,也能轻松追踪到某一台设备的所有操作。

📌 核心原则: 日志系统要做到“可复现”。也就是说,拿着日志文件,你能在脑子里把整台设备的测试过程重演一遍。如果做不到,说明日志还不够详细。

最后提一句日志的存储策略。产线一天可能产生几个GB的日志,不能无限存。我一般建议:按天分文件,保留30天。超过30天的自动压缩归档,保留90天。再老的,如果没出问题,就可以删了。但注意,test_recordstest_items表里的数据要永久保留——那是产品的“出生证明”。

好了,软件架构这块就聊到这儿。下一章我们讲讲产线搭建的实战细节,包括工位布局、节拍计算、人员培训这些接地气的东西。到时候见。