FAAP/RTD 测试数据传输系统 — 数据流与工作流


一、系统全局数据流

1.1 端到端数据流总览

整个系统的数据从 DUT(被测设备)出发,经过 3 次网络跳转,最终到达 RTD 云端数据库:

┌──────────┐      ┌──────────────┐      ┌──────────┐      ┌──────────┐      ┌──────────┐
│   DUT    │      │  FaapProxy   │      │    PC    │      │   MBS    │      │ RTD 云端  │
│  被测设备 │─────►│  中间代理     │─────►│  客户端   │─────►│  缓冲服务 │─────►│ 中央数据库 │
└──────────┘      └──────────────┘      └──────────┘      └──────────┘      └──────────┘
     数据源            第 1 跳               第 2 跳            第 3 跳           数据终点
   gRPC Unary    gRPC Server Streaming    gRPC Unary       gRPC over HTTPS
   (一问一答)       (长连接持续推送)        (一问一答)        (带认证的远程调用)

1.2 为什么需要 3 跳而不是直连

两端 存在意义
第 1 跳 DUT → FaapProxy DUT 上的测试软件只负责发数据,不关心下游。FaapProxy 做协议转换、字段补充、多 DUT 隔离
第 2 跳 FaapProxy → PC 用 Server Streaming 实现"有数据就推"的实时性,PC 负责展示和路由分发
第 3 跳 PC → MBS MBS 提供断网保护(本地缓存 + 自动重发),PC 不需要处理网络故障

1.3 数据流中的消息类型

在整条链路中流动的消息可以归为 5 大类:

消息类型 含义 何时发送 数量
Header (InitiateTestSession) 测试会话开始 每次测试最开始 1 次
TestPointData 单个测试点结果(值+Pass/Fail+限值) 每测完一项 N 次
AuxiliaryData 辅助环境数据(温度/电压/电流等) 测试前后 M 次
DataPointGroup 数据点分组(把多个测试点归为一组) 一组测试点发完后 K 次
Footer (FinalizeTestSession) 测试会话结束 测试全部完成 1 次

关键规则:Header 必须第一个发,Footer 必须最后一个发。中间的 TestPoint、Auxiliary、Group 可以交叉发送。


二、第 1 跳详解:DUT → FaapProxy

2.1 通信协议

  • 方式:gRPC Unary RPC(一个请求 → 一个响应)
  • 端口:FaapProxy 监听 50052
  • Proto 服务DutTestDataService + CtrlService

2.2 DUT 发送的 RPC 调用

DUT 调用顺序:
│
├── 1. GetSeqIds(count=200)              ← 预分配 200 个序列号
│       返回: {start: 100000, sep: 2, stop: 100400}
│
├── 2. AddAuxiliaryData(board_temp)      ← 测试前环境:板卡温度
├── 3. AddAuxiliaryData(voltage)         ← 测试前环境:供电电压
├── 4. AddAuxiliaryData(current)         ← 测试前环境:供电电流
│
├── 5. AddTestPoint(TP_TX_PWR_001)       ← 测试点:TX功率端口1
├── 6. AddTestPoint(TP_TX_PWR_002)       ← 测试点:TX功率端口2
├── 7. AddTestPoint(TP_TX_PWR_003)       ← 测试点:TX功率端口3
├── 8. AddTestPoint(TP_TX_PWR_004)       ← 测试点:TX功率端口4(SBT)
├── 9. AddDataPointGroup(tx_output_power) ← 把上面4个点归组
│
├── 10-14. 更多测试组...
│
├── 15. AddAuxiliaryData(temp_post)      ← 测试后环境:板卡温度
├── 16. AddAuxiliaryData(power)          ← 测试后环境:功耗
│
└── 17. FinalizeTestSequence             ← 通知测试结束

2.3 FaapProxy 对每条消息的处理

AddTestPoint 为例,FaapProxy 收到后的内部处理流程:

DUT 发来 AddTestPointRequest
        │
        ▼
