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 -- 记录创建时间
);
断网保护原理:
- 每条消息先写 SQLite,再发云端
- 如果云端发送失败,数据已经安全存在本地
- 网络恢复后,后台服务扫描 SQLite 中未发送的记录,重新发送
- 发送成功后从 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%)

浙公网安备 33010602011771号