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

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.