┌─────────────────────────────────────────┐
│ 步骤 1:识别 DUT                         │
│   peer = context.peer()                 │
│   dut_ip = "127.0.0.1"                  │
│   (从 gRPC 连接元信息提取来源 IP)        │
└─────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────┐
│ 步骤 2:查缓存                           │
│   cached = FaapProxyCache.get(dut_ip)   │
│   获取: session_id, filter_attributes,   │
│         dut_position, supported_msgs    │
│   (缓存在 PC 调 InitializeTestAsync     │
│    时写入)                              │
└─────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────┐
│ 步骤 3:校验                             │
│   if session_id 为空 → 返回错误          │
│   (V1 版本新增的必填校验)               │
└─────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────┐
│ 步骤 4:补充字段                         │
│   payload.filter_attributes = cached    │
│   (DUT 不知道工厂代码和产品族,          │
│    由 FaapProxy 从缓存补上)             │
└─────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────┐
│ 步骤 5:包装为 FaapProxyReturnMessage    │
│   msg = FaapProxyReturnMessage(         │
│       test_point_v1 = payload           │
│   )                                     │
│   (统一包装为 oneof 消息,方便流式传输)  │
└─────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────┐
│ 步骤 6:写入队列                         │
│   message_queues[dut_position].put(msg) │
│   (按 DUT 位置隔离的 asyncio.Queue)     │
└─────────────────────────────────────────┘
        │
        ▼
  返回 AddTestPointResponse(status=OK)

2.4 序列号(SeqId)分配机制

DUT 请求: GetSeqIds(session_id, number_of_requested_ids=200)

FaapProxy 内部逻辑:
  初始值 = 100000
  步长 = 2 (偶数)
  本次分配: start=100000, stop=100400

DUT 使用的 ID: 100000, 100002, 100004, ..., 100398
PC SW 保留的 ID: 100001, 100003, 100005, ..., 100399 (奇数,目前未使用)

为什么步长是 2:历史设计决策——偶数给 DUT 的 FAAP 软件,奇数预留给 PC 软件(如果 PC 需要插入额外的标注点)。这样两端各自递增不会冲突。

2.5 FinalizeTestSequence 的特殊处理

当 DUT 发送 FinalizeTestSequence 时,FaapProxy 做两件事:

# 1. 向队列写入 SequenceFinalized 消息(PC 会收到这条)
msg = FaapProxyReturnMessage(sequence_finalized=TerminateTest(status=OK))
await queue.put(msg)

# 2. 向队列写入 None 哨兵值(通知 Server Streaming 循环结束)
await queue.put(None)

这样 PC 端的 for response in stream 循环会在收到 None 后自然终止。


三、第 2 跳详解:FaapProxy → PC 客户端(Server Streaming)

3.1 通信协议

  • 方式:gRPC Server Streaming(客户端发一个请求,服务端持续返回数据流)
  • 端口:50052(同一端口,不同的 Service)
  • Proto 服务FaapProxyService.InitializeTestAsync
  • 底层:HTTP/2 长连接,服务端有数据就推,客户端实时收到,无轮询

3.2 连接建立流程

PC 客户端                                FaapProxy
    │                                       │
    │── InitializeTestRequest ─────────────►│
    │   包含:                                │
    │   - dut_ip: "127.0.0.1"              │
    │   - dut_position: 1                   │
    │   - dut_product_number: "KRC 161..."  │
    │   - filter_attributes: {AIR5322, A40} │
    │   - supported_messages: [6种消息类型]  │
    │   - test_station_id: "ts_python..."   │
    │                                       │
    │                                       │── 写入缓存 (key=dut_ip)
    │                                       │── 创建/获取 Queue[position=1]
    │                                       │
    │◄── FaapProxyServerStarted ───────────│  (第1条响应)
    │                                       │
    │◄── TestPointV1 ──────────────────────│  (DUT发数据后)
    │◄── AuxiliaryV1 ──────────────────────│
    │◄── DataPointGroupV1 ─────────────────│
    │◄── ... 持续推送 ... ──────────────────│
    │                                       │
    │◄── SequenceFinalized ────────────────│  (DUT结束后)
    │                                       │── 流结束
    │                                       │

3.3 FaapProxyReturnMessage 消息结构(oneof)

FaapProxy 向 PC 推送的所有消息都包装在 FaapProxyReturnMessage 这个 oneof 结构中:

