EDS与PUS实践(五):用EdsLib与UDP验证服务类型17线包

  • 本地打包后立刻在同一个函数里解包,只能说明基本接口能调用。把发送端和接收端拆成两个进程,中间真的走一次 UDP,测试边界会清晰很多。

上一篇完成了 PUS 服务类型 17 的 EDS 建模。本文只增加一个变量:让 packed bytes 离开打包进程,经 UDP 到达独立接收进程。这样可以观察真实发送长度、进程边界和接收端校验,但不会把“传输成功”夸大成“服务行为已经实现”。

本章在整个系列中的位置

前面的验证都可以在一个进程、一个函数里完成:初始化对象,Pack,立即 Unpack。这样适合检查 API,却不会暴露实际发送长度、接收长度、端口绑定、进程启动顺序和错误归属。本章把传输边界单独拉出来:

1
2
3
4
5
进程 A:EDS native object → EdsLib → packed TC

│ UDP,只搬运 bytes

进程 B:packed TC → EdsLib → decoded native object

两端仍使用同一份 EDS 数据库,因此它不是独立协议实现交叉验证。本章的价值是把“编解码”和“跨进程传输”接起来,并建立之后替换中间接收端的稳定接口。

开始前已经具备什么

进入本章前,以下内容应当已经单独通过:

  • 四种 ST17 类型可以生成;
  • TC[17,1] 和 TC[17,3] 的 packed size 与手算一致;
  • Complete Pack/Unpack 能检查固定值、长度和 PEC;
  • EdsLib_Initialize() 已在进程启动时调用;
  • 固定输入的 packed hex 可以逐字段解释。

如果单进程 Pack/Unpack 尚未稳定,不应先加 UDP。否则收到 unpack failed 时无法判断是模型、编码、截断还是传输问题。

这一步到底想验证什么

目标很克制,只验证下面这条链:

1
2
3
4
5
6
7
EDS XML
↓ sedstool
生成数据库

C/EdsLib 打包 TC[17,x]
↓ 一个 UDP 数据报
C/EdsLib 按预期类型解包

这里还没有实现“收到 TC 后自动产生 TM”的服务行为。发送什么类型,接收端就按同一类型解包,主要检查 XML、EdsLib 和 UDP 传输之间有没有断层。

将结论写成可证伪的断言

本章要证明的不是“UDP 能发包”这种泛化结论,而是以下具体断言:

  1. 发送端 sendto() 的返回值等于 EDS packed byte 数;
  2. 接收端 recvfrom() 的返回值等于发送长度;
  3. 接收端收到的 hex 与发送端逐字节相同;
  4. Complete Unpack 成功,说明固定值、长度和 PEC 仍有效;
  5. 解包得到的 APID、Subtype、Source ID 和 Process ID 等于发送输入;
  6. 超时、长度不符、解包失败能以不同错误退出。

每条都可以通过日志和退出码观察。若最后只打印 PASS,测试的可解释性仍然不足。

为什么先只发送 TC

这一阶段发送端是命令构造器,接收端是同类型解码器,所以发送 TC[17,1]/TC[17,3] 最直接。接收端不会自动变成服务提供者,也不会把 TC 变成 TM。等下一篇加入 puslib 服务处理器后,链路才扩展为 TC 请求方向和 TM 报告方向。

为什么参考 cmdUtil 和 tlm_decode

NASA EdsLibcfecfs/util 中有命令发送和遥测解码工具。它们展示了比较完整的地面端思路:

  • 根据任务数据库定位类型;
  • 给字段赋值;
  • EdsLib 打包;
  • UDP 发送;
  • 接收 UDP;
  • 根据路由信息确定类型;
  • EdsLib 解包和打印。

这些工具还依赖 cFE 头、MissionLib、TopicId 和任务接口表,不能原样拿来验证一套独立的服务类型 17 EDS 数据库。最小实验只保留“EdsLib + UDP”主线,类型直接选 TC[17,1] 或 TC[17,3]。

