This lesson introduces the TOSSIM simulator. You will become familiar with how to compile TOSSIM and use some of its functionality. You will learn how to:
HilTimerMilliC
, while there is also an
implementation for atmega128 platforms that replaces the HPL
components of the hardware clocks. The former is general and
can be used for any platform, but lacks the fidelity of
capturing an actual chips behavior, as the latter
does. Similarly, TOSSIM can replace a packet-level
communication component for packet-level simulation, or
replace a low-level radio chip component for a more precise
simulation of the code execution.
TOSSIM is a discrete event simulator. When it runs, it pulls
events of the event queue (sorted by time) and executes them.
Depending on the level of simulation, simulation events can
represent hardware interrupts or high-level system events
(such as packet reception). Additionally, tasks are simulation
events, so that posting a task causes it to run a short time
(e.g., a few microseconds) in the future.
TOSSIM is a library: you must write a program that configures
a simulation and runs it. TOSSIM supports two programming
interfaces, Python and C++. Python allows you to interact with
a running simulation dynamically, like a powerful
debugger. However, as the interpretation can be a performance
bottleneck when obtaining results, TOSSIM also has a C++
interface. Usually, transforming code from one to the other is
very simple.
TOSSIM currently does not support gathering power
measurements.
TOSSIM is a TinyOS library. Its core code lives in tos/lib/tossim
. Every TinyOS
source directory has an optional sim
subdirectory,
which contains simulation implementations of that package. For
example, tos/chips/atm128/timer/sim
contains TOSSIM implementations of some of the Atmega128 timer
abstractions.
To compile TOSSIM, you pass the sim
option to make:
$ cd apps/Blink $ make micaz sim
Currently, the only platform TOSSIM supports is the micaz. You should see output similar to this:
mkdir -p build/micaz placing object files in build/micaz writing XML schema to app.xml compiling BlinkAppC to object file sim.o ncc -c -fPIC -o build/micaz/sim.o -g -O0 -tossim -fnesc-nido-tosnodes=1000 -fnesc-simulate -fnesc-nido-motenumber=sim_node\(\) -finline-limit=100000 -Wall -Wshadow -DDEF_TOS_AM_GROUP=0x7d -Wnesc-all -target=micaz -fnesc-cfile=build/micaz/app.c -board=micasb -Wno-nesc-data-race BlinkAppC.nc -fnesc-dump=components -fnesc-dump=variables -fnesc-dump=constants -fnesc-dump=typedefs -fnesc-dump=interfacedefs -fnesc-dump=tags -fnesc-dumpfile=app.xml compiling Python support into pytossim.o and tossim.o g++ -c -shared -fPIC -o build/micaz/pytossim.o -g -O0 /home/pal/src/tinyos-2.x/tos/lib/tossim/tossim_wrap.cxx -I/usr/include/python2.3 -I/home/pal/src/tinyos-2.x/tos/lib/tossim -DHAVE_CONFIG_H g++ -c -shared -fPIC -o build/micaz/tossim.o -g -O0 /home/pal/src/tinyos-2.x/tos/lib/tossim/tossim.c -I/usr/include/python2.3 -I/home/pal/src/tinyos-2.x/tos/lib/tossim linking into shared object ./_TOSSIMmodule.so g++ -shared build/micaz/pytossim.o build/micaz/sim.o build/micaz/tossim.o -lstdc++ -o _TOSSIMmodule.so copying Python script interface TOSSIM.py from lib/tossim to local directory
Compiling TOSSIM has five basic steps. Let's go through them one by one.
writing XML schema to app.xml
The first thing the TOSSIM build process does is use nesc-dump to produce an XML document that describes the application. Among other things, this document descibes the name and type of every variable.
Besides introducing all of these new compilation steps, the
sim
option changes the include paths of the
application. If the application has a series of includes
-Ia -Ib -Ic
Then the sim option transforms the list to
-Ia/sim -Ib/sim -Ic/sim -I%T/lib/tossim -Ia -Ib -Ic
This means that any system-specific simulation
implementations will be used first, followed by generic TOSSIM
implementations, followed by standard implementations. The
sim
option also passes a bunch of arguments to the
compiler, so it knows to compile for simulation.
The product of this step is an object file, sim.o
,
which lives in the platform's build directory. This object
file has a set of C functions which configure the simulation
and control execution.
compiling Python support into pytossim.o and tossim.o g++ -c -shared -fPIC -o build/micaz/pytossim.o -g -O0 \ /home/pal/src/tinyos-2.x/tos/lib/tossim/tossim_wrap.cxx \ -I/usr/include/python2.3 -I/home/pal/src/tinyos-2.x/tos/lib/tossim \ -DHAVE_CONFIG_H g++ -c -shared -fPIC -o build/micaz/tossim.o -g -O0 \ /home/pal/src/tinyos-2.x/tos/lib/tossim/tossim.c \ -I/usr/include/python2.3 -I/home/pal/src/tinyos-2.x/tos/lib/tossim
The next step compiles the support for the C++ and Python
programming interfaces. The Python interface is actually built
on top of the C++ interface. Calling a Python object calls a
C++ object, which then calls TOSSIM through the C
interface. tossim.o
contains the C++ code, while
pytossim.o
contains the Python support. These files
have to be compiled separately because C++ doesn't understand
nesC, and nesC doesn't understand C++.
linking into shared object ./_TOSSIMmodule.so g++ -shared build/micaz/pytossim.o build/micaz/sim.o build/micaz/tossim.o -lstdc++ -o _TOSSIMmodule.so
The next to last step is to build a shared library that contains the TOSSIM code, the C++ support, and the Python support.
copying Python script interface TOSSIM.py from lib/tossim to local directory
Finally, there is the Python code that calls into the
shared object. This code exists in lib/tossim
, and
the make process copies it into the local directory.
Go into the RadioCountToLeds
application and build
TOSSIM:
$ cd tinyos-2.x/apps/RadioCountToLeds $ make micaz sim
We'll start with running a simulation in Python. You can either write a script and just tell Python to run it, or you can use Python interactively. We'll start with the latter. Fire up your Python interpreter:
$ python
You should see a prompt like this:
Python 2.3.4 (#1, Nov 4 2004, 14:13:38) [GCC 3.4.2 20041017 (Red Hat 3.4.2-6.fc3)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>>
The first thing we need to do is import TOSSIM and create a TOSSIM object. Type
>>> from TOSSIM import * >>> t = Tossim([])
The square brackets are an optional argument that lets you
access variables in the simulation. We'll get to how to use
that later. In this case, we're telling TOSSIM that there are
no variables that we want to look at. The way you run a TOSSIM
simulation is with the runNextEvent
function. For
example:
>>> t.runNextEvent() 0
When you tell TOSSIM to run the next event, it returns 0. This means that there was no next event to run. The reason is simple: we haven't told any nodes to boot. This snippet of code will tell mote 32 to boot at time 45654 and run its first event (booting):
>>> m = t.getNode(32); >>> m.bootAtTime(45654); >>> t.runNextEVent() 1
Instead of using raw simulation ticks, you can also use the
call ticksPerSecond()
. However, you want to be careful
to add some random bits into this number: having every node
perfectly synchronized and only different in phase in terms of
seconds can lead to strange results.
>>> m = t.getNode(32); >>> m.bootAtTime(4 * t.ticksPerSecond() + 242119); >>> t.runNextEVent() 1
Now, runNextEvent
returns 1, because there was an
event to run. But we have no way of knowing whether the node
has booted or not. We can find this out in one of two ways.
The first is that we can just ask it:
>>> m.isOn() 1 >>> m.turnOff() >>> m.isOn() 0 >>> m.bootAtTime(560000) >>> t.runNextEvent() 0 >>> t.runNextEvent() 1
Note that the first runNextEvent
returned 0. This
is because when we turned the mote off, there was still an
event in the queue, for its next timer tick. However, since
the mote was off when the event was handled in that call,
runNextEvent
returned 0. The second call to
runNextEvent
returned 1 for the second boot event, at
time 560000.
A Tossim object has several useful functions. In Python,
you can generally see the signature of an object with the
dir
function. E.g.:
>>> t = Tossim([]) >>> dir(t) ['__class__', '__del__', '__delattr__', '__dict__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__swig_getmethods__', '__swig_setmethods__', '__weakref__', 'addChannel', 'currentNode', 'getNode', 'init', 'mac', 'newPacket', 'radio', 'removeChannel', 'runNextEvent', 'setCurrentNode', 'setTime', 'this', 'thisown', 'time', 'timeStr']
The most common utility functions are:
currentNode()
: returns the ID of the current node.getNode(id)
: returns an object representing a specific moterunNextEvent()
: run a simulation eventtime()
: return the current time in simulation ticks as a large integer timeStr()
: return a string representation of the current timeinit()
: initialize TOSSIMmac()
: return the object representing the media access layerradio()
: return the object representing the radio modeladdChannel(ch, output)
: add output as an output to channel ch removeChannel(ch, output)
: remove output as an output to channel ch ticksPerSecond()
: return how many simulation ticks there are in a simulated second The next section discusses the last two.
The second approach to know whether a node is on is to tell
it to print something out when it boots. TOSSIM has a
debugging output system, called dbg
. There are four
dbg
calls:
dbg
: print a debugging statement preceded by the node ID.dbg_clear
: print a debugging statement which is not preceded by the node ID. This allows you to easily print out complex data types, such as packets, without interspersing node IDs through the output.dbgerror
: print an error statement preceded by the node IDdbgerror_clear
: print an error statement which is not preceded by the node IDGo into RadioCountToLedsC
and modify the Boot.booted
event
to print out a debug message when it boots, such as this:
event void Boot.booted() { call Leds.led0On(); dbg("Boot", "Application booted.\n"); call AMControl.start(); }
Calls to the debugging calls take two or more
parameters. The first parameter ("Boot" in the above example)
defines the output channel. An output channel is simply
a string. The second and subsequent parameters are the message
to output. They are identical to a printf statement. For example
RadioCountToLedsC
has this call:
event message_t* Receive.receive(message_t* bufPtr, void* payload, uint8_t len) { dbg("RadioCountToLedsC", "Received packet of length %hhu.\n", len); ... }which prints out the length of received packet as an 8-bit unsigned value (%hhu).
Once you have added the debugging statement to the event,
recompile the application with make micaz sim
and
start up your Python interpreter. Load the TOSSIM module and
schedule a mote to boot as before:
>>> from TOSSIM import * >>> t = Tossim([]) >>> m = t.getNode(32); >>> m.bootAtTime(45654);
This time, however, we want to see the debugging message that the mote has booted. TOSSIM's debugging output can be configured on a per-channel basis. So, for example, you can tell TOSSIM to send the "Boot" channel to standard output, but another channel, say "AM", to a file. Additionally, you can configureBy default, a channel has no destination, and so messages to it are discarded.
In this case, we want to send the Boot channel to standard
output. To do this, we need to import the sys
Python
package, which lets us refer to standard out. We can then tell
TOSSIM to send Boot messages to this destination:
>>> import sys >>> t.addChannel("Boot", sys.stdout); 1The return value shows that the channel was added successfully. Run the first simulation event, and the mote boots:
>>> t.runNextEvent() DEBUG (32): Application booted. 1
The only difference between debug and error functions is
the string output at the beginning of a message. Debug
statements print DEBUG (n)
, while error statements
print ERROR (n)
.
A debugging statement can have multiple output channels. Each channel name is delimited by commas:
event void Boot.booted() { call Leds.led0On(); dbg("Boot,RadioCountToLedsC", "Application booted.\n"); call AMControl.start(); }If a statement has multiple channels and those channels share outputs, then TOSSIM only prints the message once. For example, if both the Boot channel and RadioCountToLedsC channel were connected to standard out, TOSSIM will only print one message. For example, this series of debug statements
event void Boot.booted() { call Leds.led0On(); dbg("Boot,RadioCountToLedsC", "Application booted.\n"); dbg("RadioCountToLedsC", "Application booted again.\n"); dbg("Boot", "Application booted a third time.\n"); call AMControl.start(); }when configured so
>>> import sys >>> t.addChannel("Boot", sys.stdout) >>> t.addChannel("RadioCountToLedsC", sys.stdout)will print out this:
DEBUG (32): Application booted. DEBUG (32): Application booted again. DEBUG (32): Application booted a third time.
A channel can have multiple outputs. For example, this
script will tell TOSSIM to write RadioCountToLedsC
messages to
standard output, but to write Boot
messages to both standard
output and a file named log.txt
:
>>> import sys >>> f = open("log.txt", "w") >>> t.addChannel("Boot", f) >>> t.addChannel("Boot", sys.stdout) >>> t.addChannel("RadioCountToLedsC", sys.stdout)
When you start TOSSIM, no node can communicate with any other. In order to be able to simulate network behavior, you have to specify a network topology. Internally, TOSSIM is structured so that you can easily change the underlying radio simulation, but that's beyond the scope of this tutorial. The default TOSSIM radio model is signal-strength based. You provide a graph to the simulator that describes the propagation strengths. You also specify noise floor, and receiver sensitivity. There are some very early results that describe current sensor platforms (e.g., the mica2) in these terms. Because all of this is through a scripting interface, rather than provide a specific radio model, TOSSIM tries to provide a few low-level primitives that can express a wide range of radios and behavior.
You control the radio simulation through a Python Radio object:
>>> from TOSSIM import * >>> t = Tossim([]) >>> r = t.radio() >>> dir(r) ['__class__', '__del__', '__delattr__', '__dict__', '__doc__', '__getattr__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__swig_getmethods__', '__swig_setmethods__', '__weakref__', 'add', 'connected', 'gain', 'remove', 'setNoise', 'this', 'thisown',]
The first set of methods (with the double underscores) are ones that you usually don't call. The important ones are at the end. They are:
add(src, dest, gain)
: Add a link from src
to dest with gain. When src transmits, dest
will receive a packet attenuated by the gain value.connected(src, dest)
: Return whether there is a
link from src to dest.gain(src, dest)
: Return the gain value of the
link from src to dest.remove(src, dest)
: Remove the link from
src to dest.setNoise(node, mean, variance)
: Set the noise
floor at node to be a gaussian distribution with
mean and variance.sensitivity()
: Return the receive sensitivity of
the nodes.setSensitivity(val)
: Set the receive sensitivity
of nodes to be val. The sensitivity is how much
stronger a signal must be for it to be received
uncorrupted. E.g., a sensitivity of 3.0 (the default value)
means that a packet must be 3dBm greater than the sum of
noise and concurrent transmissions for it to be received
uncorrupted.threshold()
: Return the CCA threshold.setThreshold(val)
: Set the CCA threshold value in
dBm.The default is -95.The Radio object only deals with physical layer
propagation. The MAC object deals with the data link layer,
packet lengths, and radio bandwidth. The default TOSSIM MAC
object is for a CSMA protocol. You get a reference to the MAC
object by calling mac()
on a Tossim object:
>>> mac = t.mac()The default MAC object has a large number of functions, for controlling backoff behavior, packet preamble length, radio bandwidth, etc. All time values are specified in terms of radio symbols, and you can configure the number of symbols per second and bits per symbol. By default, the MAC object is configured to act like the standard TinyOS 2.0 CC2420 stack: it has 4 bits per symbol and 64k symbols per second, for 256kbps. This is a subset of the MAC functions that could be useful for changing backoff behavior. Every accessor function has a corresponding set function that takes an integer as a parameter. E.g., there's
int
initHigh()
and void setInitHigh(int val)
. The
default value for each parameter is shown italicized in
parentheses.
Any and all of these configuration constants can be changed
at compile time with #define directives. Look at
tos/lib/tossim/sim_csma.h
.
Because the radio connectivity graph can be scripted, you can easily store topologies in files and then load the file. Alternatively, you can store a topology as a script. For example, this script will load a file which specifies each link in the graph as a line with three values, the source, the destination, and the gain, e.g.:
1 2 -54.0means that when 1 transmits 2 hears it at -54 dBm. Create a file
topo.txt
that looks like this:
1 2 -54.0 2 1 -55.0 1 3 -60.0 3 1 -60.0 2 3 -64.0 3 2 -64.0
This script will read such a file:
>>> f = open("topo.txt", "r") >>> lines = f.readlines() >>> for line in lines: ... s = line.split() ... if (len(s) > 0): ... print " ", s[0], " ", s[1], " ", s[2]; ... r.add(int(s[0]), int(s[1]), float(s[2]))
Now, when a node transmits a packet, other nodes will hear it.
This is a complete script for simulating packet transmission with
RadioCountToLedsC. Save it as a file test.py
:
from TOSSIM import * import sys t = Tossim([]) r = t.radio() f = open("topo.txt", "r") lines = f.readlines() for line in lines: s = line.split() if (len(s) > 0): print " ", s[0], " ", s[1], " ", s[2]; r.add(int(s[0]), int(s[1]), float(s[2])) t.addChannel("RadioCountToLedsC", sys.stdout) t.addChannel("Boot", sys.stdout) t.getNode(1).bootAtTime(100001); t.getNode(2).bootAtTime(800008); t.getNode(3).bootAtTime(1800009); r.setNoise(1, -100.0, 5.0) r.setNoise(2, -100.0, 5.0) r.setNoise(3, -100.0, 5.0) for i in range(0, 100): t.runNextEvent()
Run it by typing python test.py
. You should see
output that looks like this:
1 2 -54.0 2 1 -55.0 1 3 -60.0 3 1 -60.0 2 3 -64.0 3 2 -64.0 DEBUG (1): Application booted. DEBUG (1): Application booted again. DEBUG (1): Application booted a third time. DEBUG (2): Application booted. DEBUG (2): Application booted again. DEBUG (2): Application booted a third time. DEBUG (3): Application booted. DEBUG (3): Application booted again. DEBUG (3): Application booted a third time. DEBUG (1): RadioCountToLedsC: timer fired, counter is 1. DEBUG (1): RadioCountToLedsC: packet sent. DEBUG (2): RadioCountToLedsC: timer fired, counter is 1. DEBUG (2): RadioCountToLedsC: packet sent. DEBUG (3): RadioCountToLedsC: timer fired, counter is 1. DEBUG (3): RadioCountToLedsC: packet sent. DEBUG (1): Received packet of length 2. DEBUG (3): Received packet of length 2. DEBUG (2): Received packet of length 2. DEBUG (1): RadioCountToLedsC: timer fired, counter is 2. DEBUG (1): RadioCountToLedsC: packet sent. DEBUG (2): RadioCountToLedsC: timer fired, counter is 2. DEBUG (2): RadioCountToLedsC: packet sent. DEBUG (3): RadioCountToLedsC: timer fired, counter is 2. DEBUG (3): RadioCountToLedsC: packet sent. DEBUG (1): Received packet of length 2.
If you set the noise to be 30 plus or minus 5 dBm instead of 80 plus or minus 5 dBm, then nodes will never transmit, as the default CCA threshold is -95 dBm. You'll see something like this:
1 2 -54.0 2 1 -55.0 1 3 -60.0 3 1 -60.0 2 3 -64.0 3 2 -64.0 DEBUG (1): Application booted. DEBUG (1): Application booted again. DEBUG (1): Application booted a third time. DEBUG (2): Application booted. DEBUG (2): Application booted again. DEBUG (2): Application booted a third time. DEBUG (3): Application booted. DEBUG (3): Application booted again. DEBUG (3): Application booted a third time. DEBUG (1): RadioCountToLedsC: timer fired, counter is 1. DEBUG (1): RadioCountToLedsC: packet sent. DEBUG (2): RadioCountToLedsC: timer fired, counter is 1. DEBUG (2): RadioCountToLedsC: packet sent. DEBUG (3): RadioCountToLedsC: timer fired, counter is 1. DEBUG (3): RadioCountToLedsC: packet sent. DEBUG (1): RadioCountToLedsC: timer fired, counter is 2. DEBUG (2): RadioCountToLedsC: timer fired, counter is 2. DEBUG (3): RadioCountToLedsC: timer fired, counter is 2.
Because the nodes backoff perpetually, they never transmit the packet and so subsequent attempts to send fail. Although it only takes a few simulation events to reach the first timer firings, it takes many simulation events (approximately 4000) to reach the second timer firings. This is because the nodes have MAC backoff events. If you want to simulate in terms of time, rather than events, you can always do something like this, which simulates 5 seconds from the first node boot:
t.runNextEvent(); time = t.time() while (time + 50000000000 > t.time()): t.runNextEvent()
TOSSIM allows you to specify a network topology in terms of gain. However, this raises the problem of coming up with a topology. There are two approaches you can take. The first is to take data from a real world network and input this into TOSSIM. The second is to generate it from applying a theoretical propagation model to a physical layout. The standard file format is
noise n avg std gain src dest gwhere each statement is on a separate line. The noise statement defines the noise observed at node n with an average of avg and a standard deviation of std. The gain statement defines a propagation gain g when src transmits to dest. This is a snippet of python code that will parse this file format:
f = open("mirage-1.txt", "r") lines = f.readlines() for line in lines: s = line.split() if (len(s) > 0): if (s[0] == "gain"): r.add(int(s[1]), int(s[2]), float(s[3])) elif (s[0] == "noise"): r.setNoise(int(s[1]), float(s[2]), float(s[3]))
TOSSIM has a tool for the second option of generating a
network topology using a theoretical propagation model. The
tool is written in Java and is
net.tinyos.sim.PropagationModel
. The tool takes a
single command line parameter, the name of a configuration
file, e.g.:
java net.tinyos.sim.PropagationModel config.txt
The format of a configuration file is beyond the scope of
this document: the tool has its own documentation. TOSSIM has two sample configuration
files generated from the tool in
tos/lib/tossim/topologies
. The first is grid.txt
, which is a 10x10 grid of nodes
spaced roughly 40 feet apart. Each node is placed randomly
within a 40'x40' "cell." The cells follow a strict grid. The
second file is scatter.txt
, which is
100 nodes scattered randomly (with a uniform distribution)
over a 360'x360' area. Note that the tool uses random numbers,
these configuration files can generate multiple different
network topologies. Network topology files generated from the
tool follow the same format as mirage-1.txt
.
TOSSIM allows you to inspect variables in a running TinyOS program. Currently, you can only inspect basic types. For example, you can't look at fields of structs, but you can look at state variables.
When you compile TOSSIM, the make system generates a large XML file that contains a lot of information about the TinyOS program, including every component variable and its type. If you want to examine the state of your program, then you need to give TOSSIM this information so it can parse all of the variables properly. You do this by instantiating a Python object that parses the XML file to extract all of the relevant information. You have to import the Python support package for TOSSIM to do this:
from tinyos.tossim.TossimApp import * n = NescApp()
Instantiating a NescApp
can take quite a while:
Python has to parse through megabytes of XML. So be patient
(you only have to do it once). NescApp has two optional
arguments. The first is the name of the application being
loaded. The second is the XML file to load. The default for
the latter is app.xml
, which is the name of the file
that the make system generates. The default for the former is
"Unknown App." So this code behaves identically to that
above:
from tinyos.tossim.TossimApp import * n = NescApp("Unknown App", "app.xml")
You fetch a list of variables from a NescApp object by
calling the function variables
on the field
variables
:
vars = n.variables.variables()
To enable variable inspection, you pass this list to a Tossim object when you instantiate it:
t = Tossim(vars)
The TOSSIM object now knows the names, sizes, and types of
all of the variables in the TinyOS application. This
information allows the TOSSIM support code to take C variables
and properly tranform them into Python variables. This
currently only works for simple types: if a component declares
a structure, you can't access its fields. But let's say we
want to read the counter in RadioCountToLedsC
. Since each mote
in the network has its own instance of the variable, we need
to fetch it from a specific mote:
m = t.getNode(0) v = m.getVariable("RadioCountToLedsC.counter")
The name of a variable is usually C.V, where
C is the component name and V is the variable.
In the case of generic components, the name is C.N.V,
where N is an integer that describes which instance.
Unfortunately, there is currently no easy way to know what
N is from nesC source, so you have to root through
app.c
in order to know.
Once you have a variable object (v
in the above
code), you can fetch its value with the getData()
function:
counter = v.getData()
Because getData()
transforms the underlying C type
into a Python type, you can then use its return value in
Python expressions. For example, this script will start a
simulation of five nodes and run it until node 0's counter
reaches 10:
from sys import * from random import * from TOSSIM import * from tinyos.tossim.TossimApp import * n = NescApp() t = Tossim(n.variables.variables()) r = t.radio() f = open("topo.txt", "r") lines = f.readlines() for line in lines: s = line.split() if (len(s) > 0): if (s[0] == "gain"): r.add(int(s[1]), int(s[2]), float(s[3])) elif (s[0] == "noise"): r.setNoise(int(s[1]), float(s[2]), float(s[3])) for i in range (0, 9): m = t.getNode(i) m.bootAtTime(randint(1000, 2000) * 1000000) m = t.getNode(0) v = m.getVariable("RadioCountToLedsC.counter") while (v.getData() < 10): t.runNextEvent()
The TOSSIM examples
subdirectory also has an example script, named
variables.py
.
TOSSIM allows you to dynamically inject packets into a network. Packets can be scheduled to arrive at any time. If a packet is scheduled to arrive in the past, then it arrives immediately. Injected packets circumvent the radio stack: it is possible for a node to receive an injected packet while it is in the midst of receiving a packet from another node over its radio.
TinyOS 2.0 has support for building Python packet objects. Just like the standard Java toolchain, you can build a packet class based on a C structure. The packet class gives you a full set of packet field mutators and accessors. If an application has a packet format, you can generate a packet class for it, instantiate packet objects, set their fields, and have nodes receive them.
The RadioCountToLeds
application Makefile has an
example of how to do this. First, it adds the Python class as a
dependency for building the application. Whenever you compile
the app, if the Python class doesn't exist, make will build it
for you:
BUILD_EXTRA_DEPS = RadioCountMsg.py RadioCountMsg.class
The Makefile also tells make how to generate RadioCountMsg.py:
RadioCountMsg.py: RadioCountToLeds.h mig python -target=$(PLATFORM) $(CFLAGS) -python-classname=RadioCountMsg RadioCountToLeds.h RadioCountMsg -o $@
The rule says to generate RadioCountMsg.py by calling mig with the python parameter. The Makefile also has rules on how to build Java class, but that's not important for TOSSIM. Since we've been using RadioCountToLeds so far, the Python class should be there already.
RadioCountMsg.py defines a packet format, but this packet is
contained in the data payload of another format. If a node is
sending a RadioCountMsg
over AM, then the RadioCountMsg
structure is put into the AM payload, and might look something
like this:
AM Header | RadioCountMsg | AM Footer |
If it is sending it over a routing protocol. the packet is put in the routing payload, and might look something like this:
AM Header | Routing Header | RadioCountMsg | AM Footer |
If you want to send a RadioCountMsg
to a node, then you need
to decide how to deliver it. In the simple AM case, you place
the RadioCountMsg
structure in a basic AM packet. In the routing
case, you put it in a routing packet, which you then put inside
an AM packet. We'll only deal with the simple AM case here.
To get an AM packet which you can inject into TOSSIM, you
call the newPacket
function on a Tossim object. The
returned object has the standard expected AM fields:
destination, length, type, and data,
as well as strength.
To include support for a packet format, you must import
it. For example, to include RadioCountMsg
, you have to import
it:
from RadioCountMsg import *
This snippet of code, for example, creates a RadioCountMsg
,
sets its counter to 7, creates an AM packet, stores the
RadioCountMsg
in the AM packet, and configures the AM packet so
it will be received properly (destination and type):
from RadioCountMsg import * msg = RadioCountMsg() msg.set_counter(7); pkt = t.newPacket(); pkt.setData(msg.data) pkt.setType(msg.get_amType()) pkt.setDestination(0)
The variable pkt
is now an Active Message of the AM
type of RadioCountMsg
with a destination of 0 that contains a
RadioCountMsg
with a counter of 7. You can deliver this packet
to a node with the deliver
function. The
deliver
function takes two parameters, the destination
node and the time to deliver:
pkt.deliver(0, t.time() + 3)
This call delivers pkt to node 0 at the current simulation
time plus 3 ticks (e.g., 3ns). There is also a
deliverNow
, which has no time parameter. Note that if
the destination of pkt
had been set to 1, then the
TinyOS application would not receive the packet, as it was
delivered to node 0.
Taken all together, the following script starts a simulation,
configures the topology based on topo.txt, and delivers a packet
to node 0. It can also be found as packets.py
in the TOSSIM examples
subdirectory.
import sys from TOSSIM import * from RadioCountMsg import * t = Tossim([]) m = t.mac(); r = t.radio(); t.addChannel("RadioCountToLedsC", sys.stdout); t.addChannel("LedsC", sys.stdout); for i in range(0, 2): m = t.getNode(i); m.bootAtTime((31 + t.ticksPerSecond() / 10) * i + 1); f = open("topo.txt", "r") lines = f.readlines() for line in lines: s = line.split() if (len(s) > 0): if (s[0] == "gain"): r.add(int(s[1]), int(s[2]), float(s[3])) elif (s[0] == "noise"): r.setNoise(int(s[1]), float(s[2]), float(s[3])) for i in range(0, 60): t.runNextEvent(); msg = RadioCountMsg() msg.set_counter(7); pkt = t.newPacket(); pkt.setData(msg.data) pkt.setType(msg.get_amType()) pkt.setDestination(0) print "Delivering " + msg.__str__() + " to 0 at " + str(t.time() + 3); pkt.deliver(0, t.time() + 3) for i in range(0, 20): t.runNextEvent();
Python is very useful because it is succinct, easy to write, and can be used interactively. Interpretation, however, has a significant cost: a Python/C transition on every event is a significant cost (around 100%, so it runs at half the speed). Additionally, it's often useful to step through code with a standard debugger. TOSSIM also has support for C++, so that it can be useful in these circumstances. Because many of the Python interfaces are merely wrappers around C++ objects, much of the scripting stays the same. The two major exceptions are inspecting variables and injecting packets.
In a C++ TOSSIM, there is no variable inspection. While it is possible to request memory regions and cast them to the expected structures, currently there is no good and simple way to do so. The Python support goes through several steps in order to convert variables into Python types, and this gets in the way of C++. However, as the purpose of C++ is usually to run high performance simulations (in which inspecting variables is a big cost) or debugging (when you have a debugger), this generally isn't a big problem.
There is a C++ Packet
class, which the Python
version is a simple wrapper around. In order to inject packets
in C++, however, you must build C support for a packet type and
manually build the packet. There currently is no support in mig
with which to generate C/C++ packet structures, and since most
packets are nx_struct types, they cannot be parsed by
C/C++. Furthermore, as many of the fields are nx types, they are
big endian, while x86 processors are little endian. Still, if you
want to deliver a packet through C++, you can do so.
Usually, the C++ and Python versions of a program look pretty similar. For example:
Python | C++ |
import TOSSIM import sys from RadioCountMsg import * t = TOSSIM.Tossim([]) r = t.radio(); for i in range(0, 999): m = t.getNode(i); m.bootAtTime(5000003 * i + 1); r.setNoise(i, -99.0, 3.0); for j in range (0, 2): if (j != i): r.add(i, j, -50.0); for i in range(0, 1000000): t.runNextEvent(); |
#include <tossim.h> int main() { Tossim* t = new Tossim(NULL); Radio* r = t->radio(); for (int i = 0; i < 999; i++) { Mote* m = t->getNode(i); m->bootAtTime(5000003 * i + 1); r->setNoise(i, -99.0, 3); for (int j = 0; j < 2; j++) { if (i != j) { r->add(i, j, -50.0); } } } for (int i = 0; i < 1000000; i++) { t->runNextEvent(); } } |
To compile a C++ TOSSIM, you have to compile the top-level
driver program (e.g, the one shown above) and link it against
TOSSIM. Usually the easiest way to do this is to link it against
the TOSSIM objects rather than the shared library. Often, it's
useful to have a separate Makefile to do this with. E.g.,
Makefile.Driver
:
all: make micaz sim g++ -g -c -o Driver.o Driver.c -I../../tos/lib/tossim/ g++ -o Driver Driver.o build/micaz/tossim.o build/micaz/sim.o
Since Driver is a C++ program, you can use gdb on it to step through your TinyOS code, inspect variables, set breakpoints, and do everything else you can normally do. Unfortunately, as gdb is designed for C and not nesC, the component model of nesC means that a single command can have multiple providers; referring to a specific command requires specifying the component, interface, and command. For example, to break on entry to the redOff command of the Leds interface of LedsC, one must type:
$ gdb Driver GNU gdb Red Hat Linux (6.0post-0.20040223.19rh) Copyright 2004 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db library "/lib/tls/libthread_db.so.1". (gdb) break *LedsP$Leds$led0Toggle Breakpoint 1 at 0x804f184: file LedsP.nc, line 73.
nesC translates component names to C names using $. $ is a legal but almost-never-used character in some versions of C, so nesC prohibits it and uses it internally. The leading * is necessary so dbg can parse the $s. With the above breakpoint set, gdb will break whenever a mote toggles led0.
Variables have similar names. For example, to inspect the packet of RadioCountToLedsC in the RadioCountToLeds application,
(gdb) print RadioCountToLedsC$packet $1 = {{header = {{data = ""}, {data = ""}, {data = ""}, {data = ""}, { data = ""}}, data = {{data = ""}}, footer = {{ data = ""}, {data = ""}}, metadata = {{data = ""}, {data = ""}, { data = ""}, {data = ""}, {data = ""}}} }
For those who know gdb very well, you'll recognize this as a print of an array, rather than a single variable: there are more than 1000 instances of the message_t struct. This is because TOSSIM simulates many motes; rather than there being a single RadioCountToLedsC$packet, there is one for every node. To print the packet of a specific node, you have to index into the array. This, for example, will print the variable for node 6:
(gdb) print RadioCountToLedsC$packet[6] $2 = {header = {{data = ""}, {data = ""}, {data = ""}, {data = ""}, { data = ""}}, data = {{data = ""}}, footer = {{ data = ""}, {data = ""}}, metadata = {{data = ""}, {data = ""}, { data = ""}, {data = ""}, {data = ""}}}
If you want to print out the variable for the node TOSSIM is currently simulating, you can do this:
(gdb) print RadioCountToLedsC$counter[sim_node()] $4 = 0
You can also set watchpoints (although, as to be expected, they are slow:
(gdb) watch UscGainInterferenceModelC$receiving[23] Hardware watchpoint 2: UscGainInterferenceModelC$receiving[23]
This variable happens to be an internal variable in the packet-level network simulation, which keeps track of whether the radio thinks it is receiving a packet. So setting the above watchpoint will cause gdb to break whenever node 23 starts receiving a packet or returns to searching for packet preambles.
Generic components add another wrinkle. Since they use
a code-copying approach, each instance of a generic has its
own separate functions and variables (this is mostly due to the
fact that you can pass types to them). Take, for example,
AMQueueImplP
, which is used in both the radio
AM stack and the serial AM stack. If you use gdb on an
application that uses both serial and radio communication and
try to break on its Send.send, you'll see an error:
(gdb) break *AMQueueImplP$Send$send No symbol "AMQueueImplP$Send$send" in current context.
nesC gives each generic a unique number. So if you have an application in which there is a single copy of AMQueueImplP, its name will actually be AMQueueImplP$0. For example, in RadioCountToLeds, this will work:
(gdb) break *AMQueueImplP$0$Send$send Breakpoint 5 at 0x8051b29: file AMQueueImplP.nc, line 79.
If you have multiple instances of a generic in a
program, there is unfortunately no easy way to figure out each one's
name besides looking at the source code or stepping into them.
E.g., if you application uses serial and radio communication,
knowing which stack has AMQueueImpl$0 and which has AMQueueImplP$1
requires either stepping through their send operation or looking
at their app.c
files.