CCSDS(Consultative Committee for Space Data Systems)是一个国际组织,提供了一套标准和规范,用于处理航天数据系统。
包org.orekit.files.ccsds
提供了处理CCSDS消息的类。
该包按照CCSDS消息的层次结构进行分层子包组织,还包括一些实用的子包。下面的类图描述了这种静态组织结构。
org.orekit.files.ccsds.section
子包定义了所有CCSDS消息中通用的部分:Header
、Metadata
和Data
。它们都扩展了Orekit特定的Section
接口,用于在解析结束时进行检查。Metadata
和Data
被组合在一个Segment
结构中。
org.orekit.files.ccsds.ndm
子包定义了一个顶层抽象类Ndm
,代表导航数据消息。所有的CCSDS消息都扩展了这个顶层抽象类。Ndm
是一个包含一个Header
和一个或多个Segment
对象的容器,具体取决于消息类型(例如Opm
只包含一个段,而Oem
可能包含多个段)。
每个CCSDS消息类型都有相应的子包,其中包含每个官方发布的建议的中间子包:org.orekit.files.ccsds.ndm.adm.apm
、org.orekit.files.ccsds.ndm.adm.aem
、org.orekit.files.ccsds.ndm.cdm
、org.orekit.files.ccsds.ndm.odm.opm
、org.orekit.files.ccsds.ndm.odm.oem
、org.orekit.files.ccsds.ndm.odm.omm
、org.orekit.files.ccsds.ndm.odm.ocm
和org.orekit.files.ccsds.ndm.tdm
。每个子包包含与消息类型对应的逻辑结构,其中至少有一个##m
类表示一个完整的消息。由于某些数据在多个类型中是共用的,可能会有一些中间类以避免代码重复。这些类是实现细节,不在前面的类图中显示。如果消息类型具有逻辑块(例如OPM中的状态向量块、开普勒元素块、机动块),则每个逻辑块都有一个专用的类。
顶层消息还包含一些Orekit特定的数据,这些数据对于构建某些对象是必需的,但在CCSDS消息中不存在。例如,这包括IERS约定、数据上下文和ODM的引力系数,因为在这些消息中它有时是可选的。
这种组织结构是从Orekit 11.0开始引入的。在此之前,CCSDS的层次结构(包括头部、段、元数据和数据)没有在API中重现,而是使用了一个扁平的结构。
这种组织结构意味着希望访问原始内部条目的用户必须遍历整个层次结构。对于只允许一个段的消息类型,除了使用message.getSegments().get(0).getMetadata()
和message.getSegments().get(0).getData()
之外,还可以使用message.getMetadata()
和message.getData()
的快捷方式。如果相关,还提供了其他快捷方式来访问与Orekit兼容的对象,如下面的代码片段所示:
Opm opm = ...;
AbsoluteDate creationDate = opm.getHeader().getCreationDate();
Vector3D dV = opm.getManeuver(0).getdV();
SpacecraftState state = opm.generateSpacecraftState();
// 通过困难的方式获取轨道日期:
AbsoluteDate orbitDate = opm.getSegments().get(0).getData().getStateVectorBlock().getEpoch();
可以通过解析现有的消息或使用设置器从头开始创建消息来获取消息,从原始元素开始逐步构建,通过逻辑块、数据、元数据、段、头部和最后消息。
通过设置解析器来解析文本消息以构建某种类型的Ndm
对象。每种消息类型都有自己的解析器,但是单个ParserBuilder
可以构建所有类型的解析器。一旦创建,就可以使用解析器的parseMessage
方法并提供数据源来解析消息。它将返回解析后的消息,以前一节中所示的层次结构容器形式。
在构建ParserBuilder
时,需要预先设置用于构建某些对象但在CCSDS消息中不存在的Orekit特定数据。这包括例如IERS约定、数据上下文和ODM的引力系数,因为在这些消息中有时是可选的。
ParseBuilder
中的ParsedUnitsBehavior
设置用于选择在解析时如何处理消息中的单位,以符合CCSDS标准中指定的强制单位。
IGNORE_PARSE
表示完全忽略消息中解析的单位,数值将使用标准中指定的单位进行解释CONVERT_COMPATIBLE
表示检查消息中解析的单位与标准的维度兼容性,并在可能的情况下接受STRICT_COMPLIANCE
表示检查消息中解析的单位与标准的维度兼容性,并仅在它们相等时接受CCSDS标准在处理单位方面存在歧义。在多个地方,它们指出单位仅用于“信息目的”,甚至“通过[插入关键字]关键字列出的单位不会覆盖所选[插入类型]中指定的强制单位”。这意味着应该使用IGNORE_PARSE
以符合标准,并且可以默默接受指定错误单位的消息。其他地方指出,表格指定了“要使用的单位”,并且“如果显示单位,则它们必须与表格中指定的单位完全匹配(包括大小写)”。这意味着应该使用STRICT_COMPLIANCE
以符合标准,并且应该拒绝指定错误单位的消息并显示错误消息。通常在文件解析中的最佳实践是在解析时宽松,在写入时严格。由于逻辑上认为当消息明确指定单位时,这些单位是实际用于生成消息的单位,因此我们认为CONVERT_COMPATIBLE
是宽容的一个好折衷方案。因此,默认设置是将ParseBuilder
的行为设置为CONVERT_COMPATIBLE
,但用户可以根据自己的需求配置其构建器。Orekit中使用的单位解析器还具有丰富的功能,可以处理使用人性化Unicode字符编写的单位,例如km/s²或√km(而CCSDS标准将使用km/s**2或km**0.5)。
Orekit 11.0引入的一个变化是使用流畅的API(方法withXxx()
)逐步设置解析器的方式已经移动到顶级ParserBuilder
中,该构建器可以构建所有CCSDS消息的解析器。另一个变化是解析器是可变对象,在解析过程中收集数据。因此,它们不能在多线程环境中使用。推荐的使用解析器的方式是设置一个ParserBuilder
,并从每个线程中调用其buildXymParser()
方法,为每个消息专门分配一个解析器,并在使用后丢弃它。在单线程情况下,在parseMethod
返回后可以安全地重用在循环中使用的解析器,但是从构建器中构建一个新的解析器简单且开销很小,因此在单线程应用程序中仍然建议使用现有的ParseBuilder
为每个消息构建一个新的解析器。
解析器会自动识别消息是否采用键值对表示法(KVN)或扩展标记语言(XML)格式,并相应地进行适配。这对用户来说是透明的,并且适用于所有CCSDS消息类型。
要解析的数据是使用DataSource
对象提供的,该对象结合了名称和流打开器,并可以直接从这些元素、文件名或标准的Java File
实例构建。DataSource
对象延迟了文件的实际打开,直到调用parseMessage
方法,并在解析之后正确关闭文件,即使由于某些解析错误而中断解析。
自12.0版本以来,所有解析器都具有过滤功能。用户可以在ParserBuilder
中添加任意数量的过滤器,以在从DataSource
提取令牌并将其提供给解析器之间的时间内实时更改解析的令牌。此功能有几个用例。
OBJECT_ID
,这在CCSDS标准中是禁止的。可以通过设置一个过滤器来修复这些不符合规范的消息,该过滤器识别具有空值的OBJECT_ID
条目,并将其替换为设置为unknown
的值,然后将更改后的令牌传递回解析器。CCSDS_##M_VERS
条目中的值2.0替换为3.0,并设置第二个过滤器,将ORIGINATOR
条目替换为包含初始条目和一个额外的虚构MESSAGE_ID
条目的列表,从而将生成的ODM V3 MESSAGE_ID
添加到缺少它的ODM V2消息中。OemParser
和OcmParser
还有一个额外的特性:它们还实现了通用的EphemerisFileParser
接口,因此当星历可以从各种格式(CCSDS、CPF、SP3)中读取时,它们可以以更通用的方式使用。EphemerisFileParser
接口定义了一个parse(dataSource)
方法,该方法类似于CCSDS特定的parseMessage(dataSource)
方法。
由于解析器是根据解析的消息类型进行参数化的,因此所有解析器中的parseMessage
和parse
方法已经返回具有正确特定消息类型的对象。不再需要像Orekit 11.0之前的版本中那样对返回值进行强制转换。
以下代码片段展示了如何解析OEM文件,这里使用文件名创建数据源,并使用解析器构建器的默认值:
Oem oem = new ParserBuilder().buildOemParser().parseMessage(new DataSource(fileName));
通过使用特定的消息类型的写入器类,并使用对应于所需消息格式的低级生成器,可以编写CCSDS消息。对于键值表示法,使用KvnGenerator
,对于扩展标记语言,使用XmlGenerator
。
所有CCSDS消息都有一个对应的写入器,实现了CCSDS特定的MessageWriter
接口。该接口允许先写入已构建的消息,或者先写入头部,然后循环写入段。
星历类型的消息(AEM、OEM和OCM)还实现了通用星历写入器接口(AttitudeEphemerisFileWriter
和EphemerisFileWriter
),除了CCSDS特定接口外,还可以在非CCSDS数据构建星历数据时以更通用的方式使用。这些接口中的通用write
方法接受实现通用AttitudeEphemerisFile.AttitudeEphemerisSegment
和EphemerisFile.EphemerisSegment
接口的对象作为参数。由于这些接口不提供CCSDS写入器所需的头部和元数据信息的访问权限,因此必须事先提供这些信息给写入器。这是通过在写入器的构造函数中直接提供头部和元数据模板来完成的。当然,非CCSDS消息格式的写入器将使用不同的策略来获取其特定的元数据。在CCSDS的情况下,提供的元数据只是一个不完整的模板:帧、起始时间和停止时间将在可用的数据要写入时填充,因为它们会因每个段而改变。在构建写入器时,模板参数不会被修改,它的内容会被复制到一个内部对象中,当创建每个段时,通过添加适当的帧和时间数据来修改该内部对象。
如果星历数据必须按照由传播器实时生成的方式进行写入,则星历类型的消息也可以以流式方式使用(使用特定的Streaming##MWriter
类)。这些特定的写入器提供了一个newSegment()
方法,返回一个固定步长的处理器,用于注册到传播器。如果星历必须分成不同的段,以防止在两个时间范围之间的离散事件(如机动)之间进行插值,则必须在离散事件时间使用newSegment()
方法检索新的步长处理器,并且必须使用新的传播器(或者适当地调用propagator.getMultiplexer().remove(oldSegmentHandler)
和propagator.getMultiplexer().add(newSegmentHandler)
)。所有段将正确地收集在生成的CCSDS消息中。使用相同的传播器和相同的事件处理器将无法按预期工作:传播器将通过重置状态正常运行离散事件,但星历不会意识到这个变化,而只会继续相同的段。在读取以这种方式生成的消息时,读取器将不会意识到在该机动周围不应使用插值,因为该事件不会出现在消息中。
根据文件处理的最佳实践,当写入CCSDS消息时,Orekit严格遵守标准中指定的单位。如果低级生成器配置为写入单位(写入单位是可选的),则单位将是标准单位,并且语法将是CCSDS语法。为了更好地符合和兼容其他系统,这个选择不能定制,它由库强制执行。
本节描述了CCSDS框架的设计。这是一个实现细节,仅对Orekit开发人员或希望扩展它的人有用,例如通过添加对新消息类型的支持。对于简单地解析或编写CCSDS消息,这并不是必需的。
解析的第一层是词法分析。它的目的是从数据源中读取字符流,并生成ParseToken
流。提供了两种不同的词法分析器:KvnLexicalAnalyzer
用于键值表示法,XmlLexicalAnalyzer
用于扩展标记语言。 LexicalAnalyzerSelector
实用类根据从数据源读取的前几个字节选择其中之一。如果找到XML声明的开头(“<?xml …>”),则选择XmlLexicalAnalyzer
,否则选择KvnLexicalAnalyzer
。检测适用于UCS-4、UTF-16和UTF-8编码,带或不带字节顺序标记,并且与字节序无关。这个XML声明在通用XML文档中是可选的(至少对于XML 1.0),但CCSDS消息和XML 1.1规范都要求它存在。在读取了允许选择的前几个字节之后,字符流将被重置到开头,以便所选的词法分析器再次看到这些字符。即使DataSource
是网络流,也可以通过一些内部缓冲实现此功能。词法分析器创建后,消息解析器通过调用其accept
方法向该分析器注册自己,并等待词法分析器调用它来处理从字符流生成的标记。这类似于访问者设计模式,解析器访问由词法分析器生成的标记。
下面的类图展示了词法分析的静态结构:
词法分析的动态视图如下序列图所示:
消息解析中的第二层解析是语义分析。它的目的是读取ParseToken
对象流,并逐步构建CCSDS消息。对于KVN中的原始条目(例如EPOCH_TZERO = 1998-12-18T14:28:15.1172
)或XML中的原始条目(例如<EPOCH_TZERO>1998-12-18T14:28:15.1172</EPOCH_TZERO>
),语义分析是独立于消息格式的:两个词法分析器都将生成一个类型设置为TokenType.ENTRY
,名称设置为EPOCH_TZERO
,内容设置为1998-12-18T14:28:15.1172
的ParseToken
。该标记将传递给消息解析器进行处理,解析器可以忽略该标记是从KVN还是XML消息中提取的。这极大地简化了两种格式的解析,并避免了代码重复。然而,对于头部、段、元数据、数据或逻辑块等更高级别的结构,情况就不再如此。对于所有这些情况,解析器必须知道消息是在键值表示法还是扩展标记语言中。因此,词法分析器在开始解析时调用解析器的reset
方法,并将消息格式作为参数传递,以便解析器知道格式并知道如何处理更高级别的结构。
CCSDS消息非常复杂,包含许多子结构,我们希望解析多种类型的消息(APM、AEM、OPM、OEM、OMM、OCM和TDM,截至11.0版本)。需要管理数百个键(即ParseToken
可能具有的许多不同名称)。在11.0版本之前,Orekit使用了一个单独的大枚举类来管理所有这些键,但随着支持更多的消息类型,这种方式变得难以管理。从11.0版本开始,该框架基于这样一个事实:这些众多的键属于一小组逻辑块,总是作为一个整体进行解析(头部、元数据、状态向量、协方差等)。因此,解析是通过解析器在少量众所周知的状态之间切换来执行的。当一个状态处于活动状态时,例如元数据解析,查找仅限于元数据中允许的键。如果出现未知的令牌,则解析器假定当前部分已完成,并切换到另一个状态,该状态被声明为在元数据之后使用的回退状态。在这种情况下,可能是专用于数据解析的状态。这是状态设计模式的一种实现。解析器始终具有一个当前的ProcessingState
,只要它可以处理词法分析器提供给它的令牌,它就保持活动状态,并且它具有一个回退的ProcessingState
,当当前状态无法处理令牌时切换到该状态。以下类图显示了这个设计:
所有解析器在其reset
方法被词法分析器在消息开始时调用时设置初始处理状态,并通过预测下一个状态可能是什么来管理回退处理状态。这对于每种消息类型都是高度特定的,不幸的是还取决于消息格式(KVN vs. XML)。例如,在KVN消息中,初始处理状态是HeaderProcessingState
,但在XML消息中,它实际上是XmlStructureProcessingState
,只有当处理XML <header>
开始元素时才触发HeaderProcessingState
。CCSDS消息类型也不是非常一致,这使得实现更加复杂。例如,APM在KVN版本中没有META_START
、META_STOP
、DATA_START
或DATA_STOP
键,而AEM都有,而OEM有META_START
、META_STOP
,但没有DATA_START
和DATA_STOP
。所有解析器都扩展了AbstractMessageParser
抽象类,该抽象类声明了几个钩子(prepareHeader
、inHeader
、finalizeHeader
、prepareMetadata
等),各个状态可以调用这些钩子,以便解析器跟踪自身所处的位置并相应地准备回退处理状态。例如,当KvnStructureProcessingState
看到META_START
键时,它会调用prepareMetadata
钩子,当XmlStructureProcessingState
看到metadata
开始元素时,它也会调用prepareMetadata
。然后,解析器就知道元数据解析即将开始,并设置好回退状态。不幸的是,在KVN格式中,APM没有META_START
键,因此prepareMetadata
不会自动调用,因此解析本身必须自己处理它(当检测到第一个元数据令牌时会处理它)。
当解析器不切换状态时,一个状态是活动的,并按顺序处理所有即将到来的令牌。每个处理状态可能采用不同的策略,这取决于它处理的部分。处理状态总是相当小的。一些处理状态可以从消息类型重用到消息类型(例如HeaderProcessingState
、KvnStructureProcessingState
或XmlStructureProcessingstate
),并作为单独的类实现。其他处理状态是特定于一个消息类型(因此也是特定于一个解析器)的,它们在解析器内部作为单个私有方法实现。方法引用用于直接指向这些方法。这使得一个解析器类可以同时提供ProcessingState
接口的多个实现。以下示例摘自TdmParser
,它显示了当在KVN消息中看到DATA_START
键或在XML消息中看到<data>
开始元素时,将调用prepareData
并分配一个ObservationsBlock
来保存即将到来的观测数据。然后将回退处理状态设置为私有方法processDataToken
,以便可以正确处理下一个令牌,该令牌在此阶段预期为表示观测的数据令牌:
public boolean prepareData() {
observationsBlock = new ObservationsBlock();
setFallback(this::processDataToken);
return true;
}
在大多数情况下,一个部分中允许的键是固定的,因此它们在一个枚举中定义。处理状态(在这种情况下通常是解析器内部的一个私有方法)然后只需使用枚举类的标准valueOf
方法选择与令牌名称对应的常量,并委托给它处理令牌内容。枚举常量通常只调用令牌的processAs
方法之一,将其指向用于存储令牌内容的元数据/数据/逻辑块设置器。对于既重用某些来自更一般部分的键又添加自己的键的部分,可以连续检查多个枚举类型。这种设计的典型示例是OemParser
中的processMetadataToken
方法,它是一个作为ProcessingState
的单个私有方法,并尝试使用枚举MetadataKey
、OdmMetadataKey
、CommonMetadataKey
和最后OemMetadataKey
来填充元数据部分。在某些情况下,使用枚举的这种设计无法工作,例如用户定义的数据和关键字。在这种情况下,使用临时实现。
添加新的消息类型(假设为XYZ消息)涉及以下步骤:
Ndm
的Xyz
类,XyzData
容器,XyzSection1Key
、XyzSection2Key
…枚举,XyzParser
,ParserBuilder
中创建buildXyzParser
方法,XyzWriter
类。在上面的列表中,创建XyzParser
可能是最耗时的任务。在这个新的解析器中,需要设置状态切换逻辑,使用现有的全局结构和头部的类,并使用私有方法processSection1Token
,processSection2Token
等来处理每个逻辑块的标记。
当CCSDS发布消息格式的新版本时,向现有消息添加新的键通常包括在数据容器中添加一个字段,具有一个setter和一个getter,并添加一个枚举常量,该常量将被现有处理状态识别,并调用标记的processAs
方法之一,要求它调用新的setter。
下面的类图展示了写入的实现:
在这个图中,只显示了OpmWriter
和OemWriter
,但其他格式的写入器也存在,具有类似的结构。
当构建顶层写入器时,它们会配置对头部和元数据容器的引用。这就是允许OemWriter
实现EphemerisFileWriter
并能够将任何星历写入为OEM的原因,即使星历本身没有任何CCSDS特定的元数据和头部。可以使用传播器从头开始创建星历,甚至可以在计算过程中即时写入,如果将OemWriter
嵌入到StreamingOemWriter
中。
写入器本身不会直接写入数据,而是将其委托给Generator
接口的某个实现,该接口是解析部分中的LexicalAnalyzer
的对应物。有两个Generator
的实现,一个生成键值表示法(Key-Value Notation),另一个生成扩展标记语言(XML)。