VR-Link C# API Documentation
 All Classes Namespaces Files Functions Variables Enumerations Enumerator Properties Events Macros Pages
8.1 - Working with PDUs

Table of Contents

Protocol Data Units (PDUs) are the mechanism through which DIS applications send data.

VR-Link's DtPdu class (defined in pdu.h) is used to represent DIS PDUs. DtPdu is an abstract base class. All PDU classes are derived from DtPdu. Every interaction supported by VR-Link has an interaction PDU. For example fire interactions are represented by DtFirePdu. Interaction PDUs that have HLA counterparts are derived from DtInteraction, an essentially empty class, which is in turn derived from DtPdu. We have provided typedefs so that you can refer to, for example, a DtFirePdu as a DtFireInteraction in protocol-independent code.

All DIS 4, 5, and 6 PDUs have PDU classes implemented in VR-Link. Since there are nearly 70 PDUs, we do not describe all of their inspectors and mutators in this manual.

The derived PDU classes provide mutator and inspector functions to set and examine the values of PDU fields. Header information is accessed through DtPdu member functions. In general, an inspector function has the same names as the field being inspected, and returns the value it is inspecting, for example DtEntityStatePdu::entityType(). Mutator functions have the same names, preceded by the word "set", for example, DtEntityStatePdu::setEntityType().

Please see the header files for the exact prototypes of all PDU class member functions.

8.1.1 Sending PDUs

The procedure for sending a PDU is identical to the procedure for sending interactions, described in 4.3.1 Sending Interactions. This section contains additional DIS-specific information.

You send PDUs using the DtExerciseConn::send() or DtExerciseConn::sendStamped() functions. The sendStamped() member function sends a copy of the PDU that has the exercise ID and timestamp fields in the PDU header set to the DtExerciseConn's exercise ID, and the current time. The send() member function leaves the PDU header unmodified.

The following example shows how to create and send a Transmitter PDU:

DtExerciseConn exchange(...);
...
// Create a DtTransmitterPdu
DtTransmitterPdu pdu;
// Fill the PDU with data
pdu.setEntityId(DtEntityIdentifier(1,2,3));
pdu.setRadioId(4);
pdu.setTransmitState(DtOn);
...
// Send to the exercise
exConn.sendStamped(pdu);

Normally, send() and sendStamped() send the PDU to the DtExerciseConn's default destination IP address. (For details, please see 4.2.2 Creating an Exercise Connection for DIS.) However, the functions have an optional argument that specifies a different destination address. For example, to send a PDU to the machine with address 207.86.232.1, use:

DtInetAddr("207.86.232.1");
exConn.sendStamped(pdu, &addr);
Note
Applications usually send interaction PDUs as described in this section. They usually use DtEntityPublisher to manage the sending of entity state PDUs.

8.1.2 Receiving PDUs

Applications handle incoming PDUs through callbacks. The process for receiving PDUs is virtually identical to that described in 4.3.2 Receiving Interactions. This section has additional DIS-specific information.

As described in the 4 - The Protocol Independent Interface, a callback is registered with VR-Link using a PDU class's static addCallback() function, and within the callback function, you can use the PDU class's inspector functions to examine the fields of the PDU.

Note
The DIS version of DtExerciseConn has callback registration and unregistration functions called addPduCallback() and removePduCallback(). However, these functions are meant to be used only by a PDU class's static addCallback() function, and not directly by application code. If an application uses the DtExerciseConn functions directly, then the PDU class never gets registered with the DtPduFactory (this occurs within a PDU class's addCallback() function), and VR-Link will not be able to correctly create instances of the class to pass to your callback functions. For more information, please see 8.1.6 DtPduFactory.

8.1.2.1 More Control Over Receiving PDUs

Most applications use VR-Link's callback mechanism to handle incoming PDUs, leaving the job of actually reading the PDUs and dispatching them to the callbacks to DtExerciseConn::drainInput(). However, to give you more flexibility and control over reading PDUs from the network, DtExerciseConn provides access to the functions used by drainInput().

The drainInput() function works by repeatedly calling readAndProcess(). The readAndProcess() function tries to read a single PDU from the network using netRead(), then passes the PDU to processPdu(), from which all callbacks are invoked. Application code can use any of these functions instead of calling drainInput().

8.1.2.2 Using the netRead() Function

The netRead() function tries to read a packet from the network, constructs an instance of a PDU class from the network representation, and returns it. If you use netRead() directly, note the following considerations:

