EDS与PUS实践(六):用puslib交叉验证服务类型17的TC与TM

  • 如果 TC 和 TM 都由同一份 XML、同一套 EdsLib 代码生成和解析,测试有可能只是“自己同意自己”。引入不读取 EDS XML 的 puslib,才算多了一条相对独立的检查路径。

上一篇的 C→UDP→C 路径验证了进程间传输,但打包和解包仍由同一份 EDS 数据库完成。本文把中间端替换为独立的 Python PUS 实现:C/EdsLib 只负责请求和最终响应的线格式,puslib 负责解析请求、执行 PUS 服务类型 17 语义并生成响应。

本章为什么是系列的最后一步

前五篇已经逐步建立证据:XML 能生成、EdsLib 能 Pack/Unpack、线包符合手算结构、UDP 能跨进程保持字节。但 C 发送端与 C 接收端都使用同一份 DataTypeDB。如果 XML 把某个字段宽度写错,两端可能仍然彼此同意。

本章要打破这项共同依赖。Python 端不读取 EDS XML,也不链接 EdsLib,而是使用 puslib 自己的 packet 与 service 实现。它必须能解析 C/EdsLib 产生的 TC,并独立产生能被 C/EdsLib 接受的 TM。

本章的逻辑顺序是:先明确独立边界,再对齐任务剖面,然后接入 ST17 服务对象,补足上游库缺少的 17.3/17.4 行为,最后分别验证正常与失败路径。UDP 仍然只是连接两端的薄适配层。

开始前必须明确的验证边界

“使用两个语言”不自动等于独立。真正的独立性来自:

  • C 端按照 EDS DataTypeDB 编解码;
  • Python 端按照 puslib 类与 Policy 编解码;
  • Python 端不调用 EdsLib,也不读取生成数据库;
  • 两端只共享一份书面任务剖面和线上 bytes;
  • 固定向量与标准条款用于裁决分歧。

本地对 puslib 的 ST17.3/17.4 做过局部扩展,所以它仍不是第三方认证工具。交叉一致性提高可信度,但最终语义仍要回到 ECSS 41C。

交叉验证的结构

puslib 是 Python 实现的 ECSS PUS 库。它不读取当前的 EDS XML,而是按照自己的包类和服务实现解析字节。

最终链路是:

1
2
3
4
5
6
7
8
C / EdsLib
打包 TC[17,1] 或 TC[17,3]
↓ UDP 1234
Python / puslib
独立解析 TC、执行 PUS 服务类型 17、生成 TM
↓ UDP 2234
C / EdsLib
解包 TM[17,2] 或 TM[17,4]

两端只有线包字节是共同输入,这正是交叉验证想要的边界。

把责任再展开一点:

阶段 实现 是否读取 EDS XML 主要验证点
构造 TC native object C/EdsLib 通过生成数据库间接使用 固定约束和任务字段
TC 序列化 C/EdsLib packed 布局、长度、PEC
TC 反序列化 Python/puslib 独立实现能否接受 TC
执行 PUS 服务类型 17 Python/puslib 17.1/17.3 行为与失败语义
TM 序列化 Python/puslib 独立产生响应字节
TM 解包 C/EdsLib EDS 能否接受响应及校验 PEC

如果最后成功,至少说明两套实现对当前任务剖面的关键字段达成一致,而不只是同一个编解码器往返。

两个方向验证不同问题

C→Python 的 TC 方向验证:C/EdsLib 能否生成 puslib 接受的 CCSDS/PUS 主头、Source ID、application data 和 PEC。Python→C 的 TM 方向验证:puslib 的 PUS-C TM 次头、Destination ID、时间、source data 和 PEC 能否满足 EDS 模型。

只做 TC 单向解析,只能证明请求格式;只让 Python 自己创建并解析 TM,又无法证明 EDS 模型。必须让一端生成、另一端消费,才形成交叉。

服务行为是链路中间的一层

