DUECA/DUSIME
Some Recipes for using DUECA Channels

Introduction

DUECA uses "channels" as a means to communicate between different parts of a simulation. Since a re-write (and the step in 2014 from the DUECA 0.x series to DUECA 2.x and above), these are implemented by the UnifiedChannel class. Its name suggests that these channels offer many possibilities, and that is true. While another page discusses the difference between the old DUECA channels and the new unified channel type, this page looks at the properties of the DUECA (unified) channels, and typical ways in which you may use these channels.

Events versus Stream

Channels are meant to transport simple C++ structs (the DCO objects, see the information on the code generator ). A typical use of DCO objects in the context of flight simulation would be a DCO object (or multiple objects) transmitting the information from the pilot's input devices, such as sticks, throttle levers, etc. While the simulation is running, such devices always provide an input; there is position and force on the controls, etc. In DUECA, this is called the "time aspect", in this case the data is Continuous. When this data is written, the writing module provides a time span for the data; indicating from what time to what time this value is valid. Since having no value for a certain time can cause a problem elsewhere in the simulation, a stream channel needs to continuously be fed with data, and the validity intervals for that data need to match up while the simulation is running.

For other parts of the simulation, it is not very convenient to transmit the data as continuous. For example when describing the key inputs to a flight management system, it is not necessary to continuously transmit "there was no key pressed" at times no key was pressed. And in some cases a pilot might hit two keys in the period for a single simulation model update. The time aspect for this data is defined as Events in DUECA. Event data is stamped with a single time point (not a time span!), and a DUECA channel configured for events will happily accept multiple events for a single time, but also function when no event for a certaint time is written. A channel with event data does not need to be filled continuously.

Endpoints and Entries, writing, reading

Single writer, multiple readers, stream

In simple cases, a channel is written to by one module, there is only one DCO type written, and it can be read by multiple other modules. In that case the channel has one entry.

channel-single-stream.svg
Channel with a single entry, one writing module, stream data, multiple readers

This illustration shows a channel with only one writing token (right hand side), and therefore only one entry, entry #0. In this case there are two (but there could equally be 0 or more) reading tokens.

An entry has an entry number, optionally it has a label (string), describing the entry, and it only accommodates one type of data, i.e., one DCO type. A channel with a single entry only has entry number 0. In cases where you build a simulation for a single aircraft, or a controller or data recording application for a single device, such channels are exactly what you need.

Given a write token w_token, of type ChannelWriteToken, as a member variable of your DUECA module class, in your constructor list you can define the writing end of this channel as:

w_token(getId(), NameSet("MyData://myentity"),
"MyData", "entry label"),

You must have created and listed (in your comm-objects.lst file), the MyData DCO class. The arguments to this call are explained as follows:

CHECK_TOKEN(w_token);

The read tokens for such a channel are created as follows:

r_token(getId(), NameSet("MyData://myentity"), "MyData"),

These first three arguments are the same as for the write token. The further arguments for the first variation of the read token constructor (which in this case can all use the default value), are:;

To write this channel, the writing token is used with a datawriter:

DataWriter<MyData> d(w_token, ts);
// use d.data() to access and write the MyData object
d.data().<member> = <value>;

On the other end you can read the token as follows:

try {
DataReader<MyData> d(r_token, ts);
// do stuff with d.data
}
except (const std::exception& e) {
// notify something is wrong.
}