The optional return code argument to netRead() is a pointer to a return code that is set depending on the results of the netRead() operation as follows:

So, an application can use netRead() as follows:

DtExerciseConn exConn(...);
...
int retCode;
while (1)
{
DtPdu* pdu = exConn.netRead(&retCode);
if (retCode == DtNET_READ_SUCCESS)
{
// do something with the PDU
...
delete pdu;
}
}

8.1.2.3 Using the readUntil() Function

The readUntil() function is similar to drainInput() in that it repeatedly reads and processes PDUs. It is different in that it returns when the predicate() function returns true when called on the PDU being processed, or after timeout seconds, whichever happens first. If timeout seconds elapse, it returns DtOtherPduKind. Otherwise, it returns the kind of the PDU that caused the predicate() function to return true. You must provide the predicate() function. It has the following signature:

int predicate(DtPdu *pdu, void *arg);

readUntil()'s signature is as follows:

DtPduKind readUntil(DtPredicate predicate, void* arg,
DtTime timeout, DtTime sleepTime);

The arg argument passed to readUntil() is passed as the arg argument to predicate() each time it is called.

If no PDU is available to be read, then readUntil() sleeps for sleepTime seconds before polling the socket again. A timeout less than 0 causes readUntil() to poll continuously, without sleeping.

8.1.3 Inspecting and Setting Data in a PDU Header

The DtPdu class has functions to inspect and set the data in a DIS PDU's header. Other fields of the various PDUs are accessed through member functions of the classes derived from DtPdu.

The following DtPdu member functions return the values of their respective fields in the PDU's header:

The DtPduKind and DtProtocolFamily enumerations are in disEnums.h.

The following table describes other functions for setting and returning data.

DtPdu Member Functions
Function Description
timeStamp() Returns a floating point number representing the time (in seconds past the hour) that is encoded in the PDU’s time stamp.
timeStampType() Returns either DtTimeStampRelative or DtTimeStampAbsolute, depending on the type of the PDU’s timestamp.
setExerciseId() Sets the value of PDU header’s exercise ID field.
setTimeStamp() Sets the PDU’s timestamp to time modulo 3600, and the timestamp type to relOrAbs. Time is the time in seconds at which the PDU’s data is to be considered valid. relOrAbs</zargumen> must be either DtTimeStampRelative or DtTimeStampAbsolute. Default: DtTimeStampRelative.
setVersion() Sets the value of the Protocol Version field.
print() Prints the entire contents of a PDU in human readable form, including header and data, by calling printHeader() followed by printData(). This is the function used by the netdump utility to print the contents of PDUs. The printHeader() function prints the information contained in the PDU’s header, while printData() is a virtual function that prints the data below the header. The base class version of printData() prints the data in hexadecimal format.

Other header information is set at the time the DtPdu is constructed, and cannot be set later. For details, please see 8.1.5 DtPdu Constructors.

8.1.4 Getting a PDU's Network Representation

DtPdu::packet() returns a pointer to a buffer containing the network representation of the PDU – the exact bytes that would be sent onto the network if you were to send a DtPdu at any given time. This is a pointer to an internal buffer that, in most circumstances, you should not try to modify. Use member functions to set values for particular fields.

However, if you want to directly change the data that will go out to the network, you can modify a network representation. If you do so, it is your responsibility to keep the data consistent and to ensure that the buffer represents a valid PDU.

Note
This will only work for those DtPdu classes that store their current data in their network representation buffer, (as opposed to storing it in some other representation and merely converting to network representation when packet() is called). While this has been the chosen implementation for all PDU classes so far, we might choose other implementations in the future.

8.1.5 DtPdu Constructors

Since DtPdu is an abstract class, you cannot directly create an instance of a DtPdu. You can only create instances of classes derived from DtPdu.

All PDU classes have at least two constructors, a blank PDU constructor, and a from-network-representation constructor.

8.1.5.1 Constructing a Blank PDU

The blank PDU constructor constructs a blank, minimal PDU. By minimal, we mean that for variable length PDUs the size is equal to the smallest size that a PDU of that type can legally have. For example, a DtDataPdu created with the blank PDU constructor has zero fixed datum fields and zero variable datum fields; a DtEntityStatePdu created with the blank PDU constructor contains zero articulated parts. By blank, we mean that all bytes of the PDU below the header typically contain zeros (although in some cases other values more accurately represent the concept of a field being blank).

8.1.5.2 PDU Header Information