Python 收到 TC 后不是立即把字节改个 subtype 返还。它先反序列化为 PusTcPacket,放入 PUS 服务类型 17 的输入队列,服务根据 subtype 和注册表处理,再向抽象输出流写 PusTmPacket。UDP 适配器最后才从输出流取包并发送。

这样 packet codec、service logic 和 socket 各自可单元测试。若跨进程失败,可以先运行不带 UDP 的服务测试,判断问题是在状态机还是线格式。

两个 UDP 端口为何分开

示例使用 1234 承载 TC、2234 承载 TM,方向清晰,也避免同一 socket 上同时承担请求和响应状态。端口号只是测试配置,不进入 PUS 包。真实系统可以使用不同传输机制,只要仍保持请求与报告的包边界。

Python 适配器只做连接

Python 文件看起来比一个 recvfrom() 长,是因为 puslib 还需要任务策略。核心流程其实只有:

1
2
3
4
5
6
7
8
9
tc_bytes, _ = tc_socket.recvfrom(65535)
tc_packet = PusTcPacket.deserialize(
tc_bytes, has_source_field=True)

test_service.enqueue(tc_packet)
test_service.process()

tm_packet = tm_output.get()
tm_socket.sendto(bytes(tm_packet), tm_address)

适配器没有重新手写 CCSDS/PUS 字段解析。TC 反序列化、PEC 检查、服务处理和 TM 序列化都交给 puslib。

实际适配器还做了几项边界检查:

  • 只接受 PUS 服务类型 17 且 Message Subtype 为 1 或 3;
  • 为测试注册 APID 和目标进程 ID;
  • 每次只取一条预期 PUS 服务类型 17 功能报告;
  • 17.1 必须得到 17.2,17.3 成功必须得到 17.4;
  • 17.4 的 source data 必须等于 17.3 的 application data;
  • 最后才把序列化后的 TM 发回 C 接收端。

网络代码只是 bind/recvfrom/sendto。真正的协议逻辑仍在 PusTcPacketPusService17PusService1PusTmPacket 中,因此适配器不会成为第二套手写 PUS 实现。

按实际代码拆解适配器

第一步调用 set_policy(DemoPolicy()),在创建或解析 TM 之前安装任务策略。第二步绑定 TC 端口并设置超时,避免测试永久阻塞。第三步调用:

1
2
tc_packet = PusTcPacket.deserialize(
tc_bytes, has_source_field=True)

has_source_field=True 对应当前 TC 次头包含 16 bit Source ID 的剖面。这里由 puslib 完成空间包/PUS 字段解析和 PEC 检查,适配器没有用切片手工取 service byte。

还要注意,当前 puslib 解析器检查“主头声明的包长不能超过输入缓冲区”,但不会自动要求输入缓冲区恰好等于 Packet Data Length + 7;尾随额外字节可能不参与当前包 CRC。严格的 UDP 适配器还应在反序列化前显式比较数据报长度与主头声明总长。现有正常路径发送精确长度,不触发该差异,但它属于后续负向测试应补的边界。

第四步限制输入只接受 Service 17 且 subtype 为 1 或 3。这是测试程序的范围检查,防止误把其他服务送进 ST17 实例。第五步创建 PusIdent、服务类型 1、服务类型 17 和 QueuedOutput

1
2
3
4
tm_output = QueuedOutput()
ident = PusIdent(APID)
request_verification = PusService1(ident, tm_output)
test_service = PusService17(ident, request_verification, tm_output)

第六步注册进程 ID 与成功判据,把 TC 入队并调用 process()。第七步从输出队列取得报告,检查 subtype 和 Process ID,再用 bytes(tm_packet) 让 puslib 序列化,最后通过 TM 端口发送。

为什么只取队列中的一条报告仍然成立

当前 TC 将 ACK flags 设为 NONE,所以服务类型 1 的成功 acceptance/start/completion 报告不会进入队列;正常路径唯一输出就是 TM[17,2] 或 TM[17,4]。适配器因而可以取第一条并要求其为 ST17 报告。