可以复用的是结构,不是所有依赖

cmdUtil 的价值在于展示“定位消息类型—填写字段—Pack—sendto”的通用流程;tlm_decode 展示“recvfrom—路由类型—Unpack—遍历显示”。其中类型路由依赖 cFE MissionLib 的接口数据库和 TopicId 映射,当前独立 PUS 数据库没有这套任务信息。

若为了让原工具编译而伪造 TopicId 或 cFE 头,测试会混入一套并不存在的接口语义。更诚实的最小修改是删除 cFS 专属路由,保留 EdsLib 与 UDP,并要求调用者明确给出预期消息类型。

这两个参考工具不使用 Python puslib

它们是 C/EdsLib/cFE MissionLib 路线,与后续 Python puslib 是两套不同角色。前者说明 NASA EDS/cFS 环境下怎样构造与解码任务消息;后者提供不读取当前 EDS XML 的独立 PUS 实现。不能因为名字里都有 PUS 或 ground tool 就认为内部机制相同。

发送端的核心

发送端做的事情并不多:

1
2
3
4
5
6
initialize_edslib();
initialize_native_tc(type_id, &tc);
fill_mission_fields(&tc);

pack_complete(type_id, &tc, packet, &packet_size);
send_udp("127.0.0.1", port, packet, packet_size);

一个 UDP 数据报承载一个完整 PUS 包。没有再套一层自定义长度或 JSON,这样接收端拿到的就是 EdsLib 生成的原始线包。

发送端可以进一步拆成六个可检查的步骤:

  1. 初始化 EdsLib 数据库;
  2. 根据命令行选 TC[17,1] 或 TC[17,3] 的类型 ID;
  3. 查询 packed size 并初始化 native object;
  4. 填写 APID、序列号、Source ID,以及 17.3 的进程 ID;
  5. PackCompleteObject() 生成长度和 PEC;
  6. sendto() 只发送 (packed_bits + 7) / 8 个字节。

最后一点很关键:缓冲区可以定义成 2 KiB,但不能把 2 KiB 全部发出去。否则接收端看到的不是“一个 PUS 包”,而是 PUS 包后跟大量未使用字节。

发送前的检查顺序

推荐按下面顺序失败即退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
Type ID 有效?

GetTypeInfo 成功?packed size 不超过缓冲区?

InitializeNativeObject 成功?

所有任务字段赋值成功?

PackCompleteObject 成功?actual_id 正确?

打印 valid_bytes 范围内的 hex

sendto 返回值是否等于 valid_bytes?

sendto() 返回负数,报告 errno;若返回长度不等于期望,也应判失败。虽然 UDP 本地发送通常全报文成功,测试代码不应假设系统调用永不出错。

发送地址属于配置,不属于消息

127.0.0.1:1234 只决定数据报交给哪个本机 socket。它不会进入 PUS packed bytes,也不会影响 APID、Service Type 或 PEC。修改端口后,预期 PUS hex 应保持不变;若 hex 变化,说明传输配置错误地渗入数据模型。

为什么一个数据报只放一个空间包

这种映射不是 PUS 强制要求,而是测试设计:UDP 本身保留数据报边界,一个数据报一个完整包可以避免额外分帧协议。若一次 UDP 放多个空间包,接收端还要根据 Packet Data Length 循环切分;若一个空间包拆成多个 UDP,又要增加重组状态。最小验证没有必要引入这些变量。

如何在不写死 C 成员时给字段赋值

参考工具允许用户用字符串指定字段,例如:

1
2
3
4
PriHdr.AppId=321
PriHdr.SequenceCount=1
SecHdr.SourceId=42
ApplicationProcessId=1

实现方式是:

1
2
3
4
5
字段路径
↓ EdsLib_DisplayDB_LocateSubEntity()
得到成员类型和 native 绝对偏移
↓ EdsLib_Scalar_FromString()
把文本转换并写入 native object