message FaapProxyReturnMessage {
  oneof msg {
    TestPointDataMessage    test_point_v1;              // 测试点数据
    AuxiliaryDataMessage    auxiliary_v1;               // 辅助数据
    DataPointGroupMessage   data_point_group_v1;       // 数据点分组
    TerminateTest           sequence_finalized;        // 测试序列结束
    ClientRuntimeError      runtime_error;             // 运行时错误
    FaapStillAlive          faap_still_alive;          // 心跳(看门狗)
    FaapProxyServerStarted  faap_proxy_server_started; // 服务端就绪通知
  }
}

oneof 的含义:每条消息只会是其中一种类型。PC 端通过 response.WhichOneof('msg') 判断当前是哪种,再做对应处理。

3.4 PC 客户端收到消息后的处理分支

for response in stream:
    msg_case = response.WhichOneof('msg')
    
    match msg_case:
        case 'faap_proxy_server_started':
            # 仅记录日志,确认连接建立
            
        case 'test_point_v1':
            # 1. 控制台彩色输出(绿色PASS / 红色FAIL)
            # 2. 写入本地 TXT 文件
            # 3. 放入 RTD 转发队列(后台线程异步发送)
            
        case 'auxiliary_v1':
            # 1. 控制台输出(青色)
            # 2. 放入 RTD 转发队列
            
        case 'data_point_group_v1':
            # 1. 控制台输出(黄色)
            # 2. 放入 RTD 转发队列
            
        case 'sequence_finalized':
            # 记录结束标志,后续退出循环
            
        case 'faap_still_alive':
            # 心跳,忽略
            
        case 'runtime_error':
            # 记录错误日志

3.5 Server Streaming 的关键特性

特性 说明
实时性 基于 HTTP/2,FaapProxy 有数据就推,PC 毫秒级收到
长连接 一次 RPC 调用,整个测试过程保持连接
流量控制 HTTP/2 内置流量控制,不会因为 PC 处理慢而丢数据
有序性 同一个 gRPC 流中消息保证有序到达
取消机制 PC 可以随时取消流,FaapProxy 收到 CancelledError

四、第 3 跳详解:PC 客户端 → RTD MBS

4.1 通信协议

  • 方式:gRPC Unary RPC
  • 端口:MBS 监听 50053(本地模拟),生产环境是 localhost:58766
  • Proto 服务TestResultMessageService

4.2 PC 客户端的双线程架构

┌─────────────────────────────────────────────────────────────┐
│ PC 客户端进程                                                │
│                                                             │
│  ┌──────────────────────────┐     ┌──────────────────────┐ │
│  │ 主线程                    │     │ 后台 RTD 转发线程     │ │
│  │                          │     │                      │ │
│  │ for msg in stream:       │     │ while running:       │ │
│  │   控制台输出              │     │   msg = queue.get()  │ │
│  │   写本地文件             │ put │   stub.AddTestPoint  │ │
│  │   queue.put(msg) ────────┼────►│   Data(msg)          │ │
│  │                          │     │                      │ │
│  │ (不阻塞,立即继续接收)    │     │ (网络慢也不影响主线程) │ │
│  └──────────────────────────┘     └──────────────────────┘ │
│                                            │                │
│                                            ▼ gRPC Unary     │
│                                      RTD MBS (50053)        │
└─────────────────────────────────────────────────────────────┘

为什么要双线程

  • 主线程负责从 FaapProxy 流中接收数据,必须快速消费不能阻塞(否则流量控制会反压 FaapProxy)
  • RTD 转发涉及网络 I/O,可能有延迟。如果在主线程中同步调用,一旦网络慢就会阻塞接收
  • 通过 queue.Queue 解耦:主线程只管放,后台线程只管发

4.3 RTD 三段式调用时序

PC 客户端                              RTD MBS
    │                                     │
    │─── InitiateTestSession ────────────►│  ← Header(最先发)
    │    payload: HeaderMessage {          │
    │      session_id, filter_attributes, │
    │      test_station_id, dut_position  │
    │    }                                │
    │◄── Response(status=OK) ────────────│
    │                                     │
    │  ══ 开始接收 FaapProxy 流 ══         │
    │                                     │
    │─── AddAuxiliaryData ───────────────►│  ← 辅助数据 ×N
    │─── AddTestPointData ───────────────►│  ← 测试点 ×M
    │─── AddDataPointGroup ──────────────►│  ← 分组 ×K
    │    ... 持续发送 ...                  │
    │                                     │
    │  ══ FaapProxy 流结束 ══              │
    │                                     │
    │─── FinalizeTestSession ────────────►│  ← Footer(最后发)
    │    payload: FooterMessage {          │
    │      session_id, pass, stop_time,   │
    │      no_test_point_msg,             │
    │      no_auxiliary_data_msg          │
    │    }                                │
    │◄── Response(status=OK) ────────────│
    │                                     │