若以后打开 ACK 位,队列中会同时出现 ST01 成功报告,适配器必须按 Service/Message Subtype 分类,不能继续假设第一条就是功能报告。这项假设应写进测试配置。

Python 代码为什么比 recvfrom()+sendto()

多出的代码主要是策略对齐、服务对象装配和断言,而不是重新实现协议。删掉这些检查虽然更短,却会让默认时间格式、Destination ID 或错误输出悄悄改变,交叉验证也失去解释性。

为什么还要有 Policy

PUS 有一些任务可选项,两个实现必须对这些选项达成一致。例如当前约定:

  • PUS 版本为 2,即 PUS-C;
  • TM 有 16 位 Destination ID;
  • 时间使用 CUC 4+2;
  • CUC 不带前导字节;
  • TM 时间字段和 EDS XML 的定义一致。

puslib 默认策略不一定和这套线格式相同,因此通过一个很小的 DemoPolicy 覆盖 TM 和 CUC Time 的创建参数。这不是绕过标准,而是在显式声明任务裁剪。

当前合并模型的关键约定是:

1
2
3
4
5
PUS version           = 2
TM message type count = 不存在
Destination ID = 16 bit,示例值 42
Time = CUC,4-byte coarse + 2-byte fine
CUC preamble = 不在线传输

Policy 只应该承载这类任务级选择,不应该把 PUS 服务类型 17 的目标进程宽度强塞进公共 puslib Policy。Application Process ID 的位宽属于该任务的 PUS 服务类型 17 数据定义,因此本地扩展把它作为原始 bytes 保存;这样不会影响其他服务或另一任务对进程 ID 的选择。

DemoPolicy.CucTime() 做了什么

它为测试固定 seconds、fraction、4 byte coarse、2 byte fine,并关闭线上 preamble。固定时间让 TM golden vector 可重复;真实系统应从时钟取得值,但仍要保持编码宽度和 preamble 约定。

DemoPolicy.PusTmPacket() 做了什么

它统一设置 PUS 版本 2、无 Message Type Counter、Destination ID=42。服务实现只需声明 service/subtype 和数据,不必在每个报告里重复这些任务字段。

Policy 不应该隐藏服务专属行为

“Process ID 是 16 bit”只属于 ST17 当前任务定义;“未登记进程返回 TM[1,4]”属于 ST17 行为;都不应加到影响所有服务的公共 Policy。Policy 处理的是包公共裁剪,服务模块处理的是服务语义。

两端对齐 Policy 的方法

建议维护一张单独任务剖面表,把每项分别指向 EDS XML 和 Python Policy 位置。交叉失败时逐项比较,而不是边试边改默认值直到测试偶然通过。

17.3/17.4 的补充

使用的 puslib 0.4.0 快照已经有 PUS 服务类型 17 的基础实现,但主要覆盖 17.1/17.2。为了验证第二组消息,在本地服务模块补了:

  1. 注册可测试的 Application Process ID;
  2. TC[17,3] 查找对应测试回调;
  3. 回调成功时生成 TM[17,4];
  4. TM[17,4] 返回请求中的同一进程 ID;
  5. 未登记或测试失败时走失败报告。

扩展尽量留在 pus_017_test.py 内,没有为了一个服务修改公共 Process API,也没有改变 PUS 服务类型 1 的确认语义。这一点对第三方库维护很重要:局部需求不要轻易污染所有服务。

补充后的核心状态可以写成:

1
2
3
4
5
6
7
8
9
收到 TC[17,1]
├─ app_data 非空 → TM[1,4] 非法应用数据
└─ app_data 为空 → TM[17,2]

收到 TC[17,3]
├─ app_data 缺失 → TM[1,4] 非法应用数据
├─ 进程未登记 → TM[1,4] 进程不可测试
├─ 回调返回 false/抛异常 → TM[1,8] 连接测试失败
└─ 回调成功 → TM[17,4],原样返回进程 ID

