EDS与PUS实践(四):用EDS描述服务类型17的四种消息
- 真正开始写服务类型 17 的 EDS XML 后才发现,难点不在 XML 语法,而在于区分标准固定内容和任务需要自行声明的内容。把公共包头、服务约束和业务字段分层后,文件会清楚很多。
上一篇已经把 CCSDS 主头、PUS-C 次头和 PUS 服务类型 17 的服务语义拆开。本文继续把这些规则落到 EDS 模型中,重点不是记 XML 标签,而是建立“标准条款 → 任务剖面 → 类型约束 → 生成结果”的对应关系。
本章要记录的实现过程
前三篇已经分别完成概念分层、生成流程和协议分析。现在才开始写服务类型 17 的 XML,是因为模型应当由需求推导,而不是由“能用哪些标签”反推需求。
本章按下面顺序完成建模:
- 把标准固定项、任务声明项和运行时行为分开;
- 确定 XML 文件之间的依赖层次;
- 先建立 CCSDS/PUS 公共包,再建立 ST17 抽象基类;
- 定义四种具体消息和 Application Process ID;
- 用 Interface 描述请求与报告关系;
- 从 resolved XML、生成头和数据库反查结果;
- 用尺寸、字段、PEC 和负向测试证明模型不是“只会生成”。
本章实现的是消息数据契约和接口元数据,不实现进程登记、连接测试回调和失败报告状态机。后者属于服务运行时,留到独立实现验证时处理。
写 XML 前先冻结任务剖面
标准允许任务声明若干字段,若不先冻结,XML 与 Python 对端很容易各用一套默认值。当前示例明确采用:
| 项目 | 当前选择 | 性质 |
|---|---|---|
| PUS 版本 | PUS-C,字段值 2 | 标准版本选择 |
| TC Source ID | 16 bit | 任务声明 |
| TM Destination ID | 16 bit | 任务声明 |
| TM 时间 | CUC 4+2,无线上 preamble | 任务声明 |
| Message Type Counter | 不存在 | 任务裁剪 |
| PEC | CRC-16-CCITT,2 byte | 任务选择/模型声明 |
| Application Process ID | 16 bit 大端枚举 | ST17 任务声明 |
只有 Service Type=17 和四个 Message Subtype 是服务标准固定值。上表其他宽度不能写成“PUS-C 对所有任务都规定如此”。
文件为什么要拆开
最小实验最初只有三份 XML:
1 | |
后面加入多个服务时,又把共用的 PUS-C 主头/次头抽到 pus_common.xml。目前更适合扩展的关系是:
1 | |
这样 APID、序列号、PUS 版本、Source ID 和 PEC 类型不会在每个服务里复制一遍。
每层只拥有自己的变化原因
base_types.xml 只因基础标量约定变化而改变;ccsds_spacepacket.xml 只因空间包公共结构变化而改变;pus_common.xml 保存 PUS-C 任务剖面;st17.xml 只描述测试服务。这样增加 ST03 不需要复制或修改 ST17,调整 ST17 进程枚举也不会碰公共包头。
文件拆分不是为了目录好看,而是让依赖方向单向:上层可以引用下层,下层不应反向知道某个服务。若 pus_common.xml 出现 Tc17_1,说明公共层已经被具体服务污染。
XML 包名比文件名更关键
EDS 引用通常使用 PACKAGE/Type 这样的逻辑名称。文件名用于构建系统发现输入,Package name 则参与类型命名空间。重命名文件却不改包名,与改包名却不更新引用,影响不同。
排查“类型找不到”时应同时确认:文件是否进入 EdsTool 输入列表、Package 名是否正确、引用路径是否与包名一致、依赖文件是否在解析阶段可见。
为什么服务专属枚举不放进 common
Application Process ID 只被 ST17 使用,并且不同任务可能采用不同列表。把它放进 common 会让其他服务无意义地依赖 ST17 任务选择。除非多个服务真的共享同一任务级应用过程标识类型,否则优先留在 st17.xml。
先定义服务类型 17 的公共基类
TC 的公共约束可以写成一个抽象容器:
1 | |
TM 也有对应的 TmSt17Base。具体消息只需要继续约束消息子类型号,不必重复整套公共字段。
这种继承不仅减少 XML,也让 EdsLib 能通过约束识别派生类型。
约束如何形成类型判别链
以 TC[17,3] 为例,类型身份由多层约束共同组成:
1 | |
运行库从基类对象读取约束字段时,可以逐层识别更具体类型。abstract="true" 表示 TcSt17Base 只是分类和复用节点,不应作为完整可发送消息。
哪些字段适合做 ValueConstraint
只有“选择了这个类型就必须恒定”的字段才适合做约束,例如方向、协议版本、服务号和消息子类型。APID、序列计数和 Source ID 往往随任务实例或每个包变化,不应为方便初始化而错误固定。
约束过少会让派生类型无法唯一识别;约束过多会把本应运行时变化的字段写死。判断标准是:这个值是类型身份,还是消息实例数据?
17.1 和 17.2 为什么没有业务字段
TC[17,1] 是 are-you-alive 请求,TM[17,2] 是响应。按照 41C 的数据结构,它们没有 application/source data。
“没有业务字段”并不等于空包。完整线包仍然包含:
1 | |
TC[17,1] 的具体类型只需要增加 Message Subtype 约束和包尾 CRC:
1 | |
“省略 application data”要体现在模型结构里
最直接的表达是具体类型不增加普通 EntryList,只继承公共头并增加 Trailer。不要为了让结构看起来完整,加入一个长度为 0 的占位字段或保留字节;那会改变 packed layout,独立实现也会把它当真实应用数据。
对当前剖面,TC[17,1] 的长度可手算为:
1 | |
TM[17,2] 虽然同样没有 source data,但 TM 次头包含 Destination ID 和时间,所以不能照搬 TC 的总长度。无正文只说明服务专用数据为零,不说明 TC/TM 总包同长。
Trailer 为什么属于具体消息
公共基类不带 PEC,可以让每个具体消息明确决定是否存在错误控制,并避免基类与派生类型重复追加。当前四种 ST17 消息都在具体类型末尾声明同一 ErrorControlEntry,这是任务剖面的选择。
17.3 为什么必须有进程 ID
TC[17,3] 要求对指定应用过程做连接测试,因此业务数据不能省略。当前任务把它声明成 16 位枚举:
1 | |
然后在 TC[17,3] 和 TM[17,4] 中都加入:
1 | |
标准规定它是任务声明的枚举,并没有替所有任务决定 16 位。选择 16 位是当前数据契约的一部分,换任务时可以调整,但发送端、接收端和独立参考实现必须保持一致。
TC 和 TM 必须引用同一个类型
若 TC[17,3] 使用 16 bit 而 TM[17,4] 另定义一个 8 bit 类型,两个文件都可能通过 Schema,服务却无法“原样返回同一标识”。让两者引用同一个 ApplicationProcessId,编译期模型就能防止宽度漂移。
枚举表与可测试列表不是一回事
XML 枚举表定义线上值的符号含义;服务运行时注册表定义当前可以测试的目标及回调。DemoProcess2=2 存在于枚举中,不代表进程 2 当前已注册或连接正常。
因此解包 TC[17,3] 时,值 2 可以是格式合法的枚举;服务执行时仍要查注册表。未登记目标的失败属于业务有效性判断,不应让 EdsLib CRC/结构解包承担。
未声明枚举值怎样处理要显式决定
数据类型层可通过范围或枚举约束表达允许值,但不同工具对未知枚举的解析和显示策略可能不同。服务层无论如何都要把“目标是否在可测试列表”作为权威判断,并返回标准要求的执行阶段失败报告。
接口关系也写进 XML
数据类型说明“包长什么样”,接口则说明哪条请求对应哪条报告:
1 | |
EdsLib 的 IntfDB 可以查询这些关系,但它不会替应用调用 UDP,也不会自动执行连接测试。
为什么仍值得声明 Interface
固定示例可以直接在 C 代码里写 Type ID,但当服务增多后,通用工具希望按 Test/AreYouAlive 找到请求和报告类型。Interface 把这种关系保存在可查询元数据中,避免工具维护第二张手写映射表。
它还可以作为模型审查点:若 OnBoardConnectionTest 的报告误指向 Tm17_2,即使四个数据类型各自正确,接口关系仍然错误。IntfDB 让这类错误能够被程序化检查。
Interface 不是函数调用描述的全部实现
Command 中的 mode="in"/mode="out" 表达参数方向,不等于 C 函数原型,也不承诺同步调用。PUS 请求与报告可能跨网络、异步到达。应用仍需建立关联、队列、超时和路由机制。
写完 XML 后怎样自查
目前使用的检查顺序:
- sedstool 能否解析全部引用;
- 生成的 packed bit size 是否符合手工计算;
- 初始化后服务号、消息子类型号、TC/TM 方向是否正确;
- 17.1/17.2 是否没有多余业务字节;
- 17.3/17.4 是否都有相同宽度的进程 ID;
- 长度字段能否由 EdsLib 自动填写;
- PEC 是否随报文内容变化;
- 改坏约束或 CRC 后是否会解包失败。
自查要从静态证据走到动态证据
可以把八项检查分为四层:
- 解析层:输入文件、引用、Schema、resolved XML;
- 生成层:C 类型、Type ID、数据库表项和尺寸;
- 运行层:Initialize、Pack、Unpack、字段输出;
- 对照层:手算长度、golden vector、独立实现和负向包。
只跑最外层脚本并看到 PASS,无法知道前面哪项真正被检查。一个好的测试日志应至少显示类型名、packed bits、完整 hex、关键字段和坏包拒绝结果。
四种消息都要单独检查
17.1 和 17.2 能发现 TC/TM 公共头差异;17.3 和 17.4 能发现业务枚举位置与方向差异。只测 17.1,无法证明 TM 时间字段正确,也无法证明 Application Process ID 已进入模型。
先做一张“标准要求—模型位置”对照表
写 XML 前先把每条要求放到正确层次,可以避免把所有字段都塞进 st17.xml:
| 要求 | 应放位置 | 原因 |
|---|---|---|
| 主头 6 字节与字段位宽 | ccsds_spacepacket.xml |
所有空间包共用 |
| Packet Type、APID、序列、长度 | CCSDS 公共类型 | 与具体 PUS 服务无关 |
| PUS 版本、ACK、Source/Destination ID | pus_common.xml |
多个 PUS 服务共用 |
| Service Type 固定为 17 | 服务类型 17 的抽象基类约束 | 四种消息共用 |
| Message Subtype 1/2/3/4 | 四种具体消息约束 | 用于区分消息类型 |
| 17.3/17.4 进程 ID | 两个具体消息正文 | 17.1/17.2 不允许有该字段 |
| 请求与报告对应关系 | 服务类型 17 的 Interface | 描述接口,不实现行为 |
| 可测试进程列表与成功判据 | 服务实现代码/配置 | 属于运行时行为,不是线格式 |
这张表也适用于其他服务:先区分公共线格式、服务固定约束、消息正文和运行时行为,再开始写类型。
为每条表项指定证据
“主头 6 byte”可以由 CCSDS 标准与 packed size 证明;“17.3 有进程 ID”可由 8.17.2.3、resolved XML 和生成类型证明;“未登记目标返回开始执行失败”必须由服务测试日志证明。把证据类型写在设计表中,能够防止用 XML 截图代替行为验证。
公共包类型怎样组合
一个可复用的公共模型通常包含三层:
1 | |
方括号中的字段必须按任务剖面决定。早期最小模型保留了 16 位 Message Type Counter;另一份合并后的剖面将它裁剪为不存在。二者都不能孤立地称为“唯一 PUS-C 格式”,但同一条交叉验证链路两端必须选同一种。
公共头也要避免重复定义 CCSDS 类型
VersionId、APID、Sequence Flags、Sequence Count 和 Length 等标量应引用 CCSDS 包已有类型;PUS common 只组合它们并增加 PUS 次头字段。这样空间包位宽有单一来源,多个服务不会各自维护 11 bit APID。
TC 与 TM 公共次头不要强行合并
TC 有 ACK Flags 和 Source ID,TM 有 Time Reference Status、Destination ID 和时间等任务字段。为了“少一个类型”把二者做成同一结构,通常会引入无意义字段或条件布局。共享基础标量即可,TC/TM 容器应分别清晰表达各自线格式。
长度字段为什么适合由模型计算
CCSDS Packet Data Length 是“整包字节数减 7”。如果业务代码先打包正文,再手工回写长度,容易出现三个问题:
- 修改正文后忘记同步长度;
- 把整包长或数据域长直接写入;
- CRC 先于长度计算,导致 PEC 覆盖的是错误内容。
EDS 的 LengthEntry 能把长度与容器最终尺寸关联起来。完整打包会先完成长度等特殊字段,再完成依赖整个对象的 Error Control。具体 XML 标定表达式需要结合工具链的换算方向核对,不能只凭常数名字猜测;最可靠的检查仍是对 13 字节包验证长度字段等于 6。
为什么仅看公式名字不够
LengthEntry 的校准表达式可能描述“字段值到实际长度”的正向关系,工具在打包时再做逆向求值。看到常数 7 不能仅凭直觉判断应加还是减,应结合生成数据库语义和实际包验证。
最稳妥的三个断言是:
1 | |
若这三条同时成立,才能证明模型、打包和接收端解释一致。
长度与可变消息的后续问题
ST17 当前四种消息都是固定长度,因此 CompleteObject API 足够。其他 PUS 服务可能包含可变数组或可选数据,需要 VarSize API 与更复杂的 LengthEntry 关系。不要把固定长度实现直接推广到所有服务。
PEC 应放在具体消息的 Trailer
当前模型在具体 TC/TM 类型末尾声明 ErrorControlEntry。这样 CRC 自然覆盖此前已编码的主头、次头和正文,同时 PEC 自身不参与输入。
把 PEC 定义成普通可写 uint16 会留下两个隐患:应用可能忘记计算,解包也不一定执行错误控制验证。ErrorControlEntry 的意义正是告诉运行库“这是特殊字段,不是普通业务参数”。
CRC 初始化是运行时前置条件
应用启动时必须调用 EdsLib_Initialize()。EdsLib 头文件明确说明 DataTypeDB 初始化会建立错误控制算法需要的查找表。漏调时,XML 仍可生成、普通字段也可能打包,但 PEC 结果没有可靠保证。
PEC 变化应成为测试断言
改变 APID、序列计数、Source ID 或 Application Process ID 后,PEC 都应随受保护字节变化。测试可以固定其他字段,只改一个值,确认新包仍能 Complete Unpack,而把旧 PEC 强行放回时会失败。
不要在 Pack 之后再改长度或正文
Complete Pack 已经按照最终内容计算 CRC。之后对 packed buffer 的任何受保护 byte 修改,都会使 PEC 失效。若测试要构造合法变体,应修改 native object 后重新 Pack;若要测试坏包,才在 Pack 后故意翻 bit。
17.3/17.4 的进程 ID 要满足四个一致
若任务选择 16 位大端枚举,至少检查:
- TC[17,3] 和 TM[17,4] 引用同一个 EDS 类型;
- puslib 或另一独立实现也按 2 字节大端解释;
- 成功响应原样返回请求中的值;
- 未声明枚举值的处理策略清楚。
标准要求目标标识是任务声明的枚举,但服务实现还必须维护“可测试进程列表”。枚举中存在一个值,不自动意味着该进程当前可测试;线格式合法性与运行时可用性是两个判断。
再增加两个行为一致性
除了文中四点,完整服务还应检查:
- 未登记 ID 产生 TM[1,4],而不是伪造 TM[17,4];
- 已登记但测试失败产生 TM[1,8],而不是静默超时。
前四点偏向数据契约,后两点属于服务状态机。把它们放在同一测试计划中,但不要假装都由 XML 完成。
Interface 声明能带来什么
Interface 中的 Command 把请求和报告类型放在同一语义入口下。配合 IntfDB,通用程序可以查询:
Test接口有哪些命令;AreYouAlive的输入类型是 TC[17,1];- 对应输出类型是 TM[17,2];
OnBoardConnectionTest的参数/返回类型是什么。
它不会创建线程、打开 UDP、调用回调或等待响应。Interface 是可查询的接口元数据,不是 RPC 框架。把这一点弄清后,就不会期待“XML 写了 Command,服务就能自动运行”。
什么时候 IntfDB 值得进入目标系统
若目标程序编译期就固定调用四个消息类型,直接使用生成宏最小;若需要按接口名加载服务、生成命令帮助或动态发现参数,IntfDB 才带来明显收益。是否链接 IntfDB 应由产品需求决定,而不是因为 XML 中存在 Interface 就强制带入。
从生成物反查模型是否落地
生成完成后,建议依次检查:
*_eds_datatypes.h中是否生成四种具体 native 类型;*_eds_defines.h中是否存在相应类型索引;- DataTypeDB 中 17.3/17.4 的 packed size 是否比 17.1/17.2 多 16 bit;
- DisplayDB 中是否能按完整名字查到类型和
ApplicationProcessId; - IntfDB 中两个 Command 是否分别关联正确请求/报告;
- resolved XML 中的基类、约束和引用是否与源模型一致。
若生成头里字段正确但 packed size 不对,应优先看编码和容器布局;若类型根本没生成,再回到输入源清单、包名和引用解析阶段。
生成物审查不等于手工修改生成物
审查的目的是验证模型被工具正确解释。发现字段或尺寸错误后,修改源 XML、任务配置或生成插件,再重新生成。直接改 *_eds_datatypes.h 只会让下一次构建覆盖,并造成头文件与数据库不一致。
用 TypeInfo 做尺寸表
可以写一个很小的工具,对四种类型调用 GetTypeInfo() 并打印 native bytes 与 packed bits。预期关系比绝对值更重要:TC[17,3] 比 TC[17,1] 多 16 bit,TM[17,4] 比 TM[17,2] 多 16 bit;TC 与 TM 的绝对长度因次头不同而不同。
这张表若与手算一致,说明业务字段确实进入 packed layout,而不是只出现在 native struct 中。
XML 建模中最常见的四个反模式
把 APID 当成 17.3 进程 ID。 两者层级不同,前者在主头,后者在正文。
给 17.1 添加目标进程参数。 标准规定 TC[17,1] 无参数;带目标的是 TC[17,3]。
每个服务复制完整包头。 短期看文件自包含,长期会让字段宽度、时间格式和 CRC 定义发生漂移。
只检查 XML 是否能生成。 两端共同使用错误模型时仍可往返成功,必须加入手算尺寸、已知向量、坏包和独立实现测试。
把任务选择写成标准事实。 16 bit Process ID、CUC 4+2 和无 Message Type Counter 都是当前剖面,不是 PUS-C 对所有任务的固定格式。
在公共层加入服务专属字段。 这会让其他服务被迫依赖 ST17,并使后续修改影响范围扩大。
用 Interface 代替服务实现。 元数据只能描述关系,不能执行进程测试或产生失败报告。
扩展到其他服务时可以复用什么
增加新服务时通常保留 base_types.xml、ccsds_spacepacket.xml 和 pus_common.xml,新增 stXX.xml:
- 建立该服务的 TC/TM 抽象基类并固定 Service Type;
- 为每种消息子类型建立具体派生类型;
- 按标准表格定义各自正文;
- 在 Interface 中声明请求、报告与参数关系;
- 生成后按同一套尺寸、约束、PEC 和独立实现流程验证。
能够复用的是建模方法和公共协议层,不能机械复制的是每个服务的业务字段、状态机与失败语义。
扩展新服务的最小工作单
例如加入 ST03 时,不先复制 st17.xml,而是重新从标准列出:消息清单、每种消息的数据结构、任务声明项、周期/按需行为、失败条件和与 ST01 的关系。然后只复用 CCSDS/PUS common、生成基础设施和 Pack/Unpack 测试框架。
这种方式表面比改服务号慢,实际能避免把 ST17 的无正文消息、进程 ID 和测试回调错误带进完全不同的服务。
一次可复现的模型验证流程
最终流程可以整理成:
1 | |
前 3 步证明生成链可复现,4~8 步证明 EDS/EdsLib 内部行为,9 步降低共同错误风险,10 步保证结果以后仍可解释。
本章小结
服务类型 17 的 EDS 模型不是“四个结构体加一个服务号”。它由公共空间包、任务 PUS 次头、ST17 抽象约束、四个具体消息、任务枚举、特殊长度/错误控制和接口关系共同组成。
最重要的建模原则是让每项要求只出现在正确层次:协议公共字段进入 common,类型身份进入 ValueConstraint,消息正文进入具体 EntryList,PEC 进入 Trailer,运行状态和成功判据留给服务代码。下一篇只把已经生成并验证的 packed TC 通过 UDP 送到另一进程,观察传输层是否保持字节不变。
这里可以放一张四种消息生成类型的截图:
图片预留:截取生成头文件中 TC[17,1]、TM[17,2]、TC[17,3]、TM[17,4] 四种类型,保存为
eds-pus-04-service-type-17-eds-model/st17-generated-types.png。
一个阶段性结论
XML 正确不等于只通过 Schema。真正有用的验证是:标准条款能在模型中定位、生成尺寸正确、固定字段正确、字节正确、坏包能拒绝,并且另一套实现能读懂。
下一篇先做一个更小的闭环:把 EdsLib 产生的 TC packed bytes 通过 UDP 交给另一个 C 进程,再由 EdsLib 解包。它还不执行服务类型 17 的请求—报告行为,但能把“模型与编解码”同“进程间传输”分开验证。
参考资料
EDS与PUS实践(四):用EDS描述服务类型17的四种消息
https://goko-son626.github.io/post/eds-pus-04-service-type-17-eds-model.html