关键规则

  • Header 在 FaapProxy 流建立之前发送
  • Footer 在 FaapProxy 流结束之后发送
  • 数据消息在流接收过程中通过后台线程异步发送

4.4 RTD 转发的重试机制

def _call_with_retry(msg_type, payload, max_attempts=3, initial_backoff=1.0):
    """
    重试策略:
    - 最多重试 3 次
    - 仅对 UNAVAILABLE 状态码重试(网络不可达)
    - 指数退避: 1s → 1.5s → 2.25s
    - 其他错误码直接抛出不重试
    """
    backoff = initial_backoff
    for attempt in range(max_attempts):
        try:
            call_rtd(msg_type, payload)
            return  # 成功
        except grpc.RpcError as e:
            if e.code() == UNAVAILABLE and attempt < max_attempts - 1:
                sleep(backoff)
                backoff *= 1.5  # 指数增长
            else:
                raise  # 其他错误或重试用尽

五、MBS 内部处理流程

5.1 MBS 收到消息后的处理

PC 调用 InitiateTestSession(Header)
        │
        ▼
┌────────────────────────────────────┐
│ 1. 校验 FilterAttributes           │
│    - 不能为空                       │
│    - product_family 格式校验        │
│    - serial number 格式校验         │
│    - test_station_id 不能为空       │
│    - dut_position > 0              │
└────────────────────────────────────┘
        │
        ▼
┌────────────────────────────────────┐
│ 2. 持久化到 SQLite                  │
│    - rtd_sessions 表: 会话元数据    │
│    - rtd_messages 表: 原始消息备份  │
│    (断网保护:即使后续发云端失败,   │
│     数据不丢)                      │
└────────────────────────────────────┘
        │
        ▼
┌────────────────────────────────────┐
│ 3. 写入云端转发队列                  │
│    cloud_forwarder.enqueue(msg)    │
│    (后台线程异步发往 RTD 云端)      │
└────────────────────────────────────┘
        │
        ▼
  返回 Response(status=OK)

5.2 SQLite 持久化结构

-- 会话表:每次测试一条记录
CREATE TABLE rtd_sessions (
    id INTEGER PRIMARY KEY,
    device_id TEXT,           -- 设备序列号 (如 "C821234567")
    start_time TEXT,          -- 测试开始时间 (UTC ISO格式)
    seq_id INTEGER,           -- 序列号计数器
    filter_attributes BLOB,   -- FilterAttributes 的序列化字节
    header_payload BLOB,      -- 完整 Header 消息的序列化字节
    not_for_rtd INTEGER,      -- 是否不发往 RTD (标记位)
    created_at TEXT           -- 记录创建时间
);

-- 消息表:每条 RPC 调用一条记录
CREATE TABLE rtd_messages (
    id INTEGER PRIMARY KEY,
    session_device_id TEXT,    -- 关联的设备序列号
    message_type TEXT,         -- 消息类型 (InitiateTestSession/TestPointData/...)
    payload BLOB,             -- 完整 Request 消息的序列化字节
    created_at TEXT           -- 记录创建时间
);

断网保护原理

  1. 每条消息先写 SQLite,再发云端
  2. 如果云端发送失败,数据已经安全存在本地
  3. 网络恢复后,后台服务扫描 SQLite 中未发送的记录,重新发送
  4. 发送成功后从 SQLite 删除

5.3 云端转发(模拟)

当前 Python 实现中,云端转发是模拟的(只记录日志):

class RtdCloudForwarder:
    def _run(self):
        while running:
            msg_type, description = queue.get()
            # 模拟网络延迟
            time.sleep(0.01)
            # 记录转发(真实环境会调用 RTD 云端 gRPC)
            logger.debug("→ RTD Cloud: %s - %s", msg_type, description)