这里的错误码 170 和 171 是当前任务为两种失败原因分配的值,不是 ECSS 为所有任务固定的通用数值。标准规定失败报告与语义,具体错误码取值需要任务声明。

为什么扩展只放在 pus_017_test.py

上游 0.4.0 测试只覆盖 17.1→17.2,现有 Service 17 也主要提供 are-you-alive。补充 17.3/17.4 时,最小影响策略是让该服务自己维护:

1
self._connection_tests: dict[bytes, Callable[[], bool]] = {}

键是任务定义的原始进程 ID bytes,值是该进程的成功判据回调。没有给所有 Process 对象增加 add_testable_process(),没有修改公共 Policy 的 APID 类型,也没有改变服务类型 1 的成功确认接口。

add_testable_process() 的职责

注册时把 bytearray 归一化成不可变 bytes,拒绝空 ID,并检查回调可调用。它实现的是 41C 6.17.4.1 要求的“可测试应用过程列表”和成功判据的任务声明入口。

真实系统的回调可以检查心跳、消息往返或任务状态;示例回调只返回 True,用于证明流程。不能把演示回调成功描述成真实应用健康。

process() 的分发逻辑

服务从输入队列取尽所有 TC,先交给服务类型 1 处理 acceptance,然后按 subtype 进入 17.1 或 17.3 分支。注册的 subservice 只有 1 和 3,适配器还在入队前做范围检查,因此不会把未知 subtype 当成 17.3。

17.1 分支为何检查空正文

标准要求 application data field omitted。puslib 反序列化后可能把“无数据”表示成 Noneb'',实现接受这两种内部空表示;任何非空数据都产生非法应用数据失败,不生成 17.2。

17.3 分支为何保留原始 bytes

ECSS 只规定字段是任务声明枚举,没有固定宽度。若服务内部先强制转成 Python int,再按某个公共默认宽度序列化,容易把前导零和任务宽度丢掉。使用原始 bytes 作为注册键并原样放入 17.4,能够保持 EDS 的 2 byte 大端选择,同时不把 2 byte 写死到公共库。

异常为什么等同连接测试失败

回调返回 false 或抛异常,都表示成功判据未满足。服务把它们转换为 completion failure,避免测试进程崩溃导致请求永远无响应。实际系统还应记录异常原因,但线上 failure code 与 failure data 应按任务定义控制,不能泄漏任意 Python 异常文本。

为什么失败报告不能被 ACK_NONE 吞掉

测试请求把四个成功确认请求位都清零,目的是确认“标准要求的失败通知”与“请求方是否订阅成功确认”不是同一件事。

若直接复用只在 ACK 位开启时才输出报告的成功确认接口,未知进程或测试失败可能什么都不返回。局部实现因此显式构造 TM[1,4] 或 TM[1,8],但没有给公共 PUS 服务类型 1 API 增加 force_report 参数。这样既满足本服务失败路径,也不改变其他服务对 ACK 的既有行为。

成功确认和强制失败报告是两套条件

ACK flags 让请求方选择是否接收成功的 acceptance/start/progress/completion 通知。ST17 标准又明确要求特定失败时生成 failed start 或 failed completion。若通用 API 把二者统一为“对应 ACK 位为 0 就不发”,会漏掉强制失败语义。

局部 _write_failure_report() 直接构造服务类型 1 报告,包含原 TC 的 request ID 和任务 failure code。它只服务 ST17 扩展,不修改 ST01 公共源码,减少对其他服务的回归风险。

为什么仍要单测输出数量

ACK_NONE 的正常 17.1 应只产生一个 17.2;未知进程应只产生一个 1.4;回调失败应只产生一个 1.8。若多出成功确认或功能报告,说明分支在失败后没有及时 return,或 ACK 处理不符合当前测试条件。

当前交叉测试没有覆盖非零 ACK 位