When a blank PDU is constructed, the protocol version is set to the value of the VR-Link global variable DtProtocolVersionToSend, (declared in pdu.h). See DIS Versions for a list of version numbers.

PDU kind and protocol family depend on what type of PDU class you are creating (based on the value returned by the virtual function internalGetPduKind(), which must be defined for each derived PDU class). The length is the PDU's minimal length, as defined above. The exerciseId and time stamp are zero.

There is an optional buffer argument to the blank PDU constructor. In most cases, you can ignore this argument and use the default, DtUSE_INTERNAL_BUFFER. Blank DtPdu's are constructed as follows:

DtFirePdu pdu();

or:

DtFirePdu pdu;

Alternate uses of the buffer argument are outlined in 8.1.7 Using External Buffers in a PDU Class.

The blank PDU constructor is typically used when creating PDUs that you are sending. After creating the PDU, you can use its member functions to set the various data, then pass it to DtExerciseConn::sendStamped() for sending.

8.1.5.3 Constructing a PDU from a Network Representation

The from-network-representation constructor takes a pointer to a network representation of a PDU as an argument, and constructs a DtPdu object, which represents this PDU. By network representation, we mean a buffer containing the exact bytes that would be used to represent the PDU on the network.

It is rare that an application will need to directly use the from-network-representation constructor for a DtPdu. It is most commonly used only by a DtPduFactory, which is used by DtExerciseConn after receiving a packet from the network in order to construct the PDU class object that is passed to application callbacks.

Like the blank PDU constructor, the from-network-representation PDU class constructor has an optional buffer argument. This argument is used in the same way as the buffer argument to the blank PDU constructor, and is discussed in 8.1.7 Using External Buffers in a PDU Class.

Each of the PDU class from-network-representation constructors requires a network representation of that type of PDU as an argument. For instance, the DtFirePdu constructor specifically requires a DtNetFirePdu.

When constructing a DtPdu from a network representation through a PDU class constructor, the argument passed must be a valid network representation of a PDU. You can use the status() member function to determine whether a DtPdu represents valid data after construction. If status() returns DtSTATUS_OK, it represents valid data. Otherwise, results of using the DtPdu are undefined. The meaning of the value returned by status() depends on the type of PDU, and is meant to give some indication of why the PDU representation is invalid.

Currently, status is not verified by all DtPdu constructors. Therefore, a status of DtSTATUS_OK does not necessarily mean that a PDU is valid. However, a status other than that means that the PDU is not valid.

To construct a DtPdu object from a network representation of a PDU whose type you do not know, you can use the DtPduFactory::createPdu() member function (defined in pduFactory.h).

8.1.6 DtPduFactory

A DtPduFactory implements a kind of "virtual construction", by maintaining a table of associations between PDU kinds and functions that create a corresponding PDU class object.

The createPdu() function checks the PDU kind in the packet, then passes it and the buffer argument to the right creator function from the table. The creator in turn, is just a wrapper around the appropriate PDU class from-network-representation constructor (a constructor itself cannot be added to a table). A pointer to the newly constructed object is returned by createPdu().

If the PDU kind in a packet does not have an individual PDU class implemented (that is, there is no creator registered with the DtPduFactory for the PDU kind), a DtUnknownPdu is created. For more information, please see 8.2.1 Using DtUnknownPdu.

You can use DtExerciseConn::pduFactory() to get a pointer to the PDU factory that a DtExerciseConn uses to create PDU class objects.

Given a buffer containing the network representation of a PDU (perhaps you read it from a file), you can use this DtPduFactory to construct the right type of PDU class as follows:

DtExerciseConn exConn(...);
...
// buffer contains the network representation of some PDU
char *buffer = ...;
// Grab a pointer to our DtExerciseConn's DtPduFactory
DtPduFactory* fact = exConn.pduFactory();
// Cast the buffer to a DtNetPacket* before passing it to
// createPdu to indicate you are treating it as the network
// representation of a PDU.
DtPdu* pdu = fact->createPdu((DtNetPacket*) buffer);

The createPdu() function rejects packets (returns NULL) that have a protocol version less than DtProtocolVersionToRecvMin or greater than DtProtocolVersionToRecvMax. These are global variables declared in pdu.h. They default to 4 and 6 respectively. See DIS Versions for a list of version numbers.

By default, a DtExerciseConn creates an empty DtPduFactory, one that is not aware of any associations between PDU kinds and PDU classes. However, when you call a PDU class's addCallback() function, that class registers itself with the exercise connection's PDU factory. If you are using DtPduFactory, and are not using VR-Link's PDU callback mechanism, you must register creator functions with the DtPduFactory for all PDU classes you might want to work with.

