CCSDS
The org.orekit.files.ccsds package provides classes to handle parsing
and writing CCSDS messages.
Users point of view
Organization
The package is organized in hierarchical sub-packages that reflect the sections
hierarchy from CCSDS messages, plus some utility sub-packages. The following class
diagram depicts this static organization.
The org.orekit.files.ccsds.section sub-package defines the generic sections
found in all CCSDS messages: Header, Metadata and Data. All extends the
Orekit-specific Section interface that is used for checks at the end of parsing.
Metadata and Data are gathered together in a Segment structure.
The org.orekit.files.ccsds.ndm sub-package defines a single top-level abstract
class NDMFile, which stands for Navigation Data Message. All CCDSD messages extend
this top-level abstract class. NDMfile is a container for one Header and one or
more Segment objects, depending on the file type (for example OPMFile only contains
one segment whereas OEMFile may contain several segments).
There are as many sub-packages as there are CCSDS message types, with
intermediate sub-packages for each officialy published recommendation:
org.orekit.files.ccsds.ndm.adm.apm, org.orekit.files.ccsds.ndm.adm.aem,
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,
and org.orekit.files.ccsds.ndm.tdm. Each contain the logical structures
that correspond to the message type, among which at least one ##MFile
class that represents a complete message/file. As some data are common to
several types, there may be some intermediate classes in order to avoid
code duplication. These classes are implementation details and not displayed
in the previous class diagram. If the message type has logical blocks (like state
vector block, Keplerian elements block, maneuvers block in OPM), then
there is one dedicated class for each logical block.
The top-level file also contains some Orekit-specific data that are mandatory
for building some objects but is not present in the CCSDS messages. This
includes for example IERS conventions, data context, and gravitational
coefficient for ODM files as it is sometimes optional in these messages.
This organization has been introduced with Orekit 11.0. Before that, the CCSDS
hierarchy with header, segment, metadata and data was not reproduced in the API
but a flat structure was used.
This organization implies that users wishing to access raw internal entries must
walk through the hierarchy. For message types that allow only one segment, there
are shortcuts to use file.getMetadata() and file.getData() instead of
file.getSegments().get(0).getMetadata() and file.getSegments().get(0).getData()
respectively. Where it is relevant, other shortcuts are provided to access
Orekit-compatible objects as shown in the following code snippet:
OPMFile opm = ...;
AbsoluteDate fileDate = opm.getHeader().getCreationDate();
Vector3D dV = opm.getManeuver(0).getdV();
SpacecraftState state = opm.generateSpacecraftState();
// getting orbit date the hard way:
AbsoluteDate orbitDate = opm.getSegments().get(0).get(Data).getStateVectorBlock().getEpoch();
Message files can be obtained by parsing an existing file or by using
the setters to create it from scratch, bottom up starting from the
raw elements and building up through logical blocks, data, metadata,
segments, header and finally file.
Parsing
Parsing a text message to build some kind of NDMFile object is performed
by setting up a parser. Each message type has its own parser. Once created,
its parseMessage method is called with a data source. It will return the
parsed file as a hierarchical container as depicted in the previous
section.
The Orekit-specific data that are mandatory for building some objects but are
not present in the CCSDS messages are set up when building the parser. This
includes for example IERS conventions, data context, and gravitational
coefficient for ODM files as it is sometimes optional in these messages.
One change introduced in Orekit 11.0 is that the progressive set up of
parser using the fluent API (methods withXxx()) has been removed. Now the
few required parameters are all set at once in the constructor. Another change
is that the parsers are mutable objects that gather the data during the parsing.
They can therefore not be used in multi-threaded environment. The recommended way
to use parsers is to either dedicate one parser for each message and drop it
afterwards, or to use a single-thread loop.
Parsers automatically recognize if the file is in Key-Value Notation (KVN) or in
eXtended Markup Language (XML) format and adapt accordingly. This is
transparent for users and works with all CCSDS message types. The data to
be parsed is provided using a DataSource object, which combines a name
and a stream opener and can be built directly from these elements, from a file name,
or from a File instance. The DataSource object delays
the real opening of the file until the parseMessage method is called and
takes care to close it properly after parsing, even if parsing is interrupted
due to some parse error.
The OEMParser and OCMParser have an additional feature: they also implement
the generic EphemerisFileParser interface, so they can be used in a more
general way when ephemerides can be read from various formats (CCSDS, CPF, SP3).
The EphemerisFileParser interface defines a parse(dataSource) method that
is similar to the CCSDS-specific parseMessage(dataSource) method.
As the parsers are parameterized with the type of the parsed file, the parseMessage
and parse methods in all parsers already have the specific type, there is no need
to cast the returned value.
The following code snippet shows how to parse an oem file, in this case using a
file name to create the data source:
OEMParser parser = new OEMParser(conventions, simpleEOP, dataContext,
missionReferenceDate, mu, defaultInterpolationDegree);
OEMFile oem = parser.parseMessage(new DataSource(fileName));
Writing
Writing a CCSDS message is done by using a specific writer class for the message
type and using a low level generator corresponding to the desired file format,
KVNGenerator for Key-Value Notation or XMLGenerator for eXtended Markup Language.
Ephemeris-type messages (AEM, OEM and OCM) implement the generic ephemeris writer
interfaces (AttitudeEphemerisFileWriter and EphemerisFileWriter) in addition
to the CCSDS-specific API, so they can be used in a more general way when ephemerides
data was built from non-CCSDS data. The generic write methods in these interfaces
take as arguments objects that implement the generic
AttitudeEphemerisFile.AttitudeEphemerisSegment and EphemerisFile.EphemerisSegment
interfaces. As these interfaces do not provide access to header and metadata informations
that CCSDS writers need, these informations must be provided beforehand to the
writers. This is done by providing directly the header and a metadata template in
the constructor of the writer. Of course, non-CCSDS writers would use different
strategies to get their specific metadata. The metadata provided is only a template that
is incomplete: the frame, start time and stop time will be filled later on when
the data to be written is available, as they will change for each segment. The
argument used as the template is not modified when building a writer, its content
is copied in an internal object that is modified by adding the proper frame and
time data when each segment is created.
Ephemeris-type messages can also be used in a streaming way (with specific
Streaming##MWriter classes) if the ephemeris data must be written as it is produced
on-the-fly by a propagator. These specific writers provide a newSegment() method that
returns a fixed step handler to register to the propagator. If ephemerides must be split
into different segments, in order to prevent interpolation between two time ranges
separated by a discrete event like a maneuver, then a new step handler must be retrieved
using the newSegment() method at discrete event time and a new propagator must be used.
All segments will be gathered properly in the generated CCSDS file. Using the same
propagator and same event handler would not work as expected. The propagator would run
just fine through the discrete event that would reset the state, but the ephemeris would
not be aware of the change and would just continue the same segment. Upon reading the
file produces this way, the reader would not be aware that interpolation should not be
used around this maneuver as the event would not appear in the file.
TODO: describe CCSDS-specific API
Developers point of view
This section describes the design of the CCSDS framework. It is an implementation
detail and is useful only for Orekit developers or people wishing to extend it,
perhaps by adding support for new messages types. It is not required to simply
parse or write CCSDS messages.
Parsing
The first level of parsing is lexical analysis. Its aim is to read the
stream of characters from the data source and to generate a stream of
ParseToken. Two different lexical analyzers are provided: KVNLexicalAnalyzer
for Key-Value Notation and XMLLexicalAnalyzer for eXtended Markup Language.
The LexicalAnalyzerSelector utility class selects one or the other of these lexical
analyzers depending on the first few bytes read from the data source. If the
start of the XML declaration (“<?xml …>”) which is mandatory in all XML documents
is found, then XMLLexicalAnalyzer is selected, otherwise KVNLexicalAnalyzer
is selected. Detection works for UCS-4, UTF-16 and UTF-8 encodings, with or
without a Byte Order Mark, and regardless of endianness. After the first few bytes
allowing selection have been read, the characters stream is reset to beginning so
the selected lewical analyzer will see these characters again. Once the lexical
analyzer has been created, the message parser registers itself to this analyzer by calling
its accept method, and wait for the lexical analyzer to call it back for processing
the tokens it will generate from the characters stream. This is akin to the visitor
design pattern with the parser visiting the tokens as they are produced by the lexical
analyzer.
The following class diagram presents the static structure of lexical analysis:
The dynamic view of lexical analysis is depicted in the following sequence diagram:
The second level of parsing is message parsing is syntax analysis. Its aim is
to read the stream of ParseToken objects and to progressively build the CCSDS message
from them. Syntax analysis of primitive entries like EPOCH_TZERO = 1998-12-18T14:28:15.1172
in KVN or <EPOCH_TZERO>1998-12-18T00:00:00.0000</EPOCH_TZERO> in XML is independent
of the file format: in both lexical analyzers will generate a ParseToken with type set
to TokenType.ENTRY, name set to EPOCH_TZERO and content set to 1998-12-18T00:00:00.0000.
This token will be passed to the message parser for processing and the parse may ignore
that the token was extract from a KVN or an XML file. This simplifies a lot parsing of both
formats and avoids code duplication. This is unfortunately not true for higher level structures
like header, segments, metadata, data or logical blocks. For all these cases, the parser must
know if the file is in Key-Value Notation or in eXtended Markup Language, so the lexical
analyzer starts parsing by calling the parser reset method with the file format as an
argument, so the parser knows how to handle the higher level structures.
CCSDS messages are complex, with a lot of sub-structures and we want to parse several types
(APM, AEM, OPM, OEM, OMM, OCM and TDM as of version 11.0). There are hundreds of keys to
manage (i.e. a lot of different names a ParseToken can have). Prior to version 11.0, Orekit
used a single big enumerate class for all these keys, but it proved unmanageable as more
message types were supported. The framework set up with version 11.0 is based on the fact
these numerous keys belong to a smaller set of logical blocks that are always parsed as a
whole (header, metadata, state vector, covariance…). Parsing can be performed with the
parser switching between a small number of well-known states. When one state is active,
say metadata parsing, then lookup is limited to the keys allowed in metadata. If an
unknown token arrives, then the parser assumes we have finished the current section, and
it switches into another state, say data parsing, that is the fallback to use after
metadata. This is an implementation of the State design pattern. Parsers always have
one current ProcessingState that remains active as long as it can process the tokens
provided to it by the lexical analyzer, and the have a fallback ProcessingState to
switch to when a token could not be handled by the current one. The following class
diagram shows this design:
All parsers set up the initial processing state when their reset method is called
by the lexical analyzer at the beginning of the message, and they manage the fallback
processing state by anticipating what the next state could be when one state is
activated. This is highly specific for each message type, and unfortunately also
depends on file format (KVN vs. XML). As an example, in KVN files, the initial
processing state is already the HeaderProcessingState, but in XML file it is
rather XMLStructureProvessingState and HeaderProcessingState is triggered only
when the XML <header> start element is processed. CCSDS messages type are also not
very consistent, which makes implementation more complex. As an example, APM files
don’t have META_START, META_STOP, DATA_START or DATA_STOP keys in the
KVN version, whereas AEM file have both, and OEM have META_START, META_STOP
but have neither DATA_START nor DATA_STOP. All parsers extend the AbstractMessageParser
abstract class from which declares several hooks (prepareHeader, inHeader,
finalizeHeader, prepareMetadata…) which can be called by various states
so the parser knows where it is and prepare the fallback processing state. The
prepareMetadata hook for example is called by KVNStructureProcessingState
when it sees a META_START key, and by XMLStructureProcessingState when it
sees a metadata start element. The parser then knows that metadata parsing is
going to start an set up the fallback state for it.
When the parser is not switching states, one state is active and processes all
upcoming token one after the other. Each processing state may adopt a different
strategy for this, depending on the section it handles. Processing states are
always quite small. Some processing states that can be reused from message type
to message type (like HeaderProcessingState, KVNStructureProcessingState or
XMLStructureProcessingstate) are implemented as classes. Other processing
states that are specific to one message type (and hence to one parser), are
implemented as a single private method within the parser and method references
are used to point directly to the method. This allows one parser class to
provide simultaneously several implementations of the ProcessingState interface.
In many cases, the keys that are allowed in a section are fixed so they are defined
in an enumerate. The processing state (in this case often a private method within
the parser) then simply selects the enum constant using the standard valueOf method
from the enumerate class and delegates token processing to it. The enum constant
then just call one of the processAs method from the token, pointing it to the
metadata/data/logical block setter to call for storing the token content. For
sections that both reuse some keys from a more general section and add their
own keys, several enumerate types can be checked in row. A typical example of this
design is the processMetadataToken method in OEMParser, which is a single
private method acting as a ProcessingState and tries the enumerates MetadataKey,
ODMMetadataKey, OCommonMetadataKey and finally OEMMetadataKey to fill up
the metadata section.
Adding a new message type (lets name it XYZ message) involves creating the XYZFile
class that extends NDMFile, creating the XYZData container for the data part,
and creating one or more XYZSection1Key, XYZSection2Key… enumerates for each
logical blocks that are allowed in the message format. The final task is to create
the XYZParser and set up the state switching logic, using existing classes for
the global structure and header, and private methods processSection1Token,
processSection2Token… for processing the tokens from each logical block.
Adding a new key to an existing message when a new version of the message format
is published by CCSDS generally consist in adding one field in the data container
with a setter and a getter, and one enum constant that will be recognized by
the existing processing state and that will call one of the processAs method from
the token, asking it to call the new setter.
Writing
The following class diagram presents the implementation of writing:
TODO explain diagram