这里必须额外记录一个源码差异:ECSS-E-ST-70-41C 7.4.4.1d 规定 acknowledgement flags 的 bit3/bit2/bit1/bit0 依次代表 acceptance/start/progress/completion;当前 vendored puslib 0.4.0 的 AckFlag 枚举数值则按 bit0/bit1/bit2/bit3 依次命名为这四项,顺序相反。

本实验所有成功确认位都为 0,因此这项差异不会影响当前 17.1/17.3 正常功能报告和局部强制失败报告,但也意味着当前结果不能宣称验证了非零 ACK flags 的 ST01 成功确认选择。未来开启 ACK 位以前,应先修正或适配该映射,并单独用四个单 bit 向量验证线上 nibble 与报告阶段。

puslib 在这条链里怎样流转对象

1
2
3
4
5
6
7
8
9
10
11
UDP bytes
↓ PusTcPacket.deserialize(...)
PusTcPacket
↓ service.enqueue()
服务类型 17 输入队列
↓ service.process()
测试回调 / 服务类型 1 失败报告 / 服务类型 17 成功报告
↓ QueuedOutput
PusTmPacket
↓ bytes(packet)
UDP bytes

QueuedOutput 是测试用输出流:服务只负责向抽象输出流写报告,不关心报告最终经 UDP、文件还是其他通道发送。适配器从队列取出 PusTmPacket 再负责网络发送。这种边界让服务单元测试可以完全不启动 socket。

队列边界让测试分成两层

服务单元测试直接构造 PusTcPacket、调用 enqueue/process,再检查 QueuedOutput,用于验证状态机。跨进程测试则在此基础上增加 serialize/deserialize 和 UDP,用于验证线格式与传输。

若单元测试失败,不需要启动 socket;若单元测试通过而跨进程失败,优先检查 Policy、PEC、字段宽度和端口。分层能显著缩短定位路径。

对象与 bytes 的转换点只有两个

Python 侧只有入口 PusTcPacket.deserialize(tc_bytes, ...) 和出口 bytes(tm_packet) 直接接触线包。服务内部始终处理 packet 对象。这两个点最适合记录 hex、长度与异常,其他函数不应重复切片解析。

固定字节必须绑定到明确的任务剖面

TC 侧的一组已知输入是:

1
2
3
4
5
TC[17,1]
19 41 c0 01 00 06 20 11 01 00 2a 1b 4c

TC[17,3],Application Process ID = 1
19 41 c0 01 00 08 20 11 03 00 2a 00 01 9c 72

两包分别为 13 和 15 字节,后者多出的 00 01 是 16 位进程 ID,因此 Packet Data Length 从 6 变为 8,PEC 也随之变化。

TM 不能脱离任务剖面只写一串“标准答案”。早期最小模型包含一个 16 位 Message Type Counter,所以固定时间下的 TM[17,2]/TM[17,4] 分别比不含计数器的模型多 2 字节;当前合并模型明确设置 msg_type_counter=None。如果直接拿旧 TM golden vector 检查新模型,会得到长度和字段错位,而问题不是 CRC 算错,而是两端采用了不同剖面。

因此 golden vector 旁边至少要记录:

  • EDS XML 或任务剖面版本;
  • APID、Sequence Count、Source/Destination ID;
  • 是否有 Message Type Counter;
  • 时间编码、长度和值;
  • Application Process ID 宽度和值;
  • PEC 算法。

稳定断言应优先检查协议关系:类型、长度、字段位置、CRC、请求/响应对应关系;只有所有输入固定时,才逐字节比较完整 TM。

为什么 TC 向量比 TM 更容易固定

TC 示例可以固定 APID、Sequence Count、ACK、Source ID 和 Process ID,所有输入都由命令构造器控制。TM 还包含时间和独立序列计数,若不通过 Policy 固定,连续两次合法输出也会不同。

因此调试顺序可以是:先让 puslib 稳定解析固定 TC;再固定测试时间,比较 TM;最后恢复真实时间,只检查结构、字段关系和 CRC,而不要求完整 hex 恒定。