这种方式适合通用命令工具;固定业务程序直接访问生成成员更容易得到编译期检查。最小实验保留通用赋值思路,是为了尽量复用 EdsLib 原有工具的结构,而不是另造一套报文字段解析器。

动态赋值必须检查三次

第一,LocateSubEntity() 是否精确找到路径;第二,字符串能否转换为字段类型;第三,值是否在 EDS 范围内。只检查最后 Pack 状态可能让错误字段保持默认值,最终产生一个合法但不是用户想要的包。

字段指针通常由 native 对象基址加成员绝对 byte offset 得到,不能把 packed bit offset 当 native pointer 偏移。DisplayDB 返回的是对 native object 的定位信息,随后仍由 DataTypeDB Pack 处理线上排列。

通用工具和固定业务代码的取舍

测试 CLI 需要动态字段路径,便于试不同类型;嵌入式热路径更适合生成结构成员和编译期 Type ID,减少字符串表和解析开销。两者可以共享同一个 pack_message() 核心,无需强迫整个系统只用一种访问方式。

接收端的核心

接收端固定等待一包:

1
2
3
4
5
6
fd = socket(AF_INET, SOCK_DGRAM, 0);
bind(fd, local_port);
size = recvfrom(fd, packet, sizeof(packet), 0, ...);

unpack_complete(expected_type, &decoded, packet, size);
print_fields(&decoded);

测试程序明确传入预期类型,这是一种刻意简化。没有 MissionLib 时,不应该假装已经拥有完整的自动路由能力。

接收端同样需要严格区分“缓冲区容量”和“实收长度”:

1
2
3
4
5
6
7
8
9
10
11
ssize_t received = recvfrom(fd, packet, sizeof(packet), 0, NULL, NULL);

if (received != expected_packed_bytes)
{
/* 长度不符,不能用整个 packet 缓冲区继续解包 */
}

/* 简化后的关键调用;实际数据库符号就是 PUS_DATABASE。 */
status = EdsLib_DataTypeDB_UnpackCompleteObject(
&PUS_DATABASE, &actual_id, &decoded, packet,
sizeof(decoded), (uint32_t)received * 8U);

UDP 保留数据报边界:一次 sendto() 对应接收端的一次完整数据报,不需要像 TCP 那样自行处理粘包/拆包。但如果接收缓冲区太小,数据报会被截断;如果没有超时,程序也可能永久等待。因此测试程序最好设置接收超时,并将超时与解包失败分开报告。

bind() 完成才算接收端 ready

仅启动进程不代表端口已经可用。测试脚本应等待接收端完成 socket 创建和 bind() 后再发送。可以由接收端在成功 bind 后输出 ready 标记,脚本读取到标记再启动发送端。

固定 sleep 1 在本机通常能工作,却把调度速度当成协议条件;负载较高时仍可能先发后绑,UDP 数据报没有连接重试,会直接丢失。

长度验证应早于 Unpack

接收端知道预期固定类型时,可以先取得 expected_packed_bytes。若 received 不等于预期,立即报告:

  • 小于预期:数据报可能被错误构造或截断;
  • 大于预期:可能选错类型,或发送端把整个缓冲区发出;
  • 等于预期但 Unpack 失败:再检查约束、长度字段和 PEC。

这种顺序能把 socket 层长度问题与 EdsLib 验证问题分开。

接收缓冲区截断要显式检测

普通 recvfrom() 返回的数据可能已被内核截断。更严格的 Linux/POSIX 测试可使用足够大的缓冲区,或通过 recvmsg()MSG_TRUNC 检查。最小程序至少保证缓冲区大于数据库给出的最大包长,并把实收长度打印出来。

为什么没有 MissionLib 仍然能做这一步