真实生产环境的云端地址:

  • 生产:https://apigw.radiounits-realtimetestdata.ericsson.net:9443
  • 开发:https://apigw-dev.radiounits-realtimetestdata.ericsson.net:9443

六、完整工作流时序图(按时间顺序)

6.1 准备阶段

时刻 T0: RTD MBS 启动
  └── 初始化 SQLite → 启动云端转发线程 → 监听 50053

时刻 T1: FaapProxy 启动
  └── 注册 3 个 gRPC 服务 → 监听 50052

时刻 T2: PC 客户端启动
  ├── 连接 RTD MBS (50053) → 成功
  ├── 启动 RTD 转发后台线程
  ├── 发送 RTD Header (InitiateTestSession) → MBS 收到并持久化
  ├── 连接 FaapProxy (50052) → 成功
  ├── 调用 InitializeTestAsync (Server Streaming)
  │     → FaapProxy 缓存 DUT 信息
  │     → FaapProxy 返回 FaapProxyServerStarted
  └── 进入 stream 等待循环

6.2 数据传输阶段

时刻 T3: DUT 启动并连接 FaapProxy
  └── 连接成功

时刻 T4: DUT 请求序列号
  └── GetSeqIds(200) → FaapProxy 分配 [100000, 100400)

时刻 T5-T7: DUT 发送测试前环境数据
  DUT → AddAuxiliaryData(board_temp=45.2°C)
    → FaapProxy 补字段 → Queue.put()
      → PC Stream 收到 → 控制台显示 → RTD Queue.put()
        → 后台线程 → MBS.AddAuxiliaryData → SQLite + 云端队列

时刻 T8-T11: DUT 发送测试点(TX Output Power 组)
  DUT → AddTestPoint(TP_TX_PWR_001, value=23.5, PASS)
    → FaapProxy 补字段 → Queue.put()
      → PC Stream 收到 → 绿色[PASS]显示 → 写TXT → RTD Queue.put()
        → 后台线程 → MBS.AddTestPointData → SQLite + 云端队列

  DUT → AddTestPoint(TP_TX_PWR_003, value=21.8, FAIL)  ← 低于下限22.0!
    → 同上路径 → PC 红色[FAIL]显示

  DUT → AddDataPointGroup(tx_output_power, children=[...])
    → 同上路径

时刻 T12-T16: 更多测试组 (TX EVM, RX Performance)
  ... 同上流程 ...

时刻 T17-T18: DUT 发送测试后环境数据
  DUT → AddAuxiliaryData(board_temp_post=52.3°C)
  DUT → AddAuxiliaryData(power_consumption=120.5W)

6.3 结束阶段

时刻 T19: DUT 发送结束信号
  DUT → FinalizeTestSequence
    → FaapProxy:
        1. Queue.put(SequenceFinalized)  ← PC 会看到 [END]
        2. Queue.put(None)               ← 哨兵,终止 Streaming

时刻 T20: PC 流结束
  PC Stream 循环退出
    → 计算: all_pass = (fail_count == 0) → False (有1个FAIL)
    → 发送 RTD Footer: FinalizeTestSession(pass=False, tp=7, aux=5)
      → MBS 收到 → 持久化 → 打印汇总

时刻 T21: 清理
  PC → 等待 RTD 转发队列排空 (0.5s)
     → 停止后台线程
     → 关闭 gRPC 连接
     → 打印统计摘要

七、单条消息的完整生命周期

以一条 TestPoint 消息(TP_TX_PWR_003 = 21.8 dBm, FAIL) 为例,追踪它从产生到存储的完整旅程:

步骤  位置            操作                                    协议/机制
────  ─────────────  ───────────────────────────────────────  ──────────────────
 1    DUT            测量 TX 功率 = 21.8 dBm                  物理测量
 2    DUT            判定 21.8 < 22.0 (下限) → FAIL           本地逻辑
 3    DUT            构建 AddTestPointRequest                  Protobuf 序列化
 4    DUT→FaapProxy  gRPC Unary 调用 AddTestPoint              HTTP/2 + Protobuf
 5    FaapProxy      get_ip_from_peer → 识别是哪个 DUT         从 context 提取
 6    FaapProxy      查缓存 → 获取 filter_attributes           内存字典查询
 7    FaapProxy      校验 SessionId 不为空                     业务校验
 8    FaapProxy      补充 FilterAttributes                     字段覆盖
 9    FaapProxy      包装为 FaapProxyReturnMessage(test_point_v1)  Protobuf 构建