每个向量旁边应保留解释

以 15 byte TC[17,3] 为例,至少记录:前 6 byte 主头、5 byte TC 次头、2 byte Process ID、2 byte PEC;Length=8;Process ID=1 编码为 00 01。向量本身不能替代这些解释。

向量发生变化时怎样判断

先比较任务剖面与输入值,再定位第一处不同 byte。若从 TM Destination ID 之后全部错位,优先怀疑可选计数器或时间前导;若只有最后 2 byte 不同,优先检查 CRC 参数或此前某个未打印字段;若第 5~6 byte length 不同,先重新计算整包结构。

除了正常路径,还测失败

只有 happy path 不够。当前还检查:

  • 未登记的进程 ID;
  • 连接测试回调返回失败;
  • TC 服务号或消息子类型号不匹配;
  • 没有产生服务类型 17 的预期 TM;
  • TM[17,4] 没有返回原进程 ID。

本地不经 UDP 的服务测试覆盖四个关键场景:

输入 预期唯一输出 关键断言
TC[17,1],无正文 TM[17,2] source data 为空
TC[17,3],ID=1,回调成功 TM[17,4] 返回 00 01
TC[17,3],ID=99,未登记 TM[1,4] 失败码 170
TC[17,3],ID=1,回调失败 TM[1,8] 失败码 171

先做不经 UDP 的服务单元测试,再做完整跨进程测试,能把失败定位为“服务状态机问题”或“线格式/传输问题”。所有逻辑一股脑放进一个 UDP 脚本,失败时很难知道是哪层出错。

单元测试应直接对应标准分支

正常 17.1 对应 6.17.3;正常 17.3、未登记目标和判据失败对应 6.17.4.2 的不同条目。测试名应表达语义,例如 test_on_board_connection_unknown_process_reports_start_failure,而不是只写 test_case_3

非法正文还需要覆盖

17.1 带非空 app data 应被拒绝;17.3 缺少 app data 也应失败。它们验证 8.17 的消息结构,而不是连接回调。若只测未知 ID,无法知道服务是否区分“字段缺失”和“值未登记”。

失败报告也要验证 payload

除了 Service=1、Subtype=4/8,还应检查 request ID 能关联原 TC,failure code 宽度与当前 Policy 一致,任务错误码数值正确。只检查 subtype 容易漏掉报告内容错位。

一次完整交叉验证应怎样判定

建议按下面顺序检查,而不是只看脚本最后是否打印 PASS:

  1. C 端打印所选 EDS 类型、packed size 和 TC hex;
  2. Python 端反序列化后打印 Service/Message Subtype 与 app data;
  3. PUS 服务类型 17 处理器选择正确分支并调用预期回调;
  4. Python 端生成唯一正确类型的 TM;
  5. C 端收到的长度等于目标 EDS 类型 packed size;
  6. UnpackCompleteObject() 成功,说明长度、约束和 PEC 均通过;
  7. 17.4 的进程 ID 与请求相同;
  8. 任一进程异常或超时都使总脚本非零退出并清理后台进程。

这套判据能明确交叉验证覆盖了哪些边界,而不是用“一条命令跑完”掩盖内部是否真的互相独立。

建议把判据落实为分层退出码

总脚本可以依赖每个进程非零退出,并在失败时打印阶段:

1
2
3
4
5
6
7
8
BUILD        生成/编译失败
TC_PACK C/EdsLib 无法构造请求
TC_TRANSPORT puslib 未收到或超时
TC_PARSE puslib 拒绝线包
SERVICE 未产生预期报告
TM_TRANSPORT C 端未收到报告
TM_UNPACK EDS 拒绝响应
ASSERT 字段关系不满足

不必真的规定这些字符串为协议,只要日志能保留层次。这样一次失败不会被统一包装成“cross validation failed”。

C 端最终检查为何最重要

