EDS与PUS实践(0):先弄清EDS和PUS是什么
- 第一次同时看到 EDS、SEDS、EdsLib、CCSDS、PUS、TC、TM 这些词时,很容易把它们理解成同一套协议里的不同模块。实际上,它们分属“数据描述”“线包结构”“应用服务”和“实现工具”几个层次。先把边界理清,后面写 XML、调用 EdsLib 和验证服务类型 17 才不会越学越乱。
这篇作为整个系列的第 0 篇,不急着写 XML,也不急着运行代码,只先回答四个问题:
- EDS 和 PUS 各自是什么;
- 它们分别解决什么问题,又不负责什么;
- 二者怎样与 CCSDS 空间包、EdsLib 和业务程序配合;
- 标准、源码和辅助资料应该去哪里找、按什么顺序看。
如果只想先记住一句话,可以记成:
PUS 规定一类遥控请求和遥测报告“应该表达什么”;EDS 把消息的类型、字段、编码与接口“机器可读地描述出来”;EdsLib 再依据 EDS 模型完成对象初始化、查询和编解码。
三者有关系,但不是同一件事。
为什么要先写这一篇
后面的实践会依次出现 XML、生成代码、运行时数据库、C 接口、CCSDS 空间包、PUS 服务类型 17、UDP 和 Python。若只沿着命令执行顺序学习,很容易得到一种错觉:好像把 XML 交给工具、再调用一个打包函数,PUS 服务就完成了。实际工程里,这些步骤分别回答不同问题:
- 标准要求系统提供什么能力;
- 任务选择了怎样的包格式和字段宽度;
- EDS 怎样把这种选择写成数据模型;
- EdsLib 怎样执行模型中的编码规则;
- 应用代码怎样实现请求对应的动作;
- 测试怎样证明两个独立实现理解了同一串字节。
如果不先建立这张地图,后面每看到一个新名词都会临时猜它属于哪一层。例如 APID、Application Process ID 和 EdsLib_Id_t 都可能以整数出现,但它们一个属于空间包主头,一个属于服务类型 17 的业务数据,一个属于本机运行时数据库。数值长得像,不表示语义相同。
因此第 0 篇的目标不是记忆所有缩写,而是形成三种判断能力:看到一个字段时知道去哪个标准找定义;看到一个函数时知道它在生成阶段还是运行阶段;看到一次“测试通过”时能说清它究竟证明了什么。
在这之前,需要具备什么基础
读完整个系列不要求先掌握 cFS,也不要求会写复杂 XML。真正需要的前置知识只有几项:
- 能看懂基本的 C 结构体、指针、数组和函数返回值;
- 知道一个字节是 8 bit,并理解大端与小端描述的是多字节值在线上的排列;
- 知道构建过程通常包含“生成源码—编译—链接”,而不是所有文件都在目标设备上动态解释;
- 知道网络或串口最终搬运的是字节,协议需要规定这些字节怎样解释。
后面会用 CMake 驱动生成与编译,但不会把 CMake 本身当成 EDS 的一部分;会用 UDP 建立两个进程之间的传输边界,但不会把 UDP 当成 PUS 标准规定的链路;会借助 puslib 做独立解析,但不会把某个 Python 库的默认配置当成 ECSS 的唯一格式。
先划清这些边界,可以避免为了理解 EDS 又同时钻进操作系统、网络栈和大型飞行软件框架,学习范围会小很多。
先看它们在整个通信链路中的位置
一条经过简化的星地数据链路可以画成这样:
1 | |
EDS 不单独占据其中某一层。它更像一份贯穿数据层和接口层的机器可读说明书:哪些字段组成一个对象、每个字段占多少 bit、采用什么字节序、某个具体消息固定哪些值、组件提供哪些接口,都可以进入 EDS 模型。
EdsLib 则是使用这份说明书的工具与运行库。它不会替业务程序决定“收到测试命令后到底测试什么”,但能把 C 对象按照描述打包成线包,也能把收到的字节恢复为对象并检查类型约束。
再加一条“生命周期”视角
上面的分层图回答“每一层负责什么”,还需要另一张图回答“一条消息怎样走完一生”:
1 | |
这条链上有四份容易被误认为“唯一真相”的东西:标准文本、任务剖面、EDS XML 和生成代码。更准确的关系是:标准给出可选范围和强制要求,任务剖面作出具体选择,EDS 把选择变成机器可读契约,生成代码则是契约在某一版工具链下的派生产物。真正应该人工维护的是任务定义和 XML,不是生成的 C 文件。
三条互相独立的正确性
整个系列始终围绕三类正确性展开:
| 正确性 | 核心问题 | 典型证据 |
|---|---|---|
| 模型正确性 | XML 是否准确表达标准和任务剖面 | resolved XML、尺寸、约束、生成数据库 |
| 编解码正确性 | 对象是否能稳定转换成规定线包 | 已知向量、CRC、负向用例、跨架构一致性 |
| 服务行为正确性 | 收到请求后是否执行正确动作并产生正确报告 | 状态机、正常/失败路径、独立实现交叉测试 |
同一个测试不能自动覆盖三者。XML 能生成不等于服务行为正确;自己打包再自己解包不等于线格式与标准一致;UDP 收到相同字节也不等于已经执行测试服务。后面每篇都会明确自己处于哪一层。
图片预留:绘制“应用功能 → PUS → CCSDS Space Packet → 链路”纵向分层图,并在右侧画一条跨越数据和接口层的 EDS 描述线,保存为
eds-pus-00-introduction/eds-pus-layers.png。
EDS:让数据和接口成为可处理的模型
EDS 是 Electronic Data Sheet,可直译为电子数据表。这个名字容易让人联想到 Excel,但这里的重点并不是“表格”,而是使用结构化、机器可处理的方式描述设备、软件组件及其数据接口。
在当前这条技术路径中,更准确的标准名称是 SEDS:
Spacecraft Onboard Interface Services—XML Specification for Electronic Data Sheets
它对应 CCSDS 876.0-B-1。SEDS 可以理解为 CCSDS 为 EDS 定义的一套 XML 语言和模型。
一份 EDS 通常描述哪些内容
按实际项目的使用范围,一份 EDS 可以包含以下几类信息:
| 内容 | 例子 | 解决的问题 |
|---|---|---|
| 基础数据类型 | 无符号整数、浮点数、枚举、字符串 | 一个值是什么类型 |
| 线上编码 | 位宽、字节序、符号、有效范围 | 一个值怎样变成 bit 和 byte |
| 组合类型 | 容器、成员、数组 | 一条消息由哪些字段组成 |
| 继承与约束 | 服务类型固定为 17、消息子类型固定为 1 | 怎样从公共包派生具体消息 |
| 接口模型 | 命令、参数、提供者、使用者 | 谁提供什么操作,参数是什么 |
| 元数据 | 名称、说明、单位、枚举显示 | 工具怎样查询和展示数据 |
并不是每个 EDS 文件都必须把这些内容全部写一遍。更合理的组织方式是分层复用:
1 | |
例如 uint16 只定义一次;公共 TC 包头只定义一次;服务类型 17 的四种消息再通过继承和约束复用这些定义。
EDS 不等于“XML 生成结构体”
把 XML 转成 C 结构体确实是 EDS 工具链的一项输出,但这只是表面结果。真正有价值的是,生成阶段还可以把以下信息保存到运行时数据库中:
- 类型尺寸与 packed 尺寸;
- 成员名称、偏移和嵌套关系;
- 整数、浮点数、字符串等编码规则;
- 基类、派生类型与固定值约束;
- 类型名称与运行时 Type ID 的对应关系;
- 组件接口与命令参数关系。
因此,程序不必为每一种消息都重新手写一套移位、掩码和大小端转换代码。它可以把 native C 对象交给 EdsLib,再由运行时数据库指导编解码。
EDS 的边界在哪里
EDS 可以描述一条命令含有哪些字段,却不会自动实现命令的业务行为。
例如 XML 能描述:
1 | |
但“根据 Application Process ID 找到被测进程”“怎样判断连接测试成功”“失败时产生哪种报告”,仍然要由服务实现代码完成。数据模型和业务逻辑是两层,不能因为 XML 能生成 C 代码,就认为服务也已经自动实现。
为什么不用手写结构体和移位代码
小型协议完全可以手写编码器。例如把一个 16 位整数拆成高低两个字节并不困难。问题出现在类型数量、继承关系和任务变更同时增长之后:
- 相同的公共头会散落在多个结构体和编码函数中;
- 字段由 8 bit 改为 16 bit 时,构造、解析、打印和测试都要同步修改;
- 本机结构体可能因 ABI 对齐产生填充,不能直接作为线包;
- 服务号、方向和版本等固定值靠人工重复填写,很容易产生合法但语义错误的包;
- 通用调试工具不知道成员名称、类型和偏移,只能为每种消息写专用代码。
EDS 的收益并不是消灭所有 C 代码,而是把重复的数据契约集中到一份模型中。应用仍然负责业务动作,但不必各自发明字段布局。生成器和运行库也能够围绕同一模型提供尺寸查询、类型识别、显示和编解码。
EDS 中“类型”比 C 语言类型多了什么
一个 EDS 整数类型除了说明它是整数,还可以描述位宽、符号、字节序、范围、单位和显示方式。一个容器除了列出成员,还可以表达基类、派生类型、约束、长度项和错误控制项。一个接口除了列出参数,还能把命令与输入输出类型建立关系。
例如服务类型 17 的公共 TC 类型可以约束 PacketType=TC、PusVersionId=2、ServiceType=17。具体 TC[17,1] 再增加 MessageSubtype=1。这些值不是某次运行临时填入的业务数据,而是“这个类型之所以是这个类型”的组成部分。把它们写成约束后,初始化和派生类型识别都能使用这份信息。
数据模型不是服务状态机
EDS 擅长静态结构,PUS 服务行为往往包含时间顺序和条件分支。以 TC[17,3] 为例,数据模型可以保证包中存在一个 Application Process ID,却不知道该 ID 当前是否登记、进程是否响应、何种判据代表连接成功。后者需要运行时表、回调函数和服务状态机。
这一边界也决定了测试方法:打包/解包测试验证字段与编码;服务单元测试验证分支和报告;跨进程测试验证两个实现能否通过真实字节连接。把三者拆开,错误才容易定位。
EdsLib:EDS 的工具链与运行时实现
NASA EdsLib 是一套面向 CCSDS EDS 的开源实现。仓库中的几个主要部分可以先这样理解:
| 目录/组件 | 主要职责 | 运行在哪里 |
|---|---|---|
tool/、sedstool |
读取 XML、建立 DOM、执行生成脚本 | 构建主机 |
edslib/eds/ |
从已解析模型生成 C/H 和数据库源码 | 构建主机 |
edslib/fsw/ |
对象初始化、类型查询、打包、解包等 C 运行库 | 主机或目标系统 |
cfecfs/ |
面向 cFE/cFS 的额外适配与工具 | 特定集成环境 |
这里最重要的边界是:
1 | |
目标系统通常不需要携带 XML、Lua 和 sedstool。XML 在构建主机上先生成 C/H,之后再用本机或交叉编译器编译运行库、生成数据库和应用程序。
另外,EdsLib 可以独立使用。cfecfs/ 说明它能够与 cFE/cFS 集成,不代表 EdsLib 必须依赖 cFS。若当前目标只是验证自己定义的消息,直接使用 edslib/fsw/ 的运行库就足够了。
构建时和运行时为什么必须分开理解
sedstool 需要 XML 解析、Lua 处理和代码生成环境,它面对的是“模型”。目标程序面对的是已经编译好的类型与数据库,它面对的是“对象和字节”。这一区分对嵌入式系统尤其重要:
1 | |
目标板并不需要理解 XML,也不必安装 Lua。它只需要能够编译和链接 EdsLib Runtime 以及生成数据库。反过来,主机上生成的目标数据库源码可以跨平台重新编译,但主机的 .o 文件不能直接拿到 ARM 上链接,因为目标 ABI 已经不同。
packed 线格式则应该由 EDS 明确控制。只要两端采用同一任务剖面,一个小端主机和一个大端目标都应产生相同的线上字节。后续 Pack/Unpack 实践正是在验证这层隔离。
运行时数据库不是普通业务数据库
这里的 DataTypeDB、DisplayDB 和 IntfDB 不是 SQLite,也不是保存运行日志的数据库。它们是由 EDS 生成并编译进程序的元数据表:
- DataTypeDB 保存类型布局、编码、尺寸、成员、继承和约束,编解码依赖它;
- DisplayDB 保存类型名、成员名和枚举显示信息,便于按名字查询和打印;
- IntfDB 保存组件、接口、命令与参数之间的关系,便于动态发现接口。
固定类型、资源受限的程序可以只保留编解码必需部分;通用命令行工具则可能需要名称和接口数据库。是否链接某个数据库,是功能与资源之间的工程选择,不是“用了 EDS 就必须全部带上”。
PUS:统一遥控请求和遥测报告的应用服务
PUS 是 Packet Utilization Standard。当前常用的标准版本是 ECSS-E-ST-70-41C,标准名称为 Telemetry and telecommand packet utilization。
它关心的是航天器远程监视和控制中,遥控包与遥测包应该怎样被使用。标准定义了一组服务,并规定这些服务中的请求、报告、消息结构和行为要求。
TC、TM、服务类型和消息子类型
- TC(Telecommand):遥控。通常表示从服务使用者发给服务提供者的请求;
- TM(Telemetry):遥测。通常表示状态、结果、事件或数据报告;
- Service Type:服务类型,说明消息属于哪类功能;
- Message Subtype:消息子类型,说明它是该服务中的哪一种请求或报告。
因此 TC[17,1] 的含义不是“编号为 17.1 的普通包”,而是:
1 | |
类似地,TM[17,2] 是服务类型 17 的消息子类型 2 遥测报告。
PUS 包含哪些服务
ECSS-E-ST-70-41C 提供的是可按任务裁剪的服务集合,不要求每个应用过程实现全部服务。常见服务包括:
| 服务类型 | 名称 | 典型用途 |
|---|---|---|
| 1 | Request verification | 报告一条 TC 的接收、开始、进展、完成或失败 |
| 3 | Housekeeping and diagnostic data reporting | 周期或按需报告参数集合 |
| 5 | Event reporting | 上报信息、异常、故障等事件 |
| 8 | Function management | 请求执行任务定义的功能 |
| 17 | Test | 存活测试和应用过程连接测试 |
| 20 | Parameter management | 读取或修改参数值 |
这张表只是建立直觉,不是标准内容的替代品。实际实现时还要查看所选服务的完整要求、允许的消息、参数类型和成功/失败路径。
PUS 不是消息编号清单
只记住“17.1 发请求、17.2 回响应”仍然不够。PUS 条款通常同时包含两类内容:
- 系统要求:服务提供哪些能力、何时拒绝、怎样判定成功、产生什么通知;
- 接口要求:对应 TC/TM 的服务号、消息子类型和应用数据结构。
服务类型 17 的系统要求位于 6.17,消息结构位于 8.17。只看第 8 章可以写出长得正确的包,却可能漏掉“不可测试进程要在开始执行阶段失败”“连接判据失败要在完成执行阶段失败”等行为。只看第 6 章又无法确定线上字段。后面建模时会把这两部分分别落到 XML 和服务代码。
一个服务和一个应用过程是什么关系
PUS 以应用过程之间的请求和报告为背景。一个应用过程可以提供某种服务,另一个实体作为服务使用者发起请求。标准规定交互语义,但实际软件怎样映射进程、线程、任务或组件,由任务架构决定。
这也是为什么 TC[17,3] 的 Application Process ID 不能在没有任务定义时随意解释成操作系统 PID。它是任务声明的标识,值域、宽度和到实际组件的映射都必须在任务层确定。
PUS 服务类型 17 为什么适合入门
服务类型 17 的消息少、交互直观,又能覆盖 TC、TM、公共包头、应用数据和响应关系,因此适合用来走通第一条完整链路。
它包含两组交互:
1 | |
第一组回答“这一端是否还活着”;第二组针对任务声明的 application process 做连接测试。具体什么条件算连接正常,属于任务定义的行为,不是 EDS 编解码器可以代替的。
从学习角度看,这四种消息刚好形成递进:
- 17.1/17.2 没有服务专用数据,适合先检查公共主头、次头、长度和 PEC;
- 17.3/17.4 增加一个任务声明的枚举,适合观察应用数据怎样改变长度和 CRC;
- 17.3 存在拒绝、执行失败和成功三条路径,能引出服务类型 1 的请求验证报告;
- 请求与响应方向相反,能验证 TC/TM 两类次头是否都建模正确。
它足够小,可以把每个字节解释清楚;又不只是“编码一个结构体”,能够暴露模型、编解码和服务行为之间的边界。
CCSDS 空间包和 PUS 是什么关系
PUS 不会凭空重新定义一套最底层包格式。PUS TC/TM 使用 CCSDS Space Packet。空间包协议应查阅 CCSDS 133.0-B-2。
一个用于当前学习范围的简化视图是:
1 | |
几个容易混淆的点:
- CCSDS 主头和 PUS 次头不是二选一。 PUS 包通常同时具有空间包主头和 PUS 包次头;
- APID 不等于 PUS 服务类型。 APID 用于识别应用过程或包流,服务类型表示请求/报告的功能类别;
- PUS 没有规定 UDP。 UDP 只是开发验证时承载完整线包的一种方便方式;
- 空间包长度字段有自己的编码规则。 不能简单把它当作整个缓冲区的字节数;
- 具体字段宽度和可选项要结合标准与任务裁剪。 示例代码不能反过来定义标准。
为什么需要“任务剖面”这层
标准往往允许任务选择字段宽度、时间格式、错误控制和可选项。真正互操作时,双方不仅要说“都支持 PUS-C”,还要对齐一份更具体的约定,例如:
1 | |
这份约定不是 ECSS 对所有任务的统一答案,而是某一条测试链路的输入。EDS XML、C 程序和 puslib Policy 必须同时服从它。若其中一端多出 2 字节 Message Type Counter,后续所有字段都会错位,即使双方都声称“实现了 PUS-C”。
从协议栈角度看封装
应用首先构造一个 PUS 消息,PUS 消息作为 CCSDS Space Packet 的数据域,空间包再被下层传输承载。开发环境用 UDP 时,完整空间包通常原样作为 UDP payload;真实系统也可能再封装进传输帧、总线帧或专用链路。
每一层都有自己的长度和校验语义。CCSDS Packet Data Length 不等于 UDP Length;PUS PEC 也不等于 UDP checksum。下层校验成功不能替代上层 PEC,上层的服务号也不能指导 IP 路由。清楚封装关系后,抓包和日志才不会把不同层的字段混为一谈。
EDS 和 PUS 到底怎样配合
可以把它们的分工压缩成一张表:
| 问题 | 主要由谁回答 |
|---|---|
| 需要实现哪些遥控请求和遥测报告 | PUS 标准 + 任务裁剪 |
| 收到请求后应该做什么、何时报告成功或失败 | PUS 服务逻辑 + 应用代码 |
| 包里有哪些字段、各字段怎样编码 | CCSDS/PUS 标准 + EDS 模型 |
| XML 怎样生成 C 类型和运行时数据库 | sedstool / EDS 生成插件 |
| C 对象怎样打包为字节或从字节恢复 | EdsLib Runtime |
| 字节怎样到达另一端 | UDP、总线、串口或实际通信栈 |
以 TC[17,3] 为例,一条完整路径可以是:
- ECSS PUS 说明它是服务类型 17 的连接测试请求,并规定需要表达的服务数据;
- EDS XML 将公共空间包头、PUS TC 次头和消息参数建模为具体类型;
- sedstool 生成 C 类型、Type ID 宏和 DataTypeDB 源码;
- C 程序填写任务需要的 Application Process ID;
- EdsLib 按数据库规则把对象打包成连续字节;
- 测试程序通过 UDP 把这一个完整线包送给另一进程;
- 服务实现解析请求,执行连接测试并产生
TM[17,4]; - 接收端再用 EdsLib 解包并检查字段、类型约束和响应对应关系。
这条链路里没有任何一层能单独证明全部正确:
- XML 能生成,只能说明模型通过了工具的基本检查;
- EdsLib 能打包和解包,只能说明本模型的编解码路径可运行;
- 自己打包再自己解包,可能让同一个错误互相抵消;
- UDP 收发成功,只能说明字节被送达;
- 独立实现交叉解析成功,才能进一步降低共同实现错误的风险;
- 真正的服务行为仍需检查请求、响应和失败路径。
把需求逐步翻译成实现
实现一个服务时,可以沿着以下问题逐步翻译,而不是直接打开 XML 编辑器:
- 标准要求哪些能力,哪些是最小必选,哪些要声明后才提供;
- 每种请求和报告的方向、服务号、消息子类型及应用数据是什么;
- 当前任务选了哪些可选字段和具体编码;
- 哪些字段属于所有 CCSDS 包、所有 PUS TC/TM、某一服务、某一消息;
- 哪些内容可由 EDS 静态描述,哪些必须留给运行时服务代码;
- 正常路径和每个失败阶段应该产生什么证据;
- 有没有独立实现或已知向量可以避免“自己验证自己”。
这七个问题分别产生服务需求表、任务剖面、EDS 类型、服务状态机和测试矩阵。最后的 XML 只是其中一个交付物。
用一次修改检验分层是否合理
假设 Application Process ID 从 16 bit 改为 8 bit。合理分层下,主要改动应该集中在任务类型声明、独立实现策略和对应测试向量;公共 CCSDS 主头、服务类型 17 的消息号、UDP 传输代码和业务回调接口不应全部重写。
再假设 UDP 换成 RTOS 消息队列。合理分层下,EDS 模型、EdsLib Pack/Unpack 和服务状态机不变,只替换传输适配层。如果换一种传输就必须修改 XML 中的 PUS 字段,说明层次已经耦合。
用一个最小案例把所有角色串起来
假设地面需要确认某个星上测试服务仍可响应。需求层先选择 PUS 的 are-you-alive 交互,而不是自己发明一个字符串 PING:
1 | |
任务随后决定这条链路采用 APID 321、TC Source ID 42、PUS-C、CRC-16-CCITT;TM 采用 Destination ID 42 和 CUC 4+2 时间。这些数值与可选项形成任务剖面,不是服务类型 17 对所有系统的固定答案。
EDS 模型把这些选择分层表达:CCSDS XML 定义 6 byte 主头,PUS common 定义 TC/TM 次头,ST17 抽象基类固定 Service Type=17,TC[17,1] 与 TM[17,2] 再分别固定 subtype 1 和 2。因为二者没有业务数据,具体类型只需要公共头、约束和包尾 PEC。
sedstool 从 XML 生成 native C 类型和 DataTypeDB。发送程序取得 TC[17,1] Type ID,调用 Initialize 填入固定约束,再写 APID、序列号和 Source ID。Complete Pack 依据数据库输出 13 byte:
1 | |
这 13 byte 可以作为 UDP payload 送到另一进程。若另一端只是用同一 DataTypeDB 解包,只证明传输后模型仍自洽;若另一端由不读取 XML 的 puslib 解析、执行服务并生成 TM[17,2],C 端再用 EDS 解包 TM,证据才扩展为两个实现的双向一致。
最后仍要准确描述结果:收到 17.2 说明承载测试子服务的应用过程执行了最低限度功能,且请求和报告通信路径可用;它不证明所有星上应用、硬件和实时性能正常。
再把案例改成 TC[17,3]
若要测试另一个星上应用过程,消息改为 TC[17,3],并增加任务声明的 Application Process ID。假设它是 16 bit 大端枚举且目标值为 1,线包比 17.1 多出 00 01,总长从 13 byte 变为 15 byte,Packet Data Length 与 PEC都随之变化。
服务运行时查询“可测试进程列表”:
- 目标未登记,在开始执行阶段产生 TM[1,4];
- 目标已登记但回调判定失败,在完成执行阶段产生 TM[1,8];
- 判据成功,产生 TM[17,4] 并返回同一 Process ID。
这里能清楚看到三层各自的工作:EDS 保证进程 ID 在线包中的结构,EdsLib 保证编码与完整性,服务代码保证列表、回调和失败阶段。任何一层都不能替代另外两层。
遇到陌生名词时用四问定位
后续继续阅读源码或标准时,可以先问:
- 它是在构建主机出现,还是目标运行时出现?
- 它描述本机对象、线上字段、接口关系,还是业务动作?
- 它由 CCSDS/ECSS 固定,还是由任务声明?
- 它的正确性应由生成检查、编解码测试、传输测试还是服务测试证明?
例如 TopicId 属于某些 cFE/MissionLib 集成环境的任务路由概念,不是 PUS 标准字段;EdsLib_Id_t 是本机数据库索引,不是线上 APID;UDP port 是测试传输配置,不进入 PUS PEC。用四问定位,比背诵越来越多缩写更可靠。
建议保留的学习产物
仅看懂一次终端输出很容易遗忘。围绕一个服务,最好持续维护下面几份小而明确的产物:
- 一张标准条款索引:系统要求、消息结构、公共包头和 CRC 分别在哪一节;
- 一张任务剖面表:所有可选字段、位宽、时间与 PEC 选择;
- 一张消息字段表:每种 TC/TM 的层次、偏移、长度和固定值;
- 一张模型映射表:标准字段对应哪个 XML 类型、成员或约束;
- 一组已知向量:固定输入、完整 hex、逐字段解释和版本信息;
- 一组负向用例:每次只破坏长度、固定值、正文或 PEC 中的一项;
- 一张验证边界表:每个测试证明什么、没有证明什么。
这些产物比复制构建命令更有长期价值。工具版本、目录和脚本可能变化,但只要数据契约和证据链仍清楚,就能迅速判断新实现是否保持兼容。
学习笔记中应避免的三种句式
第一种是“标准规定 Process ID 为 16 bit”,因为 41C 只规定这里是任务声明的枚举;16 bit 是当前选择。第二种是“UDP 测试证明 ST17 服务正确”,因为 C→UDP→C 只覆盖传输和同数据库解包。第三种是“XML 实现了连接测试”,因为 XML 只描述消息,成功判据由服务代码执行。
把句子改成“在当前任务剖面下选择……”“本测试验证到……层”“运行时服务负责……”以后,技术结论会准确很多,也便于其他读者复现。
最后还应记录标准版本、源码提交或版本号、构建工具版本与测试日期。相同名称的库可能改变默认 Policy,标准也可能有不同 Issue;没有版本上下文的“正确结果”,几个月后很难判断还能否复用。技术笔记的目标不是保存一次成功截图,而是让后来者能从同样输入得到同样结论,并知道输入变化后应重新检查哪一层。
这些概念不要再混在一起
EDS、SEDS、EdsLib
- EDS 是电子数据表这一类思想和模型;
- SEDS 是 CCSDS 876.0-B-1 定义的 XML 规范;
- EdsLib 是处理这类 XML 并在运行时使用生成数据库的一套具体开源实现。
类似于“C 语言”“ISO C 标准”“某个 C 编译器”之间有关联,但不能互相替代。
PUS、CCSDS 和 cFS
- PUS 是 ECSS 的遥测遥控包利用标准,定义服务及其消息语义;
- CCSDS Space Packet Protocol 定义空间包协议,是 PUS 包所依托的基础;
- cFS 是 NASA 的飞行软件框架,可以集成 EDS,也可以承载应用,但不是实现 PUS 或使用 EdsLib 的前提;
- cFE 是 cFS 的核心飞行执行环境,软件总线等概念属于这套框架,不应误认为是 PUS 标准字段。
EdsLib、puslib 和 PUSopen
- EdsLib 侧重依据 EDS 模型处理对象和编解码;
- puslib 是 Python 编写的 PUS 实现,可用于学习、测试和构造独立解析端;
- PUSopen 是另一套 PUS/CCSDS 软件实现及文档,其公开资料适合辅助理解协议分层和服务交互。
后两者是实现或学习材料,不是 ECSS 标准本身。判断某个字段或行为是否合规时,最终仍要回到标准文本和当前采用的任务裁剪。
资料应该从哪里获取
第一层:标准原文
这是确认名称、字段、约束和服务行为的最终依据。
- CCSDS Publications:CCSDS 官方出版物入口;
- CCSDS 876.0-B-1:SEDS XML 规范;
- CCSDS 133.0-B-2:Space Packet Protocol;
- ECSS Standards 与 ECSS Document Tree:ECSS 标准入口和文档关系;
- ECSS-E-ST-70-41C:PUS 标准页面及附件。
阅读标准时要先确认封面上的编号、Issue/Revision 和发布日期。网上的总结可能基于早期版本,不能只因为标题相似就混用。
第二层:实现源码和测试
标准告诉我们“必须怎样”,源码告诉我们“这个工具具体怎样做”。
- NASA EdsLib:观察 XML 处理、生成脚本、运行时接口与单元测试;
- NASA cFS-EDS-GroundStation:理解 EdsLib 与 cFS/MissionLib 结合后的命令构造和遥测解析;
- pxntus/puslib:观察 Python 中的 PUS packet 和服务实现,并作为独立验证端。
源码阅读最有价值的顺序通常不是从第一行看到最后一行,而是:README → 构建入口 → 公开头文件/API → 最小测试 → 具体实现。
第三层:辅助教程和产品文档
PUSopen 文档 对服务模型、协议分层和若干服务交互给出了较直观的说明,适合第一次建立整体图景。但它描述的是该产品所实现和选取的功能,不能把产品 API 或实现范围当成 ECSS 的全部要求。
博客、视频和代码片段也适合降低入门成本,但最好遵循一个原则:
教程用来找方向,源码用来理解实现,标准用来裁决对错。
怎样阅读几百页标准而不迷路
不需要从第一页顺序读到最后一页。围绕一个具体消息,阅读路径可以是:
- 在术语部分确认 request、report、application process 等概念;
- 在第 6 章找到该服务的能力、有效性和失败要求;
- 在第 8 章找到消息子类型和应用/源数据结构;
- 回到第 7 章确认公共 TC/TM 包字段和任务声明项;
- 查附录中的消息汇总和 CRC 定义做交叉核对;
- 最后对照实现源码,判断库的默认策略是否等于当前任务剖面。
记录时要把“标准明确规定”“标准允许任务声明”“当前示例作出的选择”三种句子分开。例如“Application Process ID 是枚举”来自 8.17;“它采用 16 bit 大端”是当前模型的任务选择;“值 1 映射到 DemoProcess1”则只是示例枚举表。混写后,读者会误以为示例值也是标准固定值。
怎样验证二手资料
看到一段教程或代码时,可以用四个问题快速筛查:
- 它依据的是 PUS-A、PUS-C 还是未说明版本;
- 它把任务可选字段写成固定字段了吗;
- 它展示的是标准消息,还是某个框架内部头;
- 它的测试是同实现往返,还是存在独立对照。
如果四项都没有说明,这份资料仍可用于找关键词,但不应直接成为数据契约。
推荐的阅读与实践顺序
不建议一开始就顺序通读几百页标准,也不建议直接复制一个大型示例。更高效的方式是围绕一条最小闭环逐步扩展。
第一步:只建立分层概念
先能回答以下问题:
- TC 和 TM 有什么方向和语义差异;
- CCSDS 主头、PUS 次头、应用数据分别在哪里;
- Service Type 和 Message Subtype 怎样标识消息;
- EDS 模型、EdsLib 运行库、业务服务代码分别负责什么。
这一步不要求记住所有字段值。
第二步:选一组最小消息
使用 TC[17,1] 与 TM[17,2] 建立第一个闭环,因为它们没有复杂的应用数据,却能覆盖请求、响应、TC、TM 和两种次头。
第三步:从线包反推 XML
先在纸上列出:
1 | |
再决定哪些是公共基类、哪些是派生类型约束、哪些是具体消息参数。这样写出来的 XML 会服务于线包,而不是为了“看起来像一个完整 XML”堆标签。
第四步:走通生成和运行时
观察 XML 怎样经过 sedstool 变成:
- C 类型定义;
- Type ID 与索引;
- DataTypeDB;
- 可选的 DisplayDB 和 IntfDB。
随后用 EdsLib 初始化、填写、打包、解包和检查一个对象。
第五步:加入传输和独立实现
把 packed bytes 通过 UDP 交给另一进程,并让 Python puslib 作为独立端处理。这里的重点不是 UDP,而是让两个实现对同一份线包达成一致。
第六步:再扩展消息和服务
加入 TC[17,3]/TM[17,4] 后,就能看到带应用参数的消息怎样建模。学会一个服务并不等于其他服务只需替换服务号;能复用的是工程方法,具体消息、状态机、时序和失败语义仍要逐项阅读相应标准章节。
怎样判断自己是否真正理解了
学习完成后,不妨脱离文章回答下面的问题:
- EDS、SEDS 和 EdsLib 分别指思想、标准还是实现?
- 为什么生成了 C 结构体仍然需要 DataTypeDB?
- 为什么不能把 native struct 直接通过 socket 发送?
- APID、服务号、消息子类型和
EdsLib_Id_t各属于哪一层? - TC[17,1] 为什么没有应用数据但整包并不为空?
- TC[17,3] 为什么需要 Application Process ID,它的位宽由谁决定?
- XML 能否决定连接测试成功?为什么?
- 一次 C→UDP→C 的往返证明了什么,又没有证明什么?
- 为什么还要让 puslib 这样的独立实现参与?
- 把 UDP 换成消息队列后,哪些层应该保持不变?
若能用自己的话回答,并能从标准、XML、生成数据库和运行日志中各拿出一份证据,才算形成了可迁移的方法,而不只是记住几个命令。
本篇的结论与下一步
本篇没有实现代码,但已经确定后面六篇的共同框架:先从标准与任务剖面得到数据契约,再用 EDS 表达契约,用 EdsLib 执行编解码,用服务代码实现行为,最后通过分层测试逐步增加证据强度。
下一篇从这条链路的最左端开始,具体追踪 XML 经过 sedstool 后为何不只得到结构体,还会生成类型索引和三个用途不同的运行时数据库。理解生成物的职责后,第二篇的 API 调用才不会像一组没有来由的函数。
本系列接下来怎样展开
这篇只提供地图,后面六篇按“模型 → 运行时 → 协议 → 服务定义 → 通信验证 → 交叉验证”逐步收窄:
- EDS与PUS实践(一):从EDS XML到C代码与运行时数据库:弄清生成阶段到底产生什么;
- EDS与PUS实践(二):用EdsLib完成对象打包与解包:掌握运行时最核心的对象与线包转换;
- EDS与PUS实践(三):从CCSDS空间包理解PUS服务类型17:建立空间包、PUS 次头和服务消息的分层;
- EDS与PUS实践(四):用EDS描述服务类型17的四种消息:把标准要求落到可生成的 XML 模型;
- EDS与PUS实践(五):用EdsLib与UDP验证服务类型17线包:检查 C 对象、线包和进程间传输;
- EDS与PUS实践(六):用puslib交叉验证服务类型17的TC与TM:使用独立实现降低自编自解的盲区。
最后留一张判断表
后面遇到问题时,可以先判断它属于哪一层:
| 现象 | 优先检查 |
|---|---|
| XML 找不到类型或引用 | EDS 命名空间、类型引用、生成顺序 |
| C 结构能编译但 packed bytes 不对 | EDS encoding、位宽、字节序、约束 |
| 服务号和消息子类型不对 | PUS 消息定义与派生类型约束 |
| APID、序列或长度不对 | CCSDS Space Packet 主头规则 |
| TC 能解析但没有正确 TM | PUS 服务逻辑和任务行为 |
| C 与 Python 各自通过、互相不通 | 两端采用的包头、长度、时间、CRC 或任务裁剪不一致 |
| UDP 收不到数据 | 地址、端口、进程启动顺序和传输代码,而不是 EDS 本身 |
把问题放回正确层次,排查速度通常会快很多。下一篇从 EDS XML 的生成链路开始,具体看一份模型怎样变成 C 代码和可供 EdsLib 使用的运行时数据库。
EDS与PUS实践(0):先弄清EDS和PUS是什么
https://goko-son626.github.io/post/eds-pus-00-introduction.html