If you did not specify the token as a trigger for your activity, the data for your time span (ts) may not yet have been written. Note that in this case your simulation may also become sensitive to race conditions (depending on computer timing, fresh data is or isn't there). If you don't care that much, read with a modified datareader:

try {
DataReader<MyData,MatchIntervalStartOrEarlier> d(r_token, ts);
// do stuff with d.data
// to see how old your data really is, use d.timeSpec();
if (d.timeSpec().getValidityStart() < ts.getValidityStart()) {
std::cout << "old data" << std::endl;
}
else {
std::cout << "fresh data" << std::endl;
}
}
except (const std::exception& e) {
// notify something is wrong.
}

This takes the time span defined by ts, and first tries to find the data in the channel that matches that time, but if not available, takes the latest data in the channel that matches.

TLDR;

Single writer, multiple readers, event

If the writing token is created as

w_token(getId(), NameSet("MyData://myentity"),
"MyData", "entry label", Channel::Events),

And the reading token(s) similarly as above:

r_token(getId(), NameSet("MyData://myentity"), "MyData"),

The single entry for this channel will contain events, so for each (integer) time point there may be (1) no data, (2) a single data point or (3) multiple data points, as illustrated in the figure below.

channel-single-event.svg
Channel with a single entry, one writing module, event data, multiple readers

The reading will by default follow the Channel::ReadAllData logic, and events come in one by one, in the same order as written by the write token. If the time is omitted from the read action:

DataReader<MyData> dr(r_token);

Any up to that point available event data from the channel/entry will be returned. If the activity's current time is given:

DataReader<MyData> dr(r_token, ts);

only data from before or at the time span start given in ts is returned.

TLDR;

Multiple writers, (one or) multiple readers, stream

It is also possible to have multiple entries in a channel. Each entry is then associated with a channel write token, a module may declare multiple channel write tokens, or multiple modules may declare channel write tokens on the channel.

If the writing tokens are created as

w_token1(getId(), NameSet("MyData://myentity"),
"MyData", "entry label", Channel::Continuous, Channel::OneOrMoreEntries),
w_token2(getId(), NameSet("MyData://myentity"),
"MaybeOtherData", "another entry label", Channel::Continuous, Channel::OneOrMoreEntries),
w_token3(getId(), NameSet("MyData://myentity"),
"MyData", "another entry label", Channel::Continuous, Channel::OneOrMoreEntries),

The writing tokens need to allow multiple entries (with Channel::OneOrMoreEntries), otherwise one or more of the tokens will not become valid. This type of channels is typically used in distributed simulation with multiple "entities". Each entity then represents an aircraft, other vehicle or the like.

And a reading token can be created as:

r_token(getId(), NameSet("MyData://myentity"),
"MyData", entry_any, Channel::Continuous, Channel::OneOrMoreEntries),

This reading token will become valid when at least one of the entries in the channel is being written with a MyData data type, and can be used to read all entries that contain "MyData". The entry_any entry id indicates that multiple entries may be read with the same token.

channel-multiple-stream.svg
Channel with multiple entries, one or more writing modules, stream data, multiple readers

In the example above, there are three writing tokens, each writing one entry. In principle, not all entries need to have the same data type, and event and stream entries may be mixed, but in general those set-ups are not really recommended. The rate at which writing is done may also be varied, in the example entry #2 is written at a higher update rate. Note that when creating a write token the entry number is automatically assigned, it cannot be chosen. One of the reading tokens reads multiple entries, the second reading token has been specified to read only one specific entry.

To extract all data from the channel now requires some extra effort. One possible way of doing this is by the following:

// "recycle" the read token to the first entry
r_token.selectFirstEntry();
// check that the token is actually linked to an entry
while (r_token.haveEntry()) {
try {
// read the data
DataReader<MyData> r(r_token, ts);
// or if not sure whether data has been written
// DataReader<MyData, MatchIntervalStartOrEarlier> r(r_token, ts);
// do things with the data, for example print
std::cout << r.data() << std::endl;
// you can also get to know more about the entry, like the id
std::cout << "entry id " << r_token.getEntryId() << std::endl;
// and the label given on creation
std::cout << "entry label " << r_token.getEntryLabel() << std::endl;
}
except (const std::exception& e) {
// uh oh, something is wrong
}
// over to the next entry if it is there
// note that you may not have an active/live DataReader on this token
// when calling this. The DataReader above was deleted when we left the
// 'try' scope.
r_token.selectNextEntry();
}

Of course, depending on what you want to do with the data in the channel, this can be a rather clumsy way to read from the channel. The most common case for multiple entries with identical (or similar) types of continuous data in the channel is for distribted simulation with multiple entities, e.g., multiple aircraft in a dogfight or formation flight. In that case is is convenient to track all these entries, and link them up to a known aircraft. The entry label can be freely chosen, and you may use this to for example specify the aircraft type, livery and tail number, so a reader of your channel can use this information.

You can use a ChannelWatcher to monitor changes in a channel. The ChannelWatcher can have a callback function that is invoked when entries are added to or removed from the channel. It is also possible to poll (regularly check) the ChannelWatcher for changes, we will use that in the example below. The information returned from the ChannelWatcher enables you to create a read token for the specific entry that was created. As an example, add to your module:

// a data structure to collect the read token and possibly some
// additional information
struct FlightData {
// maybe collect the name
std::string name;
// with the token
ChannelReadToken r_token;
// new flightdata objects use the channel name, entry id, and store the label
FlightData(const dueca::GlobalId& id, std::string& channelname, std::string& label, entryid entry) :
name(label),
// the reading token is for a specific entry in the channel
r_token(id, NameSet(channelname), "MyData", entry)
{
// created name and token
}
~FlightData() { }
};
// the watcher
ChannelWatcher watch_flights;
// collected flights indexed by entry id
std::map<entryid,FlightData> flights;

In your constructor, initialize the watcher, with polling option:

watch_flights(NameSet("MyData://myentity"), true),

And when updating, do something like:

// room for information
ChannelEntryInfo einfo;
// check for any changes (added or removed entries)
while (watch_flights.checkChange(einfo)) {
if (einfo.clientid != GlobalId() && einfo.dataclass == std::string("MyData")) {
// addition to the flights
flights.emplace(
std::piecewise_construct,
std::forward_as_tuple(einfo.entryid),
std::forward_as_tuple("MyData://myentity", einfo.label, einfo.entryid));
}
else if (einfo.dataclass == std::string("MyData")) {
// remove
flights.erase(einfo.clientid);
}
else {
std::cout << "something changed, but I don't know how to handle data class "
<< einfo.dataclass << std::endl;
}
}
// and cycle through the flights, something like
for (const auto &f: flights) {
if (f.second.r_token.isValid() && f.second.r_token.haveVisibleSets()) {
// in this example read latest data
DataReader<MyData> r(f.second.r_token);
// and do something
std::cout << r.data() << std::endl;
}
}

There is a possible refinement to this process, because a DCO class can inherit from another DCO class. As an example, if you need that flexibility, you might define a DCO class Aircraft to contain the parameters you supply to all air vehicles, and then more specific classes that inherit from that Aircraft class, like B737, A320. For a 3D world view you can then draw the specific aircraft types, but for a radar view you can instruct your code to access the channel with the Aircraft DCO class, and treat all aircraft the same.

To check whether a class returned by the ChannelWatcher has a base class that you can process, you can use the DataClassRegistry::isCompatible call.

You now have an explicit overview of entries being created in and removed from the channel. In this way, the data read from the channel can more easily be linked to other data; you can track the individual flights. In addition, reading tokens attached to a single entry is more efficient than having a token iterate over all entries.

TLDR;

Multiple writers, (one or) multiple readers, event

For a channel with multiple event entries, the reading of the data can be done as in the previous section for stream channels. There is however one additional possibility for reading event data that can be useful in "partyline" situation, where events can be sent from multiple locations (modules), but all events are treated equally and it is not important to assemble or correlate the events from a specific sender. Writing tokens are created as normal. Note that for event tokens, it usually makes no sense to create multiple of these for the same channel from a single module:

w_token(getId(), NameSet("MyData://myentity"),
"MyData", "from module Joe", Channel::Events, Channel::OneOrMoreEntries),

And somewhere else

w_token2(getId(), NameSet("MyData://myentity"),
"MyData", "from module Jane", Channel::Events, Channel::OneOrMoreEntries),

If you want to read from this partyline channel, simply create a read token that connects to all entries again:

r_token(getId(), NameSet("MyData://myentity"),
"MyData", entry_any, Channel::Events, Channel::OneOrMoreEntries)

Reading in partlyline mode can be done with a single datareader:

while (r_token.haveVisibleSets()) {
DataReader<MyData,VirtualJoin> r(r_token);
// do something with the data
}

TLDR;