10    FaapProxy      写入 asyncio.Queue[position=1]            异步队列 put
11    FaapProxy      返回 AddTestPointResponse(OK) 给 DUT      HTTP/2 响应
12    FaapProxy      InitializeTestAsync 从 Queue 取出消息      异步队列 get
13    FaapProxy→PC   通过 Server Streaming 推送                 HTTP/2 流帧
14    PC             WhichOneof('msg') → 'test_point_v1'       类型判断
15    PC             控制台输出: 红色 [FAIL] TP_TX_PWR_003...   终端 ANSI 颜色
16    PC             写入 TestResult/20260611/TestResult_*.txt  文件 I/O
17    PC             rtd_queue.put(("test_point_v1", payload))  queue.Queue put
18    PC 后台线程    rtd_queue.get() → 取出消息                 queue.Queue get
19    PC→MBS        gRPC Unary 调用 AddTestPointData            HTTP/2 + Protobuf
20    MBS            persist_message("TestPointData", bytes)    SQLite INSERT
21    MBS            cloud_forwarder.enqueue("TEST_POINT",...)  queue.Queue put
22    MBS 后台线程   从队列取出 → 模拟发往 RTD 云端              (模拟)
23    MBS            返回 AddTestPointDataResponse(OK)          HTTP/2 响应

总耗时(本地模拟):约 5-20ms(无真实网络延迟)
真实生产环境:DUT→PC 约 1-5ms(局域网),PC→云端 约 50-200ms(公网 HTTPS)


八、队列机制详解

系统中有 3 个关键的队列,分别解耦不同环节:

8.1 队列 1:FaapProxy 内部(asyncio.Queue)

生产者: DutTestDataService (收到 DUT 数据后写入)
消费者: FaapProxyService.InitializeTestAsync (读取并推给 PC)
隔离策略: 按 DutPosition 每个位置一个独立队列

作用: 解耦 DUT 数据接收 和 PC 流式推送
好处: DUT 发数据不需要等 PC 处理完,PC 慢了也不阻塞 DUT

8.2 队列 2:PC 客户端内部(queue.Queue)

生产者: 主线程 (从 Stream 收到消息后放入)
消费者: 后台 RTD 转发线程 (取出后调 MBS gRPC)
隔离策略: 单队列,顺序消费

作用: 解耦 流数据接收 和 RTD 网络调用
好处: MBS 调用慢或超时不影响主线程接收 FaapProxy 的流

8.3 队列 3:MBS 内部(queue.Queue)

生产者: TestResultMessageService (收到 PC 调用后写入)
消费者: RtdCloudForwarder 后台线程 (取出后发往云端)
隔离策略: 单队列

作用: 解耦 PC 请求处理 和 云端发送
好处: 云端慢/断网不影响 MBS 响应 PC 的请求

8.4 队列连接关系图

DUT                  FaapProxy                    PC Client                   MBS
 │                      │                            │                         │
 │  gRPC Unary          │                            │                         │
 ├─────────────────────►│                            │                         │
 │                      ▼                            │                         │
 │              ┌──────────────┐                     │                         │
 │              │ asyncio.Queue │ ← 队列1             │                         │
 │              │ [position=1] │                     │                         │
 │              └──────┬───────┘                     │                         │
 │                     │ Server Streaming            │                         │
 │                     └────────────────────────────►│                         │
 │                                                   ▼                         │
 │                                           ┌──────────────┐                  │
 │                                           │ queue.Queue   │ ← 队列2         │
 │                                           │ (RTD 转发)    │                  │
 │                                           └──────┬───────┘                  │
 │                                                  │ gRPC Unary               │
 │                                                  └─────────────────────────►│
 │                                                                             ▼
 │                                                                     ┌──────────────┐
 │                                                                     │ queue.Queue   │ ← 队列3
 │                                                                     │ (云端转发)    │
 │                                                                     └──────┬───────┘
 │                                                                            │
 │                                                                            ▼
 │                                                                     RTD 云端 (模拟)

