RACE and DDS
DDS is a platform independent, peer-to-peer publish/subscribe mechanism that is widely used for distributed and embedded systems. As such, it is a primary candidate for interfacing external systems to RACE, which includes actors to publish to and read from DDS topics.
The DDS specifications are owned by the OMG and include
- a wire protocol specification which is the basis for runtime-interoperability of DDS implementation from different vendors
- programming language specific APIs to create vendor independent, source compatible client applications
Most notably, the OMG publishes a open sourced DDS API for Java, which is the basis for RACE integration.
DDS Payload Data
Since DDS is supposed to work across processes and platforms it requires marshalling and un-marshalling of data that is supposed to be transmitted/received. As a programming language-neutral standard, DDS uses the OMG CORBA IDL to define application specific payload data types, which resembles C structure syntax:
module dds {
struct FlightRecord {
string cs;
double lat;
...
long long date;
};
};
Such *.idl files have to be translated into *.java files, for which DDS vendors provide their own specialized tools. However, this being based on CORBA IDL the idlj tool that is distributed with Oracle J2SE is sufficient to generate the basic Java sources (without vendor specific extensions of course). Assuming the following directory structure in RACE:
my-module/
src/
main/
scala/
java/
idl/
FlightRecord.idl
the required Java files can be generated by executing:
> idlj -td my-module/src/main/java my-module/src/main/idl/*.idl
which then creates the following Java sources:
my-module/src/main/java/
dds/
FlightRecord.java
FlightRecordHelper.java
FlightRecordHolder.java
Since the underlying data types are structs (fully mutable, no methods or supertypes), they are usually not directly used in RACE and are either embedded or translated into full Java or Scala classes.
DDSImport/ExportActors
The primary constructs for using DDS within RACE are the gov.nasa.race.actors.imports.DDSImportActor
and the
gov.nasa.race.actors.exports.DDSExportActor
, which can be directly instantiated from RACE configuration files:
...
{ name = "ddsPublisher"
class = "gov.nasa.race.actors.exports.DDSExportActor"
read-from = "fpos/out"
translator.class = "gov.nasa.race.data.dds.FlightPos2FlightRecord" // optional
writer = { // mandatory
class = ".data.dds.FlightRecordWriter" // this encapsulates TypeSupport[T],Topic[T] and Writer[T]
topic = "myTopic"
}
},
...
{ name = "ddsSubscriber"
class = "gov.nasa.race..actors.imports.DDSImportActor"
write-to = "fpos/in"
translator.class = "gov.nasa.race.data.dds.FlightRecord2FlightPos" // optional
reader = { // mandatory
class = ".data.dds.FlightRecordReader"
topic = "myTopic"
}
} ...
Note that both actor classes support a optional embedded translator (which is a normal gov.nasa.race.common.ConfigurableTranslator
)
and require mandatory reader
/ writer
specifications.
DDSReaders/Writers
Those are the key implementation constructs for interfacing RACE with DDS. You have to write one for each DDS
DataReader<T>
and DataWriter<T>
that is used by the application, but the concrete classes only have to
provide abstract method implementations and can be as simple as:
class FlightRecordReader (val config: Config) extends DDSReader[dds.FlightRecord](config)
class FlightRecordWriter (val config: Config) extends DDSWriter[dds.FlightRecord](config) {
override def write (o: Any) = {
o match {
case fr: dds.FlightRecord => writer.write(fr)
case other => // ignored for now
}
}
}
The concrete DDSReader
only has to specify the concrete payload type (dds.FlightRecord
in this example).
The concrete DDSWriter
implements the abstract write(Any)
method, which checks for supported data types and then
uses the superclass-instantiated DDS DataWriter<T>
instance to publish. While the write
implementation can do
on-the-fly translation (e.g. org.nasa.gov.race.data.FlightPos
to dds.FightRecord
), it is usually better to
configure DDSExportActors
with an explicit translator so that new DDS-RACE type mappings can be
later-on added without having to update/modify the respective DDSWriter
.
One of the defining features of DDS is a rich set of QoS options when instantiating DataReaders/Writers. The standard RACE DDSReader/DDSWriter implementations are just basic, but encapsulate respective instantiations in dedicated template methods that can be overridden in concrete classes, to do more specialized initialization that can make use of RACE configuration data.
Test-Tools
RACE comes with simple ddsserver
and ddsclient
applications, which are kept under:
test-tools/
dds-server/
dds-client/
Both implement stand-alone (non-RaceActor) systems that read/write FlightRecord
instances from a configurable
DDS topic. They represent external systems that should be connected to RACE.
RACE comes with three test/demo configurations:
config/
imports/
dds-import.conf
dds-roundtrip.conf
exports/
dds-export.conf
dds-import.conf
can be used with the ddsserver
and dds-export.conf
with the ddsclient
by executing
from different terminals:
Terminal 1 Terminal 2
> script/ddsclient > ./race config/exports/dds-export.conf
or:
> ./race/config/imports/dds-import.conf > script/ddsserver
The round trip example does not require any additional test tool.
Caveats
The OMG only publishes an abstract Java interface. RACE therefore builds on machines that don't have a proprietary DDS implementation, but it cannot run DDS applications unless such a implementation is configured (which usually takes a commercial license). This requires machine-specific, local build configurations as described in Local Build Configuration.
At the time of this writing, only the PrismTech Vortex implementation (Vortex Cafe) supports OMGs Java 5 PSM, and
only in a slightly modified version. The omgDDS
jar dependency that is used in the standard build.sbt
of
RACE therefore has to be replaced by a local-build.sbt
like this:
import RaceBuild._
val raceActors = extensibleProjects("race-common").settings(
resolvers += "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository",
libraryDependencies += "com.prismtech.cafe" % "cafe" % "2.2.2"
)
This assumes that Vortex Cafe is installed in the local Maven repository, which can be done with scripts that are provided in the Vortex distribution.