Each PDU class has a static member function called create(), which can serve as that PDU's creator function. These creator functions have the following signature:

DtPdu *create(const DtNetPacket *initial,
DtBufferPtr buffer = DtUSE_INTERNAL_BUFFER);

So, for example, to change a DtExerciseConn's DtPduFactory so that it will create a DtEntityStatePdu to represent PDU kind number 10, you can do this:

DtExerciseConn exConn(...);
...
exConn.pduFactory()->addCreator(DtPduKind(10), DtEntityStatePdu::create);

A NULL creator function can be added for a PDU kind to indicate that there is no PDU class for that kind and DtUnknownPdu should be used instead.

8.1.7 Using External Buffers in a PDU Class

Each DtPdu object stores a network representation of the PDU it represents. It is a pointer to this network representation that is returned by packet(). Normally, the memory used for this network representation is allocated internally by the PDU class constructor and deleted by the destructor.

In some cases, you might want the DtPdu class to use a specific area of memory for storage of its network representation (for example, memory from the stack, or from a shared memory pool). For this purpose, all PDU class constructors have an optional buffer argument. You can cast a pointer to a particular area of memory to a DtBufferPtr and pass it as the buffer argument to indicate that a DtPdu should store its network representation there:

char buffer[144];
// construct blank PDU, but use buffer to store network
// representation
DtEntityStatePdu pdu(DtBufferPtr(buffer));

However, the most common reason why you would want to have a DtPdu use an external buffer for storage of its network representation is increased efficiency. When constructing a DtPdu from a network representation of a PDU, if you specify that the memory already occupied by the network representation be used by the PDU to store its network representation, a buffer copy may be avoided, for example:

DtNetFirePdu* netPacket = ......;
DtFirePdu(netPacket, DtBufferPtr(netPacket));

In fact, this is what DtExerciseConn does when it creates a PDU class object. It reads a packet from the network into a buffer, then instructs the PDU class object to use this buffer for its network representation. The data does not need to be copied into a buffer internal to the DtPdu.

When you pass a pointer to external memory to the blank DtPdu constructor, keep the following considerations in mind:

8.1.8 Copying PDUs

DtPdu has the copy constructor and assignment operator overloaded. You can copy data as follows:

DtPdu pdu1, pdu2;
pdu1 = pdu2;

pdu1 now contains the same data as pdu2. The two objects do not, however, share any data objects.

Classes derived from DtPdu do not usually have copy constructors or assignment operators. To ensure that data is copied correctly from one PDU to another, you must use the network representation of the PDU, for example:

DtFirePdu pdu1;
DtFirePdu pdu2((const DtNetFirePdu*)pdu1.netRep());

As with the standard PDU class constructors, the DtPdu constructor has an optional buffer argument. (For more information, please see 8.1.7 Using External Buffers in a PDU Class.) To copy a DtPdu using a buffer, do the following:

DtPdu pdu1;
DtPdu pdu2(pdu1, buffer);

To copy a PDU derived from DtPdu using an external buffer, do the following:

DtFirePdu pdu1;
int sz = pdu1.length();
char* bfr = new char[sz];
DtFirePdu pdu2((const DtNetFirePdu*)pdu1.netRep(), (DtBufferPtr)bfr);

8.1.9 Getting an Object ID

DtExerciseConn has a DIS-specific function – nextId(). The nextId() member function obtains an ID for use with an object that you are simulating. It returns an object of the type DtObjectId (defined in objectId.h). You can choose your own entity IDs rather than obtain them using nextId().

DtObjectId is typedef-ed to DtEntityIdentifier (defined in hostStructs.h) – a class that represents the DIS identification triplet of site ID, application number, and entity number. DtObjectID is used rather than DtEntityIdentifier or RTI::ObjectID() when we want to refer to an identifier in a protocol-independent fashion.

The DIS version returns an identifier consisting of the DtExerciseConn's site and host, and an entity number that is one greater than the one returned in the previous call to nextId(). There is no guarantee that the entity ID is unique in the DIS exercise.

[Home] [Top of Page] [Working with Non-Standard PDUs >>]


Document ID: Generated on Mon Apr 25 03:39:01 EDT 2022 from SVN revision 242502
Copyright © 2005-2021 MAK Technologies. All Rights Reserved (www.mak.com)