Python 端能创建 PusTmPacket 不代表它符合 EDS 模型。C 端以 TM[17,2] 或 TM[17,4] 的具体 Type ID 调用 Complete Unpack,长度、固定 subtype、时间布局和 PEC 任一不一致都会失败。最后再比较 17.4 Process ID,才完成响应验证。

交叉验证能发现的典型不一致

不一致 常见表现
TC Source ID 是否存在/宽度不同 puslib 从错误偏移读正文或 PEC
TM 是否有 Message Type Counter TM 长度差 2 字节,后续字段错位
CUC 是否带 preamble 时间与正文整体偏移
进程 ID 宽度不同 17.3 app data 无法匹配注册 ID
Packet Data Length 算法错误 对端拒绝反序列化或 EdsLib 长度校验失败
CRC 参数/覆盖范围不同 字段似乎正确,但 PEC 校验失败
17.3 失败阶段处理错误 应返回 1.4 却返回 1.8,或完全无报告

这些问题大多是单实现往返测试不容易发现的。

共同错误仍然可能存在

两个实现可能都参考了同一份错误任务剖面,或者本地扩展恰好复制了 EDS 的误解。因此还需要标准条款、手算尺寸和必要的第三方/标准向量。交叉验证降低风险,不把概率降为零。

“能解析”与“行为一致”分开报告

puslib 成功 deserialize TC 证明线格式基本一致;产生正确 TM subtype 和失败阶段才证明服务行为一致;C 端成功 Unpack TM 又证明反向线格式一致。报告应分别列出,不用一句“puslib 验证通过”覆盖三层。

这里可以放一张完整运行日志:

图片预留:截取 EdsLib → puslib → EdsLib 一次完整运行日志,保存为 eds-pus-06-service-type-17-puslib-cross-validation/puslib-cross-check.png

怎样看待测试结论

puslib 是独立实现,因此它能发现字段宽度、PUS 版本、时间格式、CRC 和响应类型不一致的问题。但它不是标准认证工具,本地又对 17.3/17.4 做了扩展,所以结论仍然要写准确:

当前 EDS 数据库与指定版本、指定任务策略下的 puslib 能互相解析,并完成 PUS 服务类型 17 两组正常请求/报告和若干失败路径。

相比一句“测试通过”,这句话更长,但也更真实。它仍然不等于标准认证,也不证明真实进程健康、实时性、链路可靠性或所有 PUS 服务都已实现。

当前结论成立所依赖的条件

  • 使用记录中的 EDS XML 与生成数据库版本;
  • 使用当前 vendored puslib 版本及本地 ST17 扩展;
  • 使用同一任务剖面:Source/Destination ID、时间、计数器、PEC;
  • TC 使用 ACK_NONE;
  • DemoProcess1 的 16 bit ID 为 00 01
  • 测试回调返回预设结果;
  • UDP 在本机 loopback 上完成传输。

这些条件变化后,需要重新判断哪些断言仍适用。把条件写清楚不是削弱成果,而是让结果可复现。

还没有覆盖的工程问题

真实系统还需要多请求并发、序列计数持久性、重复/乱序包处理、超时、进程动态上下线、真实成功判据、线程安全、资源上限和安全机制。本文只验证最小单请求功能与若干失败分支,不把实验工具描述成完整飞行服务。

怎样把方法迁移到其他服务

交叉验证框架可以复用,服务语义不能照抄:

  1. EDS 增加新服务的具体 TC/TM 类型;
  2. C 端仍用统一的类型查询、Pack/Unpack 和 UDP 适配;
  3. puslib 端启用或局部补充对应服务对象;
  4. Policy 只处理任务级公共字段;
  5. 每个服务独立列出正常、拒绝、执行失败和边界输入;
  6. golden vector 与任务剖面版本绑定。

例如 PUS 服务类型 1 是请求验证服务(Request verification service),服务类型 3 是星务与诊断数据报告服务(Housekeeping and diagnostic data reporting service),服务类型 8 是功能管理服务(Function management service);它们可以共享 CCSDS/PUS 头和测试通道,却有完全不同的状态机和正文。最重要的复用是分层与验证方法,而不是复制 PUS 服务类型 17 的代码。

