SimulationManual PDF
SimulationManual PDF
Simulation Manual
Version 5.4.1
Copyright © 2016 András Varga and OpenSim Ltd.
OMNeT++ Simulation Manual –
Chapters
Contents v
1 Introduction 1
2 Overview 3
4 Simple Modules 49
13 Eventlog 337
15 Testing 349
iii
18 Embedding the Simulation Kernel 385
References 501
Index 504
OMNeT++ Simulation Manual –
Contents
Contents v
1 Introduction 1
1.1 What Is OMNeT++? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Organization of This Manual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2 Overview 3
2.1 Modeling Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1.1 Hierarchical Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.2 Module Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.3 Messages, Gates, Links . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.4 Modeling of Packet Transmissions . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.5 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.6 Topology Description Method . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Programming the Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3 Using OMNeT++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.3.1 Building and Running Simulations . . . . . . . . . . . . . . . . . . . . . . . 6
2.3.2 What Is in the Distribution . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
v
3.6 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.6.1 Assigning a Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.6.2 Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.6.3 volatile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.6.4 Units . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.6.5 XML Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.7 Gates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.8 Submodules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.9 Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.9.1 Channel Specification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.9.2 Channel Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.10 Multiple Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.10.1Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.10.2Connection Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.11 Parametric Submodule and Connection Types . . . . . . . . . . . . . . . . . . . . . 37
3.11.1Parametric Submodule Types . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.11.2Conditional Parametric Submodules . . . . . . . . . . . . . . . . . . . . . . 39
3.11.3Parametric Connection Types . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.12 Metadata Annotations (Properties) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.12.1Property Indices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.12.2Data Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
3.12.3Overriding and Extending Property Values . . . . . . . . . . . . . . . . . . . 43
3.13 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.14 Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.14.1Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
3.14.2Name Resolution, Imports . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
3.14.3Name Resolution With "like" . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
3.14.4The Default Package . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4 Simple Modules 49
4.1 Simulation Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.1.1 Discrete Event Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.1.2 The Event Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.1.3 Events and Event Execution Order in OMNeT++ . . . . . . . . . . . . . . . 50
4.1.4 Simulation Time . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.1.5 FES Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
4.2 Components, Simple Modules, Channels . . . . . . . . . . . . . . . . . . . . . . . . 52
4.3 Defining Simple Module Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.3.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.3.2 Constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.3.3 Initialization and Finalization . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.4 Adding Functionality to cSimpleModule . . . . . . . . . . . . . . . . . . . . . . . . 58
4.4.1 handleMessage() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.4.2 activity() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.4.3 How to Avoid Global Variables . . . . . . . . . . . . . . . . . . . . . . . . . . 68
4.4.4 Reusing Module Code via Subclassing . . . . . . . . . . . . . . . . . . . . . 68
4.5 Accessing Module Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
4.5.1 Volatile and Non-Volatile Parameters . . . . . . . . . . . . . . . . . . . . . . 69
4.5.2 Changing a Parameter’s Value . . . . . . . . . . . . . . . . . . . . . . . . . . 70
4.5.3 Further cPar Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.5.4 Emulating Parameter Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
4.5.5 handleParameterChange() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
4.6 Accessing Gates and Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.6.1 Gate Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
4.6.2 Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
4.6.3 The Connection’s Channel . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
4.7 Sending and Receiving Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.7.1 Self-Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
4.7.2 Sending Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
4.7.3 Broadcasts and Retransmissions . . . . . . . . . . . . . . . . . . . . . . . . 80
4.7.4 Delayed Sending . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
4.7.5 Direct Message Sending . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
4.7.6 Packet Transmissions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
4.7.7 Receiving Messages with activity() . . . . . . . . . . . . . . . . . . . . . . . . 86
4.8 Channels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.8.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.8.2 The Channel API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.8.3 Channel Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.9 Stopping the Simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4.9.1 Normal Termination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4.9.2 Raising Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
4.10 Finite State Machines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.10.1Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.11 Navigating the Module Hierarchy . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
4.11.1Module Vectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
4.11.2Component IDs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
4.11.3Walking Up and Down the Module Hierarchy . . . . . . . . . . . . . . . . . 96
4.11.4Finding Modules by Path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
4.11.5Iterating over Submodules . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
4.11.6Navigating Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.12 Direct Method Calls Between Modules . . . . . . . . . . . . . . . . . . . . . . . . . 98
4.13 Dynamic Module Creation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
4.13.1When To Use . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
4.13.2Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
4.13.3Creating Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
4.13.4Deleting Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
4.13.5Module Deletion and finish() . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
4.13.6Creating Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
4.13.7Removing Connections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
4.14 Signals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
4.14.1Design Considerations and Rationale . . . . . . . . . . . . . . . . . . . . . . 104
4.14.2The Signals Mechanism . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
4.14.3Listening to Model Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
4.15 Signal-Based Statistics Recording . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
4.15.1Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
4.15.2Declaring Statistics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.15.3Statistics Recording for Dynamically Registered Signals . . . . . . . . . . . 117
4.15.4Adding Result Filters and Recorders Programmatically . . . . . . . . . . . . 118
4.15.5Emitting Signals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
4.15.6Writing Result Filters and Recorders . . . . . . . . . . . . . . . . . . . . . . 120
13 Eventlog 337
13.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
13.2 Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
13.2.1File Name . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
13.2.2Recording Intervals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
13.2.3Recording Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
13.2.4Recording Message Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
13.3 Eventlog Tool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
13.3.1Filter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
13.3.2Echo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
15 Testing 349
15.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
15.1.1Verification, Validation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
15.1.2Unit Testing, Regression Testing . . . . . . . . . . . . . . . . . . . . . . . . . 349
15.2 The opp_test Tool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
15.2.1Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
15.2.2Terminology . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
15.2.3Test File Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
15.2.4Test Description . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
15.2.5Test Code Generation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
15.2.6PASS Criteria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 355
15.2.7Extra Processing Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
15.2.8Unresolved . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
15.2.9opp_test Synopsys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
15.2.10
Writing the Control Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
15.3 Smoke Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359
15.4 Fingerprint Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
15.4.1Fingerprint Computation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
15.4.2Fingerprint Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
15.5 Unit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
15.6 Module Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
15.7 Statistical Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
15.7.1Validation Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
15.7.2Statistical Regression Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
15.7.3Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
Index 504
OMNeT++ Simulation Manual – Introduction
Chapter 1
Introduction
OMNeT++ itself is not a simulator of anything concrete, but rather provides infrastructure
and tools for writing simulations. One of the fundamental ingredients of this infrastructure
is a component architecture for simulation models. Models are assembled from reusable
components termed modules. Well-written modules are truly reusable, and can be combined
in various ways like LEGO blocks.
Modules can be connected with each other via gates (other systems would call them ports), and
combined to form compound modules. The depth of module nesting is not limited. Modules
communicate through message passing, where messages may carry arbitrary data structures.
Modules can pass messages along predefined paths via gates and connections, or directly to
their destination; the latter is useful for wireless simulations, for example. Modules may
have parameters that can be used to customize module behavior and/or to parameterize
the model’s topology. Modules at the lowest level of the module hierarchy are called simple
modules, and they encapsulate model behavior. Simple modules are programmed in C++, and
make use of the simulation library.
OMNeT++ simulations can be run under various user interfaces. Graphical, animating user
interfaces are highly useful for demonstration and debugging purposes, and command-line
user interfaces are best for batch execution.
1
OMNeT++ Simulation Manual – Introduction
The simulator as well as user interfaces and tools are highly portable. They are tested on the
most common operating systems (Linux, Mac OS/X, Windows), and they can be compiled out
of the box or after trivial modifications on most Unix-like operating systems.
OMNeT++ also supports parallel distributed simulation. OMNeT++ can use several mecha-
nisms for communication between partitions of a parallel distributed simulation, for example
MPI or named pipes. The parallel simulation algorithm can easily be extended, or new ones
can be plugged in. Models do not need any special instrumentation to be run in parallel –
it is just a matter of configuration. OMNeT++ can even be used for classroom presentation
of parallel simulation algorithms, because simulations can be run in parallel even under the
GUI that provides detailed feedback on what is going on.
OMNEST is the commercially supported version of OMNeT++. OMNeT++ is free only for aca-
demic and non-profit use; for commercial purposes, one needs to obtain OMNEST licenses
from Simulcraft Inc.
• The chapters 8 and 14explain how to customize the network graphics and how to write
NED source code comments from which documentation can be generated.
• Chapters 9, 10, 11 and 12 deal with practical issues like building and running simula-
tions and analyzing results, and describe the tools OMNeT++ provides to support these
tasks.
2
OMNeT++ Simulation Manual – Overview
Chapter 2
Overview
Network
Simple modules
Compound module
Modules communicate with messages that may contain arbitrary data, in addition to usual
attributes such as a timestamp. Simple modules typically send messages via gates, but it
is also possible to send them directly to their destination modules. Gates are the input and
output interfaces of modules: messages are sent through output gates and arrive through
input gates. An input gate and output gate can be linked by a connection. Connections are
created within a single level of module hierarchy; within a compound module, corresponding
gates of two submodules, or a gate of one submodule and a gate of the compound module
can be connected. Connections spanning hierarchy levels are not permitted, as they would
3
OMNeT++ Simulation Manual – Overview
hinder model reuse. Because of the hierarchical structure of the model, messages typically
travel through a chain of connections, starting and arriving in simple modules. Compound
modules act like "cardboard boxes" in the model, transparently relaying messages between
their inner realm and the outside world. Parameters such as propagation delay, data rate
and bit error rate, can be assigned to connections. One can also define connection types
with specific properties (termed channels) and reuse them in several places. Modules can
have parameters. Parameters are used mainly to pass configuration data to simple modules,
and to help define model topology. Parameters can take string, numeric, or boolean values.
Because parameters are represented as objects in the program, parameters – in addition to
holding constants – may transparently act as sources of random numbers, with the actual
distributions provided with the model configuration. They may interactively prompt the user
for the value, and they might also hold expressions referencing other parameters. Compound
modules may pass parameters or expressions of parameters to their submodules.
OMNeT++ provides efficient tools for the user to describe the structure of the actual system.
Some of the main features are the following:
4
OMNeT++ Simulation Manual – Overview
Module types can be stored in files separately from the place of their actual usage. This means
that the user can group existing module types and create component libraries. This feature
will be discussed later, in chapter 11.
2.1.5 Parameters
Modules can have parameters. Parameters can be assigned in either the NED files or the
configuration file omnetpp.ini.
Parameters can be used to customize simple module behavior, and to parameterize the model
topology.
Parameters can take string, numeric or boolean values, or can contain XML data trees. Nu-
meric values include expressions using other parameters and calling C functions, random
variables from different distributions, and values input interactively by the user.
5
OMNeT++ Simulation Manual – Overview
• message, packet
The classes are also specially instrumented, allowing one to traverse objects of a running
simulation and display information about them such as name, class name, state variables or
contents. This feature makes it possible to create a simulation GUI where all internals of the
simulation are visible.
• NED language topology description(s) (.ned files) that describe the module structure with
parameters, gates, etc. NED files can be written using any text editor, but the OMNeT++
IDE provides excellent support for two-way graphical and text editing.
6
OMNeT++ Simulation Manual – Overview
• Message definitions (.msg files) that let one define message types and add data fields to
them. OMNeT++ will translate message definitions into full-fledged C++ classes.
• Simple module sources. They are C++ files, with .h/.cc suffix.
• Simulation kernel. This contains the code that manages the simulation and the simula-
tion class library. It is written in C++, compiled into a shared or static library.
• User interfaces. OMNeT++ user interfaces are used in simulation execution, to facilitate
debugging, demonstration, or batch execution of simulations. They are written in C++,
compiled into libraries.
Simulation programs are built from the above components. First, .msg files are translated into
C++ code using the opp_msgc. program. Then all C++ sources are compiled and linked with
the simulation kernel and a user interface library to form a simulation executable or shared
library. NED files are loaded dynamically in their original text forms when the simulation
program starts.
User Interfaces
The primary purpose of user interfaces is to make the internals of the model visible to the
user, to control simulation execution, and possibly allow the user to intervene by changing
variables/objects inside the model. This is very important in the development/debugging
phase of the simulation project. Equally important, a hands-on experience allows the user to
get a feel of the model’s behavior. The graphical user interface can also be used to demonstrate
a model’s operation.
The same simulation model can be executed with various user interfaces, with no change in
the model files themselves. The user would typically test and debug the simulation with a
powerful graphical user interface, and finally run it with a simple, fast user interface that
supports batch execution.
7
OMNeT++ Simulation Manual – Overview
Component Libraries
Module types can be stored in files separate from the place of their actual use, enabling the
user to group existing module types and create component libraries.
A simulation executable can store several independent models that use the same set of simple
modules. The user can specify in the configuration file which model is to be run. This allows
one to build one large executable that contains several simulation models, and distribute it as
a standalone simulation tool. The flexibility of the topology description language also supports
this approach.
8
OMNeT++ Simulation Manual – Overview
The Windows version of OMNeT++ contains a redistribution of the MinGW gcc compiler, to-
gether with a copy of MSYS that provides Unix tools commonly used in Makefiles. The MSYS
directory also contains various 3rd party open-source libraries needed to compile and run
OMNeT++.
9
OMNeT++ Simulation Manual – Overview
10
OMNeT++ Simulation Manual – The NED Language
Chapter 3
Component-Based. Simple modules and compound modules are inherently reusable, which
not only reduces code copying, but more importantly, allows component libraries (like
the INET Framework, MiXiM, Castalia, etc.) to exist.
Interfaces. Module and channel interfaces can be used as a placeholder where normally
a module or channel type would be used, and the concrete module or channel type
is determined at network setup time by a parameter. Concrete module types have to
“implement” the interface they can substitute. For example, given a compound module
type named MobileHost contains a mobility submodule of the type IMobility (where
IMobility is a module interface), the actual type of mobility may be chosen from the
module types that implemented IMobility (RandomWalkMobility, TurtleMobility,
etc.)
Inheritance. Modules and channels can be subclassed. Derived modules and channels may
add new parameters, gates, and (in the case of compound modules) new submodules and
connections. They may set existing parameters to a specific value, and also set the gate
size of a gate vector. This makes it possible, for example, to take a GenericTCPClientApp
module and derive an FTPClientApp from it by setting certain parameters to a fixed
value; or to derive a WebClientHost compound module from a BaseHost compound
module by adding a WebClientApp submodule and connecting it to the inherited TCP
submodule.
Packages. The NED language features a Java-like package structure, to reduce the risk of
11
OMNeT++ Simulation Manual – The NED Language
name clashes between different models. NEDPATH (similar to Java’s CLASSPATH) has also
been introduced to make it easier to specify dependencies among simulation models.
Inner types. Channel types and module types used locally by a compound module can be
defined within the compound module, in order to reduce namespace pollution.
NOTE: The NED language has changed significantly in the 4.0 version. Inheritance,
interfaces, packages, inner types, metadata annotations, inout gates were all added in
the 4.0 release, together with many other features. Since the basic syntax has changed as
well, old NED files need to be converted to the new syntax. There are automated tools for
this purpose, so manual editing is only needed to take advantage of new NED features.
The NED language has an equivalent tree representation which can be serialized to XML; that
is, NED files can be converted to XML and back without loss of data, including comments.
This lowers the barrier for programmatic manipulation of NED files; for example extracting
information, refactoring and transforming NED, generating NED from information stored in
other systems like SQL databases, and so on.
NOTE: This chapter is going to explain the NED language gradually, via examples. A
more formal and concise treatment can be found in Appendix B.
12
OMNeT++ Simulation Manual – The NED Language
submodules:
node1: Node;
node2: Node;
node3: Node;
...
connections:
node1.port++ <--> {datarate=100Mbps;} <--> node2.port++;
node2.port++ <--> {datarate=100Mbps;} <--> node4.port++;
node4.port++ <--> {datarate=100Mbps;} <--> node6.port++;
...
}
The above code defines a network type named Network. Note that the NED language uses the
familiar curly brace syntax, and “//” to denote comments.
NOTE: Comments in NED not only make the source code more readable, but in the
OMNeT++ IDE they also are displayed at various places (tooltips, content assist, etc), and
become part of the documentation extracted from the NED files. The NED documentation
system, not unlike JavaDoc or Doxygen, will be described in Chapter 14.
The network contains several nodes, named node1, node2, etc. from the NED module type
Node. We’ll define Node in the next sections.
The second half of the declaration defines how the nodes are to be connected. The double
arrow means bidirectional connection. The connection points of modules are called gates,
and the port++ notation adds a new gate to the port[] gate vector. Gates and connections
will be covered in more detail in sections 3.7 and 3.9. Nodes are connected with a channel
that has a data rate of 100Mbps.
NOTE: In many other systems, the equivalent of OMNeT++ gates are called ports. We
have retained the term gate to reduce collisions with other uses of the otherwise over-
loaded word port: router port, TCP port, I/O port, etc.
13
OMNeT++ Simulation Manual – The NED Language
The above code would be placed into a file named Net6.ned. It is a convention to put every
NED definition into its own file and to name the file accordingly, but it is not mandatory to do
so.
One can define any number of networks in the NED files, and for every simulation the user
has to specify which network to set up. The usual way of specifying the network is to put the
network option into the configuration (by default the omnetpp.ini file):
[General]
network = Network
Later sections will cover the concepts used (inner types, channels, the DatarateChannel
built-in type, inheritance) in detail.
14
OMNeT++ Simulation Manual – The NED Language
simple module types which we are going to assemble into a compound module. We’ll have one
simple module for traffic generation (App), one for routing (Routing), and one for queueing up
packets to be sent out (Queue). For brevity, we omit the bodies of the latter two in the code
below.
simple App
{
parameters:
int destAddress;
...
@display("i=block/browser");
gates:
input in;
output out;
}
simple Routing
{
...
}
simple Queue
{
...
}
By convention, the above simple module declarations go into the App.ned, Routing.ned and
Queue.ned files.
NOTE: Note that module type names (App, Routing, Queue) begin with a capital letter,
and parameter and gate names begin with lowercase – this is the recommended naming
convention. Capitalization matters because the language is case sensitive.
Let us look at the first simple module type declaration. App has a parameter called destAd-
dress (others have been omitted for now), and two gates named out and in for sending and
receiving application packets.
The argument of @display() is called a display string, and it defines the rendering of the
module in graphical environments; "i=..." defines the default icon.
Generally, @-words like @display are called properties in NED, and they are used to annotate
various objects with metadata. Properties can be attached to files, modules, parameters,
gates, connections, and other objects, and parameter values have a very flexible syntax.
15
OMNeT++ Simulation Manual – The NED Language
parameters:
int address;
@display("i=misc/node_vs,gold");
gates:
inout port[];
submodules:
app: App;
routing: Routing;
queue[sizeof(port)]: Queue;
connections:
routing.localOut --> app.in;
routing.localIn <-- app.out;
for i=0..sizeof(port)-1 {
routing.out[i] --> queue[i].in;
routing.in[i] <-- queue[i].out;
queue[i].line <--> port[i];
}
}
Compound modules, like simple modules, may have parameters and gates. Our Node module
contains an address parameter, plus a gate vector of unspecified size, named port. The ac-
tual gate vector size will be determined implicitly by the number of neighbours when we create
a network from nodes of this type. The type of port[] is inout, which allows bidirectional
connections.
The modules that make up the compound module are listed under submodules. Our Node
compound module type has an app and a routing submodule, plus a queue[] submodule
vector that contains one Queue module for each port, as specified by [sizeof(port)]. (It
is legal to refer to [sizeof(port)] because the network is built in top-down order, and the
node is already created and connected at network level when its submodule structure is built
out.)
In the connections section, the submodules are connected to each other and to the parent
module. Single arrows are used to connect input and output gates, and double arrows connect
inout gates, and a for loop is utilized to connect the routing module to each queue module,
and to connect the outgoing/incoming link (line gate) of each queue to the corresponding
16
OMNeT++ Simulation Manual – The NED Language
We have created the NED definitions for this example, but how are they used by OMNeT++?
When the simulation program is started, it loads the NED files. The program should already
contain the C++ classes that implement the needed simple modules, App, Routing and Queue;
their C++ code is either part of the executable or is loaded from a shared library. The simu-
lation program also loads the configuration (omnetpp.ini), and determines from it that the
simulation model to be run is the Network network. Then the network is instantiated for
simulation.
The simulation model is built in a top-down preorder fashion. This means that starting from
an empty system module, all submodules are created, their parameters and gate vector sizes
are assigned, and they are fully connected before the submodule internals are built.
***
In the following sections we’ll go through the elements of the NED language and look at them
in more detail.
Both the parameters and gates sections are optional, that is, they can be left out if there is
no parameter or gate. In addition, the parameters keyword itself is optional too; it can be left
out even if there are parameters or properties.
Note that the NED definition doesn’t contain any code to define the operation of the module:
that part is expressed in C++. By default, OMNeT++ looks for C++ classes of the same name
as the NED type (so here, Queue).
One can explicitly specify the C++ class with the @class property. Classes with namespace
qualifiers are also accepted, as shown in the following example that uses the mylib::Queue
class:
17
OMNeT++ Simulation Manual – The NED Language
simple Queue
{
parameters:
int capacity;
@class(mylib::Queue);
@display("i=block/queue");
gates:
input in;
output out;
}
If there are several modules whose C++ implementation classes are in the same namespace,
a better alternative to @class is the @namespace property. The C++ namespace given with
@namespace will be prepended to the normal class name. In the following example, the C++
classes will be mylib::App, mylib::Router and mylib::Queue:
@namespace(mylib);
simple App {
...
}
simple Router {
...
}
simple Queue {
...
}
The @namespace property may not only be specified at file level as in the above example, but
for packages as well. When placed in a file called package.ned, the namespace will apply to
all components in that package and below.
The implementation C++ classes need to be subclassed from the cSimpleModule library class;
chapter 4 of this manual describes in detail how to write them.
Simple modules can be extended (or specialized) via subclassing. The motivation for subclass-
ing can be to set some open parameters or gate sizes to a fixed value (see 3.6 and 3.7), or to
replace the C++ class with a different one. Now, by default, the derived NED module type will
inherit the C++ class from its base, so it is important to remember that you need to write out
@class if you want it to use the new class.
The following example shows how to specialize a module by setting a parameter to a fixed
value (and leaving the C++ class unchanged):
simple Queue
{
int capacity;
...
}
18
OMNeT++ Simulation Manual – The NED Language
In the next example, the author wrote a PriorityQueue C++ class, and wants to have a
corresponding NED type, derived from Queue. However, it does not work as expected:
simple PriorityQueue extends Queue // wrong! still uses the Queue C++ class
{
}
The correct solution is to add a @class property to override the inherited C++ class:
simple PriorityQueue extends Queue
{
@class(PriorityQueue);
}
NOTE: When there is a temptation to add code to a compound module, then encapsulate
the code into a simple module, and add it as a submodule.
A compound module declaration may contain several sections, all of them optional:
module Host
{
types:
...
parameters:
...
gates:
...
submodules:
...
connections:
...
}
Modules contained in a compound module are called submodules, and they are listed in the
submodules section. One can create arrays of submodules (i.e. submodule vectors), and the
submodule type may come from a parameter.
Connections are listed under the connections section of the declaration. One can create
connections using simple programming constructs (loop, conditional). Connection behaviour
can be defined by associating a channel with the connection; the channel type may also come
from a parameter.
Module and channel types only used locally can be defined in the types section as inner
types, so that they do not pollute the namespace.
1 Although the C++ class for a compound module can be overridden with the @class property, this is a feature that
should probably never be used. Encapsulate the code into a simple module, and add it as a submodule.
19
OMNeT++ Simulation Manual – The NED Language
Compound modules may be extended via subclassing. Inheritance may add new submod-
ules and new connections as well, not only parameters and gates. Also, one may refer to
inherited submodules, to inherited types etc. What is not possible is to "de-inherit" or modify
submodules or connections.
In the following example, we show how to assemble common protocols into a "stub" for wireless
hosts, and add user agents via subclassing.2
module WirelessHostBase
{
gates:
input radioIn;
submodules:
tcp: TCP;
ip: IP;
wlan: Ieee80211;
connections:
tcp.ipOut --> ip.tcpIn;
tcp.ipIn <-- ip.tcpOut;
ip.nicOut++ --> wlan.ipIn;
ip.nicIn++ <-- wlan.ipOut;
wlan.radioIn <-- radioIn;
}
The WirelessHost compound module can further be extended, for example with an Ethernet
port:
2 Module types, gate names, etc. used in the example are fictional, not based on an actual OMNeT++-based model
framework
20
OMNeT++ Simulation Manual – The NED Language
3.5 Channels
Channels encapsulate parameters and behaviour associated with connections. Channels are
like simple modules, in the sense that there are C++ classes behind them. The rules for
finding the C++ class for a NED channel type is the same as with simple modules: the default
class name is the NED type name unless there is a @class property (@namespace is also
recognized), and the C++ class is inherited when the channel is subclassed.
Thus, the following channel type would expect a CustomChannel C++ class to be present:
channel CustomChannel // requires a CustomChannel C++ class
{
}
The practical difference compared to modules is that one rarely needs to write custom chan-
nel C++ class because there are predefined channel types that one can subclass from, inher-
iting their C++ code. The predefined types are: ned.IdealChannel, ned.DelayChannel and
ned.DatarateChannel. (“ned” is the package name; one can get rid of it by importing the
types with the import ned.* directive. Packages and imports are described in section 3.14.)
IdealChannel has no parameters, and lets through all messages without delay or any side
effect. A connection without a channel object and a connection with an IdealChannel behave
in the same way. Still, IdealChannel has its uses, for example when a channel object is
required so that it can carry a new property or parameter that is going to be read by other
parts of the simulation model.
DelayChannel has two parameters:
• delay is a double parameter which represents the propagation delay of the message.
Values need to be specified together with a time unit (s, ms, us, etc.)
• disabled is a boolean parameter that defaults to false; when set to true, the channel
object will drop all messages.
• datarate is a double parameter that represents the data rate of the channel. Values
need to be specified in bits per second or its multiples as unit (bps, kbps, Mbps, Gbps,
etc.) Zero is treated specially and results in zero transmission duration, i.e. it stands
for infinite bandwidth. Zero is also the default. Data rate is used for calculating the
transmission duration of packets.
• ber and per stand for Bit Error Rate and Packet Error Rate, and allow basic error
modelling. They expect a double in the [0, 1] range. When the channel decides (based
on random numbers) that an error occurred during transmission of a packet, it sets an
error flag in the packet object. The receiver module is expected to check the flag, and
discard the packet as corrupted if it is set. The default ber and per are zero.
NOTE: There is no channel parameter that specifies whether the channel delivers the
message object to the destination module at the end or at the start of the reception; that is
decided by the C++ code of the target simple module. See the setDeliverOnReception-
Start() method of cGate.
The following example shows how to create a new channel type by specializing Datarate-
Channel:
21
OMNeT++ Simulation Manual – The NED Language
NOTE: The three built-in channel types are also used for connections where the channel
type is not explicitly specified.
One may add parameters and properties to channels via subclassing, and may modify existing
ones. In the following example, we introduce distance-based calculation of the propagation
delay:
channel DatarateChannel2 extends ned.DatarateChannel
{
double distance @unit(m);
delay = this.distance / 200000km * 1s;
}
Parameters are primarily intended to be read by the underlying C++ class, but new parameters
may also be added as annotations to be used by other parts of the model. For example, a cost
parameter may be used for routing decisions in routing module, as shown in the example
below. The example also shows annotation using properties (@backbone).
channel Backbone extends ned.DatarateChannel
{
@backbone;
double cost = default(1);
}
3.6 Parameters
Parameters are variables that belong to a module. Parameters can be used in building the
topology (number of nodes, etc), and to supply input to C++ code that implements simple
modules and channels.
Parameters can be of type double, int, bool, string and xml; they can also be declared
volatile. For the numeric types, a unit of measurement can also be specified (@unit prop-
erty), to increase type safety.
Parameters can get their value from NED files or from the configuration (omnetpp.ini). A
default value can also be given (default(...)), which is used if the parameter is not assigned
otherwise.
The following example shows a simple module that has five parameters, three of which have
default values:
simple App
{
parameters:
string protocol; // protocol to use: "UDP" / "IP" / "ICMP" / ...
int destAddress; // destination address
22
OMNeT++ Simulation Manual – The NED Language
This definition sets the protocol parameter to a fixed value ("ICMP/ECHO"), and changes
the default values of the sendInterval and packetLength parameters. protocol is now
locked down in PingApp, its value cannot be modified via further subclassing or other ways.
sendInterval and packetLength are still unassigned here, only their default values have
been overwritten.
Now, let us see the definition of a Host compound module that uses PingApp as submodule:
module Host
{
submodules:
ping : PingApp {
packetLength = 128B; // always ping with 128-byte packets
}
...
}
This definition sets the packetLength parameter to a fixed value. It is now hardcoded that
Hosts send 128-byte ping packets; this setting cannot be changed from NED or the configu-
ration.
It is not only possible to set a parameter from the compound module that contains the sub-
module, but also from modules higher up in the module tree. A network that employs several
Host modules could be defined like this:
23
OMNeT++ Simulation Manual – The NED Language
network Network
{
submodules:
host[100]: Host {
ping.timeToLive = default(3);
ping.destAddress = default(0);
}
...
}
Parameter assignment can also be placed into the parameters block of the parent compound
module, which provides additional flexibility. The following definition sets up the hosts so
that half of them pings host #50, and the other half pings host #0:
network Network
{
parameters:
host[*].ping.timeToLive = default(3);
host[0..49].ping.destAddress = default(50);
host[50..].ping.destAddress = default(0);
submodules:
host[100]: Host;
...
}
Note the use of asterisk to match any index, and .. to match index ranges.
If there were a number of individual hosts instead of a submodule vector, the network defini-
tion could look like this:
network Network
{
parameters:
host*.ping.timeToLive = default(3);
host{0..49}.ping.destAddress = default(50);
host{50..}.ping.destAddress = default(0);
submodules:
host0: Host;
host1: Host;
host2: Host;
...
host99: Host;
}
An asterisk matches any substring not containing a dot, and a .. within a pair of curly braces
matches a natural number embedded in a string.
In most assigments we have seen above, the left hand side of the equal sign contained a dot
and often a wildcard as well (asterisk or numeric range); we call these assignments pattern
assignments or deep assignments.
There is one more wildcard that can be used in pattern assignments, and this is the double
asterisk; it matches any sequence of characters including dots, so it can match multiple path
elements. An example:
24
OMNeT++ Simulation Manual – The NED Language
network Network
{
parameters:
**.timeToLive = default(3);
**.destAddress = default(0);
submodules:
host0: Host;
host1: Host;
...
}
Note that some assignments in the above examples changed default values, while others set
parameters to fixed values. Parameters that received no fixed value in the NED files can be
assigned from the configuration (omnetpp.ini).
IMPORTANT: A non-default value assigned from NED cannot be overwritten later in NED
or from ini files; it becomes “hardcoded” as far as ini files and NED usage are concerned.
In contrast, default values are possible to overwrite.
A parameter can be assigned in the configuration using a similar syntax as NED pattern
assignments (actually, it would be more historically accurate to say it the other way round,
that NED pattern assignments use a similar syntax to ini files):
Network.host[*].ping.sendInterval = 500ms # for the host[100] example
Network.host*.ping.sendInterval = 500ms # for the host0,host1,... example
**.sendInterval = 500ms
One often uses the double asterisk to save typing. One can write
**.ping.sendInterval = 500ms
Or if one is certain that only ping modules have sendInterval parameters, the following will
suffice:
**.sendInterval = 500ms
If there is no assignment for a parameter in NED or in the ini file, the default value (given
with =default(...) in NED) will be applied implicitly. If there is no default value, the user
will be asked, provided the simulation program is allowed to do that; otherwise there will be
an error. (Interactive mode is typically disabled for batch executions where it would do more
harm than good.)
It is also possible to explicitly apply the default (this can sometimes be useful):
**.sendInterval = default
Finally, one can explicitly ask the simulator to prompt the user interactively for the value
(again, provided that interactivity is enabled; otherwise this will result in an error):
**.sendInterval = ask
25
OMNeT++ Simulation Manual – The NED Language
NOTE: How can one decide whether to assign a parameter from NED or from an ini
file? The advantage of ini files is that they allow a cleaner separation of the model and
experiments. NED files (together with C++ code) are considered to be part of the model,
and to be more or less constant. Ini files, on the other hand, are for experimenting with
the model by running it several times with different parameters. Thus, parameters that
are expected to change (or make sense to be changed) during experimentation should be
put into ini files.
3.6.2 Expressions
Parameter values may be given with expressions. NED language expressions have a C-like
syntax, with some variations on operator names: binary and logical XOR are # and ##, while
ˆ has been reassigned to power-of instead. The + operator does string concatenation as
well as numeric addition. Expressions can use various numeric, string, stochastic and other
functions (fabs(), toUpper(), uniform(), erlang_k(), etc.).
NOTE: The list of NED functions can be found in Appendix D. The user can also extend
NED with new functions.
Expressions may refer to module parameters, gate vector and module vector sizes (using the
sizeof operator) and the index of the current module in a submodule vector (index).
Expressions may refer to parameters of the compound module being defined, of the current
module (with the this. prefix), and to parameters of already defined submodules, with the
syntax submodule.parametername (or submodule[index].parametername).
3.6.3 volatile
The volatile modifier causes the parameter’s value expression to be evaluated every time
the parameter is read. This has significance if the expression is not constant, for example it
involves numbers drawn from a random number generator. In contrast, non-volatile param-
eters are evaluated only once. (This practically means that they are evaluated and replaced
with the resulting constant at the start of the simulation.)
To better understand volatile, let’s suppose we have a Queue simple module that has a
volatile double parameter named serviceTime.
simple Queue
{
parameters:
volatile double serviceTime;
}
Because of the volatile modifier, the queue module’s C++ implementation is expected to re-
read the serviceTime parameter whenever a value is needed; that is, for every job serviced.
Thus, if serviceTime is assigned an expression like uniform(0.5s, 1.5s), every job will
have a different, random service time. To highlight this effect, here’s how one can have a
time-varying parameter by exploiting the simTime() NED function that returns the current
simulation time:
**.serviceTime = simTime()<1000s ? 1s : 2s # queue that slows down after 1000s
In practice, a volatile parameters are typically used as a configurable source of random num-
bers for modules.
26
OMNeT++ Simulation Manual – The NED Language
NOTE: This does not mean that a non-volatile parameter could not be assigned a random
value like uniform(0.5s, 1.5s). It can, but that would have a totally different effect:
the simulation would use a constant service time, say 1.2975367s, chosen randomly at
the beginning of the simulation.
3.6.4 Units
One can declare a parameter to have an associated unit of measurement, by adding the @unit
property. An example:
simple App
{
parameters:
volatile double sendInterval @unit(s) = default(exponential(350ms));
volatile int packetLength @unit(byte) = default(4KiB);
...
}
The @unit(s) and @unit(byte) declarations specify the measurement unit for the param-
eter. Values assigned to parameters must have the same or compatible unit, i.e. @unit(s)
accepts milliseconds, nanoseconds, minutes, hours, etc., and @unit(byte) accepts kilobytes,
megabytes, etc. as well.
NOTE: The list of units accepted by OMNeT++ is listed in the Appendix, see A.5.9.
Unknown units (bogomips, etc.) can also be used, but there are no conversions for them,
i.e. decimal prefixes will not be recognized.
The OMNeT++ runtime does a full and rigorous unit check on parameters to ensure “unit
safety” of models. Constants should always include the measurement unit.
The @unit property of a parameter cannot be added or overridden in subclasses or in sub-
module declarations.
27
OMNeT++ Simulation Manual – The NED Language
simple TrafGen {
parameters:
xml profile;
gates:
output out;
}
module Node {
submodules:
trafGen1 : TrafGen {
profile = xmldoc("data.xml");
}
...
}
xmldoc() also lets one select an element within an XML file. In case one has a model that
contains numerous modules that need XML input, this feature allows the user get rid of
the countless small XML files by aggregating them into a single XML file. For example, the
following XML file contains two profiles identified with the IDs gen1 and gen2:
<?xml>
<root>
<profile id="gen1">
<param>3</param>
<param>5</param>
</profile>
<profile id="gen2">
<param>9</param>
</profile>
</root>
And one can assign each profile to a corresponding submodule using an XPath-like expres-
sion:
module Node {
submodules:
trafGen1 : TrafGen {
profile = xmldoc("all.xml", "/root/profile[@id=’gen1’]");
}
trafGen2 : TrafGen {
profile = xmldoc("all.xml", "/root/profile[@id=’gen2’]");
}
}
It is also possible to create an XML document from a string constant, using the xml() func-
tion. This is especially useful for creating a default value for xml parameters. An example:
simple TrafGen {
parameters:
xml profile = xml("<root/>"); // empty document as default
...
}
The xml() function, like xmldoc(), also supports an optional second XPath parameter for
selecting a subtree.
28
OMNeT++ Simulation Manual – The NED Language
3.7 Gates
Gates are the connection points of modules. OMNeT++ has three types of gates: input, output
and inout, the latter being essentially an input and an output gate glued together.
A gate, whether input or output, can only be connected to one other gate. (For compound
module gates, this means one connection “outside” and one “inside”.) It is possible, though
generally not recommended, to connect the input and output sides of an inout gate separately
(see section 3.9).
One can create single gates and gate vectors. The size of a gate vector can be given inside
square brackets in the declaration, but it is also possible to leave it open by just writing a pair
of empty brackets (“[]”).
When the gate vector size is left open, one can still specify it later, when subclassing the
module, or when using the module for a submodule in a compound module. However, it does
not need to be specified because one can create connections with the gate++ operator that
automatically expands the gate vector.
The gate size can be queried from various NED expressions with the sizeof() operator.
NED normally requires that all gates be connected. To relax this requirement, one can anno-
tate selected gates with the @loose property, which turns off the connectivity check for that
gate. Also, input gates that solely exist so that the module can receive messages via send-
Direct() (see 4.7.5) should be annotated with @directIn. It is also possible to turn off the
connectivity check for all gates within a compound module by specifying the allowuncon-
nected keyword in the module’s connections section.
Let us see some examples.
In the following example, the Classifier module has one input for receiving jobs, which it
will send to one of the outputs. The number of outputs is determined by a module parameter:
simple Classifier {
parameters:
int numCategories;
gates:
input in;
output out[numCategories];
}
The following Sink module also has its in[] gate defined as a vector, so that it can be con-
nected to several modules:
simple Sink {
gates:
input in[];
}
The following lines define a node for building a square grid. Gates around the edges of the
grid are expected to remain unconnected, hence the @loose annotation:
simple GridNode {
gates:
inout neighbour[4] @loose;
}
WirelessNode below is expected to receive messages (radio transmissions) via direct sending,
so its radioIn gate is marked with @directIn.
29
OMNeT++ Simulation Manual – The NED Language
simple WirelessNode {
gates:
input radioIn @directIn;
}
In the following example, we define TreeNode as having gates to connect any number of
children, then subclass it to get a BinaryTreeNode to set the gate size to two:
simple TreeNode {
gates:
inout parent;
inout children[];
}
An example for setting the gate vector size in a submodule, using the same TreeNode module
type as above:
module BinaryTree {
submodules:
nodes[31]: TreeNode {
gates:
children[2];
}
connections:
...
}
3.8 Submodules
Modules that a compound module is composed of are called its submodules. A submodule
has a name, and it is an instance of a compound or simple module type. In the NED definition
of a submodule, this module type is usually given statically, but it is also possible to specify
the type with a string expression. (The latter feature, parametric submodule types, will be
discussed in section 3.11.1.)
NED supports submodule arrays (vectors) and conditional submodules as well. Submodule
vector size, unlike gate vector size, must always be specified and cannot be left open as with
gates.
It is possible to add new submodules to an existing compound module via subclassing; this
has been described in the section 3.4.
The basic syntax of submodules is shown below:
module Node
{
submodules:
routing: Routing; // a submodule
queue[sizeof(port)]: Queue; // submodule vector
30
OMNeT++ Simulation Manual – The NED Language
...
}
As already seen in previous code examples, a submodule may also have a curly brace block as
body, where one can assign parameters, set the size of gate vectors, and add/modify properties
like the display string (@display). It is not possible to add new parameters and gates.
Display strings specified here will be merged with the display string from the type to get the
effective display string. The merge algorithm is described in chapter 8.
module Node
{
gates:
inout port[];
submodules:
routing: Routing {
parameters: // this keyword is optional
routingTable = "routingtable.txt"; // assign parameter
gates:
in[sizeof(port)]; // set gate vector size
out[sizeof(port)];
}
queue[sizeof(port)]: Queue {
@display("t=queue id $id"); // modify display string
id = 1000+index; // use submodule index to generate different IDs
}
connections:
...
}
queue: Queue;
is the same as
queue: Queue {
}
A submodule or submodule vector can be conditional. The if keyword and the condition itself
goes after the submodule type, like in the example below:
module Host
{
parameters:
bool withTCP = default(true);
submodules:
tcp : TCP if withTCP;
...
}
Note that with submodule vectors, setting zero vector size can be used as an alternative to the
if condition.
31
OMNeT++ Simulation Manual – The NED Language
3.9 Connections
Connections are defined in the connections section of compound modules. Connections
cannot span across hierarchy levels; one can connect two submodule gates, a submodule
gate and the "inside" of the parent (compound) module’s gates, or two gates of the parent
module (though this is rarely useful), but it is not possible to connect to any gate outside the
parent module, or inside compound submodules.
Input and output gates are connected with a normal arrow, and inout gates with a double-
headed arrow “<-->”. To connect the two gates with a channel, use two arrows and put
the channel specification in between. The same syntax is used to add properties such as
@display to the connection.
Some examples have already been shown in the NED Quickstart section (3.2); let’s see some
more.
It has been mentioned that an inout gate is basically an input and an output gate glued
together. These sub-gates can also be addressed (and connected) individually if needed, as
port$i and port$o (or for vector gates, as port$i[k] and port$o[k]).
Gates are specified as modulespec.gatespec (to connect a submodule), or as gatespec (to con-
nect the compound module). modulespec is either a submodule name (for scalar submodules),
or a submodule name plus an index in square brackets (for submodule vectors). For scalar
gates, gatespec is the gate name; for gate vectors it is either the gate name plus an index in
square brackets, or gatename++.
The gatename++ notation causes the first unconnected gate index to be used. If all gates of
the given gate vector are connected, the behavior is different for submodules and for the en-
closing compound module. For submodules, the gate vector expands by one. For a compound
module, after the last gate is connected, ++ will stop with an error.
NOTE: Why is it not possible to expand a gate vector of the compound module? The
model structure is built in top-down order, so new gates would be left unconnected on
the outside, as there is no way in NED to "go back" and connect them afterwards.
When the ++ operator is used with $i or $o (e.g. g$i++ or g$o++, see later), it will actually
add a gate pair (input+output) to maintain equal gate sizes for the two directions.
When using built-in channel types, the type name can be omitted; it will be inferred from the
parameter names.
a.g++ <--> {delay=10ms;} <--> b.g++;
32
OMNeT++ Simulation Manual – The NED Language
Using bidirectional connections is a bit trickier, because both directions must be covered
separately:
network Network
{
parameters:
hostA.g$o[0].channel.datarate = 100Mbps; // the A -> B connection
hostB.g$o[0].channel.datarate = 100Mbps; // the B -> A connection
hostA.g$o[1].channel.datarate = 1Gbps; // the A -> C connection
hostC.g$o[0].channel.datarate = 1Gbps; // the C -> A connection
submodules:
hostA: Host;
hostB: Host;
hostC: Host;
connections:
hostA.g++ <--> ned.DatarateChannel <--> hostB.g++;
hostA.g++ <--> ned.DatarateChannel <--> hostC.g++;
Also, with the ++ syntax it is not always easy to figure out which gate indices map to the
connections one needs to configure. If connection objects could be given names to override
the default name “channel”, that would make it easier to identify connections in patterns.
This feature is described in the next section.
33
OMNeT++ Simulation Manual – The NED Language
purpose of custom channel names is to make addressing easier when channel parameters are
assigned from ini files.
The syntax for naming a channel in a connection is similar to submodule syntax: name: type.
Since both name and type are optional, the colon must be there after name even if type is
missing, in order to remove the ambiguity.
Examples:
r1.pppg++ <--> eth1: EthernetChannel <--> r2.pppg++;
a.out --> foo: {delay=1ms;} --> b.in;
a.out --> bar: --> b.in;
In the absence of an explicit name, the channel name comes from the @defaultname property
of the channel type if that exists.
channel Eth10G extends ned.DatarateChannel like IEth {
@defaultname(eth10G);
}
There’s a catch with @defaultname though: if the channel type is specified with a **.channel-
name.liketype= line in an ini file, then the channel type’s @defaultname cannot be used as
channelname in that configuration line, because the channel type would only be known as
a result of using that very configuration line. To illustrate the problem, consider the above
Eth10G channel, and a compound module containing the following connection:
r1.pppg++ <--> <> like IEth <--> r2.pppg++;
The anomaly can be avoided by using an explicit channel name in the connection, not using
@defaultname, or by specifying the type via a module parameter (e.g. writing <param> like
... instead of <> like ...).
3.10.1 Examples
Chain
34
OMNeT++ Simulation Manual – The NED Language
node[count] : Node {
gates:
port[2];
}
connections allowunconnected:
for i = 0..count-2 {
node[i].port[1] <--> node[i+1].port[0];
}
}
Binary Tree
module BinaryTree {
parameters:
int height;
submodules:
node[2^height-1]: BinaryTreeNode;
connections allowunconnected:
for i=0..2^(height-1)-2 {
node[i].left <--> node[2*i+1].parent;
node[i].right <--> node[2*i+2].parent;
}
}
Note that not every gate of the modules will be connected. By default, an unconnected gate
produces a run-time error message when the simulation is started, but this error message is
turned off here with the allowunconnected modifier. Consequently, it is the simple modules’
responsibility not to send on an unconnected gate.
Random Graph
Conditional connections can be used to generate random topologies, for example. The follow-
ing code generates a random subgraph of a full graph:
module RandomGraph {
parameters:
int count;
double connectedness; // 0.0<x<1.0
submodules:
node[count]: Node {
gates:
in[count];
out[count];
35
OMNeT++ Simulation Manual – The NED Language
}
connections allowunconnected:
for i=0..count-1, for j=0..count-1 {
node[i].out[j] --> node[j].in[i]
if i!=j && uniform(0,1)<connectedness;
}
}
Note the use of the allowunconnected modifier here too, to turn off error messages produced
by the network setup code for unconnected gates.
This pattern takes a subset of the connections of a full graph. A condition is used to “carve
out” the necessary interconnection from the full graph:
for i=0..N-1, for j=0..N-1 {
node[i].out[...] --> node[j].in[...] if condition(i,j);
}
The RandomGraph compound module (presented earlier) is an example of this pattern, but
the pattern can generate any graph where an appropriate condition(i, j) can be formulated.
For example, when generating a tree structure, the condition would return whether node j is
a child of node i or vice versa.
Though this pattern is very general, its usage can be prohibitive if the number of nodes N
is high and the graph is sparse (it has much less than N 2 connections). The following two
patterns do not suffer from this drawback.
The pattern loops through all nodes and creates the necessary connections for each one. It
can be generalized like this:
for i=0..Nnodes, for j=0..Nconns(i)-1 {
node[i].out[j] --> node[rightNodeIndex(i,j)].in[j];
}
The Hypercube compound module (to be presented later) is a clear example of this approach.
BinaryTree can also be regarded as an example of this pattern where the inner j loop is
unrolled.
The applicability of this pattern depends on how easily the rightN odeIndex(i, j) function can
be formulated.
36
OMNeT++ Simulation Manual – The NED Language
for i=0..Nconnections-1 {
node[leftNodeIndex(i)].out[...] --> node[rightNodeIndex(i)].in[...];
}
This pattern can be used if lef tN odeIndex(i) and rightN odeIndex(i) mapping functions can be
sufficiently formulated.
The Chain module is an example of this approach where the mapping functions are extremely
simple: lef tN odeIndex(i) = i and rightN odeIndex(i) = i + 1. The pattern can also be used to
create a random subset of a full graph with a fixed number of connections.
In the case of irregular structures where none of the above patterns can be employed, one can
resort to listing all connections, like one would do it in most existing simulators.
It creates a submodule vector whose module type will come from the nodeType parameter. For
example, if nodeType is set to "SensorNode", then the module vector will consist of sensor
nodes, provided such module type exists and it qualifies. What this means is that the INode
must be an existing module interface, which the SensorNode module type must implement
(more about this later).
As already mentioned, one can write an expression between the angle brackets. The expres-
sion may use the parameters of the parent module and of previously defined submodules, and
has to yield a string value. For example, the following code is also valid:
network Net6
{
parameters:
string nodeTypePrefix;
int variant;
submodules:
node[6]: <nodeTypePrefix + "Node" + string(variant)> like INode {
...
}
37
OMNeT++ Simulation Manual – The NED Language
The “<nodeType> like INode” syntax has an issue when used with submodule vectors: does
not allow one to specify different types for different indices. The following syntax is better
suited for submodule vectors:
The expression between the angle brackets may be left out altogether, leaving a pair of empty
angle brackets, <>:
module Node
{
submodules:
nic: <> like INic; // type name expression left unspecified
...
}
Now the submodule type name is expected to be defined via typename pattern assignments.
Typename pattern assignments look like pattern assignments for the submodule’s param-
eters, only the parameter name is replaced by the typename keyword. Typename pattern
assignments may also be written in the configuration file. In a network that uses the above
Node NED type, typename pattern assignments would look like this:
network Network
{
parameters:
node[*].nic.typename = "Ieee80211g";
submodules:
node: Node[100];
}
A default value may also be specified between the angle brackets; it will be used if there is no
typename assignment for the module:
module Node
{
submodules:
nic: <default("Ieee80211b")> like INic;
38
OMNeT++ Simulation Manual – The NED Language
...
}
There must be exactly one module type that goes by the simple name Ieee80211b and also
implements the module interface INic, otherwise an error message will be issued. (The im-
ports in Node’s the NED file play no role in the type resolution.) If there are two or more such
types, one can remove the ambiguity by specifying the fully qualified module type name, i.e.
one that also includes the package name:
module Node
{
submodules:
nic: <default("acme.wireless.Ieee80211b")> like INic; // made-up name
...
}
However, this pattern, when used extensively, can lead to a large number of string parameters.
Luckily, it is also possible to achieve the same effect with typename, without using extra
parameters:
module Node
{
submodules:
tcp: <default("Tcp")> like ITcp if typename!="";
}
The typename operator in a submodule’s if condition evaluates to the would-be type of the
submodule. By using the typename!="" condition, we can let the user eliminate the tcp
submodule by setting its typename to the empty string. For example, in a network that uses
the above NED type, typename pattern assignments could look like this:
network Network
{
parameters:
node1.tcp.typename = "TcpExt"; // let node1 use a custom TCP
node2.tcp.typename = ""; // no TCP in node2
submodules:
node1: Node;
39
OMNeT++ Simulation Manual – The NED Language
node2: Node;
}
Note that this trick does not work with submodule vectors. The reason is that the condition
applies to the vector as a whole, while type is per-element.
It is often also useful to be able to check, e.g. in the connections section, whether a conditional
submodule has been created or not. This can be done with the exists() operator. An
example:
module Node
{
...
connections:
ip.tcpOut --> tcp.ipIn if exists(ip) && exists(tcp);
}
Limitation: exists() may only be used after the submodule’s occurrence in the compound
module.
The expression may use loop variables, parameters of the parent module and also parameters
of submodules (e.g. host[2].channelType).
The type expression may also be absent, and then the type is expected to be specified using
typename pattern assignments:
a.g++ <--> <> like IMyChannel <--> b.g++;
a.g++ <--> <> like IMyChannel {@display("ls=red");} <--> b.g++;
40
OMNeT++ Simulation Manual – The NED Language
Using properties, one can attach extra information to NED elements. Some properties are
interpreted by NED, by the simulation kernel; other properties may be read and used from
within the simulation model, or provide hints for NED editing tools.
Properties are attached to the type, so one cannot have different properties defined per-
instance. All instances of modules, connections, parameters, etc. created from any particular
location in the NED files have identical properties.
The following example shows the syntax for annotating various NED elements:
@namespace(foo); // file property
module Example
{
parameters:
@node; // module property
@display("i=device/pc"); // module property
int a @unit(s) = default(1); // parameter property
gates:
output out @loose @labels(pk); // gate properties
submodules:
src: Source {
parameters:
@display("p=150,100"); // submodule property
count @prompt("Enter count:"); // adding a property to a parameter
gates:
out[] @loose; // adding a property to a gate
}
...
connections:
src.out++ --> { @display("ls=green,2"); } --> sink1.in; // connection prop.
src.out++ --> Channel { @display("ls=green,2"); } --> sink2.in;
}
This example declares two statistics as @statistic properties, @statistic[eed] and @statis-
tic[jitter]. Property values within the parentheses are used to supply additional info, like
a more descriptive name (title="..." or a unit (unit=s). Property indices can be conve-
niently accessed from the C++ API as well; for example it is possible to ask what indices exist
for the "statistic" property, and it will return a list containing "eed" and "jitter").
In the @statistic example the index was textual and meaningful, but neither is actually
required. The following dummy example shows the use of numeric indices which may be
41
OMNeT++ Simulation Manual – The NED Language
Note that without the index, the lines would actually define the same @foo property, and
would overwrite each other’s values.
Indices also make it possible to override entries via inheritance:
simple DummyExt extends Dummy {
@foo[2](what="grapefruits"); // 5 grapefruits instead of 5 oranges
}
The above examples are special cases of the general data model. According to the data model,
properties contain key-valuelist pairs, separated by semicolons. Items in valuelist are sepa-
rated by commas. Wherever key is missing, values go on the valuelist of the default key, the
empty string.
Value items may contain words, numbers, string constants and some other characters, but
not arbitrary strings. Whenever the syntax does not permit some value, it should be enclosed
in quotes. This quoting does not affect the value because the parser automatically drops one
layer of quotes; thus, @class(TCP) and @class("TCP") are exactly the same. If the quotes
themselves need to be part of the value, an extra layer of quotes and escaping are the solution:
@foo("\"some string\"").
There are also some conventions. One can use properties to tag NED elements; for example,
a @host property could be used to mark all module types that represent various hosts. This
property could be recognized e.g. by editing tools, by topology discovery code inside the
simulation model, etc.
The convention for such a “marker” property is that any extra data in it (i.e. within parens)
is ignored, except a single word false, which has the special meaning of “turning off” the
property. Thus, any simulation model or tool that interprets properties should handle all
42
OMNeT++ Simulation Manual – The NED Language
Properties defined on a module or channel type may be updated both by subclassing and
when using type as a submodule or connection channel. One can add new properties, and
also modify existing ones.
When modifying a property, the new property is merged with the old one. The rules of merging
are fairly simple. New keys simply get added. If a key already exists in the old property, items
in its valuelist overwrite items on the same position in the old property. A single hyphen (−)
as valuelist item serves as “antivalue”, it removes the item at the corresponding position.
Some examples:
base @prop
new @prop(a)
result @prop(a)
base @prop(a,b,c)
new @prop(,-)
result @prop(a,,c)
base @prop(foo=a,b)
new @prop(foo=A,,c;bar=1,2)
result @prop(foo=A,b,c;bar=1,2)
NOTE: The above merge rules are part of NED, but the code that interprets properties
may have special rules for certain properties. For example, the @unit property of pa-
rameters is not allowed to be overridden, and @display is merged with special although
similar rules (see Chapter 8).
3.13 Inheritance
Inheritance support in the NED language is only described briefly here, because several details
and examples have been already presented in previous sections.
In NED, a type may only extend (extends keyword) an element of the same component type:
a simple module may extend a simple module, a channel may extend a channel, a module
interface may extend a module interface, and so on. There is one irregularity, however: A
compound module may extend a simple module (and inherits its C++ class), but not vica
versa.
Single inheritance is supported for modules and channels, and multiple inheritance is sup-
ported for module interfaces and channel interfaces. A network is a shorthand for a compound
module with the @isNetwork property set, so the same rules apply to it as to compound mod-
ules.
However, a simple or compound module type may implement (like keyword) several module
interfaces; likewise, a channel type may implement several channel interfaces.
43
OMNeT++ Simulation Manual – The NED Language
IMPORTANT: When you extend a simple module type both in NED and in C++, you
must use the @class property to tell NED to use the new C++ class – otherwise the new
module type inherits the C++ class of the base!
Inheritance may:
• add new properties, parameters, gates, inner types, submodules, connections, as long
as names do not conflict with inherited names
• modify inherited properties, and properties of inherited parameters and gates
• it may not modify inherited submodules, connections and inner types
For details and examples, see the corresponding sections of this chapter (simple modules 3.3,
compound modules 3.4, channels 3.5, parameters 3.6, gates 3.7, submodules 3.8, connec-
tions 3.9, module interfaces and channel interfaces 3.11.1).
3.14 Packages
Having all NED files in a single directory is fine for small simulation projects. When a project
grows, however, it sooner or later becomes necessary to introduce a directory structure, and
sort the NED files into them. NED natively supports directory trees with NED files, and calls
directories packages. Packages are also useful for reducing name conflicts, because names
can be qualified with the package name.
NOTE: NED packages are based on the Java package concept, with minor enhance-
ments. If you are familiar with Java, you’ll find little surprise in this section.
3.14.1 Overview
When a simulation is run, one must tell the simulation kernel the directory which is the root
of the package tree; let’s call it NED source folder. The simulation kernel will traverse the
whole directory tree, and load all NED files from every directory. One can have several NED
directory trees, and their roots (the NED source folders) should be given to the simulation
kernel in the NED path variable. The NED path can be specified in several ways: as an
environment variable (NEDPATH), as a configuration option (ned-path), or as a command-line
option to the simulation runtime (-n). NEDPATH is described in detail in chapter 11.
Directories in a NED source tree correspond to packages. If NED files are in the <root>/a/b/c
directory (where <root> is listed in NED path), then the package name is a.b.c. The package
name has to be explicitly declared at the top of the NED files as well, like this:
package a.b.c;
The package name that follows from the directory name and the declared package must
match; it is an error if they don’t. (The only exception is the root package.ned file, as de-
scribed below.)
By convention, package names are all lowercase, and begin with either the project name
(myproject), or the reversed domain name plus the project name (org.example.myproject).
The latter convention would cause the directory tree to begin with a few levels of empty direc-
tories, but this can be eliminated with a toplevel package.ned.
44
OMNeT++ Simulation Manual – The NED Language
NED files called package.ned have a special role, as they are meant to represent the whole
package. For example, comments in package.ned are treated as documentation of the pack-
age. Also, a @namespace property in a package.ned file affects all NED files in that directory
and all directories below.
The toplevel package.ned file can be used to designate the root package, which is useful for
eliminating a few levels of empty directories resulting from the package naming convention.
For example, given a project where all NED types are under the org.acme.foosim package,
one can eliminate the empty directory levels org, acme and foosim by creating a package.ned
file in the source root directory with the package declaration org.example.myproject. This
will cause a directory foo under the root to be interpreted as package org.example.myproject.foo,
and NED files in them must contain that as package declaration. Only the root package.ned
can define the package, package.ned files in subdirectories must follow it.
Let’s look at the INET Framework as example, which contains hundreds of NED files in several
dozen packages. The directory structure looks like this:
INET/
src/
base/
transport/
tcp/
udp/
...
networklayer/
linklayer/
...
examples/
adhoc/
ethernet/
...
The src and examples subdirectories are denoted as NED source folders, so NEDPATH is the
following (provided INET was unpacked in /home/joe):
/home/joe/INET/src;/home/joe/INET/examples
Both src and examples contain package.ned files to define the root package:
// INET/src/package.ned:
package inet;
// INET/examples/package.ned:
package inet.examples;
45
OMNeT++ Simulation Manual – The NED Language
package) is called fully qualified name; without the package name (Queue) it is called simple
name.
Simple names alone are not enough to unambiguously identify a type. Here is how one can
refer to an existing type:
1. By fully qualified name. This is often cumbersome though, as names tend to be too long;
3. If the type is in the same package, then it doesn’t need to be imported; it can be referred
to by simple name
Types can be imported with the import keyword by either fully qualified name, or by a wild-
card pattern. In wildcard patterns, one asterisk ("*") stands for "any character sequence not
containing period", and two asterisks ("**") mean "any character sequence which may contain
period".
So, any of the following lines can be used to import a type called inet.protocols.net-
worklayer.ip.RoutingTable:
import inet.protocols.networklayer.ip.RoutingTable;
import inet.protocols.networklayer.ip.*;
import inet.protocols.networklayer.ip.Ro*Ta*;
import inet.protocols.*.ip.*;
import inet.**.RoutingTable;
If an import explicitly names a type with its exact fully qualified name, then that type must
exist, otherwise it is an error. Imports containing wildcards are more permissive, it is allowed
for them not to match any existing NED type (although that might generate a warning.)
Inner types may not be referred to outside their enclosing types, so they cannot be imported
either.
46
OMNeT++ Simulation Manual – The NED Language
...
}
and suppose that the following modules implement the IMobility module interface: inet.mo-
bility.RandomWalk, inet.adhoc.RandomWalk, inet.mobility.MassMobility. Also sup-
pose that there is a type called inet.examples.adhoc.MassMobility but it does not imple-
ment the interface.
So if mobilityType="MassMobility", then inet.mobility.MassMobility will be selected;
the other MassMobility doesn’t interfere. However, if mobilityType="RandomWalk", then it
is an error because there are two matching RandomWalk types. Both RandomWalk’s can still be
used, but one must explicitly choose one of them by providing a package name: mobility-
Type="inet.adhoc.RandomWalk".
47
OMNeT++ Simulation Manual – The NED Language
48
OMNeT++ Simulation Manual – Simple Modules
Chapter 4
Simple Modules
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.
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 inter-
esting) 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.
49
OMNeT++ Simulation Manual – Simple Modules
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.
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 pro-
cessed 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.
50
OMNeT++ Simulation Manual – Simple Modules
the model time when the event occurs is the arrival time of the message. Events like “timeout
expired” are implemented by the module sending a message to itself.
Events are consumed from the FES in arrival time order, to maintain causality. More precisely,
given two messages, the following rules apply:
1. The message with the earlier arrival time is executed first. If arrival times are equal,
2. the one with the higher scheduling priority (smaller numeric value) is executed first. If
priorities are the same,
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 SIM-
TIME_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);
51
OMNeT++ Simulation Manual – Simple Modules
NOTE: Converting a SimTime to double may lose precision, because double only has a
52-bit mantissa. Earlier versions of OMNeT++ used double for the simulation time, but
that caused problems in long simulations that relied on fine-grained timing, for example
MAC protocols. Other problems were the accumulation of rounding errors, and non-
associativity (often (x + y) + z 6= x + (y + z), see [Gol91]) which meant that two double
simulation times could not be reliably compared for equality.
• initialize(). This method is invoked after OMNeT++ has set up the network (i.e.
created modules and connected them according to the definitions), and provides a place
for initialization code;
• finish() is called when the simulation has terminated successfully, and its recom-
mended use is the recording of summary statistics.
initialize() and finish(), together with initialize()’s variants for multi-stage initial-
ization, will be covered in detail in section 4.3.3.
52
OMNeT++ Simulation Manual – Simple Modules
cObject
...
cComponent
cModule cChannel
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 simu-
lation model. Generally, handleMessage() should be preferred to activity(), due to scal-
ability 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).
NOTE: refreshDisplay() has been added in OMNeT++ 5.0. Until then, visualization-
related tasks were usually implemented as part of handleMessage(). refreshDis-
play() provides a far superior and more efficient solution.
53
OMNeT++ Simulation Manual – Simple Modules
4.3.1 Overview
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 De-
fine_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;
void HelloModule::initialize()
{
EV << "Hello World!\n";
}
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;
}
54
OMNeT++ Simulation Manual – Simple Modules
4.3.2 Constructor
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
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 termi-
nated normally. It does not get called when the simulations stops with an error message. The
55
OMNeT++ Simulation Manual – Simple Modules
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 meth-
ods:
Constructor:
Set pointer members of the module class to nullptr; postpone all other initialization
tasks to initialize().
initialize():
Perform all initialization tasks: read module parameters, initialize class variables, allo-
cate dynamic data structures with new; also allocate and initialize self-messages (timers)
if needed.
finish():
Record statistics. Do not delete anything or cancel timers – all cleanup must be done
in the destructor.
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 al-
ways 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.
NOTE: The perform-gc configuration option has been removed in OMNeT++ 4.0. Au-
tomatic garbage collection cannot be implemented reliably, due to the limitations of the
C++ language.
Invocation Order
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 termi-
nated normally.
NOTE: finish() is not called if the simulation has terminated with a runtime error.
The calling order for finish() is the reverse of the order of initialize(): first submodules,
then the encompassing compound module. 3
3 The way you can provide an initialize() function for a compound module is to subclass cModule, and tell
OMNeT++ to use the new class for the compound module. The latter is done by adding the @class(<classname>)
property into the NED declaration.
56
OMNeT++ Simulation Manual – Simple Modules
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.
Multi-Stage Initialization
At the beginning of the simulation, initialize(0) is called for all modules, then initial-
ize(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 initial-
ize(int stage) must be implemented to handle the stage=0 and stage=1 cases. 4
The callInitialize() function performs the full multi-stage initialization for that module
and all its submodules.
4 Note the const in the numInitStages() declaration. If you forget it, by C++ rules you create a different function
instead of redefining the existing one in the base class, thus the existing one will remain in effect and return 1.
57
OMNeT++ Simulation Manual – Simple Modules
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().
“End-of-Simulation” Event
4.4.1 handleMessage()
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 simula-
tion 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:
58
OMNeT++ Simulation Manual – Simple Modules
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 mod-
ule 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:
• other variables which belong to the state of the module: retry counts, packet queues,
etc.
• values retrieved/computed once and then stored: values of module parameters, gate
indices, routing information, etc.
• pointers of message objects created once and then reused for timers, timeouts, etc.
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.
59
OMNeT++ Simulation Manual – Simple Modules
Application Area
1. When you expect the module to be used in large simulations, involving several thou-
sand modules. In such cases, the module stacks required by activity() would simply
consume too much memory.
2. For modules which maintain little or no state information, such as packet sinks, han-
dleMessage() is more convenient to program.
3. Other good candidates are modules with a large state space and many arbitrary state
transition possibilities (i.e. where there are many possible subsequent states for any
state). Such algorithms are difficult to program with activity(), and better suited for
handleMessage() (see rule of thumb below). This is the case for most communication
protocols.
// ...
60
OMNeT++ Simulation Manual – Simple Modules
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);
}
61
OMNeT++ Simulation Manual – Simple Modules
A bit more realistic example is to rewrite our Generator to create packet bursts, each consist-
ing of burstLength packets.
We add some data members to the class:
• burstLength will store the parameter that specifies how many packets a burst must
contain,
• burstCounter will count in how many packets are left to be sent in the current burst.
The code:
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);
}
62
OMNeT++ Simulation Manual – Simple Modules
Pros:
Cons:
Other Simulators
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:
4.4.2 activity()
Process-Style Description
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 re-
ceive() call in its body.
63
OMNeT++ Simulation Manual – Simple Modules
Application Area
Generally you should prefer handleMessage() to activity(). The main problem with ac-
tivity() 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()
}
64
OMNeT++ Simulation Manual – Simple Modules
loop, there is no point in using activity(), and the code should be written with handleMes-
sage(). The body of the loop would then become the body of handleMessage(), state vari-
ables 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;
}
}
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):
That is, when a module has an event, the simulation kernel transfers the control to the mod-
ule’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.
65
OMNeT++ Simulation Manual – Simple Modules
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
Starter Messages
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.
66
OMNeT++ Simulation Manual – Simple Modules
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:
• limited scalability: coroutine stacks can unacceptably increase the memory require-
ments of the simulation program if there are many activity()-based simple modules;
• run-time overhead: switching between coroutines is slower than a simple function call
• does not encourage a good programming style: as module complexity grows, activity()
tends to become a large, monolythic function.
In most cases, cons outweigh pros and it is a better idea to use handleMessage() instead.
67
OMNeT++ Simulation Manual – Simple Modules
Other Simulators
• All simulation software which inherits from SIMULA (e.g. C++SIM) is based on corou-
tines, although all in all the programming model is quite different.
• The simulation/parallel programming language Maisie and its successor PARSEC (from
UCLA) also use coroutines (although implemented with “normal” preemptive threads).
The philosophy is quite similar to OMNeT++. PARSEC, being “just” a programming lan-
guage, it has a more elegant syntax but far fewer features than OMNeT++.
Define_Module(TransportProtocolExt);
void TransportProtocolExt::recalculateTimeout()
{
//...
}
68
OMNeT++ Simulation Manual – Simple Modules
NOTE: Note the @class() property, which tells OMNeT++ to use the TransportPro-
tocolExt C++ class for the module type! It is needed because NED inheritance is NED
inheritance only, so without @class() the TransportProtocolExt NED type would in-
herit the C++ class from its base NED type.
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 parame-
ters, 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.
void Source::initialize()
{
69
OMNeT++ Simulation Manual – Simple Modules
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");
...
}
NOTE: The parameter’s type cannot be changed at runtime – it must remain the type
declared in the NED file. It is also not possible to add or remove module parameters at
runtime.
The methods to set the parameter value are setBoolValue(), setLongValue(), setString-
Value(), setDoubleValue(), setXMLValue(). There are also overloaded assignment opera-
tors for various types including bool, int, long, double, const char *, and cXMLElement
*.
70
OMNeT++ Simulation Manual – Simple Modules
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 param-
eter. 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.
and
71
OMNeT++ Simulation Manual – Simple Modules
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));
}
4.5.5 handleParameterChange()
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.
NOTE: Notifications are disabled during the initialization of the component, because
they would make it very difficult to write components that work reliably under all condi-
tions. handleParameterChange() is usually triggered from another module (it does not
make much sense for a module to change its own parameters), so the relative order of
initialize() and handleParameterChange() would be effectively determined by the
initialization order of modules, which generally cannot be relied upon. After the last stage
of the initialization of the component is finished, handleParameterChange() is called by
the simulation kernel with nullptr as a parameter name. This allows the component to
react to parameter changes that occurred during the initialization phase.
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
}
72
OMNeT++ Simulation Manual – Simple Modules
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 handleParameter-
Change() from the initialize() or finish() function:
for (int i = 0; i < getNumParams(); i++)
handleParameterChange(par(i).getName());
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.
73
OMNeT++ Simulation Manual – Simple Modules
Gate Vectors
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.
Gate IDs
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.
NOTE: OMNeT++ version earlier than 4.0 did not have these guarantees – resizing a gate
vector could cause its ID range to be relocated, if it would have overlapped with the ID
range of other gate vectors. OMNeT++ 4.x solves the same problem by interpreting the
gate ID as a bitfield, basically containing bits that identify the gate name, and other bits
that hold the index. This also means that the theoretical upper limit for a gate size is now
smaller, albeit it is still big enough so that it can be safely ignored for practical purposes.
74
OMNeT++ Simulation Manual – Simple Modules
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 iden-
tify 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).
NOTE: In earlier OMNeT++ versions, gate IDs used to be small integers, so it made sense
to iterate over all gates of a module by enumerating all IDs from zero to a maximum,
skipping the holes (nullptrs). This is no longer the case with OMNeT++ 4.0 and later
versions. Moreover, the gate() method now throws an error when called with an invalid
ID, and not just returns nullptr.
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.
NOTE: When memory efficiency is of concern, it is useful to know that in OMNeT++ 4.0
and later, a gate vector will consume significantly less memory than the same number of
individual scalar gates.
cGate Methods
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.)
75
OMNeT++ Simulation Manual – Simple Modules
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 getName-
Suffix() 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.
4.6.2 Connections
Simple module gates have normally one connection attached. Compound module gates, how-
ever, 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 (re-
turned 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 4.2.
The start and end gates of the path can be found with the getPathStartGate() and getPa-
thEndGate() methods, which simply follow the previous gate and next gate pointers, respec-
tively, 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
76
OMNeT++ Simulation Manual – Simple Modules
(a) (b)
(c) (d)
Figure 4.2: (a) simple module output gate, (b) compound module output gate, (c) simple
module input gate, (d) compound module input gate
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 cDelay-
Channel 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 trans-
mission channel (see 4.7.6) of the connection path that starts at a specific output gate. The
77
OMNeT++ Simulation Manual – Simple Modules
transmission channel can be found by following the connection path until you find a chan-
nel whose isTransmissionChannel() method returns true, but cGate has a convenience
method for this, named getTransmissionChannel(). An example usage:
cChannel *txChan = gate("ppp$o")->getTransmissionChannel();
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.
4.7.1 Self-Messages
Nearly all simulation models need to schedule future events in order to implement timers,
timeouts, delays, etc. Some typical examples:
• A source module that periodically creates and sends messages needs to schedule the
next send after every send operation;
• A server which processes jobs from a queue needs to start a timer every time it begins
processing a job. When the timer expires, the finished job can be sent out, and a new
job may start processing;
• When a packet is sent by a communications protocol that employs retransmission, it
needs to schedule a timeout so that the packet can be retransmitted if no acknowledge
arrives within a certain amount of time.
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.
78
OMNeT++ Simulation Manual – Simple Modules
Scheduling an Event
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.
Cancelling an Event
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 subse-
quent 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.
79
OMNeT++ Simulation Manual – Simple Modules
Re-scheduling an Event
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
80
OMNeT++ Simulation Manual – Simple Modules
5
currently scheduled, etc., a runtime error will be generated: not owner of message.
Broadcasting Messages
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);
Retransmissions
(described in Section 7.13); it merely checks that the owner of the message is the module that wants to send it.
81
OMNeT++ Simulation Manual – Simple Modules
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");
NOTE: The fact that sendDelayed() computes the message arrival information up front
does not make a difference if the model is static, but may lead to surprising results if the
model changes in time. For example, if a connection in the path gets deleted, disabled, or
reconnected to another module during the delay period, the message will still be delivered
to the original module as if nothing happened.
Therefore, despite its performance advantage, you should think twice before using send-
Delayed() 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 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().
82
OMNeT++ Simulation Manual – Simple Modules
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 com-
plain 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 dura-
tion as parameters as well:
The transmission duration parameter is important when the message is also a packet (in-
stance 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. 6
6 Moreover,if sendDirect() with a nonzero duration was used to send the packet to the start gate of the path,
then the path cannot have a transmission channel at all. The point is that the a transission duration must be
unambiguous.
83
OMNeT++ Simulation Manual – Simple Modules
NOTE: In practice, this means that there can be only one ned.DatarateChannel
in the path. Note that unnamed channels with a datarate parameter also map to
ned.DatarateChannel.
Transmitting a Packet
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:
NOTE: If there is a channel with a propagation delay in the path before the trans-
mission channel, the delay should be manually substracted from the value returned by
getTransmissionFinishTime()! The same applies to isBusy(): it tells whether the
channel is currently busy, and not whether it will be busy when a packet that you send
gets there. It is therefore advisable that you never use propagation delays in front of a
transmission channel in a path.
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.
84
OMNeT++ Simulation Manual – Simple Modules
Receiving a Packet
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.
B C
A delay=1ms
D
datarate=1Gbps
tA tB tC tD
send()
with deliverOn-
ReceptionStart=true
default
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
85
OMNeT++ Simulation Manual – Simple Modules
{
// 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.
Aborting Transmissions
Certain protocols, for example Ethernet require the ability to abort a transmission before it
completes. The support OMNeT++ provides for this task is the forceTransmissionFinish-
Time() 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.
NOTE: The consequence of this implementation is that any change in the channel’s
parameters (delay, data rate, bit error rate, etc.) will only affect messages sent after the
change. Messages already underway will not be influenced by the change.
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!
Receiving Messages
86
OMNeT++ Simulation Manual – Simple Modules
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. 7
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());
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
...
}
7 Putaside-queue and the functions receiveOn(), receiveNew(), and receiveNewOn() were deprecated in OM-
87
OMNeT++ Simulation Manual – Simple Modules
4.8 Channels
4.8.1 Overview
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++ imple-
mentation 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.
The first two functions are usually one-liners; the channel behavior is encapsulated in the
third function, processMessage().
Transmission Channels
88
OMNeT++ Simulation Manual – Simple Modules
method is called by modules that send on a transmission channel to find out when the chan-
nel becomes available. The channel’s isBusy() method is implemented simply as return
getTransmissionFinishTime() < simTime(). For non-transmission channels, the get-
TransmissionFinishTime() return value may be any simulation time which is less than or
equal to the current simulation time.
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
89
OMNeT++ Simulation Manual – Simple Modules
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.
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;
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. 8
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;
}
90
OMNeT++ Simulation Manual – Simple Modules
{
// 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;
}
endSimulation() is rarely needed in practice because you can specify simulation time and
CPU time limits in the ini file (see later).
91
OMNeT++ Simulation Manual – Simple Modules
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.
4.10.1 Overview
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:
• There are two kinds of states: transient and steady. On each event (that is, at each call
to handleMessage()), the FSM transitions out of the current (steady) state, undergoes
a series of state changes (runs through a number of transient states), and finally arrives
at another steady state. Thus between two events, the system is always in one of the
steady states. Transient states are therefore not really a must – they exist only to group
actions to be taken during a transition in a convenient way.
• You can assign program code to handle entering and leaving a state (known as entry/exit
code). Staying in the same state is handled as leaving and re-entering the state.
• Entry code should not modify the state (this is verified by OMNeT++). State changes
(transitions) must be put into the exit code.
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 paren-
theses 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 enter-
ing and leaving each state:
92
OMNeT++ Simulation Manual – Simple Modules
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.
Debugging FSMs
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
...
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)
93
OMNeT++ Simulation Manual – Simple Modules
The log output format can be changed by undefining FSM_Print() after the inclusion of
omnetpp.ini, and providing a new definition.
Implementation
An Example
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;
// variables used
int i;
cMessage *startStopBurst;
cMessage *sendMessage;
Define_Module(BurstyGenerator);
void BurstyGenerator::initialize()
94
OMNeT++ Simulation Manual – Simple Modules
{
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);
}
95
OMNeT++ Simulation Manual – Simple Modules
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 ob-
ject, 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
96
OMNeT++ Simulation Manual – Simple Modules
For example, the parameters of the parent module are accessed like this:
double timeout = getParentModule()->par("timeout");
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 getMod-
uleByPath() has been invoked.
cModule *tcp = module->getModuleByPath("Network.host[2].tcp");
cModule *tcp = module->getModuleByPath("host[2].tcp");
97
OMNeT++ Simulation Manual – Simple Modules
• how to let the simulation kernel know that a method call across modules is taking place.
Typically, the called module is in the same compound module as the caller, so the getParent-
Module() 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 per-
forms 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.9
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
9 A check_and_cast_nullable<>() function also exists. It accepts nullptr as input, and only complains if the
98
OMNeT++ Simulation Manual – Simple Modules
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 instan-
tiating 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.
4.13.2 Overview
To understand how dynamic module creation works, you have to know a bit about how OM-
NeT++ normally instantiates modules. Each module type (class) has a corresponding fac-
tory object of the class cModuleType. This object is created under the hood by the De-
fine_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 pa-
rameter values and gate vector sizes. Thus, for compound modules it is generally required
99
OMNeT++ Simulation Manual – Simple Modules
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 pro-
cessed after the starter message.
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.)
If the createScheduleInit() all-in-one method is not applicable, one needs to use the full
procedure. It consists of five steps:
100
OMNeT++ Simulation Manual – Simple Modules
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();
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);
module->setGateSize("in", 3);
module->setGateSize("out", 3);
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 delete-
Module().
101
OMNeT++ Simulation Manual – Simple Modules
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. 10
Example:
mod->callFinish();
mod->deleteModule();
// 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 cDatarat-
eChannel) is somewhat simpler, because those classes have static create() factory func-
tions, and the step of finding the factory object can be spared. Alternatively, one can use
cChannelType’s createIdealChannel(), createDelayChannel() and createDatarateChan-
nel() static methods.
The channel object may need to be parameterized before using it for a connection. For ex-
ample, 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);
10 The finish() function has even been made protected in cSimpleModule, in order to discourage its invocation
102
OMNeT++ Simulation Manual – Simple Modules
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"));
4.14 Signals
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:
• exposing statistical properties of the model, without specifying whether and how to
record them
• receiving notifications about simulation model changes at runtime, and acting upon
them
• emitting information for other purposes, for example as input for custom animation
effects
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.
NOTE: A channel’s parent is the (compound) module that contains the connection, not
the owner of either gate the channel is connected to.
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
103
OMNeT++ Simulation Manual – Simple Modules
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.
NOTE: Because the component type that emits the signal is not part of the signal’s
identity, it is advised to choose signal names carefully. A good naming scheme facilitates
"merging" of signals that arrive from different sources but mean the same thing, and
reduces the chance of collisions between signals that accidentally have the same name
but represent different things.
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().
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.11 Using
these bitfields, it is possible to determine in constant time for the first 64 signals whether the
signal has listeners, so emit() can return immediately if there are none. For other signals,
emit() needs to examine the listener lists up to the root every time. Even if a simulation
uses more than 64 signals, in performance-critical situations it is possible to arrange that
frequently emitted signals (e.g. "txBegin") get the “fast” signal IDs, while infrequent signals
(like e.g. "routerDown") get the rest.
11 It is assumed that there will be typically less than 64 frequently used signals used at a time in a simulation.
104
OMNeT++ Simulation Manual – Simple Modules
Signal IDs
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"
NOTE: Since OMNeT++ 4.3, the lifetime of signal IDs is the entire program, and it is
possible to call registerSignal() from initializers of global variables, e.g. static class
members. In earlier versions, signal IDs were usually allocated in initialize(), and
were only valid for that simulation run.
Emitting Signals
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) cOb-
ject *. 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.
NOTE: The details parameter was added in OMNeT++ 5.0. You should update your
models to use the new listener interface or as a temporary solution, compile OMNeT++
with the WITH_OMNETPP4x_LISTENER_SUPPORT macro.
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 hasLis-
teners() 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);
}
105
OMNeT++ Simulation Manual – Simple Modules
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().
Signal Declarations
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 ques-
tion 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.)
106
OMNeT++ Simulation Manual – Simple Modules
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 notifi-
cations (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;
};
Subscribing to Signals
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. sub-
scribe() 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:
107
OMNeT++ Simulation Manual – Simple Modules
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);
NOTE: When a listener is deleted, it must already be unsubscribed from all components
it has subscribed to. This is explained in 4.14.2.
For completeness, there are methods for getting the list of signals that the component has sub-
scribed to (getLocalListenedSignals()), and the list of listeners for a given signal (getLo-
calSignalListeners()). 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
Listeners are objects that subclass from the cIListener class, which declares the following
methods:
class cIListener
{
public:
virtual ~cIListener() {}
108
OMNeT++ Simulation Manual – Simple Modules
• Several overloaded receiveSignal() methods, one for each data type. Whenever a
signal is emitted (via emit()), the matching receiveSignal() method is invoked on the
subscribed listeners.
• finish() is called by a component on its local listeners after the component’s finish()
method was called. If the listener is subscribed to multiple signals or at multiple compo-
nents, the method will be called multiple times. Note that finish() methods in general
are not invoked if the simulation terminates with an error, so that method is not a place
for doing cleanup.
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(), sub-
scribedTo() 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.)
109
OMNeT++ Simulation Manual – Simple Modules
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 compo-
nents’ 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.
NOTE: If your module has added listeners to other modules (e.g. the toplevel module),
these listeners must be unsubscribed in the module destructor at latest. Remember to
make sure the modules still exist before you call unsubscribe() on them, unless they
are an ancestor of your module in the module tree.
NOTE: Whenever you see a cModule*, cChannel*, cGate* or similar pointer kept as
state in a simple module, you should think about how it will be kept up-to-date if the
model changes at runtime.
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:
• cPreModuleAddNotification / cPostModuleAddNotification
• cPreModuleDeleteNotification / cPostModuleDeleteNotification
• cPreModuleReparentNotification / cPostModuleReparentNotification
• cPreGateAddNotification / cPostGateAddNotification
• cPreGateDeleteNotification / cPostGateDeleteNotification
• cPreGateVectorResizeNotification / cPostGateVectorResizeNotification
• cPreGateConnectNotification / cPostGateConnectNotification
• cPreGateDisconnectNotification / cPostGateDisconnectNotification
• cPrePathCreateNotification / cPostPathCreateNotification
• cPrePathCutNotification / cPostPathCutNotification
• cPreParameterChangeNotification / cPostParameterChangeNotification
• cPreDisplayStringChangeNotification / cPostDisplayStringChangeNotification
110
OMNeT++ Simulation Manual – Simple Modules
NOTE: Please look up these classes in the API documentation to see their data fields,
when exactly they get fired, and what one needs to be careful about when using them.
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);
NOTE: PRE_MODEL_CHANGE and POST_MODEL_CHANGE are fired on the module (or chan-
nel) affected by the change, and not on the module which executes the code that causes
the change. For example, pre-module-deleted is fired on the module to be removed, and
post-module-deleted is fired on its parent (because the original module no longer exists),
and not on the module that contains the deleteModule() call.
NOTE: A listener will not receive pre/post-module-deleted notifications if the whole sub-
module tree that contains the subscription point is deleted. This is because compound
module destructors begin by unsubscribing all modules/channels in the subtree before
starting recursive deletion.
4.15.1 Motivation
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:
111
OMNeT++ Simulation Manual – Simple Modules
• Provides a controllable level of detail: in some simulation runs you may want to record
all values as a time series, in other runs only record the mean, time average, minimum/-
maximum value, standard deviation etc, and in yet other runs you may want to record
the distribution as a histogram;
• Depending on the purpose of the simulation experiment, you may want to process the
results before recording them, for example record a smoothed or filtered value, record
the percentage of time the value is nonzero or over a threshold, record the sum of the
values, etc.;
• You may want aggregate statistics, e.g. record the total number of packet drops or the
average end-to-end delay for the whole network;
• You may want to record combined statistics, for example a drop percentage (drop coun-
t/total number of packets);
• You may want to ignore results generated during the warm-up period or during other
transients.
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 prop-
erty name is always statistic, and the index (here, queueLength) is the name of the statis-
tic. 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 up-
dated 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”
112
OMNeT++ Simulation Manual – Simple Modules
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.
NOTE: The configuration lets you fine-tune the list of result items even beyond the
default and all settings; see section 12.2.3.
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.
NOTE: Starting from OMNeT++ 4.4, items containing parens (e.g. count(drop)) no
longer need to be enclosed in quotation marks.
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?);
113
OMNeT++ Simulation Manual – Simple Modules
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 interpola-
tion). 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)?);
Property Keys
source : Defines the input for the recorders (see record= key). When missing, the statistic
name is taken as the signal name;
record : Contains a list of recording modes, separated by comma. Recording modes define
how to record the source (see source= key).
title : A longer, descriptive name for the statistic signal; result visualization tools may use it
as chart label, e.g. in the legend.
unit : Measurement unit of the values. This may also appear in charts.
interpolationmode : Defines how to interpolate signal values where needed (e.g. for draw-
ing); possible values are none, sample-hold, backward-sample-hold, linear.
enum : Defines symbolic names for various integer signal values. The property value must be
a string, containing name=value pairs separated by comma. Example: "IDLE=1,BUSY=2,DOWN=3".
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).
114
OMNeT++ Simulation Manual – Simple Modules
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 aver-
age 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 out-
put 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.
NOTE: You can have the list of available result filters and result recorders printed
by executing the opp_run -h resultfilters and opp_run -h resultrecorders com-
mands.
115
OMNeT++ Simulation Manual – Simple Modules
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));
HINT: To see how result filters and recorders have been set up for a particular simula-
tion, run the simulation with the debug-statistics-recording configuration option,
e.g. specify -debug-statistics-recording=true on the command line.
116
OMNeT++ Simulation Manual – Simple Modules
sum vector
sum
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 @statis-
ticTemplate 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:
117
OMNeT++ Simulation Manual – Simple Modules
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);
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);
118
OMNeT++ Simulation Manual – Simple Modules
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.
NOTE: cTimestampedValue is not part of the signal mechanism. Instead, the result
recording listeners provided by OMNeT++ have been written in a way so that they under-
stand cTimestampedValue, and know how to handle it.
An example usage:
simtime_t frameReceptionStartTime = ...;
double receivePower = ...;
cTimestampedValue tmp(frameReceptionStartTime, receivePower);
emit(recvPowerSignal, &tmp);
• Strings are recorded as 1.0, except for nullptr which is recorded as 0.0;
• Objects that can be cast to cITimestampedValue are recorded using the getSignal-
Time() and getSignalValue() methods of the class;
• Other objects are recorded as 1.0, except for nullptr which is recorded as 0.0.
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 argu-
ment that allows the same class to serve multiple signals (i.e. to return different values for
each).
12 It is safe to use a static variable here because the simulation program is single-threaded, but ensure that there
isn’t a listener somewhere that would modify the same static variable during firing.
119
OMNeT++ Simulation Manual – Simple Modules
cIListener
cResultListener
cResultFilter cResultRecorder
CountFilter, CountRecorder,
cNumericResultFilter cObjectResultFilter cNumericResultRecorder
... ...
VectorRecorder,
LastValueRecorder,
SumFilter,
HistogramRecorder,
MinFilter, PacketBitsFilter,
SumRecorder,
MaxFilter, PacketBytesFilter,
MinRecorder,
TimeAverageFilter, ...
MaxRecorder,
...
TimeAverageRecorder,
...
120
OMNeT++ Simulation Manual – Simple Modules
};
Register_ResultFilter("sumPerDuration", SumPerDurationFilter);
121
OMNeT++ Simulation Manual – Simple Modules
122
OMNeT++ Simulation Manual – Messages and Packets
Chapter 5
5.1 Overview
Messages are a central concept in OMNeT++. In the model, message objects represent events,
packets, commands, jobs, customers or other kinds of entities, depending on the model do-
main.
Messages are represented with the cMessage class and its subclass cPacket. cPacket is
used for network packets (frames, datagrams, transport packets, etc.) in a communication
network, and cMessage is used for everything else. Users are free to subclass both cMessage
and cPacket to create new types and to add data.
cMessage has the following fields; some are used by the simulation kernel, and others are
provided for the convenience of the simulation programmer:
• The name field is a string (const char *), which can be freely used by the simulation
programmer. The message name is displayed at many places in the graphical runtime in-
terface, so it is generally useful to choose a descriptive name. Message name is inherited
from cObject (see section 7.1.2).
• Message kind is an integer field. Some negative values are reserved by the simulation
library, but zero and positive values can be freely used in the model for any purpose.
Message kind is typically used to carry a value that conveys the role, type, category or
identity of the message.
• The scheduling priority field is used by the simulation kernel to determine the delivery
order of messages that have the same arrival time values. This field is rarely used in
practice.
• The send time, arrival time, source module, source gate, destination module, destination
gate fields store information about the message’s last sending or scheduling, and should
not be modified from the model. These fields are primarily used internally by the simu-
lation kernel while the message is in the future events set (FES), but the information is
still in the message object when the message is delivered to a module.
• Time stamp (not to be confused with arrival time) is a utility field, which the programmer
can freely use for any purpose. The time stamp is not examined or changed by the
simulation kernel at all.
123
OMNeT++ Simulation Manual – Messages and Packets
• The parameter list, control info and context pointer fields make some simulation tasks
easier to program, and they will be discussed later.
The cPacket class extends cMessage with fields that are useful for representing network
packets:
• The packet length field represents the length of the packet in bits. It is used by the
simulation kernel to compute the transmission duration when a packet travels through
a connection that has an assigned data rate, and also for error modeling on channels
with a nonzero bit error rate.
• The encapsulated packet field helps modeling protocol layers by supporting the concept
of encapsulation and decapsulation.
• The bit error flag field carries the result of error modelling after the packet is sent through
a channel that has a nonzero packet error rate (PER) or bit error rate (BER). It is up to
the receiver to examine this flag after having received the packet, and to act upon it.
• The duration field carries the transmission duration after the packet was sent through a
channel with a data rate.
• The is-reception-start flag tells whether this packet represents the start or the end of
the reception after the packet travelled through a channel with a data rate. This flag is
controlled by the deliver-on-reception-start flag of the receiving gate.
Descriptive message names can be very useful when tracing, debugging or demonstrating the
simulation, so it is recommended to use them. Message kind is usually initialized with a sym-
bolic constant (e.g. an enum value) which signals what the message object represents. Only
positive values and zero can be used – negative values are reserved for use by the simulation
kernel.
The following lines show some examples of message creation:
cMessage *msg1 = new cMessage();
cMessage *msg2 = new cMessage("timeout");
cMessage *msg3 = new cMessage("timeout", KIND_TIMEOUT);
Once a message has been created, its basic data members can be set with the following
methods:
void setName(const char *name);
void setKind(short k);
void setTimestamp();
void setTimestamp(simtime_t t);
void setSchedulingPriority(short p);
124
OMNeT++ Simulation Manual – Messages and Packets
The getName()/setName() methods are inherited from a generic base class in the simulation
library, cNamedObject.
Two more interesting methods:
The isPacket() method returns true if the particular message object is a subclass of cPacket,
and false otherwise. As isPacket() is implemented as a virtual function that just con-
tains a return false or a return true statement, it might be faster than calling dy-
namic_cast<cPacket*>.
The getCreationTime() method returns the creation time of the message. It is worthwhile
to mention that with cloned messages (see dup() later), the creation time of the original
message is returned and not the time of the cloning operation. This is particularly useful when
modeling communication protocols, because many protocols clone the transmitted packages
to be able to do retransmissions and/or segmentation/reassembly.
It is often necessary to duplicate a message or a packet, for example, to send one and keep a
copy. Duplication can be done in the same way as for any other OMNeT++ object:
The resulting message (or packet) will be an exact copy of the original including message
parameters and encapsulated messages, except for the message ID field. The creation time
field is also copied, so for cloned messages getCreationTime() will return the creation time
of the original, not the time of the cloning operation. 1
When subclassing cMessage or cPacket, one needs to reimplement dup(). The recommended
implementation is to delegate to the copy constructor of the new class:
1 Note, however, that the simulation library may delay the duplication of the encapsulated message until it is really
125
OMNeT++ Simulation Manual – Messages and Packets
Every message object has a unique numeric message ID. It is normally used for identifying
the message in a recorded event log file, but may occasionally be useful for other purposes as
well. When a message is cloned (msg->dup()), the clone will have a different ID.
There is also another ID called tree ID. The tree ID is initialized to the message ID. However,
when a message is cloned, the clone will retain the tree ID of the original. Thus, messages
that have been created by cloning the same message or its clones will have the same tree
ID. Message IDs are of the type long, which is is usually enough so that IDs remain unique
during the simulation run (i.e. the counter does not wrap).
The methods for obtaining message IDs:
long getId() const;
long getTreeId() const;
One of the main application areas of OMNeT++ is the simulation of telecommunication net-
works. Here, protocol layers are usually implemented as modules which exchange packets.
Packets themselves are represented by messages subclassed from cPacket.
However, communication between protocol layers requires sending additional information to
be attached to packets. For example, a TCP implementation sending down a TCP packet to
IP will want to specify the destination IP address and possibly other parameters. When IP
passes up a packet to TCP after decapsulation from the IP header, it will want to let TCP know
at least the source IP address.
This additional information is represented by control info objects in OMNeT++. Control info
objects have to be subclassed from cObject (a small footprint base class with no data mem-
bers), and can be attached to any message. cMessage has the following methods for this
purpose:
void setControlInfo(cObject *controlInfo);
cObject *getControlInfo() const;
cObject *removeControlInfo();
When a "command" is associated with the message sending (such as TCP OPEN, SEND,
CLOSE, etc), the message kind field (getKind(), setKind() methods of cMessage) should
carry the command code. When the command doesn’t involve a data packet (e.g. TCP CLOSE
command), a dummy packet (empty cMessage) can be sent.
An object set as control info via setControlInfo() will be owned by the message object.
When the message is deallocated, the control info object is deleted as well.
The following methods return the sending and arrival times that correspond to the last sending
of the message.
simtime_t getSendingTime() const;
simtime_t getArrivalTime() const;
126
OMNeT++ Simulation Manual – Messages and Packets
The following methods can be used to determine where the message came from and which
gate it arrived on (or will arrive if it is currently scheduled or under way.) There are two sets
of methods, one returning module/gate Ids, and the other returning pointers.
int getSenderModuleId() const;
int getSenderGateId() const;
int getArrivalModuleId() const;
int getArrivalGateId() const;
cModule *getSenderModule() const;
cGate *getSenderGate() const;
cModule *getArrivalModule() const;
cGate *getArrivalGate() const;
There are further convenience functions to tell whether the message arrived on a specific gate
given with id or with name and index.
bool arrivedOn(int gateId) const;
bool arrivedOn(const char *gatename) const;
bool arrivedOn(const char *gatename, int gateindex) const;
5.3 Self-Messages
127
OMNeT++ Simulation Manual – Messages and Packets
The methods getSendingTime() and getArrivalTime() are also useful with self-messages:
they return the time the message was scheduled and arrived (or will arrive; while the message
is scheduled, arrival time is the time it will be delivered to the module).
The context pointer can be used for any purpose by the simulation programmer. It is not used
by the simulation kernel, and it is treated as a mere pointer (no memory management is done
on it).
Intended purpose: a module which schedules several self-messages (timers) will need to iden-
tify a self-message when it arrives back to the module, ie. the module will have to determine
which timer went off and what to do then. The context pointer can be made to point at a data
structure kept by the module which can carry enough “context” information about the event.
The most important field cPacket has over cMessage is the message length. This field is kept
in bits, but it can also be set/get in bytes. If the bit length is not a multiple of eight, the
getByteLength() method will round it up.
void setBitLength(int64_t l);
void setByteLength(int64_t l);
void addBitLength(int64_t delta);
void addByteLength(int64_t delta);
int64_t getBitLength() const;
int64_t getByteLength() const;
Another extra field is the bit error flag. It can be accessed with the following methods:
void setBitError(bool e);
bool hasBitError() const;
128
OMNeT++ Simulation Manual – Messages and Packets
An example:
cMessage *msg = receive();
if (dynamic_cast<IPv6Datagram *>(msg) != nullptr)
{
IPv6Datagram *datagram = (IPv6Datagram *)msg;
...
}
The encapsulate() function encapsulates a packet into another one. The length of the packet
will grow by the length of the encapsulated packet. An exception: when the encapsulating
(outer) packet has zero length, OMNeT++ assumes it is not a real packet but an out-of-band
signal, so its length is left at zero.
A packet can only hold one encapsulated packet at a time; the second encapsulate() call
will result in an error. It is also an error if the packet to be encapsulated is not owned by the
module.
Decapsulation, that is, removing the encapsulated packet, is done by the decapsulate()
method. decapsulate() will decrease the length of the packet accordingly, except if it was
zero. If the length would become negative, an error occurs.
The getEncapsulatedPacket() function returns a pointer to the encapsulated packet, or
nullptr if no packet is encapsulated.
Example usage:
cPacket *data = new cPacket("data");
data->setByteLength(1024);
udp->encapsulate(data);
EV << udp->getByteLength() << endl; // --> 8+1024 = 1032
129
OMNeT++ Simulation Manual – Messages and Packets
Since the 3.2 release, OMNeT++ implements reference counting of encapsulated packets,
meaning that when a packet containing an encapsulated packet is cloned (dup()), the en-
capsulated packet will not be duplicated, only a reference count is incremented. Duplication
of the encapsulated packet is deferred until decapsulate() actually gets called. If the outer
packet is deleted without its decapsulate() method ever being called, then the reference
count of the encapsulated packet is simply decremented. The encapsulated packet is deleted
when its reference count reaches zero.
Reference counting can significantly improve performance, especially in LAN and wireless
scenarios. For example, in the simulation of a broadcast LAN or WLAN, the IP, TCP and
higher layer packets won’t be duplicated (and then discarded without being used) if the MAC
address doesn’t match in the first place.
The reference counting mechanism works transparently. However, there is one implication:
one must not change anything in a packet that is encapsulated into another! That is,
getEncapsulatedPacket() should be viewed as if it returned a pointer to a read-only object
(it returns a const pointer indeed), for quite obvious reasons: the encapsulated packet may
be shared between several packets, and any change would affect those other packets as well.
The cPacket class does not directly support encapsulating more than one packet, but one
can subclass cPacket or cMessage to add the necessary functionality.
Encapsulated packets can be stored in a fixed-size or a dynamically allocated array, or in a
standard container like std::vector. In addition to storage, object ownership needs to be
taken care of as well. The message class has to take ownership of the inserted messages, and
release them when they are removed from the message. These tasks are done via the take()
and drop() methods.
Here is an example that assumes that the class has an std::list member called messages
for storing message pointers:
void MultiMessage::insertMessage(cMessage *msg)
{
take(msg); // take ownership
messages.push_back(msg); // store pointer
}
One also needs to provide an operator=() method to make sure that message objects are
copied and duplicated properly. Section 7.12 covers requirements and conventions associated
with deriving new classes in more detail.
130
OMNeT++ Simulation Manual – Messages and Packets
// receiver:
if (msg->hasObject("histogram")) {
cObject *obj = msg->getObject("histogram");
cHistogram *histogram = check_and_cast<cHistogram *>(obj);
...
}
One needs to take care that names of the attached objects don’t conflict with each other. Note
that message parameters (cMsgPar, see next section) are also attached the same way, so their
names also count.
When no objects are attached to a message (and getParList() is not invoked), the internal
cArray object is not created. This saves both storage and execution time.
Non-cObject data can be attached to messages by wrapping them into cObject, for example
into cMsgPar which has been designed expressly for this purpose. cMsgPar will be covered in
the next section.
131
OMNeT++ Simulation Manual – Messages and Packets
132
OMNeT++ Simulation Manual – Message Definitions
Chapter 6
Message Definitions
6.1 Introduction
In practice, one needs to add various fields to cMessage or cPacket to make them useful.
For example, when modeling communication networks, message/packet objects need to carry
protocol header fields. Since the simulation library is written in C++, the natural way of
extending cMessage/cPacket is via subclassing them. However, at least three items has to
be added to the new class for each field (a private data member, a getter and a setter method)
and the resulting class needs to integrate with the simulation framework, which means that
writing the necessary C++ code can be a tedious and time-consuming task.
OMNeT++ offers a more convenient way called message definitions. Message definitions offer
a compact syntax to describe message contents, and the corresponding C++ code is automati-
cally generated from the definitions. When needed, the generated class can also be customized
via subclassing. Even when the generated class needs to be heavily customized, message def-
initions can still save the programmer a great deal of manual work.
Let us begin with a simple example. Suppose that you need a packet class that carries source
and destination addresses as well as a hop count. You may then write a MyPacket.msg file
with the following contents:
packet MyPacket
{
int srcAddress;
int destAddress;
int remainingHops = 32;
};
It is the task of the message compiler to generate C++ classes that can be instantiated from
C++ model code. The message compiler is normally invoked automatically for .msg files during
build.
When the message compiler processes MyPacket.msg, it creates the following files: My-
Packet_m.h and MyPacket_m.cc. The generated MyPacket_m.h will contain the following
class declaration:
133
OMNeT++ Simulation Manual – Message Definitions
In order to use the MyPacket class from a C++ source file, the generated header file needs to
be included:
#include "MyPacket_m.h"
...
MyPacket *pkt = new MyPacket("pkt");
pkt->setSrcAddress(localAddr);
...
The MyPacket_m.cc file will contain implementation of the generated MyPacket class as well
as “reflection” code that allows inspection of these data structures under graphical user inter-
faces like Qtenv. The MyPacket_m.cc file should be compiled and linked into the simulation;
this is normally taken care of automatically.
The following sections describe the message syntax and features in detail.
Saving the above code into a FooPacket.msg file and processing it with the message compiler,
opp_msgc, will produce the files FooPacket_m.h and FooPacket_m.cc. The header file will
contain the declaration of the generated C++ class.
The generated class will have a constructor that optionally accepts object name and message
kind, and also a copy constructor. An assignment operator (operator=()) and cloning method
(dup()) will also be generated.
class FooPacket : public cPacket
{
public:
FooPacket(const char *name=nullptr, int kind=0);
FooPacket(const FooPacket& other);
134
OMNeT++ Simulation Manual – Message Definitions
For each field in the above description, the generated class will have a protected data member,
and a public getter and setter method. The names of the methods will begin with get and
set, followed by the field name with its first letter converted to uppercase. Thus, FooPacket
will contain the following methods:
virtual int getSourceAddress() const;
virtual void setSourceAddress(int sourceAddress);
virtual int getDestAddress() const;
virtual void setDestAddress(int destAddress);
virtual bool getHasPayload() const;
virtual void setHasPayload(bool hasPayload);
Note that the methods are all declared virtual to allow overriding them.
String fields can also be declared:
packet HttpRequestMessage
{
string method; // "GET", "POST", etc.
string resource;
};
The generated getter and setter methods will return and accept const char* pointers:
virtual const char *getMethod() const;
virtual void setMethod(const char *method);
virtual const char *getResource() const;
virtual void setResource(const char *resource);
The generated object will have its own copy of the string, so it not only stores the const char*
pointer.
• logical: bool
• integral types: char, short, int, long; and their unsigned versions unsigned char,
unsigned short, unsigned int, unsigned long
• C99-style fixed-size integral types: int8_t, int16_t, int32_t, int64_t; and their un-
signed versions uint8_t, uint16_t, uint32_t, uint64_t; 1
code compiles, i.e. the shortened type names must be defined in a header file you include.
135
OMNeT++ Simulation Manual – Message Definitions
• string. Getters and setters use the const char* data type; nullptr is not allowed.
The object will store a copy of the string, not just the pointer.
• structs and classes, defined in message files or elsewhere (see in later sections 6.2.7 and
6.6)
Numeric fields are initialized to zero, booleans to false, and string fields to empty string.
Initial values for fields can be specified after an equal sign, like so:
packet RequestPacket
{
int version = HTTP_VERSION;
string method = "GET";
string resource = "/";
int maxBytes = 100*1024*1024; // 100MiB
bool keepAlive = true;
};
Macros and expressions are also accepted as initalizer values, as the code above demon-
strates. The message compiler does not check the syntax of the values, it merely copies them
into the generated C++ file. If there are errors in them, they will be reported by the C++
compiler.
Field initialization statements will be placed into the constructor of the generated class.
6.2.4 Enums
Using a @enum property, a field of the type int or any other integral type can be declared
to take its value from an enum. The message compiler will then generate code that allows
graphical user interfaces display the symbolic value of the field.
Example:
packet FooPacket
{
int payloadType @enum(PayloadType);
};
The enum itself has to be declared separately. An enum is declared with the enum keyword,
using the following syntax:
enum PayloadType
{
NONE = 0;
UDP = 1;
TCP = 2;
SCTP = 3;
};
136
OMNeT++ Simulation Manual – Message Definitions
enum PayloadType;
packet FooPacket
{
int payloadType @enum(PayloadType);
};
The generated getter and setter methods will have an extra k argument, the array index:
virtual long getRoute(unsigned k) const;
virtual void setRoute(unsigned k, long route);
When these methods are called with an index that is out of bounds, an exception will be
thrown.
In this case, the generated class will have two extra methods in addition to the getter and
setter methods: one for setting the array size, and another one for returning the current array
size.
virtual long getRoute(unsigned k) const;
virtual void setRoute(unsigned k, long route);
137
OMNeT++ Simulation Manual – Message Definitions
The set...ArraySize() method internally allocates a new array. Existing values in the array
will be preserved (copied over to the new array.)
The default array size is zero. This means that set...ArraySize(n) needs to be called before
one can start filling array elements.
In addition to primitive types, classes, structs and their typedefs may also be used as fields.
For example, given a class named IPAddress, one can write the following:
packet IPPacket
{
int version = 4;
IPAddress src;
IPAddress dest;
};
The IPAddress type must be known to the message compiler, and also at compile time to the
C++ compiler; section 6.6 will describe how to achieve that.
The generated class will contain IPAddress data members (that is, not pointers to IPAddress
objects), and the following getter and setter methods will be generated for them:
virtual IPAddress& getSrc();
virtual const IPAddress& getSrc() const;
virtual void setSrc(const IPAddress& src);
Pointer fields where the setters and the destructor would delete the previous value are not
supported yet. However, there are workarounds, as described below.
You can create a typedef for the pointer and use the typedef name as field type. Then you’ll get
a plain pointer field where neither the setter nor the destructor deletes the old value (which is
a likely memory leak).
Example (section 6.6 will explain the details):
cplusplus {{ typedef Foo *FooPtr; }} // C++ typedef
class noncobject FooPtr; // announcement for the message compiler
packet Bar
{
FooPtr fooPtr; // leaky pointer field
};
138
OMNeT++ Simulation Manual – Message Definitions
Then you can customize the class via C++ inheritance and reimplement the setter methods
in C++, inserting the missing delete statements. Customization via C++ inheritance will be
described in section 6.7.2.
6.2.9 Inheritance
By default, messages are subclassed from cMessage or cPacket. However, you can explicitly
specify the base class using the extends keyword (only single inheritance is supported):
packet Ieee80211DataFrame extends Ieee80211Frame
{
...
};
For the example above, the generated C++ code will look like this:
// generated C++
class Ieee80211DataFrame : public Ieee80211Frame {
...
};
It may seem like the message compiler would need the definition of the base class to check
the definition of the field being assigned. However, it is not the case. The message compiler
trusts that such field exists; or rather, it leaves the check to the C++ compiler.
What the message compiler actually does is derives a setter method name from the field name,
and generates a call to it into the constructor. Thus, the generated constructor for the above
packet type would be something like this:
Ieee80211DataFrame::Ieee80211DataFrame(const char *name, int kind) :
Ieee80211Frame(name, kind)
{
this->setFrameType(DATA_FRAME);
...
}
139
OMNeT++ Simulation Manual – Message Definitions
This implementation also lets one initialize cMessage / cPacket fields such as message kind
or packet length:
packet UDPPacket
{
byteLength = 16; // results in ’setByteLength(16);’ being placed into ctor
};
6.3 Classes
Until now we have only seen message and packet descriptions, which generate classes de-
rived from cMessage or cPacket. However, it is also useful to be able to generate classes
and structs, for building blocks for messages, as control info objects (see cMessage’s setCon-
trolInfo() and for other purposes. This section covers classes; structs will be described in
the next section.
The syntax for defining classes is almost the same as defining messages, only the class
keyword is used instead of message / packet. The base class can be specified with the
extends keyword, and defaults to cObject.
NOTE: cObject has no data members. It only defines virtual methods, so the only
overhead would be the vptr; however, the generated class already has a vptr because the
generated methods are also virtual. In other words, cObject adds zero overhead to the
generated class, and there is no reason not to always use it as base class.
Examples:
// generated C++
class TCPCommand : public cObject
{
...
};
140
OMNeT++ Simulation Manual – Message Definitions
6.4 Structs
Message definitions allow one to define C-style structs, “C-style” meaning “containing only
data and no methods”. These structs can be useful as fields in message classes.
The syntax is similar to that of defining messages:
struct Place
{
int type;
string description;
double coords[3];
};
The generated struct has public data members, and no getter or setter methods. The following
code is generated from the above definition:
// generated C++
struct Place
{
int type;
opp_string description; // minimal string class that wraps a const char*
double coords[3];
};
Note that string fields are generated with the opp_string C++ type, which is a minimalistic
string class that wraps const char* and takes care of allocation/deallocation. It was chosen
instead of std::string because of its significantly smaller memory footprint (the sizeof of
opp_string is the same as that of a const char* pointer).
Inheritance is supported for structs:
struct Base
{
...
};
141
OMNeT++ Simulation Manual – Message Definitions
As we’ll see in later sections, cplusplus blocks are customarily used to insert #include
directives, typedefs, #define macros and other elements into the generated header.
Example:
cplusplus {{
#include <vector>
#include "foo.h"
#define FOO_VERSION 4
typedef std::vector<int> IntVector;
}}
The message compiler does not try to make sense of the text in the body of the cplusplus
block, it just simply copies it into the generated header file.
1. Let the message compiler know about the type by announcing it; and
However, with the class keyword, the message compiler needs to know the whether the class
is derived (directly or indirectly) from cOwnedObject, cNamedObject, cObject or none of
the above, because it affects code generation. The ancestor class can be declared with the
extends keyword, like this:
class IPAddress extends void; // does not extend any "interesting" class
class ModulePtr extends void; // ditto
class IntVector extends void; // ditto
class IPCtlInfo extends cObject;
class FooOption extends cNamedObject;
class PrioQueue extends cOwnedObject;
class IPAddrExt extends IPAddress; // also OK: IPAddress has been announced
142
OMNeT++ Simulation Manual – Message Definitions
By default, that is, when extends is missing, it is assumed that the class is derived from
cOwnedObject. Thus, the following two announcements are equivalent:
class PrioQueue;
class PrioQueue extends cOwnedObject;
NOTE: Notice that this default is inconsistent with the default base class for generat-
ing classes, which is cObject (see 6.3). The reason why type announcements assume
cOwnedObject is that it is safer: a mistake will surface in the form of a compile error and
will not remain hidden until it causes some obscure runtime error.
A cplusplus block is also needed if the desired types are defined in a different message file.
The block should contain an include directive to pull in the header file generated from the
other message file. It is currently not supported to import types from other message files
directly,
Example:
cplusplus {{
#include "TCPSegment_m.h" // make types defined in TCPSegment.msg available
// for the C++ compiler
}}
// Location.h
struct Location {
double lon;
double lat;
};
143
OMNeT++ Simulation Manual – Message Definitions
// AppPacket.msg
packet AppPacket {
...
}
To be able to use the above types in a message definition (and two more, an IntVector and a
module pointer), the message file should contain the following lines:
cplusplus {{
#include <vector>
#include "IPAddress.h"
#include "Location.h"
#include "AppPacket_m.h"
typedef std::vector<int> IntVector;
typedef cModule *ModulePtr;
}};
144
OMNeT++ Simulation Manual – Message Definitions
The generated class would have the following methods (note the differences from the de-
fault names getTtl(), setTtl(), getOptions(), setOptions(), getOptionsArraySize(),
getOptionsArraySize(); also note that indices and array sizes are now short):
virtual int getTTL() const;
virtual void setTTL(int ttl);
virtual const Option& getOption(short k) const;
virtual void setOption(short k, const Option& option);
virtual short getNumOptions() const;
virtual void setNumOptions(short n);
In some older simulation models you may also see the use of the @omitGetVerb class property.
This property tells the message compiler to generate getter methods without the “get” prefix,
e.g. for a sourceAddress field it would generate a sourceAddress() method instead of the
default getSourceAddress(). It is not recommended to use @omitGetVerb in new models,
because it is inconsistent with the accepted naming convention.
According to common belief, the largest drawback of generated code is that it is difficult or
impossible to fulfill such wishes. Hand-editing of the generated files is worthless, because
they will be overwritten and changes will be lost in the code generation cycle.
However, object oriented programming offers a solution. A generated class can simply be
customized by subclassing from it and redefining whichever methods need to be different
from their generated versions. This practice is known as the Generation Gap design pattern.
It is enabled with the @customize property set on the message:
packet FooPacket
{
@customize(true);
int payloadLength;
};
If you process the above code with the message compiler, the generated code will contain a
FooPacket_Base class instead of FooPacket. Then you would subclass FooPacket_Base to
produce FooPacket, while doing your customizations by redefining the necessary methods.
145
OMNeT++ Simulation Manual – Message Definitions
There is a minimum amount of code you have to write for FooPacket, because not everything
can be pre-generated as part of FooPacket_Base, e.g. constructors cannot be inherited. This
minimum code is the following (you will find it the generated C++ header too, as a comment):
Register_Class(FooPacket);
Note that it is important that you redefine dup() and provide an assignment operator (oper-
ator=()).
So, returning to our original example about payload length affecting packet length, the code
you’d write is the following:
146
OMNeT++ Simulation Manual – Message Definitions
For an abstract field, the message compiler generates no data member, and generated get-
ter/setter methods will be pure virtual:
virtual bool getUrgentBit() const = 0;
virtual void setUrgentBit(bool urgentBit) = 0;
Usually you’ll want to use abstract fields together with the Generation Gap pattern, so that
you can immediately redefine the abstract (pure virtual) methods and supply your implemen-
tation.
1. via a typedef;
6.8.1 Typedefs
The basic idea is that if we create a typedef for the desired type, we can use it for fields just
as any other type. Example:
cplusplus {{
#include <vector>
typedef std::vector<int> IntVector;
}}
packet FooPacket {
IntVector addresses;
};
147
OMNeT++ Simulation Manual – Message Definitions
Thus, the underlying std::vector<int> is exposed and you can directly manipulate it from
C++ code, for example like this:
FooPacket *pk = new FooPacket();
pk->getAddresses().push_back(1);
pk->getAddresses().push_back(5);
pk->getAddresses().push_back(9);
// or:
IntVector& v = pk->getAddresses();
v.push_back(1);
v.push_back(5);
v.push_back(9);
1. The message compiler won’t know that your field is actually a data structure, so the
generated reflection code won’t be able to look into it;
2. The fact that STL classes are directly exposed may be a mixed blessing; on one hand
this makes it easier to manipulate its contents, but on the other hand it violates the
encapsulation principle. Container classes work best when they are used as “nuts and
bolts” for your C++ program, but they shouldn’t really be used as public API.
If you compile the above code, in the generated C++ code you will only find abstract methods
for foo and bar, but no underlying data members or method implementations. You can
implement everything as you like. You can write the following C++ file then to implement foo
and bar with std::vector and std::stack (some details omitted for brevity):
#include <vector>
#include <stack>
#include "FooPacket_m.h"
148
OMNeT++ Simulation Manual – Message Definitions
// helper method
void unsupported() {throw cRuntimeError("unsupported method called");}
public:
...
// foo methods
virtual int getFoo(unsigned int k) {return foo[k];}
virtual void setFoo(unsigned int k, int x) {foo[k]=x;}
virtual void addFoo(int x) {foo.push_back(x);}
virtual void setFooArraySize(unsigned int size) {foo.resize(size);}
virtual unsigned int getFooArraySize() const {return foo.size();}
// bar methods
virtual int getBar(unsigned int k) {...}
virtual void setBar(unsigned int k, int x) {unsupported();}
virtual void barPush(int x) {bar.push(x);}
virtual void barPop() {bar.pop();}
virtual int barTop() {return bar.top();}
virtual void setBarArraySize(unsigned int size) {unsupported();}
virtual unsigned int getBarArraySize() const {return bar.size();}
};
Register_Class(FooPacket);
Some additional boilerplate code is needed so that the class conforms to conventions, and
duplication and copying works properly:
149
OMNeT++ Simulation Manual – Message Definitions
3. setBar(int k, int x) could not be implemented, but this is not particularly a prob-
lem. The exception will materialize in a Tkenv error dialog when you try to change the
field value.
6.9 Namespaces
It is possible to place the generated classes into a C++ namespace, and also to use types from
other namespaces.
To place the generated types into a namespace, add a namespace declaration near the top of
the message file:
namespace inet;
If you are fond of hierarchical (nested) namespaces, you can declare one with a straightforward
syntax, using double colons in the namespace declaration. There is no need for multiple
nested namespace declarations as in C++:
namespace org::omnetpp::inet::ieee80211;
The above code will be translated into nested namespaces in the C++ code:
Conceptually, the namespace extends from the place of the namespace declaration to the end
of the message file. (A message file may contain only one namespace declaration.) In other
words, it does matter whether you put something above the namespace declaration line or
below it:
1. The contents of cplusplus blocks above the namespace declaration will be placed out-
side (i.e. above) the namespace block in the generated C++ header; blocks below the
namespace declaration will placed inside the C++ namespace block.
2. Type announcements are interpreted differently depending on whether they occur above
or below the namespace declaration (this will be detailed later).
3. Types defined with the message syntax are placed into the namespace of the message
file; thus, definitions must always be after the namespace declaration. Type definitions
above the namespace line will be rejected with an error message.
150
OMNeT++ Simulation Manual – Message Definitions
IMPORTANT: Includes should always be placed into a cplusplus block above the
namespace declaration.
As for typedefs and other C++ code, you need to place them above or below the namespace
declaration based on whether you want them to be in the C++ namespace or not.
1. If the type name contains a double colon (::), it is interpreted as being fully qualified
with an absolute namespace.
2. If the name is just an identifier (no double colon), the interpretation depends on whether
it is above or below the namespace declaration. If it is above, the name is interpreted as
a global type; otherwise it is interpreted as part of the package file’s namespace.
This also means that if you want to announce a global type, you either have to put the
announcement above the namespace declaration, or prefix the type with “::” to declare that
it is not part of a namespace.
When the announced types are used later (as field type, base class, etc.), they can be referred
to just with their simple names (without namespace); or alternatively with their fully quali-
fied names. When a message compiler encounters type name as field type or base class, it
interprets the type name in the following way:
1. If the type name contains a double colon (::), it is interpreted as being fully qualified
with an absolute namespace.
2. If the name is just an identifier (no double colon), and the message file’s namespace
contains that name, it is chosen; otherwise:
3. It is looked up among all announced types in all namespaces (including the global names-
pace), and there must be exactly one match. That is, if the same name exists in multiple
namespaces, it may only be referenced with fully qualified name.
151
OMNeT++ Simulation Manual – Message Definitions
cplusplus {{
// includes go above the namespace line
#include <vector>
#include "IPAddress.h"
}}
cplusplus {{
// we want IPAddressVector to be part of the namespace
typedef std::vector<IPAddress> IPAddressVector;
}}
packet FooPacket {
IPAddress source;
IPAddressVector neighbors;
};
Another example that uses a PacketData class and a NetworkPacket type from a net names-
pace:
// NetworkPacket.msg
namespace net;
class PacketData { }
packet NetworkPacket { }
// FooPacket.msg
cplusplus {{
#include "NetworkPacket_m.h"
}}
class net::PacketData;
packet net::NetworkPacket;
namespace foo;
152
OMNeT++ Simulation Manual – Message Definitions
6.11 Summary
This section summarizes the possibilities offered by message definitions.
Base functionality:
• primitive types: bool, char, short, int, long; unsigned char, unsigned short, un-
signed int, unsigned long; int8_t, int16_t, int32_t, int64_t; uint8_t, uint16_t,
uint32_t, uint64_t; float, double; simtime_t
• string, a dynamically allocated string, presented as const char *
• structs and classes, declared with the message syntax or in C++ code
• typedef’d names declared in C++ and announced to the message compiler
• fixed-size arrays of the above types
• variable-size arrays of the above types (stored as a dynamically allocated array plus an
integer for the array size)
Further features:
153
OMNeT++ Simulation Manual – Message Definitions
• inheritance
• namespaces
• customization of generated method names
• customization of the generated class via subclassing (Generation Gap pattern)
Generated code (all generated methods are virtual, although this is not written out in the
following table):
string type
fixed-size arrays
double field[4]; double getField(unsigned k);
void setField(unsigned k, double d);
unsigned getFieldArraySize();
variable-size arrays
double field[]; void setFieldArraySize(unsigned n);
unsigned getFieldArraySize();
double getField(unsigned k);
void setField(unsigned k, double d);
customized class
class Foo { class Foo_Base { ... };
@customize(true);
and you have to write:
154
OMNeT++ Simulation Manual – Message Definitions
abstract fields
double getField() = 0;
abstract double field;
void setField(double d) = 0;
155
OMNeT++ Simulation Manual – Message Definitions
156
OMNeT++ Simulation Manual – The Simulation Library
Chapter 7
OMNeT++ has an extensive C++ class library available to the user for implementing simulation
models and model components. Part of the class library’s functionality has already been cov-
ered in the previous chapters, including discrete event simulation basics, the simple module
programming model, module parameters and gates, scheduling events, sending and receiving
messages, channel operation and programming model, finite state machines, dynamic module
creation, signals, and more.
This chapter discusses the rest of the simulation library. Topics will include logging, random
number generation, queues, topology discovery and routing support, and statistics and result
collection. This chapter also covers some of the conventions and internal mechanisms of the
simulation library to allow one extending it and using it to its full potential.
7.1 Fundamentals
Classes in the OMNeT++ simulation library are part of the omnetpp namespace. To use the
OMNeT++ API, one must include the omnetpp.h header file and either import the namespace
with using namespace omnetpp, or qualify names with the omnetpp:: prefix.
Thus, simulation models will contain the
#include <omnetpp.h>
When writing code that should work with various versions of OMNeT++, it is often useful to
have compile-time access to the OMNeT++ version in a numeric form. The OMNETPP_VERSION
macro exists for that purpose, and it is defined by OMNeT++ to hold the version number in
the form major*256+minor. For example, in OMNeT++ 4.6 it was defined as
157
OMNeT++ Simulation Manual – The Simulation Library
cObject
cNamedObject
cOwnedObject
... ...
Figure 7.1: cObject is the base class for most of the simulation library
The subclasses cNamedObject and cOwnedObject add data members to implement more
functionality. The following sections discuss some of the practically important functonality
defined by cObject.
The most useful and most visible member functions of cObject are getName() and getFull-
Name(). The idea behind them is that many objects in OMNeT++ have names by default (for
example, modules, parameters and gates), and even for other objects, having a printable name
is a huge gain when it comes to logging and debugging.
getFullName() is important for gates and modules, which may be part of gate or module
vectors. For them, getFullName() returns the name with the index in brackets, while get-
Name() only returns the name of the module or gate vector. That is, for a gate out[3] in the
gate vector out[10], getName() returns "out", and getFullName() returns "out[3]". For
other objects, getFullName() simply returns the same string as getName(). An example:
cGate *gate = gate("out", 3); // out[3]
EV << gate->getName(); // prints "out"
EV << gate->getFullName(); // prints "out[3]"
NOTE: When printing out the name of an object, prefer getFullName() to getName(),
especially if the runtime type is not know. This will ensure that the vector index will also
be printed if the object has one.
158
OMNeT++ Simulation Manual – The Simulation Library
cObject merely defines these member functions, but they return an empty string. Actual
storage for a name string and a setName() method is provided by the class cNamedObject,
which is also an (indirect) base class for most library classes. Thus, one can assign names to
nearly all user-created objects. It it also recommended to do so, because a name makes an
object easier to identify in graphical runtimes like Tkenv or Qtenv.
By convention, the object name is the first argument to the constructor of every class, and it
defaults to the empty string. To create an object with a name, pass the name string (a const
char* pointer) as the first argument of the constructor. For example:
cMessage *timeoutMsg = new cMessage("timeout");
Both the constructor and setName() make an internal copy of the string, instead of just
storing the pointer passed to them.1
For convenience and efficiency reasons, the empty string "" and nullptr are treated as
interchangeable by library objects. That is, "" is stored as nullptr but returned as "". If
one creates a message object with either nullptr or "" as its name string, it will be stored as
nullptr, and getName() will return a pointer to a static "".
Hierarchical Name
getFullPath() returns the object’s hierarchical name. This name is produced by prepending
the full name (getFullName()) with the parent or owner object’s getFullPath(), separated
by a dot. For example, if the out[3] gate in the previous example belongs to a module
named classifier, which in turn is part of a network called Queueing, then the gate’s
getFullPath() method will return "Queueing.classifier.out[3]".
cGate *gate = gate("out", 3); // out[3]
EV << gate->getName(); // prints "out"
EV << gate->getFullName(); // prints "out[3]"
EV << gate->getFullPath(); // prints "Queueing.classifier.out[3]"
The getFullName() and getFullPath() methods are extensively used in graphical runtime
environments (Tkenv, Qtenv), and also when assembling runtime error messages.
In contrast to getName() and getFullName() which return const char * pointers, get-
FullPath() returns std::string. This makes no difference when logging via EV«, but when
getFullPath() is used as a "%s" argument to sprintf(), one needs to write getFull-
Path().c_str().
char buf[100];
sprintf("msg is ’%80s’", msg->getFullPath().c_str()); // note c_str()
Class Name
The getClassName() member function returns the class name as a string, including the
namespace. getClassName() internally relies on C++ RTTI.
1 In a simulation, there are usually many objects with the same name: modules, parameters, gates, etc. To
conserve memory, several classes keep names in a shared, reference-counted name pool instead of making separate
copies for each object. The runtime cost of looking up an existing string in the name pool and incrementing its
reference count also compares favorably to the cost of allocation and copying.
159
OMNeT++ Simulation Manual – The Simulation Library
An example:
const char *className = msg->getClassName(); // returns "omnetpp::cMessage"
Cloning Objects
The dup() member function creates an exact copy of the object, duplicating contained objects
also if necessary. This is especially useful in the case of message objects.
cMessage *copy = msg->dup();
dup() delegates to the copy constructor. Classes also declare an assignment operator (opera-
tor=()) which can be used to copy contents of an object into another object of the same type.
dup(), the copy constructor and the assignment operator all perform deep coping: objects
contained in the copied object will also be duplicated if necessary.
operator=() differs from the other two in that it does not copy the object’s name string, i.e.
does not invoke setName(). The rationale is that the name string is often used for identifying
the particular object instance, as opposed to being considered as part of its contents.
7.1.3 Iterators
There are several container classes in the library (cQueue, cArray etc.) For many of them,
there is a corresponding iterator class that one can use to loop through the objects stored in
the container.
For example:
cQueue queue;
//...
for (cQueue::Iterator it(queue); !it.end(); ++it) {
cObject *containedObject = *it;
//...
}
160
OMNeT++ Simulation Manual – The Simulation Library
and outputs of algorithms, the information on which decisions are based, and the performed
actions along with their parameters. In general, logging facilitates understanding which mod-
ule is doing what and why.
OMNeT++ makes logging easy and consistent among simulation models by providing its own
C++ API and configuration options. The API provides efficient logging with several predefined
log levels, global compile-time and runtime filters, per-component runtime filters, automatic
context information, log prefixes and other useful features. In the following sections, we look
at how to write log statements using the OMNeT++ logging API.
• LOGLEVEL_OFF is not a real log level, it can’t be used for actual logging. It is only useful
for configuration purposes, it completely disables logging.
• LOGLEVEL_FATAL is the highest log level. It should be used for fatal (unrecoverable) errors
that prevent the component from further operation. It doesn’t mean that the simulation
must stop immediately (because in such cases the code should throw a cRuntimeError),
but rather that the a component is unable to continue normal operation. For example,
a special purpose recording component may be unable to continue recording due to the
disk being full.
• LOGLEVEL_ERROR should be used for recoverable (non-fatal) errors that allow the compo-
nent to continue normal operation. For example, a MAC layer protocol component could
log unsuccessful packet receptions and unsuccessful packet transmissions using this
level.
• LOGLEVEL_WARN should be used for exceptional (non-error) situations that may be im-
portant for users and rarely occur in the component. For example, a MAC layer protocol
component could log detected bit errors using this level.
• LOGLEVEL_INFO should be used for high-level protocol specific details that are most likely
important for the users of the component. For example, a MAC layer protocol component
could log successful packet receptions and successful packet transmissions using this
level.
• LOGLEVEL_DETAIL should be used for low-level protocol-specific details that may be use-
ful and understandable by the users of the component. These messages may help to
161
OMNeT++ Simulation Manual – The Simulation Library
track down various protocol-specific issues without actually looking too deep into the
code. For example, a MAC layer protocol component could log state machine updates,
acknowledge timeouts and selected back-off periods using this level.
• LOGLEVEL_DEBUG should be used for high-level implementation-specific technical details
that are most likely important for the developers of the component. These messages may
help to debug various issues when one is looking at the code. For example, a MAC layer
protocol component could log updates to internal state variables, updates to complex
data structures using this level.
• LOGLEVEL_TRACE is the lowest log level. It should be used for low-level implementation-
specific technical details that are mostly useful for the developers of the component.
For example, a MAC layer protocol component could log control flow in loops and if
statements, entering/leaving methods and code blocks using this level.
The actual logging is as simple as writing information into one of these special log streams as
follows:
EV_ERROR << "Connection to server is lost.\n";
EV_WARN << "Queue is full, discarding packet.\n";
EV_INFO << "Packet received , sequence number = " << seqNum << "." << endl;
EV_TRACE << "routeUnicastPacket(" << packet << ");" << endl;
NOTE: It is not recommended to use plain printf() or std::cout for logging. Output
from EV_INFO and the other log macros can be controlled more easily from omnetpp.ini,
and it is more convenient to view using Tkenv or Qtenv.
The above C++ macros work well from any C++ class, including OMNeT++ modules. In fact,
they automatically capture a number of context specific information such as the current event,
current simulation time, context module, this pointer, source file and line number. The
final log lines will be automatically extended with a prefix that is created from the captured
information (see section 10.6).
In static class member functions or in non-class member functions an extra EV_STATICCONTEXT
macro must be present to make sure that normal log macros compile. 2
2 This is due to that in C++ it is impossible determine at compile-time whether a this pointer is accessible.
162
OMNeT++ Simulation Manual – The Simulation Library
Assuming a simple log prefix that prints the log level in brakets, the above code fragment
produces the following output in Cmdenv:
[INFO] Line starts here, and it continues here
Sometimes it might be useful to split a line into multiple lines to achieve better formatting. In
such cases, there’s no need to write multiple log statements. Simply insert new lines into the
sequence of shift operator calls:
EV_INFO << "First line" << endl << "second line" << endl;
In the produced output, each line will have the same log prefix, as shown below:
[INFO] First line
[INFO] Second line
The OMNeT++ logging API also supports direct printing to a log stream. This is mainly useful
when printing is really complicated algorithmically (e.g. printing a multi-dimensional value).
The following code could produce multiple log lines each having the same log prefix.
void Matrix::print(std::stream &output) { ... }
void Matrix::someFunction()
{
print(EV_INFO);
163
OMNeT++ Simulation Manual – The Simulation Library
7.2.6 Implementation
OMNeT++ does its best to optimize the performance of logging. The implementation fully sup-
ports conditinal compilation of log statements based on their log level. It automatically checks
whether the log is recorded anywhere. It also checks global and per-component runtime log
levels. The latter is efficiently cached in the components for subsequent checks. See section
10.6 for more details on how to configure these log levels.
The implementation of the C++ log macros makes use of the fact that the operator« is bound
more loosely than the conditional operator (?:). This solves conditional compilation, and
also helps runtime checks by redirecting the output to a null stream. Unfortunately the
operator« calls are still evaluated on the null stream, even if the log level is disabled.
Rarely just the computation of log statement parameters may be very expensive, and thus
it must be avoided if possible. In this case, it is a good idea to make the log statement
conditional on whether the output is actually being displayed or recorded anywhere. The
cEnvir::isLoggingEnabled() call returns false when the output is disabled, such as in
“express” mode. Thus, one can write code like this:
if (!getEnvir()->isLoggingEnabled())
EV_DEBUG << "CRC: " << computeExpensiveCRC(packet) << endl;
Random numbers in simulation are usually not really random. Rather, they are produced
using deterministic algorithms. Based on some internal state, the algorithm performs some
deterministic computation to produce a “random” number and the next state. Such algo-
rithms and their implementations are called random number generators or RNGs, or some-
times pseudo random number generators or PRNGs to highlight their deterministic nature.
The algorithm’s internal state is usually initialized from a smaller seed value.
Starting from the same seed, RNGs always produce the same sequence of random numbers.
This is a useful property and of great importance, because it makes simulation runs repeat-
able.
RNGs are rarely used directly, because they produce uniformly distributed random numbers.
When non-uniform random numbers are needed, mathematical transformations are used to
produce random numbers from RNG input that correspond to specific distributions. This is
called random variate generation, and it will be covered in the next section, 7.4.
It is often advantageous for simulations to use random numbers from multiple RNG instances.
For example, a wireless network simulation may use one RNG for generating traffic, and
another RNG for simulating transmission errors in the noisy wireless channel. Since seeds
for individual RNGs can be configured independently, this arrangement allows one e.g. to
perform several simulation runs with the same traffic but with bit errors occurring in different
places. A simulation technique called variance reduction is also related to the use of different
random number streams. OMNeT++ makes it easy to use multiple RNGs in various flexible
configurations.
When assigning seeds, it is important that different RNGs and also different simulation runs
use non-overlapping series of random numbers. Overlap in the generated random number
sequences can introduce unwanted correlation in the simulation results.
164
OMNeT++ Simulation Manual – The Simulation Library
Mersenne Twister
By default, OMNeT++ uses the Mersenne Twister RNG (MT) by M. Matsumoto and T. Nishimura
[MN98]. MT has a period of 219937 − 1, and 623-dimensional equidistribution property is as-
sured. MT is also very fast: as fast or faster than ANSI C’s rand().
OMNeT++ releases prior to 3.0 used a linear congruential generator (LCG) with a cycle length
of 231 −2, described in [Jai91], pp. 441-444,455. This RNG is still available and can be selected
from omnetpp.ini (Chapter 11). This RNG is only suitable for small-scale simulation studies.
As shown by Karl Entacher et al. in [EHW02], the cycle length of about 231 is too small (on
todays fast computers it is easy to exhaust all random numbers), and the structure of the
generated “random” points is too regular. The [Hel98] paper provides a broader overview of
issues associated with RNGs used for simulation, and it is well worth reading. It also contains
useful links and references on the topic.
When a simulation is executed under Akaroa control (see section 11.21), it is also possible to
let OMNeT++ use Akaroa’s RNG. This needs to be configured in omnetpp.ini (section 10.5).
Other RNGs
OMNeT++ allows plugging in your own RNGs as well. This mechanism, based on the cRNG in-
terface, is described in section 17.5. For example, one candidate to include could be L’Ecuyer’s
CMRG [LSCK02] which has a period of about 2191 and can provide a large number of guaran-
teed independent streams.
OMNeT++ can be configured to make several RNGs available for the simulation model. These
global or physical RNGs are numbered from 0 to numRN Gs − 1, and can be seeded indepen-
dently.
However, usually model code doesn’t directly work with those RNGs. Instead, there is an
indirection step introduced for additional flexibility. When random numbers are drawn in a
model, the code usually refers to component-local or logical RNG numbers. These local RNG
numbers are mapped to global RNG indices to arrive at actual RNG instances. This mapping
occurs on per-component basis. That is, each module and channel object contains a mapping
table similar to the following:
165
OMNeT++ Simulation Manual – The Simulation Library
In the example, the module or channel in question has 6 local (logical) RNGs that map to 4
global (physical) RNGs.
NOTE: Local RNG number 0 is special in the sense that all random number functions
use that RNG, unless explicitly told otherwise by specifying an rng=k argument.
The local-to-global mapping, as well as the number of number of global RNGs and their seed-
ing can be configured in omnetpp.ini (see section 10.5).
The mapping can be set up arbitrarily, with the default being identity mapping (that is, local
RNG k refers to global RNG k.) The mapping allows for flexibility in RNG and random number
streams configuration – even for simulation models which were not written with RNG aware-
ness. For example, even if modules in a simulation only use the default, local RNG number 0,
one can set up mapping so that different groups of modules use different physical RNGs.
In theory, RNGs could also be instantiated and used directly from C++ model code. However,
doing so is not recommended, because the model would lose configurability via omnetpp.ini.
Distribution Description
Continuous distributions
uniform(a, b) uniform distribution in the range [a,b)
exponential(mean) exponential distribution with the given mean
normal(mean, stddev) normal distribution with the given mean and stan-
dard deviation
166
OMNeT++ Simulation Manual – The Simulation Library
Some notes:
• intuniform() generates integers including both the lower and upper limit, so for example
the outcome of tossing a coin could be written as intuniform(1,2).
There are several ways to generate random numbers from these distributions, as described in
the next sections.
The preferred way is to use methods defined on cComponent, the common base class of mod-
ules and channels:
double uniform(double a, double b, int rng=0) const;
double exponential(double mean, int rng=0) const;
double normal(double mean, double stddev, int rng=0) const;
...
167
OMNeT++ Simulation Manual – The Simulation Library
These methods work with the component’s local RNGs, and accept the RNG index (default 0)
in their extra int parameter.
Since most simulation code is located in methods of simple modules, these methods can be
usually called in a concise way, without an explicit module or channel pointer. An example:
scheduleAt(simTime() + exponential(1.0), msg);
There are two additional methods, intrand() and dblrand(). intrand(n) generates random
integers in the range [0, n − 1], and dblrand() generates a random double on [0, 1). They also
accept an additional local RNG index that defaults to 0.
cRandom
Let us see for example cNormal. The constructor expects an RNG (cRNG pointer) and the
parameters of the distribution, mean and standard deviation. It also has a default constructor,
as it is a requirement for Register_Class(). When the default constructor is used, the
parameters can be set with setRNG(), setMean() and setStddev(). setRNG() is defined on
cRandom. The draw() method, of course, is redefined to return a random number from the
normal distribution.
An example that shows the use of a random number stream as an object:
cNormal *normal = new cNormal(getRNG(0), 0, 1); // unit normal distr.
printRandomNumbers(normal, 10);
...
Another important property of cRandom is that it can encapsulate state. That is, subclasses
can be implemented that, for example, return autocorrelated numbers, numbers from a
stochastic process, or simply elements of a stored sequence (e.g. one loaded from a trace
file).
168
OMNeT++ Simulation Manual – The Simulation Library
Both the cComponent methods and the random number stream classes described above have
been implemented with the help of standalone generator functions. These functions take a
cRNG pointer as their first argument.
double uniform(cRNG *rng, double a, double b);
double exponential(cRNG *rng, double mean);
double normal(cRNG *rng, double mean, double stddev);
...
One can also specify a distribution as a histogram. The cHistogram, cKSplit and cPSquare
classes can be used to generate random numbers from histograms. This feature is docu-
mented later, with the statistical classes.
One can easily add support for new distributions. We recommend that you write a standalone
generator function first. Then you can add a cRandom subclass that wraps it, and/or module
(channel) methods that invoke it with the module’s local RNG. If the function is registered with
the Define_NED_Function() macro (see 7.11), it will be possible to use the new distribution
in NED files and ini files, as well.
If you need a random number stream that has state, you need to subclass from cRandom.
Basic Usage
cQueue is a container class that acts as a queue. cQueue can hold objects of type derived
from cObject (almost all classes from the OMNeT++ library), such as cMessage, cPar, etc.
Normally, new elements are inserted at the back, and removed from the front.
front FRONT
back
removal insertio
pop() insert(
The member functions dealing with insertion and removal are insert() and pop().
169
OMNeT++ Simulation Manual – The Simulation Library
cQueue queue("my-queue");
cMessage *msg;
// insert messages
for (int i = 0; i < 10; i++) {
msg = new cMessage;
queue.insert(msg);
}
// remove messages
while(!queue.isEmpty()) {
msg = (cMessage *)queue.pop();
delete msg;
}
The length() member function returns the number of items in the queue, and empty() tells
whether there is anything in the queue.
There are other functions dealing with insertion and removal. The insertBefore() and
insertAfter() functions insert a new item exactly before or after a specified one, regardless
of the ordering function.
The front() and back() functions return pointers to the objects at the front and back of the
queue, without affecting queue contents.
The pop() function can be used to remove items from the tail of the queue, and the remove()
function can be used to remove any item known by its pointer from the queue:
queue.remove(msg);
Priority Queue
By default, cQueue implements a FIFO, but it can also act as a priority queue, that is, it
can keep the inserted objects ordered. To use this feature, one needs to provide a comparison
function that takes two cObject pointers, and returns -1, 0 or 1 (see the reference for details).
An example of setting up an ordered cQueue:
cQueue queue("queue", someCompareFunc);
If the queue object is set up as an ordered queue, the insert() function uses the ordering
function: it searches the queue contents from the head until it reaches the position where the
new item needs to be inserted, and inserts it there.
Iterators
The cQueue::Iterator class lets one iterate over the contents of the queue and examine
each object.
The cQueue::Iterator constructor expects the queue object in the first argument. Normally,
forward iteration is assumed, and the iteration is initialized to point at the front of the queue.
For reverse iteration, specify reverse=true as the optional second argument. After that, the
class acts as any other OMNeT++ iterator: one can use the ++ and - operators to advance it,
the * operator to get a pointer to the current item, and the end() member function to examine
whether the iterator has reached the end (or the beginning) of the queue.
170
OMNeT++ Simulation Manual – The Simulation Library
Forward iteration:
for (cQueue::Iterator iter(queue); !iter.end(), iter++) {
cMessage *msg = (cMessage *) *iter;
//...
}
Reverse iteration:
for (cQueue::Iterator iter(queue, true); !iter.end(), iter--) {
cMessage *msg = (cMessage *) *iter;
//...
}
cArray is a container class that holds objects derived from cObject. cArray implements
a dynamic-size array: its capacity grows automatically when it becomes full. cArray stores
pointers of objects inserted instead of making copies.
Creating an array:
cArray array("array");
Adding an object at a given index (if the index is occupied, you will get an error message):
cMsgPar *p = new cMsgPar("par");
int index = array.addAt(5,p);
You can also search the array or get a pointer to an object by the object’s name:
int index = array.find("par");
Par *p = (cPar *) array["par"];
You can remove an object from the array by calling remove() with the object name, the index
position or the object pointer:
array.remove("par");
array.remove(index);
array.remove(p);
The remove() function doesn’t deallocate the object, but it returns the object pointer. If you
also want to deallocate it, you can write:
delete array.remove(index);
171
OMNeT++ Simulation Manual – The Simulation Library
Iteration
cArray has no iterator, but it is easy to loop through all the indices with an integer variable.
The size() member function returns the largest index plus one.
for (int i = 0; i < array.size(); i++) {
if (array[i]) { // is this position used?
cObject *obj = array[i];
EV << obj->getName() << endl;
}
}
7.6.1 Overview
The cTopology class was designed primarily to support routing in communication networks.
A cTopology object stores an abstract representation of the network in a graph form:
One can specify which modules to include in the graph. Compound modules may also be
selected. The graph will include all connections among the selected modules. In the graph,
all nodes are at the same level; there is no submodule nesting. Connections which span
across compound module boundaries are also represented as one graph edge. Graph edges
are directed, just as module gates are.
If you are writing a router or switch model, the cTopology graph can help you determine
what nodes are available through which gate and also to find optimal routes. The cTopology
object can calculate shortest paths between nodes for you.
The mapping between the graph (nodes, edges) and network model (modules, gates, connec-
tions) is preserved: one can find the corresponding module for a cTopology node and vica
versa.
• by module type
First, you can specify which node types you want to include. The following code extracts all
modules of type Router or Host. (Router and Host can be either simple or compound module
types.)
172
OMNeT++ Simulation Manual – The Simulation Library
cTopology topo;
topo.extractByModuleType("Router", "Host", nullptr);
Any number of module types can be supplied; the list must be terminated by nullptr.
A dynamically assembled list of module types can be passed as a nullptr-terminated array of
const char* pointers, or in an STL string vector std::vector<std::string>. An example
for the former:
cTopology topo;
const char *typeNames[3];
typeNames[0] = "Router";
typeNames[1] = "Host";
typeNames[2] = nullptr;
topo.extractByModuleType(typeNames);
Second, you can extract all modules which have a certain parameter:
topo.extractByParameter("ipAddress");
You can also specify that the parameter must have a certain value for the module to be
included in the graph:
cMsgPar yes = "yes";
topo.extractByParameter("includeInTopo", &yes);
The third form allows you to pass a function which can determine for each module whether
it should or should not be included. You can have cTopology pass supplemental data to the
function through a void* pointer. An example which selects all top-level modules (and does
not use the void* pointer):
int selectFunction(cModule *mod, void *)
{
return mod->getParentModule() == getSimulation()->getSystemModule();
}
topo.extractFromNetwork(selectFunction, nullptr);
A cTopology object uses two types: cTopology::Node for nodes and cTopology::Link for
edges. (cTopology::LinkIn and cTopology::LinkOut are aliases for cTopology::Link;
we’ll talk about them later.)
Once you have the topology extracted, you can start exploring it. Consider the following code
(we’ll explain it shortly):
for (int i = 0; i < topo.getNumNodes(); i++) {
cTopology::Node *node = topo.getNode(i);
EV << "Node i=" << i << " is " << node->getModule()->getFullPath() << endl;
EV << " It has " << node->getNumOutLinks() << " conns to other nodes\n";
EV << " and " << node->getNumInLinks() << " conns from other nodes\n";
173
OMNeT++ Simulation Manual – The Simulation Library
}
}
The getNumNodes() member function returns the number of nodes in the graph, and getN-
ode(i) returns a pointer to the ith node, a cTopology::Node structure.
The correspondence between a graph node and a module can be obtained by getNodeFor()
method:
cTopology::Node *node = topo.getNodeFor(module);
cModule *module = node->getModule();
The getNodeFor() member function returns a pointer to the graph node for a given module.
(If the module is not in the graph, it returns nullptr). getNodeFor() uses binary search
within the cTopology object so it is relatively fast.
cTopology::Node’s other member functions let you determine the connections of this node:
getNumInLinks(), getNumOutLinks() return the number of connections, getLinkIn(i)
and getLinkOut(i) return pointers to graph edge objects.
By calling member functions of the graph edge object, you can determine the modules and
gates involved. The getRemoteNode() function returns the other end of the connection,
and getLocalGate(), getRemoteGate(), getLocalGateId() and getRemoteGateId() re-
turn the gate pointers and ids of the gates involved. (Actually, the implementation is a bit
tricky here: the same graph edge object cTopology::Link is returned either as cTopol-
ogy::LinkIn or as cTopology::LinkOut so that “remote” and “local” can be correctly inter-
preted for edges of both directions.)
This performs the Dijkstra algorithm and stores the result in the cTopology object. The result
can then be extracted using cTopology and cTopology::Node methods. Naturally, each call
to calculateUnweightedSingleShortestPathsTo() overwrites the results of the previous
call.
Walking along the path from our module to the target node:
cTopology::Node *node = topo.getNodeFor(this);
if (node == nullptr) {
EV << "We (" << getFullPath() << ") are not included in the topology.\n";
}
else if (node->getNumPaths()==0) {
EV << "No path to destination.\n";
}
174
OMNeT++ Simulation Manual – The Simulation Library
else {
while (node != topo.getTargetNode()) {
EV << "We are in " << node->getModule()->getFullPath() << endl;
EV << node->getDistanceToTarget() << " hops to go\n";
EV << "There are " << node->getNumPaths()
<< " equally good directions, taking the first one\n";
cTopology::LinkOut *path = node->getPath(0);
EV << "Taking gate " << path->getLocalGate()->getFullName()
<< " we arrive in " << path->getRemoteNode()->getModule()->getFullPath()
<< " on its gate " << path->getRemoteGate()->getFullName() << endl;
node = path->getRemoteNode();
}
}
175
OMNeT++ Simulation Manual – The Simulation Library
When extracting the topology from the network, cTopology uses the factory methods creat-
eNode() and createLink() to instantiate the node and link objects. These methods may be
overridden by subclassing cTopology if the need arises, for example when it is useful to be
able to store additional information in those objects.
7.7.1 cPatternMatcher
cPatternMatcher holds a pattern string and several option flags, and has a matches()
boolean function that determines whether the string passed as argument matches the pattern
with the given flags. The pattern and the flags can be set via the constructor or by calling the
setPattern() member function.
The pattern syntax is a variation on Unix glob-style patterns. The most apparent differences
to globbing rules are the distinction between * and **, and that character ranges should be
written with curly braces instead of square brackets; that is, any-letter is expressed as {a-
zA-Z} and not as [a-zA-Z], because square brackets are reserved for the notation of module
vector indices.
The following option flags are supported:
• set, e.g. {a-zA-Z} : matches any character that is contained in the set
• negated set, e.g. {^a-z}: matches any character that is NOT contained in the set
• numeric range, e.g. {38..150} : matches any number (i.e. sequence of digits) in the
given range
176
OMNeT++ Simulation Manual – The Simulation Library
• numeric index range, e.g. [38..150] : matches any number in square brackets in the
given range
• backslash, \ : takes away the special meaning of the subsequent character
NOTE: The dottedpath option was introduced to make matching OMNeT++ module paths
more powerful. When it is off (dottedpath=false), there is no difference between * and **,
they both match any character sequence. However, when matching OMNeT++ module
paths or other strings where dot is a separator character, it is useful to turn on the
dottedpath mode (dottedpath=true). In that mode, *, not being able to cross a dot, can
match only a single path component (or part of it), and ** can match multiple path
components.
Sets and negated sets can contain several character ranges and also enumeration of charac-
ters, for example {_a-zA-Z0-9} or {xyzc-f}. To include a hyphen in the set, place it at a
position where it cannot be interpreted as character range, for example {a-z-} or {-a-z}.
To include a close brace in the set, it must be the first character: {}a-z}, or for a negated
set: {^}a-z}. A backslash is always taken as literal backslash (and NOT as escape character)
within set definitions. When doing case-insensitive match, avoid ranges that include both
alpha and non-alpha characters, because they might cause funny results.
For numeric ranges and numeric index ranges, ranges are inclusive, and both the start
and the end of the range are optional; that is, {10..}, {..99} and {..} are all valid nu-
meric ranges (the last one matches any number). Only nonnegative integers can be matched.
Caveat: {17..19} will match "a17", "117" and also "963217"!
The cPatternMatcher constructor and the setPattern() member function have similar sig-
natures:
cPatternMatcher(const char *pattern, bool dottedpath, bool fullstring,
bool casesensitive);
void setPattern(const char *pattern, bool dottedpath, bool fullstring,
bool casesensitive);
There are also some more utility functions for printing the pattern, determining whether a
pattern contains wildcards, etc.
Example:
cPatternMatcher matcher("**.host[*]", true, true, true);
EV << matcher.matches("Net.host[0]") << endl; // -> true
EV << matcher.matches("Net.area1.host[0]") << endl; // -> true
EV << matcher.matches("Net.host") << endl; // -> false
EV << matcher.matches("Net.host[0].tcp") << endl; // -> false
7.7.2 cMatchExpression
The cMatchExpression class builds on top of cPatternMatcher, and lets one determine
whether an object matches a given pattern expression.
A pattern expression consists of elements in the fieldname(pattern) syntax; they check whether
the string representation of the given field of the object matches the pattern. For example,
177
OMNeT++ Simulation Manual – The Simulation Library
srcAddr(192.168.0.*) will match if the srcAddr field of the object starts with 192.168.0. A
naked pattern (without field name and parens) is also accepted, and it will be matched against
the default field of the object, which will usually be its name.
These elements can be combined with the AND, OR, NOT operators, accepted in both lower-
case and uppercase. AND has higher precedence than OR, but parentheses can be used to
change the evaluation order.
Pattern examples:
• "node*"
• "node* or host*"
The cMatchExpression class has a constructor and setPattern() method similar to those
of cPatternMatcher:
cMatchExpression(const char *pattern, bool dottedpath, bool fullstring,
bool casesensitive);
void setPattern(const char *pattern, bool dottedpath, bool fullstring,
bool casesensitive);
This means that objects to be matched must either be subclassed from cMatchExpres-
sion::Matchable, or be wrapped into some adapter class that does. cMatchExpression::Matchable
is a small abstract class with only a few pure virtual functions:
/**
* Objects to be matched must implement this interface
*/
class SIM_API Matchable
{
public:
/**
* Return the default string to match. The returned pointer will not be
* cached by the caller, so it is OK to return a pointer to a static buffer.
*/
virtual const char *getAsString() const = 0;
/**
* Return the string value of the given attribute, or nullptr if the object
* doesn’t have an attribute with that name. The returned pointer will not
* be cached by the caller, so it is OK to return a pointer to a static buffer.
*/
virtual const char *getAsString(const char *attribute) const = 0;
/**
178
OMNeT++ Simulation Manual – The Simulation Library
* Virtual destructor.
*/
virtual ~Matchable() {}
};
To be able to match instances of an existing class that is not already a Matchable, one needs
to write an adapter class. An adapter class that we can look at as an example is cMatch-
ableString. cMatchableString makes it possible to match strings with a cMatchExpres-
sion, and is part of OMNeT++:
/**
* Wrapper to make a string matchable with cMatchExpression.
*/
class cMatchableString : public cMatchExpression::Matchable
{
private:
std::string str;
public:
cMatchableString(const char *s) {str = s;}
virtual const char *getAsString() const {return str.c_str();}
virtual const char *getAsString(const char *name) const {return nullptr;}
};
An example:
cMatchExpression expr("foo* or bar*", true, true, true);
cMatchableString str1("this is a foo");
cMatchableString str2("something else");
EV << expr.matches(&str1) << endl; // -> true
EV << expr.matches(&str2) << endl; // -> false
• cStdDev keeps summary statistics (mean, standard deviation, range) of weighted or un-
weighted observations.
179
OMNeT++ Simulation Manual – The Simulation Library
• cPSquare is a class that uses the P 2 algorithm described in [JC85]. The algorithm calcu-
lates quantiles without storing the observations; one can also think of it as a histogram
with equiprobable cells.
cStatistic
cStdDev
cAbstractHistogram
(...) (...)
All classes use the double type for representing observations, and compute all metrics in the
same data type (except the observation count, which is int64_t.)
For weighted statistics, weights are also doubles. Being able to handle non-integer weights
is important because weighted statistics are often used for computing time averages, e.g.
average queue length or average channel utilization.
7.8.1 cStdDev
The cStdDev class is meant to collect summary statistics of observations. If you also need to
compute a histogram, use cHistogram (or cKSplit/cPSquare) instead, because those classes
already include the functionality of cStdDev.
cStdDev can collect unweighted or weighted statistics. This needs to be decided in the con-
structor call, and cannot be changed later. Specify true as the second argument for weighted
statistics.
cStdDev unweighted("packetDelay"); // unweighted
cStdDev weighted("queueLength", true); // weighted
Observations are added to the statistics by using the collect() or the collectweighted()
methods. The latter takes two parameters, the value and the weight.
for (double value : values)
180
OMNeT++ Simulation Manual – The Simulation Library
unweighted.collect(value);
Statistics can be obtained from the object with the following methods: getCount(), getMin(),
getMax(), getMean(), getStddev(), getVariance().
There are two getter methods that only work for unweighted statistics: getSum() and get-
SqrSum(). Plain (unweighted) sum and sum of squares are not computed for weighted obser-
vations, and it is an error to call these methods in the weighted case.
Other getter methods are primarily meant for weighted statistics: getSumWeights(), getWeight-
edSum(), getSqrSumWeights(), getWeightedSqrSum(). When called on unweighted statis-
tics, these methods simply assume a weight of 1.0 for all observations.
An example:
EV << "count = " << unweighted.getCount() << endl;
EV << "mean = " << unweighted.getMean() << end;
EV << "stddev = " << unweighted.getStddev() << end;
EV << "min = " << unweighted.getMin() << end;
EV << "max = " << unweighted.getMax() << end;
7.8.2 cHistogram
cHistogram is able to represent both uniform and non-uniform bin histograms, and supports
both weighted and unweighted observations. The histogram can be modified dynamically: it
can be extended with new bins, and adjacent bins can be merged. In addition to the bin
values (which mean count in the unweighted case, and sum of weights in the weighted case),
the histogram object also keeps the number (or sum of weights) of the lower and upper outliers
(“underflows” and “overflows”.)
Setting up and managing the bins based on the collected observations is usually delegated to
a strategy object. However, for most use cases, histogram strategies is not something the user
needs to be concerned with. The default constructor of cHistogram sets up the histogram
with a default strategy that usually produces a good quality histogram without requiring
manual configuration or a-priori knowledge about the distribution. For special use cases,
there are other histogram strategies, and it is also possible to write new ones.
Creating a Histogram
cHistogram has several constructors variants. Like with cStdDev, it needs to be decided in
the constructor call by a boolean argument whether the histogram should collect unweighted
181
OMNeT++ Simulation Manual – The Simulation Library
(false) or weighted (true) statistics; the default is unweighted. Another argument is a num-
ber of bins hint. (The actual number of bins produced might slightly differ, due to dynamic
range extensions and bin merging performed by some strategies.)
cHistogram unweighted1("packetDelay"); // unweighted
cHistogram unweighted2("packetDelay", 10); // unweighted, with ~10 bins
cHistogram weighted1("queueLength", true); // weighted
cHistogram weighted2("queueLength", 10, true); // weighted, with ~10 bins
It is also possible to provide a strategy object in a constructor call. (The strategy object may
also be set later though, using setStrategy(). It must be called before the first observation
is collected.)
cHistogram autoRangeHist("queueLength", new cAutoRangeHistogramStrategy());
This constructor can also be used to create a histogram without a strategy object, which is
useful if you want to set up the histogram bins manually.
cHistogram hist("queueLength", nullptr, true); // weighted, no strategy
cHistogram also has methods where you can provide constraints and hints for setting up
the bins: setMode(), setRange(), setRangeExtensionFactor(), setAutoExtend(), set-
NumBinsHint(), setBinSizeHint(). These methods delegate to similar methods of cAutoR-
angeHistogramStrategy.
Collecting Observations
Observations are added to the histogram in the same way as with cStdDev: using the col-
lect() and collectWeighted() methods.
Histogram bins can be accessed with three member functions: getNumBins() returns the
number of bins, getBinEdge(int k) returns the kth bin edge, getBinValue(int k) returns
the count or sum of weights in bin k, and getBinPDF(int k) returns the PDF value in the bin
(i.e. between getBinEdge(k) and getBinEdge(k+1)). The getBinInfo(k) method returns
multiple bin data (edges, value, relative frequency) packed together in a struct. Four other
methods, getUnderflowSumWeights(), getOverflowSumWeights(), getNumUnderflows(),
getNumOverflows(), provide access to the outliers.
These functions, being defined on cHistogramBase, are not only available on cHistogram
but also for cPSquare and cKSplit.
For cHistogram, bin edges and bin values can also be accessed as a vector of doubles, using
the getBinEdges() and getBinValues() methods.
An example:
EV << "[" << hist.getMin() << "," << hist.getBinEdge(0) << "): "
<< hist.getUnderflowSumWeights() << endl;
int numBins = hist.getNumBins();
for (int i = 0; i < numBins; i++) {
EV << "[" << hist.getBinEdge(i) << "," << hist.getBinEdge(i+1) << "): "
<< hist.getBinValue(i) << endl;
}
182
OMNeT++ Simulation Manual – The Simulation Library
bin 1
bin 2
bin 0
...
underflows bin N-1 overflows
0 1 2 ... N-1 N
Figure 7.6: Bin edges and bins of an N -bin histogram
EV << "[" << hist.getBinEdge(numBins) << "," << hist.getMax() << "]: "
<< hist.getOverflowSumWeights() << endl;
The getPDF(x) and getCDF(x) member functions return the value of the Probability Density
Function and the Cumulated Density Function at a given x, respectively.
Note that bins may not be immediately available during observation collection, because some
histogram strategies use precollection to gather information about the distribution before
setting up the bins. Use binsAlreadySetUp() to figure out whether bins are set up already.
Setting up the bins can be forced with the setupBins() method.
The cHistogram class has several methods for creating and manipulating bins. These meth-
ods are primarily intended to be called from strategy classes, but are also useful if you want
to manage the bins manually, i.e. without a strategy class.
For setting up the bins, you can either use createUniformBins() with the range (lo, hi)
and the step size as parameters, or specify all bin edges explicitly in a vector of doubles to
setBinEdges().
When the bins have already been set up, the histogram can be extended with new bins down
or up using the prependBins() and appendBins() methods that take a list of new bin edges
to add. There is also an extendBinsTo() method that extends the histogram with equal-
sized bins at either end to make sure that a supplied value falls into the histogram range. Of
course, extending the histogram is only possible if there are no outliers in that direction. (The
positions of the outliers is not preserved, so it is not known how many would fall in each of
the newly created bins.)
If the histogram has too many bins, adjacent ones (pairs, triplets, or groups of size n) can be
merged, using the mergeBins() method.
Example code which sets up a histogram with uniform bins:
cHistogram hist("queueLength", nullptr); // create w/o strategy object
hist.createUniformBins(0, 100, 10); // 10 bins over (0,100)
183
OMNeT++ Simulation Manual – The Simulation Library
Strategy Concept
Histogram strategies subclass from cIHistogramStrategy, and are responsible for setting
up and managing the bins.
A cHistogram is created with a cDefaultHistogramStrategy by default, which works well in
most cases. Other cHistogram constructors allow passing in an arbitrary histogram strategy.
The collect() and collectWeighted() methods of a cHistogram delegate to similar meth-
ods of the strategy object, which in turn decides when and how to set up the bins, and how to
manage the bins later. (Setting up the bins may be postponed until a few observations have
been collected, in order to gather more information for it.) The histogram strategy uses public
histogram methods like createUniformBins() to create and manage the bins.