九、缓存机制详解

9.1 FaapProxyCache 的角色

FaapProxy 中有一个全局缓存,解决了一个核心问题:DUT 调用 AddTestPoint 时,如何知道它属于哪个测试会话?

答案是:通过 DUT 的 IP 地址查缓存。

9.2 缓存的写入和读取时机

时间线:
──────────────────────────────────────────────────────────────────────
T1: PC 调用 InitializeTestAsync(dut_ip="127.0.0.1", position=1, ...)
    │
    ▼
    FaapProxyCache.add_or_update("127.0.0.1", {
        dut_position: 1,
        session_id: {...},
        filter_attributes: {family: "AIR5322", factory: A40},
        supported_messages: ["test_point_v1", "auxiliary_v1", ...],
        ...
    })
    │
    │  ← 缓存已建立,等待 DUT 数据
    │
──────────────────────────────────────────────────────────────────────
T2: DUT 调用 AddTestPoint(payload)
    │
    ▼
    peer = context.peer() → "ipv4:127.0.0.1:54321"
    dut_ip = get_ip_from_peer(peer) → "127.0.0.1"
    cached = FaapProxyCache.get("127.0.0.1")  ← 查到了!
    │
    ▼
    payload.filter_attributes = cached['filter_attributes']  ← 补字段
    queue = message_queues[cached['dut_position']]           ← 找到对应队列
──────────────────────────────────────────────────────────────────────

9.3 为什么 DUT 自己不带 FilterAttributes

  • DUT 上运行的是 FAAP 测试软件,它只关心"测量"这件事
  • 工厂代码、产品族等元数据是 PC 端(测试站控制软件)配置的
  • PC 在发起测试时把这些信息告诉 FaapProxy,FaapProxy 替 DUT 补上
  • 这样 DUT 软件不需要修改就能适配不同工厂

十、异常处理与容错

10.1 各节点的异常处理

节点 异常场景 处理方式
DUT FaapProxy 未启动 连接超时 10 秒后报错退出
FaapProxy DUT IP 未在缓存中 返回 status=5 (NOT_FOUND)
FaapProxy SessionId 为空 返回 status=3 (INVALID_ARGUMENT)
PC FaapProxy 未启动 连接超时 10 秒后报错退出
PC RTD MBS 未启动 标记 stub=None,跳过 RTD 转发
PC RTD 转发失败 重试 3 次(仅 UNAVAILABLE),然后放弃该消息
PC Stream 被取消 捕获 CancelledError,正常结束
MBS FilterAttributes 为空 记录 Warning,继续处理(不拒绝)
MBS 序列号格式不对 记录 Warning,继续处理(不拒绝)

10.2 数据丢失风险分析

故障场景 是否会丢数据 原因
DUT → FaapProxy 断开 可能丢 DUT 侧没有持久化,断了就断了
FaapProxy 崩溃 队列中的消息丢失 asyncio.Queue 是内存队列
PC → MBS 网络故障 重试 3 次后丢 当前实现没有本地缓存
MBS → 云端失败 不丢 MBS 先存 SQLite,后续重发
MBS 进程崩溃 不丢 SQLite 文件仍在磁盘上

最薄弱环节:FaapProxy 的内存队列。如果 FaapProxy 进程崩溃,队列中未推送给 PC 的数据会丢失。这是已知的设计取舍——为了实时性放弃了这一层的持久化。


十一、数据格式示例

11.1 TestPointDataMessage 实例

{
  "session_id": {
    "device_id": "C821234567",
    "start_time": "2026-06-11T10:31:15.620Z"
  },
  "point": {
    "seqid": 100010,
    "data": {
      "scalar": {
        "double_value": 21.8,
        "unit": "UNIT_DBM"
      }
    }
  },
  "tptag": "TP_TX_PWR_003",
  "tpdesc": "TX Output Power Port 3",
  "pass": false,
  "limit": {
    "gtelte": {
      "low": {"scalar": {"double_value": 22.0, "unit": "UNIT_DBM"}},
      "high": {"scalar": {"double_value": 25.0, "unit": "UNIT_DBM"}}
    }
  },
  "tp": 3,
  "tcnr": "3.28",
  "tclabel": "TX Output Power",
  "tc_type": "perf_tx_output_power",
  "filter_attributes": {
    "product_family": "AIR5322",
    "factory": "FACTORY_CODE_A40"
  }
}