推荐的扩展架构

保持四层稳定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
common packet profile
├─ EDS common XML
└─ puslib Policy

service model
├─ stXX.xml
└─ puslib 对应 service

codec adapters
├─ C/EdsLib pack/unpack
└─ Python serialize/deserialize

transport/test orchestration
└─ UDP + shell test runner

新增服务主要改变第二层及其测试向量;公共剖面不应为了一个服务随意扩展,UDP 适配也不应知道每个业务字段。

多服务时如何选择消息类型

最小 ST17 程序可由命令行固定 Type ID。服务增多后,可以建立 (PacketType, APID, ServiceType, MessageSubtype) → EdsLib Type ID 的任务映射,或利用生成接口数据库辅助发现。这个路由层应单独测试未知组合和冲突,不要在每个服务里重复 switch。

puslib 扩展的维护原则

优先使用已有公共 API;任务策略放局部 Policy;服务专属字段和错误码放服务模块或验证层;修改 vendored 公共源码前评估所有服务回归。若上游以后原生支持 17.3/17.4,应比较行为和 API,再决定移除本地补丁,而不是长期维护两套实现。

这一组实践最终得到的认识

六篇文章形成了一条完整但边界明确的路径:

1
2
3
4
5
6
7
EDS XML 数据契约
→ sedstool 生成 C 类型与数据库
→ EdsLib 构造/校验 packed bytes
→ CCSDS/PUS 标准解释每个字段
→ 服务类型 17 的 EDS XML 表达四种消息
→ UDP 建立跨进程传输边界
→ puslib 独立执行服务并交叉验证

其中最重要的不是某个脚本,而是始终分清三件事:模型是否正确、编解码是否正确、服务行为是否正确。三者需要不同证据,不能用一个“能跑”互相替代。

回看七篇文章的逻辑

第 0 篇建立地图;第一篇解释 XML 怎样成为类型与数据库;第二篇完成 EdsLib 最小 API 闭环;第三篇从标准解释 packed bytes;第四篇把标准落回 ST17 XML;第五篇增加跨进程传输;本篇用第二套实现补上服务行为和反向 TM 验证。

每一步只增加一个主要变量,因此失败时可以退回前一层:生成错看第一篇,Pack 错看第二篇,字段语义错看第三/四篇,UDP 错看第五篇,服务或互操作错看本篇。系列不是七篇并列资料,而是一条逐层增强证据的路径。

最终可以复用的不是代码量,而是方法

实现另一个 PUS 服务时,最有价值的模板是:

  1. 同时阅读系统要求和接口要求;
  2. 冻结任务剖面;
  3. 把公共、服务、消息和行为分层;
  4. 先生成并审查模型;
  5. 再做单实现 Pack/Unpack;
  6. 加入独立实现和失败路径;
  7. 最后才扩大到真实系统集成。

这套顺序避免一开始就把 XML、业务状态机、网络和框架全部揉在一起,也让每个“通过”都有明确含义。

本章小结

puslib 在这里不是替代 EdsLib,也不是标准认证器,而是独立的 PUS packet/service 实现。C/EdsLib 负责 EDS 数据契约,Python/puslib 负责另一套解析、服务状态机和响应生成,UDP 只连接 bytes。

正常路径证明 17.1→17.2 与 17.3→17.4;失败路径证明未知目标在开始阶段失败、连接判据在完成阶段失败。最终 TM 再由 EdsLib Complete Unpack,形成双向交叉。只有把版本、任务剖面、输入和未覆盖范围一起记录,这个结果才对后来者真正有用。

参考资料

EDS与PUS实践(六):用puslib交叉验证服务类型17的TC与TM

https://goko-son626.github.io/post/eds-pus-06-service-type-17-puslib-cross-validation.html

作者

GoKo Mell

发布于

2026-09-06

更新于

2026-09-20

许可协议

评论

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