原始 cmdUtil/tlm_decode 可以通过 cFE/MissionLib 的 TopicId、接口表和路由关系确定消息类型。独立的服务类型 17 实验没有这些任务数据库,所以改成由测试参数显式给出类型:

1
2
17.1 → TC[17,1] 的 EdsLib type ID
17.3 → TC[17,3] 的 EdsLib type ID

删除的是“cFS 任务路由与自动类型发现”,保留的是:DisplayDB 设置字段、DataTypeDB 打包/解包、UDP 收发和字段遍历显示。这个裁剪不会把 cFE 主头误当成 PUS 次头,也不会假装已有一套并不存在的 TopicId 映射。

代价也很明确:接收端必须事先知道期望类型。若未来需要一个端口接收多种 PUS 消息,可以先读取固定的 CCSDS/PUS 头,根据 Packet Type、Service 和 Message Subtype 查表选择 EDS 类型,再调用完整解包;那是下一层路由功能,不应偷偷塞进最小测试。

固定类型接收不是“功能不完整”的错误

测试的目标决定最小接口。当前分别启动 receiver 17.1receiver 17.3,能够精准验证对应类型。自动路由会引入公共头预解析、消息映射表和未知类型策略,增加的每项都需要独立测试。

先固定类型建立可信基线,再添加路由,能判断新失败是否来自路由层。这正是小步验证的意义。

未来自动路由的合理边界

自动路由至少需要:

  1. 检查收到的数据大于等于公共头最小长度;
  2. 读取 Packet Type、Service Type 和 Message Subtype;
  3. 结合 APID/任务接口表选择 EDS Type ID;
  4. 根据该类型查询期望 packed size;
  5. 再调用 Complete Unpack;
  6. 未知组合进入明确拒绝路径。

这张映射表可以由任务数据或 IntfDB/MissionLib 辅助生成,但不能凭服务号直接猜 C 类型。

一个 PUS 包在 UDP 中是什么样

UDP payload 直接是完整 packed PUS packet:

1
2
3
4
5
6
UDP/IP header(由操作系统处理)
└─ UDP payload
└─ CCSDS Primary Header
+ PUS Secondary Header
+ optional application data
+ PEC

代码发送的是遥控 TC[17,1] 或 TC[17,3],不是“UDP 命令头 + PUS 包”。接收端收到的也正是这串 TC 字节。该 C→C 测试不会自动产生 TM;只有引入真正的 PUS 服务类型 17 处理器后,才会出现 TC 请求和 TM 响应两条方向相反的数据流。

抓包时应该比较哪一段

Wireshark/tcpdump 会显示以太网、IP 和 UDP 头,应用日志通常只打印 UDP payload。逐字节比较时应从 CCSDS 主头的第一个 byte 开始,不要把网络头加入 PUS golden vector。

本机 loopback 可能看不到以太网头,但 IP/UDP 头仍由内核构造。无论接口如何,PUS PEC 只覆盖 packed PUS packet,不覆盖 UDP/IP 头。

UDP checksum 与 PUS PEC各自的价值

UDP checksum 检查 UDP 伪首部、头和 payload 在传输中的错误,PUS PEC 检查空间包本身的数据完整性。包可能经过存储、转发或重新封装,PUS PEC仍随包存在;UDP checksum 在每段 UDP 传输上重新计算。两者作用域不同,不应二选一。

两条实际路径

测试分别跑:

1
2
./run_eds.sh 17.1
./run_eds.sh 17.3

17.1 检查无业务数据的 TC;17.3 额外检查 16 位 Application Process ID。接收端除了返回成功,还会输出:

  • 实际收到的字节数;
  • 十六进制包;
  • APID、序列号;
  • PUS 版本;
  • Service/Message Subtype;
  • Source ID;
  • 17.3 的 Application Process ID;
  • PEC。