11.2 HeaderMessage 实例

{
  "session_id": {
    "device_id": "127_0_0_1",
    "start_time": "2026-06-11T10:30:40.860Z"
  },
  "filter_attributes": {
    "product_family": "AIR5322",
    "factory": "FACTORY_CODE_A40"
  },
  "test_station_id": "ts_python_strict",
  "dut_position": 1,
  "test_category": 0,
  "pid": {
    "product": "KRC 161 3456",
    "slash": "1"
  }
}

11.3 FooterMessage 实例

{
  "session_id": {
    "device_id": "127_0_0_1",
    "start_time": "2026-06-11T10:30:40.860Z"
  },
  "pass": false,
  "stop_time": "2026-06-11T10:31:17.730Z",
  "no_test_point_msg": 7,
  "no_auxiliary_data_msg": 5,
  "dut_position": 1
}

十二、gRPC 通信细节

12.1 各 RPC 调用的请求/响应结构

RPC 请求消息 响应消息 方向
GetSeqIds {session_id, number_of_requested_ids} {status, seq_id_range: {start, sep, stop}} DUT→FaapProxy
AddTestPoint {payload: TestPointDataMessage} {status: {code, message}} DUT→FaapProxy
AddAuxiliaryData {payload: AuxiliaryDataMessage} {status: {code, message}} DUT→FaapProxy
AddDataPointGroup {payload: DataPointGroupMessage} {status: {code, message}} DUT→FaapProxy
FinalizeTestSequence {dut_id, status} {status: {code, message}} DUT→FaapProxy
InitializeTestAsync InitializeTestRequest stream<FaapProxyReturnMessage> PC→FaapProxy
InitiateTestSession {payload: HeaderMessage} {status: {code, message}} PC→MBS
AddTestPointData {payload: TestPointDataMessage} {status: {code, message}} PC→MBS
AddAuxiliaryData {payload: AuxiliaryDataMessage} {status: {code, message}} PC→MBS
AddDataPointGroup {payload: DataPointGroupMessage} {status: {code, message}} PC→MBS
FinalizeTestSession {payload: FooterMessage} {status: {code, message}} PC→MBS

12.2 Status Code 含义

Code 名称 含义 场景
0 OK 成功 正常处理
3 INVALID_ARGUMENT 参数无效 SessionId 为空
5 NOT_FOUND 未找到 DUT IP 不在缓存中
13 INTERNAL 内部错误 未预期的异常
14 UNAVAILABLE 服务不可达 网络断开,触发重试

12.3 端口分配

端口 服务 监听者 连接者
50052 DutTestDataService + CtrlService + FaapProxyService FaapProxy DUT + PC
50053 TestResultMessageService MBS PC

十三、总结:一次完整测试的数据流统计

基于本次模拟运行的统计:

数据产生:
  DUT 发出的 RPC 调用总数: 1(GetSeqIds) + 5(Aux) + 7(TP) + 3(Group) + 1(Finalize) = 17 次

FaapProxy 处理:
  缓存查询: 16 次 (除 GetSeqIds 外每次都查)
  字段补充: 15 次 (Aux + TP + Group 都需要补 FilterAttributes)
  队列写入: 16 次 (包括最后的 SequenceFinalized + None)

PC 接收:
  Stream 消息: 1(ServerStarted) + 5(Aux) + 7(TP) + 3(Group) + 1(Finalized) = 17 条
  RTD 转发:    5(Aux) + 7(TP) + 3(Group) = 15 条 (排除控制消息)
  文件写入:    7 行 (只写 TestPoint)

MBS 接收:
  gRPC 调用: 1(Header) + 5(Aux) + 7(TP) + 3(Group) + 1(Footer) = 17 次
  SQLite 写入: 17 条 (每次调用都持久化)
  云端转发: 16 条 (Header 时还没有前面的 Aux 在队列里)

最终结论: HAS FAILURES (1/7 测试点不合格, 通过率 85.7%)
posted @ 2026-06-17 11:09  mo686  阅读(1)  评论(0)    收藏  举报