Simple modules are the active components in the model. Simple modules are programmed in C++, using the OMNeT++ class library. The following sections contain a short introduction to discrete event simulation in general, explain how its concepts are implemented in OMNeT++, and give an overview and practical advice on how to design and code simple modules.
This section contains a very brief introduction into how discrete event simulation (DES) works, in order to introduce terms we'll use when explaining OMNeT++ concepts and implementation.
A discrete event system is a system where state changes (events) happen at discrete instances in time, and events take zero time to happen. It is assumed that nothing (i.e. nothing interesting) happens between two consecutive events, that is, no state change takes place in the system between the events. This is in contrast to continuous systems where state changes are continuous. Systems that can be viewed as discrete event systems can be modeled using discrete event simulation, DES.
For example, computer networks are usually viewed as discrete event systems. Some of the events are:
This implies that between two events such as start of a packet transmission and end of a packet transmission, nothing interesting happens. That is, the packet's state remains being transmitted. Note that the definition of “interesting” events and states always depends on the intent and purposes of the modeler. If we were interested in the transmission of individual bits, we would have included something like start of bit transmission and end of bit transmission among our events.
The time when events occur is often called event timestamp; with OMNeT++ we use the term arrival time (because in the class library, the word “timestamp” is reserved for a user-settable attribute in the event class). Time within the model is often called simulation time, model time or virtual time as opposed to real time or CPU time which refer to how long the simulation program has been running and how much CPU time it has consumed.
Discrete event simulation maintains the set of future events in a data structure often called FES (Future Event Set) or FEL (Future Event List). Such simulators usually work according to the following pseudocode:
initialize -- this includes building the model and inserting initial events to FES while (FES not empty and simulation not yet complete) { retrieve first event from FES t:= timestamp of this event process event (processing may insert new events in FES or delete existing ones) } finish simulation (write statistical results, etc.)
The initialization step usually builds the data structures representing the simulation model, calls any user-defined initialization code, and inserts initial events into the FES to ensure that the simulation can start. Initialization strategies can differ considerably from one simulator to another.
The subsequent loop consumes events from the FES and processes them. Events are processed in strict timestamp order to maintain causality, that is, to ensure that no current event may have an effect on earlier events.
Processing an event involves calls to user-supplied code. For example, using the computer network simulation example, processing a “timeout expired” event may consist of re-sending a copy of the network packet, updating the retry count, scheduling another “timeout” event, and so on. The user code may also remove events from the FES, for example when canceling timeouts.
The simulation stops when there are no events left (this rarely happens in practice), or when it isn't necessary for the simulation to run further because the model time or the CPU time has reached a given limit, or because the statistics have reached the desired accuracy. At this time, before the program exits, the user will typically want to record statistics into output files.
OMNeT++ uses messages to represent
events.
Events are consumed from the FES in arrival time order, to maintain causality. More precisely, given two messages, the following rules apply:
Scheduling priority is a user-assigned integer attribute of messages.
The current simulation time can be obtained with the simTime() function.
Simulation time in OMNeT++ is represented by the C++ type simtime_t, which is by default a typedef to the SimTime class. SimTime class stores simulation time in a 64-bit integer, using decimal fixed-point representation. The resolution is controlled by the scale exponent global configuration variable; that is, SimTime instances have the same resolution. The exponent can be chosen between -18 (attosecond resolution) and 0 (seconds). Some exponents with the ranges they provide are shown in the following table.
Exponent | Resolution | Approx. Range |
-18 | 10-18s (1as) | +/- 9.22s |
-15 | 10-15s (1fs) | +/- 153.72 minutes |
-12 | 10-12s (1ps) | +/- 106.75 days |
-9 | 10-9s (1ns) | +/- 292.27 years |
-6 | 10-6s (1us) | +/- 292271 years |
-3 | 10-3s (1ms) | +/- 2.9227e8 years |
0 | 1s | +/- 2.9227e11 years |
Note that although simulation time cannot be negative, it is still useful to be able to represent negative numbers, because they often arise during the evaluation of arithmetic expressions.
There is no implicit conversion from SimTime to double, mostly because it would conflict with overloaded arithmetic operations of SimTime; use the dbl() method of SimTime or the SIMTIME_DBL() macro to convert. To reduce the need for dbl(), several functions and methods have overloaded variants that directly accept SimTime, for example fabs(), fmod(), div(), ceil(), floor(), uniform(), exponential(), and normal().
Other useful methods of SimTime include str(), which returns the value as a string; parse(), which converts a string to SimTime; raw(), which returns the underlying 64-bit integer; getScaleExp(), which returns the global scale exponent; isZero(), which tests whether the simulation time is 0; and getMaxTime(), which returns the maximum simulation time that can be represented at the current scale exponent. Zero and the maximum simulation time are also accessible via the SIMTIME_ZERO and SIMTIME_MAX macros.
// 340 microseconds in the future, truncated to millisecond boundary simtime_t timeout = (simTime() + SimTime(340, SIMTIME_US)).trunc(SIMTIME_MS);
The implementation of the FES is a crucial factor in the performance of a discrete event simulator. In OMNeT++, the FES is replaceable, and the default FES implementation uses binary heap as data structure. Binary heap is generally considered to be the best FES algorithm for discrete event simulation, as it provides a good, balanced performance for most workloads. (Exotic data structures like skiplist may perform better than heap in some cases.)
OMNeT++ simulation models are composed of modules and connections. Modules may be simple (atomic) modules or compound modules; simple modules are the active components in a model, and their behaviour is defined by the user as C++ code. Connections may have associated channel objects. Channel objects encapsulate channel behavior: propagation and transmission time modeling, error modeling, and possibly others. Channels are also programmable in C++ by the user.
Modules and channels are represented with the cModule and cChannel classes, respectively. cModule and cChannel are both derived from the cComponent class.
The user defines simple module types by subclassing cSimpleModule. Compound modules are instantiated with cModule, although the user can override it with @class in the NED file, and can even use a simple module C++ class (i.e. one derived from cSimpleModule) for a compound module.
The cChannel's subclasses include the three built-in channel types: cIdealChannel, cDelayChannel and cDatarateChannel. The user can create new channel types by subclassing cChannel or any other channel class.
The following inheritance diagram illustrates the relationship of the classes mentioned above.
Simple modules and channels can be programmed by redefining certain member functions, and providing your own code in them. Some of those member functions are declared on cComponent, the common base class of channels and modules.
cComponent has the following member functions meant for redefining in subclasses:
initialize() and finish(), together with initialize()'s variants for multi-stage initialization, will be covered in detail in section [4.3.3].
In OMNeT++, events occur inside simple modules. Simple modules encapsulate C++ code that generates events and reacts to events, implementing the behaviour of the module.
To define the dynamic behavior of a simple module, one of the following member functions need to be overridden:
Modules written with activity() and handleMessage() can be freely mixed within a simulation model. Generally, handleMessage() should be preferred to activity(), due to scalability and other practical reasons. The two functions will be described in detail in sections [4.4.1] and [4.4.2], including their advantages and disadvantages.
The behavior of channels can also be modified by redefining member functions. However, the channel API is slightly more complicated than that of simple modules, so we'll describe it in a later section ([4.8]).
Last, let us mention refreshDisplay(), which is related to updating the visual appearance of the simulation when run under a graphical user interface. refreshDisplay() is covered in the chapter that deals with simulation visualization ([8.2]).
As mentioned before, a simple module is nothing more than a C++ class which has to be subclassed from cSimpleModule, with one or more virtual member functions redefined to define its behavior.
The class has to be registered with OMNeT++ via the Define_Module() macro. The Define_Module() line should always be put into .cc or .cpp files and not header file (.h), because the compiler generates code from it.
The following HelloModule is about the simplest simple module one could write. (We could have left out the initialize() method as well to make it even smaller, but how would it say Hello then?) Note cSimpleModule as base class, and the Define_Module() line.
// file: HelloModule.cc #include <omnetpp.h> using namespace omnetpp; class HelloModule : public cSimpleModule { protected: virtual void initialize(); virtual void handleMessage(cMessage *msg); }; // register module class with OMNeT++ Define_Module(HelloModule); void HelloModule::initialize() { EV << "Hello World!\n"; } void HelloModule::handleMessage(cMessage *msg) { delete msg; // just discard everything we receive }
In order to be able to refer to this simple module type in NED files, we also need an associated NED declaration which might look like this:
// file: HelloModule.ned simple HelloModule { gates: input in; }
Simple modules are never instantiated by the user directly, but rather by the simulation kernel. This implies that one cannot write arbitrary constructors: the signature must be what is expected by the simulation kernel. Luckily, this contract is very simple: the constructor must be public, and must take no arguments:
public: HelloModule(); // constructor takes no arguments
cSimpleModule itself has two constructors:
The first version should be used with handleMessage() simple modules, and the second one with activity() modules. (With the latter, the activity() method of the module class runs as a coroutine which needs a separate CPU stack, usually of 16..32K. This will be discussed in detail later.) Passing zero stack size to the latter constructor also selects handleMessage().
Thus, the following constructor definitions are all OK, and select handleMessage() to be used with the module:
HelloModule::HelloModule() {...} HelloModule::HelloModule() : cSimpleModule() {...}
It is also OK to omit the constructor altogether, because the compiler-generated one is suitable too.
The following constructor definition selects activity() to be used with the module, with 16K of coroutine stack:
HelloModule::HelloModule() : cSimpleModule(16384) {...}
The initialize() and finish() methods are declared as part of cComponent, and provide the user the opportunity of running code at the beginning and at successful termination of the simulation.
The reason initialize() exists is that usually you cannot put simulation-related code into the simple module constructor, because the simulation model is still being setup when the constructor runs, and many required objects are not yet available. In contrast, initialize() is called just before the simulation starts executing, when everything else has been set up already.
finish() is for recording statistics, and it only gets called when the simulation has terminated normally. It does not get called when the simulations stops with an error message. The destructor always gets called at the end, no matter how the simulation stopped, but at that time it is fair to assume that the simulation model has been halfway demolished already.
Based on the above considerations, the following usage conventions exist for these four methods:
Set pointer members of the module class to nullptr; postpone all other initialization tasks to initialize().
Perform all initialization tasks: read module parameters, initialize class variables, allocate dynamic data structures with new; also allocate and initialize self-messages (timers) if needed.
Record statistics. Do not delete anything or cancel timers -- all cleanup must be done in the destructor.
Delete everything which was allocated by new and is still held by the module class. With self-messages (timers), use the cancelAndDelete(msg) function! It is almost always wrong to just delete a self-message from the destructor, because it might be in the scheduled events list. The cancelAndDelete(msg) function checks for that first, and cancels the message before deletion if necessary.
OMNeT++ prints the list of unreleased objects at the end of the simulation. When a simulation model dumps "undisposed object ..." messages, this indicates that the corresponding module destructors should be fixed. As a temporary measure, these messages may be hidden by setting print-undisposed=false in the configuration.
The initialize() functions of the modules are invoked before the first event is processed, but after the initial events (starter messages) have been placed into the FES by the simulation kernel.
Both simple and compound modules have initialize() functions. A compound module's initialize() function runs before that of its submodules.
The finish() functions are called when the event loop has terminated, and only if it terminated normally.
The calling order for finish() is the reverse of the order of
initialize(): first submodules, then the encompassing compound module.
This is summarized in the following pseudocode:
perform simulation run: build network (i.e. the system module and its submodules recursively) insert starter messages for all submodules using activity() do callInitialize() on system module enter event loop // (described earlier) if (event loop terminated normally) // i.e. no errors do callFinish() on system module clean up callInitialize() { call to user-defined initialize() function if (module is compound) for (each submodule) do callInitialize() on submodule } callFinish() { if (module is compound) for (each submodule) do callFinish() on submodule call to user-defined finish() function }
Keep in mind that finish() is not always called, so it isn't a good place for cleanup code which should run every time the module is deleted. finish() is only a good place for writing statistics, result post-processing and other operations which are supposed to run only on successful completion. Cleanup code should go into the destructor.
In simulation models where one-stage initialization provided by initialize() is not sufficient, one can use multi-stage initialization. Modules have two functions which can be redefined by the user:
virtual void initialize(int stage); virtual int numInitStages() const;
At the beginning of the simulation, initialize(0)
is called for all modules, then initialize(1),
initialize(2), etc. You can think of it like
initialization takes place in several “waves”. For each module,
numInitStages() must be redefined to return the number of init
stages required, e.g. for a two-stage init, numInitStages()
should return 2, and initialize(int stage) must be implemented to
handle the stage=0 and stage=1 cases.
The callInitialize() function performs the full multi-stage initialization for that module and all its submodules.
If you do not redefine the multi-stage initialization functions, the default behavior is single-stage initialization: the default numInitStages() returns 1, and the default initialize(int stage) simply calls initialize().
The task of finish() is implemented in several other simulators by introducing a special end-of-simulation event. This is not a very good practice because the simulation programmer has to code the models (often represented as FSMs) so that they can always properly respond to end-of-simulation events, in whichever state they are. This often makes program code unnecessarily complicated. For this reason OMNeT++ does not use the end of simulation event.
This can also be witnessed in the design of the PARSEC simulation language (UCLA). Its predecessor Maisie used end-of-simulation events, but -- as documented in the PARSEC manual -- this has led to awkward programming in many cases, so for PARSEC end-of-simulation events were dropped in favour of finish() (called finalize() in PARSEC).
This section discusses cSimpleModule's previously mentioned handleMessage() and activity() member functions, intended to be redefined by the user.
The idea is that at each event (message arrival) we simply call a user-defined function. This function, handleMessage(cMessage *msg) is a virtual member function of cSimpleModule which does nothing by default -- the user has to redefine it in subclasses and add the message processing code.
The handleMessage() function will be called for every message that arrives at the module. The function should process the message and return immediately after that. The simulation time is potentially different in each call. No simulation time elapses within a call to handleMessage().
The event loop inside the simulator handles both activity() and handleMessage() simple modules, and it corresponds to the following pseudocode:
while (FES not empty and simulation not yet complete) { retrieve first event from FES t:= timestamp of this event m:= module containing this event if (m works with handleMessage()) m->handleMessage( event ) else // m works with activity() transferTo( m ) }
Modules with handleMessage() are NOT started automatically: the simulation kernel creates starter messages only for modules with activity(). This means that you have to schedule self-messages from the initialize() function if you want a handleMessage() simple module to start working “by itself”, without first receiving a message from other modules.
To use the handleMessage() mechanism in a simple module, you must specify zero stack size for the module. This is important, because this tells OMNeT++ that you want to use handleMessage() and not activity().
Message/event related functions you can use in handleMessage():
The receive() and wait() functions cannot be used in handleMessage(), because they are coroutine-based by nature, as explained in the section about activity().
You have to add data members to the module class for every piece of information you want to preserve. This information cannot be stored in local variables of handleMessage() because they are destroyed when the function returns. Also, they cannot be stored in static variables in the function (or the class), because they would be shared between all instances of the class.
Data members to be added to the module class will typically include things like:
These variables are often initialized from the initialize() method, because the information needed to obtain the initial value (e.g. module parameters) may not yet be available at the time the module constructor runs.
Another task to be done in initialize() is to schedule initial event(s) which trigger the first call(s) to handleMessage(). After the first call, handleMessage() must take care to schedule further events for itself so that the “chain” is not broken. Scheduling events is not necessary if your module only has to react to messages coming from other modules.
finish() is normally used to record statistics information accumulated in data members of the class at the end of the simulation.
handleMessage() is in most cases a better choice than activity():
Models of protocol layers in a communication network tend to have a common structure on a high level because fundamentally they all have to react to three types of events: to messages arriving from higher layer protocols (or apps), to messages arriving from lower layer protocols (from the network), and to various timers and timeouts (that is, self-messages).
This usually results in the following source code pattern:
class FooProtocol : public cSimpleModule { protected: // state variables // ... virtual void processMsgFromHigherLayer(cMessage *packet); virtual void processMsgFromLowerLayer(FooPacket *packet); virtual void processTimer(cMessage *timer); virtual void initialize(); virtual void handleMessage(cMessage *msg); }; // ... void FooProtocol::handleMessage(cMessage *msg) { if (msg->isSelfMessage()) processTimer(msg); else if (msg->arrivedOn("fromNetw")) processMsgFromLowerLayer(check_and_cast<FooPacket *>(msg)); else processMsgFromHigherLayer(msg); }
The functions processMsgFromHigherLayer(), processMsgFromLowerLayer() and processTimer() are then usually split further: there are separate methods to process separate packet types and separate timers.
The code for simple packet generators and sinks programmed with handleMessage() might be as simple as the following pseudocode:
PacketGenerator::handleMessage(msg) { create and send out a new packet; schedule msg again to trigger next call to handleMessage; } PacketSink::handleMessage(msg) { delete msg; }
Note that PacketGenerator will need to redefine initialize() to create m and schedule the first event.
The following simple module generates packets with exponential inter-arrival time. (Some details in the source haven't been discussed yet, but the code is probably understandable nevertheless.)
class Generator : public cSimpleModule { public: Generator() : cSimpleModule() {} protected: virtual void initialize(); virtual void handleMessage(cMessage *msg); }; Define_Module(Generator); void Generator::initialize() { // schedule first sending scheduleAt(simTime(), new cMessage); } void Generator::handleMessage(cMessage *msg) { // generate & send packet cMessage *pkt = new cMessage; send(pkt, "out"); // schedule next call scheduleAt(simTime()+exponential(1.0), msg); }
A bit more realistic example is to rewrite our Generator to create packet bursts, each consisting of burstLength packets.
We add some data members to the class:
The code:
class BurstyGenerator : public cSimpleModule { protected: int burstLength; int burstCounter; virtual void initialize(); virtual void handleMessage(cMessage *msg); }; Define_Module(BurstyGenerator); void BurstyGenerator::initialize() { // init parameters and state variables burstLength = par("burstLength"); burstCounter = burstLength; // schedule first packet of first burst scheduleAt(simTime(), new cMessage); } void BurstyGenerator::handleMessage(cMessage *msg) { // generate & send packet cMessage *pkt = new cMessage; send(pkt, "out"); // if this was the last packet of the burst if (--burstCounter == 0) { // schedule next burst burstCounter = burstLength; scheduleAt(simTime()+exponential(5.0), msg); } else { // schedule next sending within burst scheduleAt(simTime()+exponential(1.0), msg); } }
Pros:
Cons:
Usually, handleMessage() should be preferred over activity().
Many simulation packages use a similar approach, often topped with something like a state machine (FSM) which hides the underlying function calls. Such systems are:
OMNeT++'s FSM support is described in the next section.
With activity(), a simple module can be coded much like an operating system process or thread. One can wait for an incoming message (event) at any point of the code, suspend the execution for some time (model time!), etc. When the activity() function exits, the module is terminated. (The simulation can continue if there are other modules which can run.)
The most important functions that can be used in activity() are (they will be discussed in detail later):
The activity() function normally contains an infinite loop, with at least a wait() or receive() call in its body.
Generally you should prefer handleMessage() to activity(). The main problem with activity() is that it doesn't scale because every module needs a separate coroutine stack. It has also been observed that activity() does not encourage a good programming style, and stack switching also confuses many debuggers.
There is one scenario where activity()'s process-style description is convenient: when the process has many states but transitions are very limited, i.e. from any state the process can only go to one or two other states. For example, this is the case when programming a network application, which uses a single network connection. The pseudocode of the application which talks to a transport layer protocol might look like this:
activity() { while(true) { open connection by sending OPEN command to transport layer receive reply from transport layer if (open not successful) { wait(some time) continue // loop back to while() } while (there is more to do) { send data on network connection if (connection broken) { continue outer loop // loop back to outer while() } wait(some time) receive data on network connection if (connection broken) { continue outer loop // loop back to outer while() } wait(some time) } close connection by sending CLOSE command to transport layer if (close not successful) { // handle error } wait(some time) } }
If there is a need to handle several connections concurrently, dynamically creating simple modules to handle each is an option. Dynamic module creation will be discussed later.
There are situations when you certainly do not want to use activity(). If the activity() function contains no wait() and it has only one receive() at the top of a message handling loop, there is no point in using activity(), and the code should be written with handleMessage(). The body of the loop would then become the body of handleMessage(), state variables inside activity() would become data members in the module class, and they would be initialized in initialize().
Example:
void Sink::activity() { while(true) { msg = receive(); delete msg; } }
should rather be programmed as:
void Sink::handleMessage(cMessage *msg) { delete msg; }
activity() is run in a coroutine. Coroutines are similar to threads, but are scheduled non-preemptively (this is also called cooperative multitasking). One can switch from one coroutine to another coroutine by a transferTo(otherCoroutine) call, causing the first coroutine to be suspended and second one to run. Later, when the second coroutine performs a transferTo(firstCoroutine) call to the first one, the execution of the first coroutine will resume from the point of the transferTo(otherCoroutine) call. The full state of the coroutine, including local variables are preserved while the thread of execution is in other coroutines. This implies that each coroutine has its own CPU stack, and transferTo() involves a switch from one CPU stack to another.
Coroutines are at the heart of OMNeT++, and the simulation programmer doesn't ever need to call transferTo() or other functions in the coroutine library, nor does he need to care about the coroutine library implementation. It is important to understand, however, how the event loop found in discrete event simulators works with coroutines.
When using coroutines, the event loop looks like this (simplified):
while (FES not empty and simulation not yet complete) { retrieve first event from FES t:= timestamp of this event transferTo(module containing the event) }
That is, when a module has an event, the simulation kernel transfers the control to the module's coroutine. It is expected that when the module “decides it has finished the processing of the event”, it will transfer the control back to the simulation kernel by a transferTo(main) call. Initially, simple modules using activity() are “booted” by events (''starter messages'') inserted into the FES by the simulation kernel before the start of the simulation.
How does the coroutine know it has “finished processing the event”? The answer: when it requests another event. The functions which request events from the simulation kernel are the receive() and wait(), so their implementations contain a transferTo(main) call somewhere.
Their pseudocode, as implemented in OMNeT++:
receive() { transferTo(main) retrieve current event return the event // remember: events = messages } wait() { create event e schedule it at (current sim. time + wait interval) transferTo(main) retrieve current event if (current event is not e) { error } delete e // note: actual impl. reuses events return }
Thus, the receive() and wait() calls are special points in the activity() function, because they are where
Modules written with activity() need starter messages to “boot”. These starter messages are inserted into the FES automatically by OMNeT++ at the beginning of the simulation, even before the initialize() functions are called.
The simulation programmer needs to define the CPU stack size for coroutines. This cannot be automated.
16 or 32 kbytes is usually a good choice, but more space may be needed if the module uses recursive functions or has many/large local variables. OMNeT++ has a built-in mechanism that will usually detect if the module stack is too small and overflows. OMNeT++ can also report how much stack space a module actually uses at runtime.
Because local variables of activity() are preserved across events, you can store everything (state information, packet buffers, etc.) in them. Local variables can be initialized at the top of the activity() function, so there isn't much need to use initialize().
You do need finish(), however, if you want to write statistics at the end of the simulation. Because finish() cannot access the local variables of activity(), you have to put the variables and objects containing the statistics into the module class. You still don't need initialize() because class members can also be initialized at the top of activity().
Thus, a typical setup looks like this in pseudocode:
class MySimpleModule... { ... variables for statistics collection activity(); finish(); }; MySimpleModule::activity() { declare local vars and initialize them initialize statistics collection variables while(true) { ... } } MySimpleModule::finish() { record statistics into file }
Pros:
Cons:
In most cases, cons outweigh pros and it is a better idea to use handleMessage() instead.
Coroutines are used by a number of other simulation packages:
If possible, avoid using global variables, including static class members. They are prone to cause several problems. First, they are not reset to their initial values (to zero) when you rebuild the simulation in Tkenv/Qtenv, or start another run in Cmdenv. This may produce surprising results. Second, they prevent you from parallelizing the simulation. When using parallel simulation, each partition of the model runs in a separate process, having their own copies of global variables. This is usually not what you want.
The solution is to encapsulate the variables into simple modules as private or protected data members, and expose them via public methods. Other modules can then call these public methods to get or set the values. Calling methods of other modules will be discussed in section [4.12]. Examples of such modules are the Blackboard in the Mobility Framework, and InterfaceTable and RoutingTable in the INET Framework.
The code of simple modules can be reused via subclassing, and redefining virtual member functions. An example:
class TransportProtocolExt : public TransportProtocol { protected: virtual void recalculateTimeout(); }; Define_Module(TransportProtocolExt); void TransportProtocolExt::recalculateTimeout() { //... }
The corresponding NED declaration:
simple TransportProtocolExt extends TransportProtocol { @class(TransportProtocolExt); // Important! }
Module parameters declared in NED files are represented with the cPar class at runtime, and be accessed by calling the par() member function of cComponent:
cPar& delayPar = par("delay");
cPar's value can be read with methods that correspond to the parameter's NED type: boolValue(), longValue(), doubleValue(), stringValue(), stdstringValue(), xmlValue(). There are also overloaded type cast operators for the corresponding types (bool; integer types including int, long, etc; double; const char *; cXMLElement *).
long numJobs = par("numJobs").longValue(); double processingDelay = par("processingDelay"); // using operator double()
Note that cPar has two methods for returning a string value: stringValue(), which returns const char *, and stdstringValue(), which returns std::string. For volatile parameters, only stdstringValue() may be used, but otherwise the two are interchangeable.
If you use the par("foo") parameter in expressions (such as 4*par("foo")+2), the C++ compiler may be unable to decide between overloaded operators and report ambiguity. This issue can be resolved by adding an explicit cast such as (double)par("foo"), or using the doubleValue() or longValue() methods.
A parameter can be declared volatile in the NED file. The volatile modifier indicates that a parameter is re-read every time a value is needed during simulation. Volatile parameters typically are used for things like random packet generation interval, and are assigned values like exponential(1.0) (numbers drawn from the exponential distribution with mean 1.0).
In contrast, non-volatile NED parameters are constants, and reading their values multiple times is guaranteed to yield the same value. When a non-volatile parameter is assigned a random value like exponential(1.0), it is evaluated once at the beginning of the simulation and replaced with the result, so all reads will get same (randomly generated) value.
The typical usage for non-volatile parameters is to read them in the initialize() method of the module class, and store the values in class variables for easy access later:
class Source : public cSimpleModule { protected: long numJobs; virtual void initialize(); ... }; void Source::initialize() { numJobs = par("numJobs"); ... }
volatile parameters need to be re-read every time the value is needed. For example, a parameter that represents a random packet generation interval may be used like this:
void Source::handleMessage(cMessage *msg) { ... scheduleAt(simTime() + par("interval").doubleValue(), timerMsg); ... }
This code looks up the the parameter by name every time. This lookup can be avoided by storing the parameter object's pointer in a class variable, resulting in the following code:
class Source : public cSimpleModule { protected: cPar *intervalp; virtual void initialize(); virtual void handleMessage(cMessage *msg); ... }; void Source::initialize() { intervalp = &par("interval"); ... } void Source::handleMessage(cMessage *msg) { ... scheduleAt(simTime() + intervalp->doubleValue(), timerMsg); ... }
Parameter values can be changed from the program, during execution. This is rarely needed, but may be useful for some scenarios.
The methods to set the parameter value are setBoolValue(), setLongValue(), setStringValue(), setDoubleValue(), setXMLValue(). There are also overloaded assignment operators for various types including bool, int, long, double, const char *, and cXMLElement *.
To allow a module to be notified about parameter changes, override its handleParameterChange() method, see [4.5.5].
The parameter's name and type are returned by the getName() and getType() methods. The latter returns a value from an enum, which can be converted to a readable string with the getTypeName() static method. The enum values are BOOL, DOUBLE, LONG, STRING and XML; and since the enum is an inner type, they usually have to be qualified with cPar::.
isVolatile() returns whether the parameter was declared volatile in the NED file. isNumeric() returns true if the parameter type is double or long.
The str() method returns the parameter's value in a string form. If the parameter contains an expression, then the string representation of the expression is returned.
An example usage of the above methods:
int n = getNumParams(); for (int i = 0; i < n; i++) { cPar& p = par(i); EV << "parameter: " << p.getName() << "\n"; EV << " type:" << cPar::getTypeName(p.getType()) << "\n"; EV << " contains:" << p.str() << "\n"; }
The NED properties of a parameter can be accessed with the getProperties() method that returns a pointer to the cProperties object that stores the properties of this parameter. Specifically, getUnit() returns the unit of measurement associated with the parameter (@unit property in NED).
Further cPar methods and related classes like cExpression and cDynamicExpression are used by the NED infrastructure to set up and assign parameters. They are documented in the API Reference, but they are normally of little interest to users.
As of version 4.2, OMNeT++ does not support parameter arrays, but in practice they can be emulated using string parameters. One can assign the parameter a string which contains all values in a textual form (for example, "0 1.234 3.95 5.467"), then parse this string in the simple module.
The cStringTokenizer class can be quite useful for this purpose. The constructor accepts a string, which it regards as a sequence of tokens (words) separated by delimiter characters (by default, spaces). Then you can either enumerate the tokens and process them one by one (hasMoreTokens(), nextToken()), or use one of the cStringTokenizer convenience methods to convert them into a vector of strings (asVector()), integers (asIntVector()), or doubles (asDoubleVector()).
The latter methods can be used like this:
const char *vstr = par("v").stringValue(); // e.g. "aa bb cc"; std::vector<std::string> v = cStringTokenizer(vstr).asVector();
and
const char *str = "34 42 13 46 72 41"; std::vector<int> v = cStringTokenizer().asIntVector(); const char *str = "0.4311 0.7402 0.7134"; std::vector<double> v = cStringTokenizer().asDoubleVector();
The following example processes the string by enumerating the tokens:
const char *str = "3.25 1.83 34 X 19.8"; // input std::vector<double> result; cStringTokenizer tokenizer(str); while (tokenizer.hasMoreTokens()) { const char *token = tokenizer.nextToken(); if (strcmp(token, "X")==0) result.push_back(DEFAULT_VALUE); else result.push_back(atof(token)); }
It is possible for modules to be notified when the value of a parameter changes at runtime, possibly due to another module dynamically changing it. A typical use is to re-read the changed parameter, and update the module's state if needed.
To enable notification, redefine the handleParameterChange() method of the module class. This method will be called back by the simulation kernel when a module parameter changes, except during initialization of the given module.
The method signature is the following:
void handleParameterChange(const char *parameterName);
The following example shows a module that re-reads its serviceTime parameter when its value changes:
void Queue::handleParameterChange(const char *parname) { if (strcmp(parname, "serviceTime")==0) serviceTime = par("serviceTime"); // refresh data member }
If your code heavily depends on notifications and you would like to receive notifications during initialization or finalization as well, one workaround is to explicitly call handleParameterChange() from the initialize() or finish() function:
for (int i = 0; i < getNumParams(); i++) handleParameterChange(par(i).getName());
Module gates are represented by cGate objects. Gate objects know to which other gates they are connected, and what are the channel objects associated with the links.
The cModule class has a number of member functions that deal with gates. You can look up a gate by name using the gate() method:
cGate *outGate = gate("out");
This works for input and output gates. However, when a gate was declared inout in NED, it is actually represented by the simulation kernel with two gates, so the above call would result in a gate not found error. The gate() method needs to be told whether the input or the output half of the gate you need. This can be done by appending the "$i" or "$o" to the gate name. The following example retrieves the two gates for the inout gate "g":
cGate *gIn = gate("g$i"); cGate *gOut = gate("g$o");
Another way is to use the gateHalf() function, which takes the inout gate's name plus either cGate::INPUT or cGate::OUTPUT:
cGate *gIn = gateHalf("g", cGate::INPUT); cGate *gOut = gateHalf("g", cGate::OUTPUT);
These methods throw an error if the gate does not exist, so they cannot be used to determine whether the module has a particular gate. For that purpose there is a hasGate() method. An example:
if (hasGate("optOut")) send(new cMessage(), "optOut");
A gate can also be identified and looked up by a numeric gate ID. You can get the ID from the gate itself (getId() method), or from the module by gate name (findGate() method). The gate() method also has an overloaded variant which returns the gate from the gate ID.
int gateId = gate("in")->getId(); // or: int gateId = findGate("in");
As gate IDs are more useful with gate vectors, we'll cover them in detail in a later section.
Gate vectors possess one cGate object per element. To access individual gates in the vector, you need to call the gate() function with an additional index parameter. The index should be between zero and size-1. The size of the gate vector can be read with the gateSize() method. The following example iterates through all elements in the gate vector:
for (int i = 0; i < gateSize("out"); i++) { cGate *gate = gate("out", i); //... }
A gate vector cannot have “holes” in it; that is, gate() never returns nullptr or throws an error if the gate vector exists and the index is within bounds.
For inout gates, gateSize() may be called with or without the "$i"/"$o" suffix, and returns the same number.
The hasGate() method may be used both with and without an index, and they mean two different things: without an index it tells the existence of a gate vector with the given name, regardless of its size (it returns true for an existing vector even if its size is currently zero!); with an index it also examines whether the index is within the bounds.
A gate can also be accessed by its ID. A very important property of gate IDs is that they are contiguous within a gate vector, that is, the ID of a gate g[k] can be calculated as the ID of g[0] plus k. This allows you to efficiently access any gate in a gate vector, because retrieving a gate by ID is more efficient than by name and index. The index of the first gate can be obtained with gate("out",0)->getId(), but it is better to use a dedicated method, gateBaseId(), because it also works when the gate vector size is zero.
Two further important properties of gate IDs: they are stable and unique (within the module). By stable we mean that the ID of a gate never changes; and by unique we not only mean that at any given time no two gates have the same IDs, but also that IDs of deleted gates do not get reused later, so gate IDs are unique in the lifetime of a simulation run.
The following example iterates through a gate vector, using IDs:
int baseId = gateBaseId("out"); int size = gateSize("out"); for (int i = 0; i < size; i++) { cGate *gate = gate(baseId + i); //... }
If you need to go through all gates of a module, there are two possibilities. One is invoking the getGateNames() method that returns the names of all gates and gate vectors the module has; then you can call isGateVector(name) to determine whether individual names identify a scalar gate or a gate vector; then gate vectors can be enumerated by index. Also, for inout gates getGateNames() returns the base name without the "$i"/"$o" suffix, so the two directions need to be handled separately. The gateType(name) method can be used to test whether a gate is inout, input or output (it returns cGate::INOUT, cGate::INPUT, or cGate::OUTPUT).
Clearly, the above solution can be quite difficult. An alternative is to use the GateIterator class provided by cModule. It goes like this:
for (cModule::GateIterator i(this); !i.end(); i++) { cGate *gate = *i; ... }
Where this denotes the module whose gates are being enumerated (it can be replaced by any cModule * variable).
Although rarely needed, it is possible to add and remove gates during simulation. You can add scalar gates and gate vectors, change the size of gate vectors, and remove scalar gates and whole gate vectors. It is not possible to remove individual random gates from a gate vector, to remove one half of an inout gate (e.g. "gate$o"), or to set different gate vector sizes on the two halves of an inout gate vector.
The cModule methods for adding and removing gates are addGate(name,type,isvector=false) and deleteGate(name). Gate vector size can be changed by using setGateSize(name,size). None of these methods accept "$i" / "$o" suffix in gate names.
The getName() method of cGate returns the name of the gate or gate vector without the index. If you need a string that contains the gate index as well, getFullName() is what you want. If you also want to include the hierarchical name of the owner module, call getFullPath().
The getType() method of cGate returns the gate type, either cGate::INPUT or cGate::OUTPUT. (It cannot return cGate::INOUT, because an inout gate is represented by a pair of cGates.)
If you have a gate that represents half of an inout gate (that is, getName() returns something like "g$i" or "g$o"), you can split the name with the getBaseName() and getNameSuffix() methods. getBaseName() method returns the name without the $i/$o suffix; and getNameSuffix() returns just the suffix (including the dollar sign). For normal gates, getBaseName() is the same as getName(), and getNameSuffix() returns the empty string.
The isVector(), getIndex(), getVectorSize() speak for themselves; size() is an alias to getVectorSize(). For non-vector gates, getIndex() returns 0 and getVectorSize() returns 1.
The getId() method returns the gate ID (not to be confused with the gate index).
The getOwnerModule() method returns the module the gate object belongs to.
To illustrate these methods, we expand the gate iterator example to print some information about each gate:
for (cModule::GateIterator i(this); !i.end(); i++) { cGate *gate = *i; EV << gate->getFullName() << ": "; EV << "id=" << gate->getId() << ", "; if (!gate->isVector()) EV << "scalar gate, "; else EV << "gate " << gate->getIndex() << " in vector " << gate->getName() << " of size " << gate->getVectorSize() << ", "; EV << "type:" << cGate::getTypeName(gate->getType()); EV << "\n"; }
There are further cGate methods to access and manipulate the connection(s) attached to the gate; they will be covered in the following sections.
Simple module gates have normally one connection attached. Compound module gates, however, need to be connected both inside and outside of the module to be useful. A series of connections (joined with compound module gates) is called a connection path or just path. A path is directed, and it normally starts at an output gate of a simple module, ends at an input gate of a simple module, and passes through several compound module gates.
Every cGate object contains pointers to the previous gate and the next gate in the path (returned by the getPreviousGate() and getNextGate() methods), so a path can be thought of as a double-linked list.
The use of the previous gate and next gate pointers with various gate types is illustrated on figure below.
The start and end gates of the path can be found with the getPathStartGate() and getPathEndGate() methods, which simply follow the previous gate and next gate pointers, respectively, until they are nullptr.
The isConnectedOutside() and isConnectedInside() methods return whether a gate is connected on the outside or on the inside. They examine either the previous or the next pointer, depending on the gate type (input or output). For example, an output gate is connected outside if the next pointer is non-nullptr; the same function for an input gate checks the previous pointer. Again, see figure below for an illustration.
The isConnected() method is a bit different: it returns true if the gate is fully connected, that is, for a compound module gate both inside and outside, and for a simple module gate, outside.
The following code prints the name of the gate a simple module gate is connected to:
cGate *gate = gate("somegate"); cGate *otherGate = gate->getType()==cGate::OUTPUT ? gate->getNextGate() : gate->getPreviousGate(); if (otherGate) EV << "gate is connected to: " << otherGate->getFullPath() << endl; else EV << "gate not connected" << endl;
The channel object associated with a connection is accessible by a pointer stored at the source gate of the connection. The pointer is returned by the getChannel() method of the gate:
cChannel *channel = gate->getChannel();
The result may be nullptr, that is, a connection may not have an associated channel object.
If you have a channel pointer, you can get back its source gate with the getSourceGate() method:
cGate *gate = channel->getSourceGate();
cChannel is just an abstract base class for channels, so to access details of the channel you might need to cast the resulting pointer into a specific channel class, for example cDelayChannel or cDatarateChannel.
Another specific channel type is cIdealChannel, which basically does nothing: it acts as if there was no channel object assigned to the connection. OMNeT++ sometimes transparently inserts a cIdealChannel into a channel-less connection, for example to hold the display string associated with the connection.
Often you are not really interested in a specific connection's channel, but rather in the transmission channel (see [4.7.6]) of the connection path that starts at a specific output gate. The transmission channel can be found by following the connection path until you find a channel whose isTransmissionChannel() method returns true, but cGate has a convenience method for this, named getTransmissionChannel(). An example usage:
cChannel *txChan = gate("ppp$o")->getTransmissionChannel();
A complementer method to getTransmissionChannel() is getIncomingTransmissionChannel(); it is usually invoked on input gates, and searches the connection path in reverse direction.
cChannel *incomingTxChan = gate("ppp$i")->getIncomingTransmissionChannel();
Both methods throw an error if no transmission channel is found. If this is not suitable, use the similar findTransmissionChannel() and findIncomingTransmissionChannel() methods that simply return nullptr in that case.
Channels are covered in more detail in section [4.8].
On an abstract level, an OMNeT++ simulation model is a set of simple modules that communicate with each other via message passing. The essence of simple modules is that they create, send, receive, store, modify, schedule and destroy messages -- the rest of OMNeT++ exists to facilitate this task, and collect statistics about what was going on.
Messages in OMNeT++ are instances of the cMessage class or one of its subclasses. Network packets are represented with cPacket, which is also subclassed from cMessage. Message objects are created using the C++ new operator, and destroyed using the delete operator when they are no longer needed.
Messages are described in detail in chapter [5]. At this point, all we need to know about them is that they are referred to as cMessage * pointers. In the examples below, messages will be created with new cMessage("foo") where "foo" is a descriptive message name, used for visualization and debugging purposes.
Nearly all simulation models need to schedule future events in order to implement timers, timeouts, delays, etc. Some typical examples:
In OMNeT++, you solve such tasks by letting the simple module send a message to itself; the message would be delivered to the simple module at a later point of time. Messages used this way are called self-messages, and the module class has special methods for them that allow for implementing self-messages without gates and connections.
The module can send a message to itself using the scheduleAt() function. scheduleAt() accepts an absolute simulation time, usually calculated as simTime()+delta:
scheduleAt(absoluteTime, msg); scheduleAt(simTime()+delta, msg);
Self-messages are delivered to the module in the same way as other messages (via the usual receive calls or handleMessage()); the module may call the isSelfMessage() member of any received message to determine if it is a self-message.
You can determine whether a message is currently in the FES by calling its isScheduled() member function.
Scheduled self-messages can be cancelled (i.e. removed from the FES). This feature facilitates implementing timeouts.
cancelEvent(msg);
The cancelEvent() function takes a pointer to the message to be cancelled, and also returns the same pointer. After having it cancelled, you may delete the message or reuse it in subsequent scheduleAt() calls. cancelEvent() has no effect if the message is not scheduled at that time.
There is also a convenience method called cancelAndDelete() implemented as if (msg!=nullptr) delete cancelEvent(msg); this method is primarily useful for writing destructors.
The following example shows how to implement a timeout in a simple imaginary stop-and-wait protocol. The code utilizes a timeoutEvent module class data member that stores the pointer of the cMessage used as self-message, and compares it to the pointer of the received message to identify whether a timeout has occurred.
void Protocol::handleMessage(cMessage *msg) { if (msg == timeoutEvent) { // timeout expired, re-send packet and restart timer send(currentPacket->dup(), "out"); scheduleAt(simTime() + timeout, timeoutEvent); } else if (...) { // if acknowledgement received // cancel timeout, prepare to send next packet, etc. cancelEvent(timeoutEvent); ... } else { ... } }
To reschedule an event which is currently scheduled to a different simulation time, it first needs to be cancelled using cancelEvent(). This is shown in the following example code:
if (msg->isScheduled()) cancelEvent(msg); scheduleAt(simTime() + delay, msg);
Once created, a message object can be sent through an output gate using one of the following functions:
send(cMessage *msg, const char *gateName, int index=0); send(cMessage *msg, int gateId); send(cMessage *msg, cGate *gate);
In the first function, the argument gateName is the name of the gate the message has to be sent through. If this gate is a vector gate, index determines though which particular output gate this has to be done; otherwise, the index argument is not needed.
The second and third functions use the gate ID and the pointer to the gate object. They are faster than the first one because they don't have to search for the gate by name.
Examples:
send(msg, "out"); send(msg, "outv", i); // send via a gate in a gate vector
To send via an inout gate, remember that an inout gate is an input and an output gate glued together, and the two halves can be identified with the $i and $o name suffixes. Thus, the gate name needs to be specified in the send() call with the $o suffix:
send(msg, "g$o"); send(msg, "g$o", i); // if "g[]" is a gate vector
When implementing broadcasts or retransmissions, two frequently occurring tasks in protocol simulation, you might feel tempted to use the same message in multiple send() operations. Do not do it -- you cannot send the same message object multiple times. Instead, duplicate the message object.
Why? A message is like a real-world object -- it cannot be at two places at the same time. Once sent out, the message no longer belongs to the module: it is taken over by the simulation kernel, and will eventually be delivered to the destination module. The sender module should not even refer to its pointer any more. Once the message arrives in the destination module, that module will have full authority over it -- it can send it on, destroy it immediately, or store it for further handling. The same applies to messages that have been scheduled -- they belong to the simulation kernel until they are delivered back to the module.
To enforce the rules above, all message sending functions check that the
module actually owns the message it is about to send. If the message is in
another module, in a queue, currently scheduled, etc., a runtime error
will be generated: not owner of message.
In your model, you may need to broadcast a message to several destinations. Broadcast can be implemented in a simple module by sending out copies of the same message, for example on every gate of a gate vector. As described above, you cannot use the same message pointer for in all send() calls -- what you have to do instead is create copies (duplicates) of the message object and send them.
Example:
for (int i = 0; i < n; i++) { cMessage *copy = msg->dup(); send(copy, "out", i); } delete msg;
You might have noticed that copying the message for the last gate is redundant: we can just send out the original message there. Also, we can utilize gate IDs to avoid looking up the gate by name for each send operation. We can exploit the fact that the ID of gate k in a gate vector can be produced as baseID + k. The optimized version of the code looks like this:
int outGateBaseId = gateBaseId("out"); for (int i = 0; i < n; i++) send(i==n-1 ? msg : msg->dup(), outGateBaseId+i);
Many communication protocols involve retransmissions of packets (frames). When implementing retransmissions, you cannot just hold a pointer to the same message object and send it again and again -- you'd get the not owner of message error on the first resend.
Instead, for (re)transmission, you should create and send copies of the message, and retain the original. When you are sure there will not be any more retransmission, you can delete the original message.
Creating and sending a copy:
// (re)transmit packet: cMessage *copy = packet->dup(); send(copy, "out");
and finally (when no more retransmissions will occur):
delete packet;
Sometimes it is necessary for module to hold a message for some time interval, and then send it. This can be achieved with self-messages, but there is a more straightforward method: delayed sending. The following methods are provided for delayed sending:
sendDelayed(cMessage *msg, double delay, const char *gateName, int index); sendDelayed(cMessage *msg, double delay, int gateId); sendDelayed(cMessage *msg, double delay, cGate *gate);
The arguments are the same as for send(), except for the extra delay parameter. The delay value must be non-negative. The effect of the function is similar to as if the module had kept the message for the delay interval and sent it afterwards; even the sending time timestamp of the message will be set to the current simulation time plus delay.
A example call:
sendDelayed(msg, 0.005, "out");
The sendDelayed() function does not internally perform a scheduleAt() followed by a send(), but rather it computes everything about the message sending up front, including the arrival time and the target module. This has two consequences. First, sendDelayed() is more efficient than a scheduleAt() followed by a send() because it eliminates one event. The second, less pleasant consequence is that changes in the connection path during the delay will not be taken into account (because everything is calculated in advance, before the changes take place).
Therefore, despite its performance advantage, you should think twice before using sendDelayed() in a simulation model. It may have its place in a one-shot simulation model that you know is static, but it certainly should be avoided in reusable modules that need to work correctly in a wide variety of simulation models.
At times it is covenient to be able to send a message directly to an input gate of another module. The sendDirect() function is provided for this purpose.
This function has several flavors. The first set of sendDirect() functions accept a message and a target gate; the latter can be specified in various forms:
sendDirect(cMessage *msg, cModule *mod, int gateId) sendDirect(cMessage *msg, cModule *mod, const char *gateName, int index=-1) sendDirect(cMessage *msg, cGate *gate)
An example for direct sending:
cModule *targetModule = getParentModule()->getSubmodule("node2"); sendDirect(new cMessage("msg"), targetModule, "in");
At the target module, there is no difference between messages received directly and those received over connections.
The target gate must be an unconnected gate; in other words, modules must have dedicated gates to be able to receive messages sent via sendDirect(). You cannot have a gate which receives messages via both connections and sendDirect().
It is recommended to tag gates dedicated for receiving messages via sendDirect() with the @directIn property in the module's NED declaration. This will cause OMNeT++ not to complain that the gate is not connected in the network or compound module where the module is used.
An example:
simple Radio { gates: input radioIn @directIn; // for receiving air frames }
The target module is usually a simple module, but it can also be a compound module. The message will follow the connections that start at the target gate, and will be delivered to the module at the end of the path -- just as with normal connections. The path must end in a simple module.
It is even permitted to send to an output gate, which will also cause the message to follow the connections starting at that gate. This can be useful, for example, when several submodules are sending to a single output gate of their parent module.
A second set of sendDirect() methods accept a propagation delay and a transmission duration as parameters as well:
sendDirect(cMessage *msg, simtime_t propagationDelay, simtime_t duration, cModule *mod, int gateId) sendDirect(cMessage *msg, simtime_t propagationDelay, simtime_t duration, cModule *mod, const char *gateName, int index=-1) sendDirect(cMessage *msg, simtime_t propagationDelay, simtime_t duration, cGate *gate)
The transmission duration parameter is important when the message is also a packet (instance of cPacket). For messages that are not packets (not subclassed from cPacket), the duration parameter is ignored.
If the message is a packet, the duration will be written into the packet, and can be read by the receiver with the getDuration() method of the packet.
The receiver module can choose whether it wants the simulation kernel to deliver the packet object to it at the start or at the end of the reception. The default is the latter; the module can change it by calling setDeliverOnReceptionStart() on the final input gate, that is, on targetGate->getPathEndGate().
When a message is sent out on a gate, it usually travels through a series of connections until it arrives at the destination module. We call this series of connections a connection path.
Several connections in the path may have an associated channel,
but there can be only one channel per path that models nonzero
transmission duration. This restriction is enforced by the simulation
kernel. This channel is called the transmission channel.
Packets may only be sent when the transmission channel is idle. This means that after each transmission, the sender module needs to wait until the channel has finished transmitting before it can send another packet.
You can get a pointer to the transmission channel by calling the getTransmissionChannel() method on the output gate. The channel's isBusy() and getTransmissionFinishTime() methods can tell you whether a channel is currently transmitting, and when the transmission is going to finish. (When the latter is less or equal the current simulation time, the channel is free.) If the channel is currently busy, sending needs to be postponed: the packet can be stored in a queue, and a timer (self-message) can be scheduled for the time when the channel becomes empty.
A code example to illustrate the above process:
cPacket *pkt = ...; // packet to be transmitted cChannel *txChannel = gate("out")->getTransmissionChannel(); simtime_t txFinishTime = txChannel->getTransmissionFinishTime(); if (txFinishTime <= simTime()) { // channel free; send out packet immediately send(pkt, "out"); } else { // store packet and schedule timer; when the timer expires, // the packet should be removed from the queue and sent out txQueue.insert(pkt); scheduleAt(txFinishTime, endTxMsg); }
The getTransmissionChannel() method searches the connection path each time it is called. If performance is important, it is a good idea to obtain the transmission channel pointer once, and then cache it. When the network topology changes, the cached channel pointer needs to be updated; section [4.14.3] describes the mechanism that can be used to get notifications about topology changes.
As a result of error modeling in the channel, the packet may arrive with the bit error flag set (hasBitError() method. It is the receiver module's responsibility to examine this flag and take appropriate action (i.e. discard the packet).
Normally the packet object gets delivered to the destination module at the simulation time that corresponds to finishing the reception of the message (ie. the arrival of its last bit). However, the receiver module may change this by “reprogramming” the receiver gate with the setDeliverOnReceptionStart() method:
gate("in")->setDeliverOnReceptionStart(true);
This method may only be called on simple module input gates, and it instructs the simulation kernel to deliver packets arriving through that gate at the simulation time that corresponds to the beginning of the reception process. getDeliverOnReceptionStart() only needs to be called once, so it is usually done in the initialize() method of the module.
When a packet is delivered to the module, the packet's isReceptionStart() method can be called to determine whether it corresponds to the start or end of the reception process (it should be the same as the getDeliverOnReceptionStart() flag of the input gate), and getDuration() returns the transmission duration.
The following example code prints the start and end times of a packet reception:
simtime_t startTime, endTime; if (pkt->isReceptionStart()) { // gate was reprogrammed with setDeliverOnReceptionStart(true) startTime = pkt->getArrivalTime(); // or: simTime(); endTime = startTime + pkt->getDuration(); } else { // default case endTime = pkt->getArrivalTime(); // or: simTime(); startTime = endTime - pkt->getDuration(); } EV << "interval: " << startTime << ".." << endTime << "\n";
Note that this works with wireless connections (sendDirect()) as well; there, the duration is an argument to the sendDirect() call.
Certain protocols, for example Ethernet require the ability to abort a transmission before it completes. The support OMNeT++ provides for this task is the forceTransmissionFinishTime() channel method. This method forcibly overwrites the transmissionFinishTime member of the channel with the given value, allowing the sender to transmit another packet without raising the “channel is currently busy” error. The receiving party needs to be notified about the aborted transmission by some external means, for example by sending another packet or an out-of-band message.
Message sending is implemented like this: the arrival time and the bit error flag of a message are calculated right inside the send() call, then the message is inserted into the FES with the calculated arrival time. The message does not get scheduled individually for each link. This implementation was chosen because of its run-time efficiency.
This is not a huge problem in practice, but if it is important to model channels with changing parameters, the solution is to insert simple modules into the path to ensure strict scheduling.
The code which inserts the message into the FES is the arrived() method of the recipient module. By overriding this method it is possible to perform custom processing at the recipient module immediately, still from within the send() call. Use only if you know what you are doing!
activity()-based modules receive messages with the receive() method of cSimpleModule. receive() cannot be used with handleMessage()-based modules.
cMessage *msg = receive();
The receive() function accepts an optional timeout
parameter. (This is a delta, not an
absolute simulation time.) If no message arrives within the timeout
period, the function returns nullptr.
simtime_t timeout = 3.0; cMessage *msg = receive(timeout); if (msg==nullptr) { ... // handle timeout } else { ... // process message }
The wait() function suspends the execution of the module for a given amount of simulation time (a delta). wait() cannot be used with handleMessage()-based modules.
wait(delay);
In other simulation software, wait() is often called hold. Internally, the wait() function is implemented by a scheduleAt() followed by a receive(). The wait() function is very convenient in modules that do not need to be prepared for arriving messages, for example message generators. An example:
for (;;) { // wait for some, potentially random, amount of time, specified // in the interarrivalTime volatile module parameter wait(par("interarrivalTime").doubleValue()); // generate and send message ... }
It is a runtime error if a message arrives during the wait interval. If you expect messages to arrive during the wait period, you can use the waitAndEnqueue() function. It takes a pointer to a queue object (of class cQueue, described in chapter [7]) in addition to the wait interval. Messages that arrive during the wait interval are accumulated in the queue, and they can be processed after the waitAndEnqueue() call returns.
cQueue queue("queue"); ... waitAndEnqueue(waitTime, &queue); if (!queue.empty()) { // process messages arrived during wait interval ... }
Channels encapsulate parameters and behavior associated with connections. Channel types are like simple modules, in the sense that they are declared in NED, and there are C++ implementation classes behind them. Section [3.5] describes NED language support for channels, and explains how to associate C++ classes with channel types declared in NED.
C++ channel classes must subclass from the abstract base class cChannel. However, when creating a new channel class, it may be more practical to extend one of the existing C++ channel classes behind the three predefined NED channel types:
Channel classes need to be registered with the Define_Channel() macro, just like simple module classes need Define_Module().
The channel base class cChannel inherits from cComponent, so channels participate in the initialization and finalization protocol (initialize() and finish()) described in [4.3.3].
The parent module of a channel (as returned by the getParentModule()) is the module that contains the connection. If a connection connects two modules that are children of the same compound module, the channel's parent is the compound module. If the connection connects a compound module to one of its submodules, the channel's parent is also the compound module.
When subclassing Channel, the following pure virtual member functions need to be overridden:
The first two functions are usually one-liners; the channel behavior is encapsulated in the third function, processMessage().
The first function, isTransmissionChannel(), determines whether the channel is a transmission channel, i.e. one that models transmission duration. A transmission channel sets the duration field of packets sent through it (see the setDuration() field of cPacket).
The getTransmissionFinishTime() function is only used with transmission channels, and it should return the simulation time the sender will finish (or has finished) transmitting. This method is called by modules that send on a transmission channel to find out when the channel becomes available. The channel's isBusy() method is implemented simply as return getTransmissionFinishTime() < simTime(). For non-transmission channels, the getTransmissionFinishTime() return value may be any simulation time which is less than or equal to the current simulation time.
The third function, processMessage() encapsulates the channel's functionality. However, before going into the details of this function we need to understand how OMNeT++ handles message sending on connections.
Inside the send() call, OMNeT++ follows the connection path denoted by the getNextGate() functions of gates, until it reaches the target module. At each “hop”, the corresponding connection's channel (if the connection has one) gets a chance to add to the message's arrival time (propagation time modeling), calculate a transmission duration, and to modify the message object in various ways, such as set the bit error flag in it (bit error modeling). After processing all hops that way, OMNeT++ inserts the message object into the Future Events Set (FES, see section [4.1.2]), and the send() call returns. Then OMNeT++ continues to process events in increasing timestamp order. The message will be delivered to the target module's handleMessage() (or receive()) function when it gets to the front of the FES.
A few more details: a channel may instruct OMNeT++ to delete the message instead of inserting it into the FES; this can be useful to model disabled channels, or to model that the message has been lost altogether. The getDeliverOnReceptionStart() flag of the final gate in the path will determine whether the transmission duration will be added to the arrival time or not. Packet transmissions have been described in section [4.7.6].
Now, back to the processMessage() method.
The method gets called as part of the above process, when the message is processed at the given hop. The method's arguments are the message object, the simulation time the beginning of the message will reach the channel (i.e. the sum of all previous propagation delays), and a struct in which the method can return the results.
The result_t struct is an inner type of cChannel, and looks like this:
struct result_t { simtime_t delay; // propagation delay simtime_t duration; // transmission duration bool discard; // whether the channel has lost the message };
It also has a constructor that initializes all fields to zero; it is left out for brevity.
The method should model the transmission of the given message starting at the given t time, and store the results (propagation delay, transmission duration, deletion flag) in the result object. Only the relevant fields in the result object need to be changed, others can be left untouched.
Transmission duration and bit error modeling only applies to packets (i.e. to instances of cPacket, where cMessage's isPacket() returns true); it should be skipped for non-packet messages. processMessage() does not need to call the setDuration() method on the packet; this is done by the simulation kernel. However, it should call setBitError(true) on the packet if error modeling results in bit errors.
If the method sets the discard flag in the result object, that means that the message object will be deleted by OMNeT++; this facility can be used to model that the message gets lost in the channel.
The processMessage() method does not need to throw error on overlapping transmissions, or if the packet's duration field is already set; these checks are done by the simulation kernel before processMessage() is called.
To illustrate coding channel behavior, we look at how the built-in channel types are implemented.
cIdealChannel lets through messages and packets without any delay or change. Its isTransmissionChannel() method returns false, getTransmissionFinishTime() returns 0s, and the body of its processMessage() method is empty:
void cIdealChannel::processMessage(cMessage *msg, simtime_t t, result_t& result) { }
cDelayChannel implements propagation delay, and it can be disabled; in its disabled state, messages sent though it will be discarded. This class still models zero transmission duration, so its isTransmissionChannel() and getTransmissionFinishTime() methods still return false and 0s. The processMessage() method sets the appropriate fields in the result_t struct:
void cDelayChannel::processMessage(cMessage *msg, simtime_t t, result_t& result) { // if channel is disabled, signal that message should be deleted result.discard = isDisabled; // propagation delay modeling result.delay = delay; }
The handleParameterChange() method is also redefined, so that
the channel can update its internal delay and isDisabled
data members if the corresponding channel parameters change during simulation.
cDatarateChannel is different. It performs model packet duration (duration is calculated from the data rate and the length of the packet), so isTransmissionChannel() returns true. getTransmissionFinishTime() returns the value of a txfinishtime data member, which gets updated after every packet.
simtime_t cDatarateChannel::getTransmissionFinishTime() const { return txfinishtime; }
cDatarateChannel's processMessage() method makes use of the isDisabled, datarate, ber and per data members, which are also kept up to date with the help of handleParameterChange().
void cDatarateChannel::processMessage(cMessage *msg, simtime_t t, result_t& result) { // if channel is disabled, signal that message should be deleted if (isDisabled) { result.discard = true; return; } // datarate modeling if (datarate!=0 && msg->isPacket()) { simtime_t duration = ((cPacket *)msg)->getBitLength() / datarate; result.duration = duration; txfinishtime = t + duration; } else { txfinishtime = t; } // propagation delay modeling result.delay = delay; // bit error modeling if ((ber!=0 || per!=0) && msg->isPacket()) { cPacket *pkt = (cPacket *)msg; if (ber!=0 && dblrand() < 1.0 - pow(1.0-ber, (double)pkt->getBitLength()) pkt->setBitError(true); if (per!=0 && dblrand() < per) pkt->setBitError(true); } }
You can finish the simulation with the endSimulation() function:
endSimulation();
endSimulation() is rarely needed in practice because you can specify simulation time and CPU time limits in the ini file (see later).
When the simulation encounters an error condition, it can throw a cRuntimeError exception to terminate the simulation with an error message. (Under Cmdenv, the exception also causes a nonzero program exit code). The cRuntimeError class has a constructor with a printf()-like argument list. An example:
if (windowSize <= 0) throw cRuntimeError("Invalid window size %d; must be >=1", windowSize);
Do not include newline (\n), period or exclamation mark in the error text; it will be added by OMNeT++.
The same effect can be achieved by calling the error() method of cModule:
if (windowSize <= 0) error("Invalid window size %d; must be >=1", windowSize);
Of course, the error() method can only be used when a module pointer is available.
Finite State Machines (FSMs) can make life with handleMessage() easier. OMNeT++ provides a class and a set of macros to build FSMs.
The key points are:
OMNeT++'s FSMs can be nested. This means that any state (or rather, its entry or exit code) may contain a further full-fledged FSM_Switch() (see below). This allows you to introduce sub-states and thereby bring some structure into the state space if it becomes too large.
FSM state is stored in an object of type cFSM. The possible states are defined by an enum; the enum is also a place to define which state is transient and which is steady. In the following example, SLEEP and ACTIVE are steady states and SEND is transient (the numbers in parentheses must be unique within the state type and they are used for constructing the numeric IDs for the states):
enum { INIT = 0, SLEEP = FSM_Steady(1), ACTIVE = FSM_Steady(2), SEND = FSM_Transient(1), };
The actual FSM is embedded in a switch-like statement, FSM_Switch(), with cases for entering and leaving each state:
FSM_Switch(fsm) { case FSM_Exit(state1): //... break; case FSM_Enter(state1): //... break; case FSM_Exit(state2): //... break; case FSM_Enter(state2): //... break; //... };
State transitions are done via calls to FSM_Goto(), which simply stores the new state in the cFSM object:
FSM_Goto(fsm, newState);
The FSM starts from the state with the numeric code 0; this state is conventionally named INIT.
FSMs can log their state transitions, with the output looking like this:
... FSM GenState: leaving state SLEEP FSM GenState: entering state ACTIVE ... FSM GenState: leaving state ACTIVE FSM GenState: entering state SEND FSM GenState: leaving state SEND FSM GenState: entering state ACTIVE ... FSM GenState: leaving state ACTIVE FSM GenState: entering state SLEEP ...
To enable the above output, define FSM_DEBUG before including omnetpp.h.
#define FSM_DEBUG // enables debug output from FSMs #include <omnetpp.h>
FSMs perform their logging via the FSM_Print() macro, defined as something like this:
#define FSM_Print(fsm,exiting) (EV << "FSM " << (fsm).getName() << ((exiting) ? ": leaving state " : ": entering state ") << (fsm).getStateName() << endl)
The log output format can be changed by undefining FSM_Print() after the inclusion of omnetpp.ini, and providing a new definition.
FSM_Switch() is a macro. It expands to a switch statement embedded in a for() loop which repeats until the FSM reaches a steady state.
Infinite loops are avoided by counting state transitions: if an FSM goes through 64 transitions without reaching a steady state, the simulation will terminate with an error message.
Let us write another bursty packet generator. It will have two states, SLEEP and ACTIVE. In the SLEEP state, the module does nothing. In the ACTIVE state, it sends messages with a given inter-arrival time. The code was taken from the Fifo2 sample simulation.
#define FSM_DEBUG #include <omnetpp.h> using namespace omnetpp; class BurstyGenerator : public cSimpleModule { protected: // parameters double sleepTimeMean; double burstTimeMean; double sendIATime; cPar *msgLength; // FSM and its states cFSM fsm; enum { INIT = 0, SLEEP = FSM_Steady(1), ACTIVE = FSM_Steady(2), SEND = FSM_Transient(1), }; // variables used int i; cMessage *startStopBurst; cMessage *sendMessage; // the virtual functions virtual void initialize(); virtual void handleMessage(cMessage *msg); }; Define_Module(BurstyGenerator); void BurstyGenerator::initialize() { fsm.setName("fsm"); sleepTimeMean = par("sleepTimeMean"); burstTimeMean = par("burstTimeMean"); sendIATime = par("sendIATime"); msgLength = &par("msgLength"); i = 0; WATCH(i); // always put watches in initialize() startStopBurst = new cMessage("startStopBurst"); sendMessage = new cMessage("sendMessage"); scheduleAt(0.0,startStopBurst); } void BurstyGenerator::handleMessage(cMessage *msg) { FSM_Switch(fsm) { case FSM_Exit(INIT): // transition to SLEEP state FSM_Goto(fsm,SLEEP); break; case FSM_Enter(SLEEP): // schedule end of sleep period (start of next burst) scheduleAt(simTime()+exponential(sleepTimeMean), startStopBurst); break; case FSM_Exit(SLEEP): // schedule end of this burst scheduleAt(simTime()+exponential(burstTimeMean), startStopBurst); // transition to ACTIVE state: if (msg!=startStopBurst) { error("invalid event in state ACTIVE"); } FSM_Goto(fsm,ACTIVE); break; case FSM_Enter(ACTIVE): // schedule next sending scheduleAt(simTime()+exponential(sendIATime), sendMessage); break; case FSM_Exit(ACTIVE): // transition to either SEND or SLEEP if (msg==sendMessage) { FSM_Goto(fsm,SEND); } else if (msg==startStopBurst) { cancelEvent(sendMessage); FSM_Goto(fsm,SLEEP); } else { error("invalid event in state ACTIVE"); } break; case FSM_Exit(SEND): { // generate and send out job char msgname[32]; sprintf(msgname, "job-%d", ++i); EV << "Generating " << msgname << endl; cMessage *job = new cMessage(msgname); job->setBitLength((long) *msgLength); job->setTimestamp(); send(job, "out"); // return to ACTIVE FSM_Goto(fsm,ACTIVE); break; } } }
If a module is part of a module vector, the getIndex() and getVectorSize() member functions can be used to query its index and the vector size:
EV << "This is module [" << module->getIndex() << "] in a vector of size [" << module->size() << "].\n";
Every component (module and channel) in the network has an ID that can be obtained from cComponent's getId() member function:
int componentId = getId();
IDs uniquely identify a module or channel for the whole duration of the simulation. This holds even when modules are created and destroyed dynamically, because IDs of deleted modules or channels are never reused for newly created ones.
To look up a component by ID, one needs to use methods of the simulation manager object, cSimulation. getComponent() expects an ID, and returns the component's pointer if the component still exists, otherwise it returns nullptr. The method has two variations, getModule(id) and getChannel(id). They return cModule and cChannel pointers if the identified component is in fact a module or a channel, respectively, otherwise they return nullptr.
int id = 100; cModule *mod = getSimulation()->getModule(id); // exists, and is a module
The parent module can be accessed by the getParentModule() member function:
cModule *parent = getParentModule();
For example, the parameters of the parent module are accessed like this:
double timeout = getParentModule()->par("timeout");
cModule's findSubmodule() and getSubmodule() member functions make it possible to look up the module's submodules by name (or name and index if the submodule is in a module vector). The first one returns the module ID of the submodule, and the latter returns the module pointer. If the submodule is not found, they return -1 or nullptr, respectively.
int submodID = module->findSubmodule("foo", 3); // look up "foo[3]" cModule *submod = module->getSubmodule("foo", 3);
cModule's getModuleByPath() member function can be used to find modules by relative or absolute path. It accepts a path string, and returns the pointer of the matching module, or nullptr if the module identified by the path does not exist.
The path is dot-separated list of module names. The special module name ^ (caret) stands for the parent module. If the path starts with a dot or caret, it is understood as relative to this module, otherwise it is taken to mean an absolute path. For absolute paths, inclusion of the toplevel module's name in the path is optional. The toplevel module itself may be referred to as <root>.
The following lines demonstrate relative paths, and find the app[3] submodule and the gen submodule of the app[3] submodule of the module in question:
cModule *app = module->getModuleByPath(".app[3]"); // note leading dot cModule *gen = module->getModuleByPath(".app[3].gen");
Without the leading dot, the path is interpreted as absolute. The following lines both find the tcp submodule of host[2] in the network, regardless of the module on which the getModuleByPath() has been invoked.
cModule *tcp = module->getModuleByPath("Network.host[2].tcp"); cModule *tcp = module->getModuleByPath("host[2].tcp");
The parent module may be expressed with a caret:
cModule *parent = module->getModuleByPath("^"); // parent module cModule *tcp = module->getModuleByPath("^.tcp"); // sibling module cModule *other = module->getModuleByPath("^.^.host[1].tcp"); // two levels up, then...
To access all modules within a compound module, one can use cModule::SubmoduleIterator.
for (cModule::SubmoduleIterator it(module); !it.end(); it++) { cModule *submodule = *it; EV << submodule->getFullName() << endl; }
To determine the module at the other end of a connection, use cGate's getPreviousGate(), getNextGate() and getOwnerModule() methods. An example:
cModule *neighbour = gate("out")->getNextGate()->getOwnerModule();
For input gates, use getPreviousGate() instead of getNextGate().
The endpoints of the connection path are returned by the getPathStartGate() and getPathEndGate() cGate methods. These methods follow the connection path by repeatedly calling getPreviousGate() and getNextGate(), respectively, until they arrive at a nullptr. An example:
cModule *peer = gate("out")->getPathEndGate()->getOwnerModule();
In some simulation models, there might be modules which are too tightly coupled for message-based communication to be efficient. In such cases, the solution might be calling one simple module's public C++ methods from another module.
Simple modules are C++ classes, so normal C++ method calls will work. Two issues need to be mentioned, however:
Typically, the called module is in the same compound module as the caller, so the getParentModule() and getSubmodule() methods of cModule can be used to get a cModule* pointer to the called module. (Further ways to obtain the pointer are described in the section [4.11].) The cModule* pointer then has to be cast to the actual C++ class of the module, so that its methods become visible.
This makes the following code:
cModule *targetModule = getParentModule()->getSubmodule("foo"); Foo *target = check_and_cast<Foo *>(targetModule); target->doSomething();
The check_and_cast<>() template function on the second line
is part of OMNeT++. It performs a standard C++ dynamic_cast, and checks
the result: if it is nullptr, check_and_cast raises an OMNeT++ error.
Using check_and_cast saves you from writing error checking
code: if targetModule from the first line is nullptr because
the submodule named "foo" was not found, or if that
module is actually not of type Foo, an exception is thrown
from check_and_cast with an appropriate error message.
The second issue is how to let the simulation kernel know that a method call across modules is taking place. Why is this necessary in the first place? First, the simulation kernel always has to know which module's code is currently executing, in order for ownership handling and other internal mechanisms to work correctly. Second, the Tkenv and Qtenv simulation GUIs can animate method calls, but to be able to do that, they need to know about them. Third, method calls are also recorded in the event log.
The solution is to add the Enter_Method() or Enter_Method_Silent() macro at the top of the methods that may be invoked from other modules. These calls perform context switching, and, in case of Enter_Method(), notify the simulation GUI so that animation of the method call can take place. Enter_Method_Silent() does not animate the method call, but otherwise it is equivalent Enter_Method(). Both macros accept a printf()-like argument list (it is optional for Enter_Method_Silent()), which should produce a string with the method name and the actual arguments as much as practical. The string is displayed in the animation (Enter_Method() only) and recorded into the event log.
void Foo::doSomething() { Enter_Method("doSomething()"); ... }
Certain simulation scenarios require the ability to dynamically create and destroy modules. For example, simulating the arrival and departure of new users in a mobile network may be implemented in terms of adding and removing modules during the course of the simulation. Loading and instantiating network topology (i.e. nodes and links) from a data file is another common technique enabled by dynamic module (and link) creation.
OMNeT++ allows both simple and compound modules to be created at runtime. When instantiating a compound module, its full internal structure (submodules and internal connections) is reproduced.
Once created and started, dynamic modules aren't any different from “static” modules.
To understand how dynamic module creation works, you have to know a bit about how OMNeT++ normally instantiates modules. Each module type (class) has a corresponding factory object of the class cModuleType. This object is created under the hood by the Define_Module() macro, and it has a factory method which can instantiate the module class (this function basically only consists of a return new <moduleclass>(...) statement).
The cModuleType object can be looked up by its name string (which is the same as the module class name). Once you have its pointer, it is possible to call its factory method and create an instance of the corresponding module class -- without having to include the C++ header file containing module's class declaration into your source file.
The cModuleType object also knows what gates and parameters the given module type has to have. (This info comes from NED files.)
Simple modules can be created in one step. For a compound module, the situation is more complicated, because its internal structure (submodules, connections) may depend on parameter values and gate vector sizes. Thus, for compound modules it is generally required to first create the module itself, second, set parameter values and gate vector sizes, and then call the method that creates its submodules and internal connections.
As you know already, simple modules with activity() need a starter message. For statically created modules, this message is created automatically by OMNeT++, but for dynamically created modules, you have to do this explicitly by calling the appropriate functions.
Calling initialize() has to take place after insertion of the starter messages, because the initializing code may insert new messages into the FES, and these messages should be processed after the starter message.
The first step is to find the factory object. The cModuleType::get() function expects a fully qualified NED type name, and returns the factory object:
cModuleType *moduleType = cModuleType::get("foo.nodes.WirelessNode");
The return value does not need to be checked for nullptr, because the function raises an error if the requested NED type is not found. (If this behavior is not what you need, you can use the similar cModuleType::find() function, which returns nullptr if the type was not found.)
cModuleType has a createScheduleInit(const char *name, cModule *parentmod) % don't break this line (for html) convenience function to get a module up and running in one step.
cModule *mod = moduleType->createScheduleInit("node", this);
createScheduleInit() performs the following steps: create(), finalizeParameters(), buildInside(), scheduleStart(now) and callInitialize().
This method can be used for both simple and compound modules. Its applicability is somewhat limited, however: because it does everything in one step, you do not have the chance to set parameters or gate sizes, and to connect gates before initialize() is called. (initialize() expects all parameters and gates to be in place and the network fully built when it is called.) Because of the above limitation, this function is mainly useful for creating basic simple modules.
If the createScheduleInit() all-in-one method is not applicable, one needs to use the full procedure. It consists of five steps:
Each step (except for Step 3.) can be done with one line of code.
See the following example, where Step 3 is omitted:
// find factory object cModuleType *moduleType = cModuleType::get("foo.nodes.WirelessNode"); // create (possibly compound) module and build its submodules (if any) cModule *module = moduleType->create("node", this); module->finalizeParameters(); module->buildInside(); // create activation message module->scheduleStart(simTime());
If you want to set up parameter values or gate vector sizes (Step 3.), the code goes between the create() and buildInside() calls:
// create cModuleType *moduleType = cModuleType::get("foo.nodes.WirelessNode"); cModule *module = moduleType->create("node", this); // set up parameters and gate sizes before we set up its submodules module->par("address") = ++lastAddress; module->finalizeParameters(); module->setGateSize("in", 3); module->setGateSize("out", 3); // create internals, and schedule it module->buildInside(); module->scheduleStart(simTime());
To delete a module dynamically, use cModule's deleteModule() member function:
module->deleteModule();
If the module was a compound module, this involves recursively deleting all its submodules. A simple module can also delete itself; in this case, the deleteModule() call does not return to the caller.
Currently, you cannot safely delete a compound module from a simple module in it; you must delegate the job to a module outside the compound module.
finish() is called for all modules at the end of the simulation, no matter how the modules were created. If a module is dynamically deleted before that, finish() will not be invoked (deleteModule() does not do it). However, you can still manually invoke it before deleteModule().
You can use the callFinish() function to invoke finish()
(It is not a good idea to invoke finish() directly). If you are
deleting a compound module, callFinish() will recursively invoke
finish() for all submodules, and if you are deleting a simple
module from another module, callFinish() will do the context switch
for the duration of the call.
Example:
mod->callFinish(); mod->deleteModule();
Connections can be created using cGate's connectTo() method. connectTo() should be invoked on the source gate of the connection, and expects the destination gate pointer as an argument. The use of the words source and destination correspond to the direction of the arrow in NED files.
srcGate->connectTo(destGate);
connectTo() also accepts a channel object (cChannel*) as an additional, optional argument. Similarly to modules, channels can be created using their factory objects that have the type cChannelType:
cGate *outGate, *inGate; ... // find factory object and create a channel cChannelType *channelType = cChannelType::get("foo.util.Channel"); cChannel *channel = channelType->create("channel"); // create connecting outGate->connectTo(inGate, channel);
The channel object will be owned by the source gate of the connection, and one cannot reuse the same channel object with several connections.
Instantiating one of the built-in channel types (cIdealChannel, cDelayChannel or cDatarateChannel) is somewhat simpler, because those classes have static create() factory functions, and the step of finding the factory object can be spared. Alternatively, one can use cChannelType's createIdealChannel(), createDelayChannel() and createDatarateChannel() static methods.
The channel object may need to be parameterized before using it for a connection. For example, cDelayChannel has a setDelay() method, and cDatarateChannel has setDelay(), setDatarate(), setBitErrorRate() and setPacketErrorRate().
An example that sets up a channel with a datarate and a delay between two modules:
cDatarateChannel *datarateChannel = cDatarateChannel::create("channel"); datarateChannel->setDelay(0.001); datarateChannel->setDatarate(1e9); outGate->connectTo(inGate, datarateChannel);
Finally, here is a more complete example that creates two modules and connects them in both directions:
cModuleType *moduleType = cModuleType::get("TicToc"); cModule *a = modtype->createScheduleInit("a", this); cModule *b = modtype->createScheduleInit("b", this); a->gate("out")->connectTo(b->gate("in")); b->gate("out")->connectTo(a->gate("in"));
The disconnect() method of cGate can be used to remove connections. This method has to be invoked on the source side of the connection. It also destroys the channel object associated with the connection, if one has been set.
srcGate->disconnect();
This section describes simulation signals, or signals for short. Signals are a versatile concept that first appeared in OMNeT++ 4.1.
Simulation signals can be used for:
Signals are emitted by components (modules and channels). Signals propagate on the module hierarchy up to the root. At any level, one can register listeners, that is, objects with callback methods. These listeners will be notified (their appropriate methods called) whenever a signal value is emitted. The result of upwards propagation is that listeners registered at a compound module can receive signals from all components in that submodule tree. A listener registered at the system module can receive signals from the whole simulation.
Signals are identified by signal names (i.e. strings), but for efficiency, at runtime we use dynamically assigned numeric identifiers (signal IDs, typedef'd as simsignal_t). The mapping of signal names to signal IDs is global, so all modules and channels asking to resolve a particular signal name will get back the same numeric signal ID.
Listeners can subscribe to signal names or IDs, regardless of their source. For example, if two different and unrelated module types, say Queue and Buffer, both emit a signal named "length", then a listener that subscribes to "length" at some higher compound module will get notifications from both Queue and Buffer module instances. The listener can still look at the source of the signal if it wants to distinguish the two (it is available as a parameter to the callback function), but the signals framework itself does not have such a feature.
When a signal is emitted, it can carry a value with it. There are multiple overloaded versions of the emit() method for different data types, and also overloaded receiveSignal() methods in listeners. The signal value can be of selected primitive types, or an object pointer; anything that is not feasible to emit as a primitive type may be wrapped into an object, and emitted as such.
Even when the signal value is of a primitive type, it is possible to convey extra information to listeners via an additional details object, which an optional argument of emit().
The implementation of signals is based on the following assumptions:
These goals have been achieved in the 4.1 version with the following implementation. First, the data structure that used to store listeners in components is dynamically allocated, so if there are no listeners, the per-component overhead is only the size of the pointer (which will be nullptr then).
Second, additionally there are two bitfields in every component that store
which one of the first 64 signals (IDs 0..63) have local listeners and
listeners in ancestor modules.
Signal-related methods are declared on cComponent, so they are available for both cModule and cChannel.
Signals are identified by names, but internally numeric signal IDs are used for efficiency. The registerSignal() method takes a signal name as parameter, and returns the corresponding simsignal_t value. The method is static, illustrating the fact that signal names are global. An example:
simsignal_t lengthSignalId = registerSignal("length");
The getSignalName() method (also static) does the reverse: it accepts a simsignal_t, and returns the name of the signal as const char * (or nullptr for invalid signal handles):
const char *signalName = getSignalName(lengthSignalId); // --> "length"
The emit() family of functions emit a signal from the module or channel. emit() takes a signal ID (simsignal_t) and a value as parameters:
emit(lengthSignalId, queue.length());
The value can be of type bool, long, double, simtime_t, const char *, or (const) cObject *. Other types can be cast into one of these types, or wrapped into an object subclassed from cObject.
emit() also has an extra, optional object pointer argument named details, with the type cObject*. This argument may be used to convey to listeners extra information.
When there are no listeners, the runtime cost of emit() is usually minimal. However, if producing a value has a significant runtime cost, then the mayHaveListeners() or hasListeners() method can be used to check beforehand whether the given signal has any listeners at all -- if not, producing the value and emitting the signal can be skipped.
Example usage:
if (mayHaveListeners(distanceToTargetSignal)) { double d = sqrt((x-targetX)*(x-targetX) + (y-targetY)*(y-targetY)); emit(distanceToTargetSignal, d); }
The mayHaveListeners() method is very efficient (a constant-time operation), but may return false positive. In contrast, hasListeners() will search up to the top of the module tree if the answer is not cached, so it is generally slower. We recommend that you take into account the cost of producing notification information when deciding between mayHaveListeners() and hasListeners().
Since OMNeT++ 4.4, signals can be declared in NED files for documentation purposes, and OMNeT++ can check that only declared signals are emitted, and that they actually conform to the declarations (with regard to the data type, etc.)
The following example declares a queue module that emits a signal named queueLength:
simple Queue { parameters: @signal[queueLength](type=long); ... }
Signals are declared with the @signal property on the module or channel that emits it. (NED properties are described in [3.12]). The property index corresponds to the signal name, and the property's body may declare various attributes of the signal; currently only the data type is supported.
The type property key is optional; when present, its value should be bool, long, unsigned long, double, simtime_t, string, or a registered class name optionally followed by a question mark. Classes can be registered using the Register_Class() or Register_Abstract_Class() macros; these macros create a cObjectFactory instance, and the simulation kernel will call cObjectFactory's isInstance() method to check that the emitted object is really a subclass of the declared class. isInstance() just wraps a C++ dynamic_cast.)
A question mark after the class name means that the signal is allowed to emit nullptr pointers. For example, a module named PPP may emit the frame (packet) object every time it starts transmiting, and emit nullptr when the transmission is completed:
simple PPP { parameters: @signal[txFrame](type=PPPFrame?); // a PPPFrame or nullptr ... }
The property index may contain wildcards, which is important for declaring signals whose names are only known at runtime. For example, if a module emits signals called session-1-seqno, session-2-seqno, session-3-seqno, etc., those signals can be declared as:
@signal[session-*-seqno]();
Starting with OMNeT++ 5.0, signal checking is turned on by default when the simulation kernel is compiled in debug mode, requiring all signals to be declared with @signal. (It is turned off in release mode simulation kernels due to performance reasons.)
If needed, signal checking can be disabled with the check-signals configuration option:
check-signals = false
When emitting a signal with a cObject* pointer, you can pass as data an object that you already have in the model, provided you have a suitable object at hand. However, it is often necessary to declare a custom class to hold all the details, and fill in an instance just for the purpose of emitting the signal.
The custom notification class must be derived from cObject. We recommend that you also add noncopyable as a base class, because then you don't need to write a copy constructor, assignment operator, and dup() function, sparing some work. When emitting the signal, you can create a temporary object, and pass its pointer to the emit() function.
An example of custom notification classes are the ones associated with model change notifications (see [4.14.3]). For example, the data class that accompanies a signal that announces that a gate or gate vector is about to be created looks like this:
class cPreGateAddNotification : public cObject, noncopyable { public: cModule *module; const char *gateName; cGate::Type gateType; bool isVector; };
And the code that emits the signal:
if (hasListeners(PRE_MODEL_CHANGE)) { cPreGateAddNotification tmp; tmp.module = this; tmp.gateName = gatename; tmp.gateType = type; tmp.isVector = isVector; emit(PRE_MODEL_CHANGE, &tmp); }
The subscribe() method registers a listener for a signal. Listeners are objects that extend the cIListener class. The same listener object can be subscribed to multiple signals. subscribe() has two arguments: the signal and a pointer to the listener object:
cIListener *listener = ...; simsignal_t lengthSignalId = registerSignal("length"); subscribe(lengthSignalId, listener);
For convenience, the subscribe() method has a variant that takes the signal name directly, so the registerSignal() call can be omitted:
cIListener *listener = ...; subscribe("length", listener);
One can also subscribe at other modules, not only the local one. For example, in order to get signals from all parts of the model, one can subscribe at the system module level:
cIListener *listener = ...; getSimulation()->getSystemModule()->subscribe("length", listener);
The unsubscribe() method has the same parameter list as subscribe(), and unregisters the given listener from the signal:
unsubscribe(lengthSignalId, listener);
or
unsubscribe("length", listener);
It is an error to subscribe the same listener to the same signal twice.
It is possible to test whether a listener is subscribed to a signal, using the isSubscribed() method which also takes the same parameter list.
if (isSubscribed(lengthSignalId, listener)) { ... }
For completeness, there are methods for getting the list of signals that the component has subscribed to (getLocalListenedSignals()), and the list of listeners for a given signal (getLocalSignalListeners()). The former returns std::vector<simsignal_t>; the latter takes a signal ID (simsignal_t) and returns std::vector<cIListener*>.
The following example prints the number of listeners for each signal:
EV << "Signal listeners:\n"; std::vector<simsignal_t> signals = getLocalListenedSignals(); for (unsigned int i = 0; i < signals.size(); i++) { simsignal_t signalID = signals[i]; std::vector<cIListener*> listeners = getLocalSignalListeners(signalID); EV << getSignalName(signalID) << ": " << listeners.size() << " signals\n"; }
Listeners are objects that subclass from the cIListener class, which declares the following methods:
class cIListener { public: virtual ~cIListener() {} virtual void receiveSignal(cComponent *src, simsignal_t id, bool value, cObject *details) = 0; virtual void receiveSignal(cComponent *src, simsignal_t id, long value, cObject *details) = 0; virtual void receiveSignal(cComponent *src, simsignal_t id, double value, cObject *details) = 0; virtual void receiveSignal(cComponent *src, simsignal_t id, simtime_t value, cObject *details) = 0; virtual void receiveSignal(cComponent *src, simsignal_t id, const char *value, cObject *details) = 0; virtual void receiveSignal(cComponent *src, simsignal_t id, cObject *value, cObject *details) = 0; virtual void finish(cComponent *component, simsignal_t id) {} virtual void subscribedTo(cComponent *component, simsignal_t id) {} virtual void unsubscribedFrom(cComponent *component, simsignal_t id) {} };
This class has a number of virtual methods:
Since cIListener has a large number of pure virtual methods, it is more convenient to subclass from cListener, a do-nothing implementation instead. It defines finish(), subscribedTo() and unsubscribedFrom() with an empty body, and the receiveSignal() methods with a bodies that throw a "Data type not supported" error. You can redefine the receiveSignal() method(s) whose data type you want to support, and signals emitted with other (unexpected) data types will result in an error instead of going unnoticed.
The order in which listeners will be notified is undefined (it is not necessarily the same order in which listeners were subscribed.)
When a component (module or channel) is deleted, it automatically unsubscribes (but does not delete) the listeners it has. When a module is deleted, it first unsubscribes all listeners from all modules and channels in its submodule tree before starting to recursively delete the modules and channels themselves.
When a listener is deleted, it must already be unsubscribed from all components at that point. If it is not unsubscribed, pointers to the dead listener object will be left in the components' listener lists, and the components will crash inside an emit() call, or when they try to invoke unsubscribedFrom() on the dead listener from their destructors. The cIListener class contains a subscription count, and prints a warning message when it is not zero in the destructor.
In simulation models it is often useful to hold references to other modules, a connecting channel or other objects, or to cache information derived from the model topology. However, such pointers or data may become invalid when the model changes at runtime, and need to be updated or recalculated. The problem is how to get notification that something has changed in the model.
The solution is, of course, signals. OMNeT++ has two built-in signals, PRE_MODEL_CHANGE and POST_MODEL_CHANGE (these macros are simsignal_t values, not names) that are emitted before and after each model change.
Pre/post model change notifications are emitted with data objects that carry the details of the change. The data classes are:
They all subclass from cModelChangeNotification, which is of course a cObject. Inside the listener, you can use dynamic_cast<> to figure out what notification arrived.
An example listener that prints a message when a module is deleted:
class MyListener : public cListener { ... }; void MyListener::receiveSignal(cComponent *src, simsignal_t id, cObject *value, cObject *details) { if (dynamic_cast<cPreModuleDeleteNotification *>(value)) { cPreModuleDeleteNotification *data = (cPreModuleDeleteNotification *)value; EV << "Module " << data->module->getFullPath() << " is about to be deleted\n"; } }
If you'd like to get notification about the deletion of any module, you need to install the listener on the system module:
getSimulation()->getSystemModule()->subscribe(PRE_MODEL_CHANGE, listener);
One use of signals is to expose variables for result collection without telling where, how, and whether to record them. With this approach, modules only publish the variables, and the actual result recording takes place in listeners. Listeners may be added by the simulation framework (based on the configuration), or by other modules (for example by dedicated result collection modules).
The signals approach allows for several possibilities:
With the signals approach the above goals can be fulfilled.
In order to record simulation results based on signals, one must add @statistic properties to the simple module's (or channel's) NED definition. A @statistic property defines the name of the statistic, which signal(s) are used as input, what processing steps are to be applied to them (e.g. smoothing, filtering, summing, differential quotient), and what properties are to be recorded (minimum, maximum, average, etc.) and in which form (vector, scalar, histogram). Record items can be marked optional, which lets you denote a “default” and a more comprehensive “all” result set to be recorded; the list of record items can be further tweaked from the configuration. One can also specify a descriptive name (“title”) for the statistic, and also a measurement unit.
The following example declares a queue module with a queue length statistic:
simple Queue { parameters: @statistic[queueLength](record=max,timeavg,vector?); gates: input in; output out; }
As you can see, statistics are represented with indexed NED properties (see [3.12]). The property name is always statistic, and the index (here, queueLength) is the name of the statistic. The property value, that is, everything inside the parentheses, carries hints and extra information for recording.
The above @statistic declaration assumes that module's C++ code emits the queue's updated length as signal queueLength whenever elements are inserted into the queue or are removed from it. By default, the maximum and the time average of the queue length will be recorded as scalars. One can also instruct the simulation (or parts of it) to record “all” results; this will turn on optional record items, those marked with a question mark, and then the queue lengths will also be recorded into an output vector.
In the above example, the signal to be recorded was taken from the statistic name. When that is not suitable, the source property key lets you specify a different signal as input for the statistic. The following example assumes that the C++ code emits a qlen signal, and declares a queueLength statistic based on that:
simple Queue { parameters: @signal[qlen](type=int); // optional @statistic[queueLength](source=qlen; record=max,timeavg,vector?); ... }
Note that beyond the source=qlen property key we have also added a signal declaration (@signal property) for the qlen signal. Declaring signals is currently optional and in fact @signal properties are currently ignored by the system, but it is a good practice nevertheless.
It is also possible to apply processing to a signal before recording it. Consider the following example:
@statistic[dropCount](source=count(drop); record=last,vector?);
This records the total number of packet drops as a scalar, and optionally the number of packets dropped in the function of time as a vector, provided the C++ code emits a drop signal every time a packet is dropped. The value and even the data type of the drop signal is indifferent, because only the number of emits will be counted. Here, count() is a result filter.
Another example:
@statistic[droppedBytes](source=sum(packetBytes(pkdrop)); record=last, vector?);
This example assumes that the C++ code emits a pkdrop signal with a packet (cPacket* pointer) as a value. Based on that signal, it records the total number of bytes dropped (as a scalar, and optionally as a vector too). The packetBytes() filter extracts the number of bytes from each packet using cPacket's getByteLength() method, and the sum() filter, well, sums them up.
Arithmetic expressions can also be used. For example, the following line computes the number of dropped bytes using the packetBits() filter.
@statistic[droppedBytes](source=sum(8*packetBits(pkdrop)); record=last, vector?);
The source can also combine multiple signals in an arithmetic expression:
@statistic[dropRate](source=count(drop)/count(pk); record=last,vector?);
When multiple signals are used, a value arriving on either signal will result in one output value. The computation will use the last values of the other signals (sample-hold interpolation). One limitation regarding multiple signals is that the same signal cannot occur twice, because it would cause glitches in the output.
Record items may also be expressions and contain filters. For example, the statistic below is functionally equivalent to one of the above examples: it also computes and records as scalar and as vector the total number of bytes dropped, using a cPacket*-valued signal as input; however, some of the computations have been shifted into the recorder part.
@statistic[droppedBytes](source=packetBits(pkdrop); record=last(8*sum), vector(8*sum)?);
The following keys are understood in @statistic properties:
The following table contains the list of predefined result filters. All filters in the table output a value for each input value.
Filter | Description |
count | Computes and outputs the count of values received so far. |
sum | Computes and outputs the sum of values received so far. |
min | Computes and outputs the minimum of values received so far. |
max | Computes and outputs the maximum of values received so far. |
mean | Computes and outputs the average (sum / count) of values received so far. |
timeavg | Regards the input values and their timestamps as a step function (sample-hold style), and computes and outputs its time average (integral divided by duration). |
constant0 | Outputs a constant 0 for each received value (independent of the value). |
constant1 | Outputs a constant 1 for each received value (independent of the value). |
packetBits | Expects cPacket pointers as value, and outputs the bit length for each received one. Non-cPacket values are ignored. |
packetBytes | Expects cPacket pointers as value, and outputs the byte length for each received one. Non-cPacket values are ignored. |
sumPerDuration | For each value, computes the sum of values received so far, divides it by the duration, and outputs the result. |
removeRepeats | Removes repeated values, i.e. discards values that are the same as the previous value. |
The list of predefined result recorders:
Recorder | Description |
last | Records the last value into an output scalar. |
count | Records the count of the input values into an output scalar; functionally equivalent to last(count) |
sum | Records the sum of the input values into an output scalar (or zero if there was none); functionally equivalent to last(sum) |
min | Records the minimum of the input values into an output scalar (or positive infinity if there was none); functionally equivalent to last(min) |
max | Records the maximum of the input values into an output scalar (or negative infinity if there was none); functionally equivalent to last(max) |
mean | Records the mean of the input values into an output scalar (or NaN if there was none); functionally equivalent to last(mean) |
timeavg | Regards the input values with their timestamps as a step function (sample-hold style), and records the time average of the input values into an output scalar; functionally equivalent to last(timeavg) |
stats | Computes basic statistics (count, mean, std.dev, min, max) from the input values, and records them into the output scalar file as a statistic object. |
histogram | Computes a histogram and basic statistics (count, mean, std.dev, min, max) from the input values, and records the reslut into the output scalar file as a histogram object. |
vector | Records the input values with their timestamps into an output vector. |
The names of recorded result items will be formed by concatenating the statistic name and the recording mode with a colon between them: "<statisticName>:<recordingMode>".
Thus, the following statistics
@statistic[dropRate](source=count(drop)/count(pk); record=last,vector?); @statistic[droppedBytes](source=packetBytes(pkdrop); record=sum,vector(sum)?);
will produce the following scalars: dropRate:last, droppedBytes:sum, and the following vectors: dropRate:vector, droppedBytes:vector(sum).
All property keys (except for record) are recorded as result attributes into the vector file or scalar file. The title property will be tweaked a little before recording: the recording mode will be added after a comma, otherwise all result items saved from the same statistic would have exactly the same name.
Example: "Dropped Bytes, sum", "Dropped Bytes, vector(sum)"
It is allowed to use other property keys as well, but they won't be interpreted by the OMNeT++ runtime or the result analysis tool.
To fully understand source and record, it will be useful to see how result recording is set up.
When a module or channel is created in the simulation, the OMNeT++ runtime examines the @statistic properties on its NED declaration, and adds listeners on the signals they mention as input. There are two kinds of listeners associated with result recording: result filters and result recorders. Result filters can be chained, and at the end of the chain there is always a recorder. So, there may be a recorder directly subscribed to a signal, or there may be a chain of one or more filters plus a recorder. Imagine it as a pipeline, or rather a “pipe tree”, where the tree roots are signals, the leaves are result recorders, and the intermediate nodes are result filters.
Result filters typically perform some processing on the values they receive on their inputs (the previous filter in the chain or directly a signal), and propagate them to their output (chained filters and recorders). A filter may also swallow (i.e. not propagate) values. Recorders may write the received values into an output vector, or record output scalar(s) at the end of the simulation.
Many operations exist both in filter and recorder form. For example, the sum filter propagates the sum of values received on its input to its output; and the sum recorder only computes the the sum of received values in order to record it as an output scalar on simulation completion.
The next figure illustrates which filters and recorders are created and how they are connected for the following statistics:
@statistic[droppedBits](source=8*packetBytes(pkdrop); record=sum,vector(sum));
It is often convenient to have a module record statistics per session, per connection, per client, etc. One way of handling this use case is registering signals dynamically (e.g. session1-jitter, session2-jitter, ...), and setting up @statistic-style result recording on each.
The NED file would look like this:
@signal[session*-jitter](type=simtime_t); // note the wildcard @statisticTemplate[sessionJitter](record=mean,vector?);
In the C++ code of the module, you need to register each new signal with registerSignal(), and in addition, tell OMNeT++ to set up statistics recording for it as described by the @statisticTemplate property. The latter can be achieved by calling getEnvir()->addResultRecorders().
char signalName[32]; sprintf(signalName, "session%d-jitter", sessionNum); simsignal_t signal = registerSignal(signalName); char statisticName[32]; sprintf(statisticName, "session%d-jitter", sessionNum); cProperty *statisticTemplate = getProperties()->get("statisticTemplate", "sessionJitter"); getEnvir()->addResultRecorders(this, signal, statisticName, statisticTemplate);
In the @statisticTemplate property, the source key will be ignored (because the signal given as parameter will be used as source). The actual name and index of property will also be ignored. (With @statistic, the index holds the result name, but here the name is explicitly specified in the statisticName parameter.)
When multiple signals are recorded using a common @statisticTemplate property, you'll want the titles of the recorded statistics to differ for each signal. This can be achieved by using dollar variables in the title key of @statisticTemplate. The following variables are available:
For example, if the statistic name is "conn:host1-to-host4(3):bytesSent", and the title is "bytes sent in connection $namePart2", it will become "bytes sent in connection host1-to-host4(3)".
As an alternative to @statisticTemplate and addResultRecorders(), it is also possible to set up result recording programmatically, by creating and attaching result filters and recorders to the desired signals.
The following code example sets up recording to an output vector after removing duplicate values, and is essentially equivalent to the following @statistic line:
@statistic[queueLength](source=qlen; record=vector(removeRepeats); title="Queue Length"; unit=packets);
The C++ code:
simsignal_t signal = registerSignal("qlen"); cResultFilter *warmupFilter = cResultFilterType::get("warmup")->create(); cResultFilter *removeRepeatsFilter = cResultFilterType::get("removeRepeats")->create(); cResultRecorder *vectorRecorder = cResultRecorderType::get("vector")->create(); opp_string_map *attrs = new opp_string_map; (*attrs)["title"] = "Queue Length"; (*attrs)["unit"] = "packets"; vectorRecorder->init(this, "queueLength", "vector", nullptr, attrs); subscribe(signal, warmupFilter); warmupFilter->addDelegate(removeRepeatsFilter); removeRepeatsFilter->addDelegate(vectorRecorder);
Emitting signals for statistical purposes does not differ much from emitting signals for any other purpose. Statistic signals are primarily expected to contain numeric values, so the overloaded emit() functions that take long, double and simtime_t are going to be the most useful ones.
Emitting with timestamp. The emitted values are associated with the current simulation time. At times it might be desirable to associate them with a different timestamp, in much the same way as the recordWithTimestamp() method of cOutVector (see [7.9.1]) does. For example, assume that you want to emit a signal at the start of every successful wireless frame reception. However, whether any given frame reception is going to be successful can only be known after the reception has completed. Hence, values can only be emitted at reception completion, and need to be associated with past timestamps.
To emit a value with a different timestamp, an object containing a (timestamp, value) pair needs to be filled in, and emitted using the emit(simsignal_t, cObject *) method. The class is called cTimestampedValue, and it simply has two public data members called time and value, with types simtime_t and double. It also has a convenience constructor taking these two values.
An example usage:
simtime_t frameReceptionStartTime = ...; double receivePower = ...; cTimestampedValue tmp(frameReceptionStartTime, receivePower); emit(recvPowerSignal, &tmp);
If performance is critical, the cTimestampedValue object may be
made a class member or a static variable to eliminate object
construction/destruction time.
Timestamps must be monotonically increasing.
Emitting non-numeric values. Sometimes it is practical to have multi-purpose signals, or to retrofit an existing non-statistical signal so that it can be recorded as a result. For this reason, signals having non-numeric types (that is, const char * and cObject *) may also be recorded as results. Wherever such values need to be interpreted as numbers, the following rules are used by the built-in result recording listeners:
cITimestampedValue is a C++ interface that may be used as an additional base class for any class. It is declared like this:
class cITimestampedValue { public: virtual ~cITimestampedValue() {} virtual double getSignalValue(simsignal_t signalID) = 0; virtual simtime_t getSignalTime(simsignal_t signalID); };
getSignalValue() is pure virtual (it must return some value), but getSignalTime() has a default implementation that returns the current simulation time. Note the signalID argument that allows the same class to serve multiple signals (i.e. to return different values for each).
You can define your own result filters and recorders in addition to the built-in ones. Similar to defining modules and new NED functions, you have to write the implementation in C++, and then register it with a registration macro to let OMNeT++ know about it. The new result filter or recorder can then be used in the source= and record= attributes of @statistic properties just like the built-in ones.
Result filters must be subclassed from cResultFilter or from one of its more specific subclasses cNumericResultFilter and cObjectResultFilter. The new result filter class needs to be registered using the Register_ResultFilter(NAME, CLASSNAME) macro.
Similarly, a result recorder must subclass from the cResultRecorder or the more specific cNumericResultRecorder class, and be registered using the Register_ResultRecorder(NAME, CLASSNAME) macro.
An example result filter implementation from the simulation runtime:
/** * Filter that outputs the sum of signal values divided by the measurement * interval (simtime minus warmup period). */ class SumPerDurationFilter : public cNumericResultFilter { protected: double sum; protected: virtual bool process(simtime_t& t, double& value, cObject *details); public: SumPerDurationFilter() {sum = 0;} }; Register_ResultFilter("sumPerDuration", SumPerDurationFilter); bool SumPerDurationFilter::process(simtime_t& t, double& value, cObject *) { sum += value; value = sum / (simTime() - getSimulation()->getWarmupPeriod()); return true; }