建议每次日志都把“类型、期望长度、实收长度、完整 hex、解包状态”放在一起。例如 TC[17,3] 至少要能回答:

  • 为什么整包比 TC[17,1] 多 2 字节;
  • 长度字段是否因此从 6 变成 8;
  • 进程 ID 的两个字节位于 PEC 前;
  • 修改进程 ID 后 PEC 是否同时变化;
  • 接收端显示的值是否与发送参数一致。

TC[17,1] 的预期观察

当前任务剖面下,TC[17,1] 是 13 byte。接收端应看到 Packet Type=TC、Service=17、Subtype=1,无 Application Process ID。长度字段为 6,最后 2 byte 是 PEC。

若接收端打印了 Process ID,说明类型或字段遍历逻辑混入 17.3;若整包 15 byte,可能发送端选错类型或增加了不应存在的业务字段。

TC[17,3] 的预期观察

在相同公共输入下,TC[17,3] 是 15 byte,长度字段为 8,PEC 前出现 2 byte 大端 Process ID。接收后不仅比较数值 1,还要确认 00 01 位于正确位置。这样能同时验证类型宽度和字节序。

两条路径为什么要分开跑

17.1 提供最小公共头基线,17.3 只增加一个服务字段。两条结果相减可以直观看到新增字段对 packed size、length 和 PEC 的影响,比只测复杂消息更容易定位。

这里可以放一张两个终端的截图:

图片预留:并排截取 UDP 发送端与接收端的终端输出,保存为 eds-pus-05-service-type-17-edslib-udp/udp-two-processes.png

为什么 UDP 不属于 PUS 服务类型 17

PUS 服务类型 17 规定的是请求、报告和数据字段,UDP 只是测试环境里的搬运方式。以后完全可以换成:

  • RTOS 消息队列;
  • 软件总线;
  • 串口或 CAN 上层适配;
  • 真实通信链路;
  • 单元测试中的内存缓冲区。

只要交给下一层的还是同一串 PUS packed bytes,EDS 数据契约不需要因为传输介质改变而重写。

这也提供了一条实用的分层接口:

1
2
int transport_send(const uint8_t *packet, size_t size);
int transport_receive(uint8_t *packet, size_t capacity, size_t *actual);

EDS/PUS 层只处理 packet 和 size;UDP、消息队列或驱动层实现这两个边界。以后换传输时,不应改动 Service/Message Subtype、长度或 PEC 的定义。

transport 接口还应表达错误

真实封装可以返回明确错误码,区分超时、缓冲区不足、系统调用失败和对端关闭等情况。EDS 层收到的只有两种状态:获得一段完整候选 packet,或传输失败;不要让 socket errno 穿透到 PUS 服务逻辑中。

RTOS 中怎样替换 UDP

在 RTOS 下,transport_send() 可以写消息队列或驱动,transport_receive() 可以阻塞等待队列。只要它交付完整 packet 与真实长度,上层 pack_message()/unpack_message() 不变。若底层最大帧小于空间包,则在 transport 内部增加分片重组,不修改 PUS XML 来迎合某次分片。

推荐的启动和收尾顺序

双进程测试应避免竞争:

  1. 先启动接收端并完成 bind()
  2. 再启动发送端;
  3. 发送端打印 packed hex 与 sendto() 返回长度;
  4. 接收端打印 recvfrom() 长度、解包状态和字段;
  5. 两端都以非零退出码报告失败;
  6. 脚本设置超时并回收后台进程。

不要用固定 sleep 1 作为唯一同步手段。更稳定的做法是让接收端输出 ready 标记,或让脚本轮询端口/进程状态;至少也要在超时时清理后台接收器,避免下次测试出现端口已占用。

一个可靠脚本承担什么职责

脚本不是协议实现,它只负责组织进程:

  • 创建或确认构建产物;
  • 启动 receiver 并等待 ready;
  • 启动 sender 并保存双方日志;
  • 等待进程退出并汇总退出码;
  • 超时时终止本次启动的后台进程;
  • 不吞掉原始错误输出。

脚本不应自己重写 CRC 或解析 PUS 字段,否则测试逻辑会散落到 shell、C 和 XML 三处。

端口冲突与遗留进程

若前一次超时留下 receiver,下一次 bind 可能报地址已使用,或者新 sender 的包被旧进程接收。脚本应记录自己启动的 PID,只清理该 PID,并在开始前检查端口状态。不要使用宽泛的 pkill 误伤其他测试。

这一步值得加入哪些负向测试

场景 预期观察
发送前翻转 Service 位且不重算 PEC 接收端 CRC/完整对象验证失败
发送前翻转正文并重新计算 PEC CRC 通过,但按错误类型/字段语义检查
截断 PEC 最后 1 字节 实收长度不符,拒绝解包
接收端故意选择 TC[17,1] 解 TC[17,3] packed size 或类型约束不符
发送到错误端口 接收超时,而不是“解包失败”

网络错误、协议错误、类型选择错误要分别报告。只有这样,测试失败才能指向真正的层次。

再补三组边界测试

场景 预期观察
sendto() 只发送前 6 byte receiver 报长度不足,不进入业务字段打印
发送合法 17.3,receiver 按 17.1 等待 先报预期长度不一致
发送端修改 APID 后重新 Pack receiver 成功,APID 与 PEC 同时变化

最后一项是正向变体:它证明任务字段可以变化且 CRC 被重新计算;前两项证明接收端没有用缓冲区默认值补齐坏包。

一次只注入一个故障

同时改 subtype、截断 PEC 并发错端口,只会得到超时,无法证明任何校验。故障注入应单变量,并记录改动前后的 hex,才能把预期失败与具体防线对应起来。

这条测试没有证明什么

它没有证明:

  • 收到 TC[17,1] 后一定生成 TM[17,2];
  • 17.3 的目标进程真的存在;
  • 失败确认行为符合 PUS 服务类型 1;
  • UDP 可以代表真实星地链路;
  • 整套系统已经集成完成。

它证明的是:XML 可以生成数据库,EdsLib 能产生符合该数据库的线包,字节经过独立进程和 UDP 后还能被完整校验、解包。

这个边界看起来保守,但结论可重复、可定位:EDS 模型生成的 TC 线包可以跨进程传输,并由同一数据契约完成长度、约束和 PEC 验证。

本章真正得到的工程接口

实现结果可以收敛成三个相互独立模块:

1
2
3
message_builder:Type ID + native fields → packed bytes
transport_udp:packed bytes ↔ UDP datagram
message_decoder:packed bytes + expected Type ID → validated object

这三个模块都不执行 ST17 业务。正因为边界清楚,下一篇可以保持 builder 和 decoder 不动,只把中间的“同数据库 receiver”替换成 puslib 服务进程;之后移植 RTOS 时,又只替换 transport。

如何准确汇报这一阶段

合适的表述是:“已验证 TC[17,1] 和 TC[17,3] 能由 EdsLib 按当前 EDS 数据库打包,经 UDP 跨进程传输后,由同一数据库完成长度、类型约束和 PEC 校验并恢复字段。”

不合适的表述是:“PUS17 服务已经通过 UDP 验证。”后者会让人误以为 TC 已触发真实测试并生成 TM,也掩盖了两端共享同一数据库的共同错误风险。

下一篇再把接收端换成不读取 EDS XML 的 puslib。届时 Python 端不仅解析 TC,还会实际执行 PUS 服务类型 17 行为并生成 TM,C/EdsLib 再从另一个 UDP 端口收回并解包。这样验证范围才从“传输闭环”扩展到“独立实现交叉闭环”。

参考资料

EDS与PUS实践(五):用EdsLib与UDP验证服务类型17线包

https://goko-son626.github.io/post/eds-pus-05-service-type-17-edslib-udp.html

作者

GoKo Mell

发布于

2026-08-30

更新于

2026-09-20

许可协议

评论

:D 一言句子获取中...