The goal of this IPython notebook is to present how different python librairies can help orchestrate and run large scale and complex simulations campaigns. We will also see how by using the IoTlab we can also reuse the exact same code we developped for Contiki operating system and push it to real nodes.
We organize our code around the idea of steps that are piece of code executed independently from each others. Because of this modularity, we can put each of those steps on an IPython cell.
You can launch code from this notebook that is provided inside the git repository. You can use the fabric command line tool from this same repository.
This demo requieres some python libraries to work. All of them use the python language but you don't know to know any python to make this demo work :-)
If you are on a debian like system you might just get all your dependencies with this simple command:
sudo apt-get -qq install python-matplotlib fabric python-pandas python-networkx python-numpy python-jinja2 python-scipy
I suppose that you got some tools that are rather very classic for Contiki development:
sudo apt-get install tshark openjdk-7-jre tshark gcc-msp430
The code of makesense is tested to avoid bugs. We provision a test machine (Ubuntu Machine) and then run a typical simulation inside it. To make your ubuntu machine just like the provisionned one go check the travis configuration
In [2]:
import os
from os.path import join as pj
from jinja2 import Environment, FileSystemLoader
ROOT_DIR = os.getcwd()
CONTIKI_FOLDER = os.path.abspath(pj(ROOT_DIR, "contiki"))
EXPERIMENT_FOLDER = pj(ROOT_DIR, "experiments")
TEMPLATE_FOLDER = pj(ROOT_DIR, "templates")
TEMPLATE_ENV = Environment(loader=FileSystemLoader(TEMPLATE_FOLDER))
COOJA_DIR = pj(CONTIKI_FOLDER, "tools", "cooja")
In [3]:
name = "dummy"
platform = "wismote"
experiment_path = pj(EXPERIMENT_FOLDER, name)
# We make sure that the experiment path exists
if not os.path.exists(experiment_path):
os.makedirs(experiment_path)
transmitting_range = 42
interference_range = 42
success_ratio_tx = 1.0
success_ratio_rx = 1.0
random_seed = 12345
timeout = 100000
In [4]:
from shutil import copyfile
# We can simply copy
file_to_copy = ["udp-client.c", "udp-server.c"]
for f in file_to_copy:
copyfile(pj(CONTIKI_FOLDER, "examples", "ipv6", "rpl-udp", f),
pj(experiment_path, f))
# Or we can use templating even for the firmware
makefile_template = TEMPLATE_ENV.get_template("dummy_makefile")
with open(pj(experiment_path, "Makefile"), "w") as f:
f.write(makefile_template.render(contiki=CONTIKI_FOLDER,
target="wismote"))
In [5]:
from fabric.api import lcd, local
# Making the COOJA simulator ready
with lcd(COOJA_DIR):
print(COOJA_DIR)
local("ant jar")
# Compiling the firmware
with lcd(experiment_path):
local("make -j TARGET=%s clean" % platform)
local("make -j TARGET=%s" % platform)
In [6]:
# Template loading
main_csc_template = TEMPLATE_ENV.get_template("dummy_main.csc")
script_template = TEMPLATE_ENV.get_template("dummy_script.js")
# Simulation script rendering
script = script_template.render(
timeout=timeout
)
# We can define dynamically the amount of nodes we want and their location on
# simulation. You can in a very short amount of lines test your experiment
# with a wide amount of topologies.
num_client = 7
servers = [{"mote_id": 1, "x": 0, "y": 0, "z": 0, "mote_type": "server"}]
clients = [{"mote_id": i, "x": i, "y": i, "z": 0, "mote_type": "client"}
for i in range(1, num_client - 1)]
# The simulation file template can also be changed and several could be written. For more information
# about how to create a campaign go check : http://makesense.readthedocs.org/en/latest/campaign.html
with open(pj(experiment_path, "main.csc"), "w") as f:
f.write(main_csc_template.render(
title="Dummy Simulation",
random_seed=random_seed,
transmitting_range=transmitting_range,
interference_range=interference_range,
success_ratio_tx=success_ratio_tx,
success_ratio_rx=success_ratio_rx,
mote_types=[
{"name": "server", "description": "server", "firmware": "udp-server.wismote"},
{"name": "client", "description": "client", "firmware": "udp-client.wismote"}
],
motes=servers + clients,
script=script))
In this step, you can launch the simulation. Our make run basically does the following thing:
java -mx512m -jar $(CONTIKI)/tools/cooja/dist/cooja.jar -nogui=main.csc -contiki=$(CONTIKI)
In [12]:
# Running the simulation locally
with lcd(experiment_path):
local("make run")
This step is the one where we will deploy to the iotlab testbed. You need to have an account and be logged. If that's not the case you checkout the following tutorial: https://www.iot-lab.info/tutorials/install-cli-tools/
Add the following lines to your .ssh/config while changing your user to your propre name of course ;-)
Host strasbourg
HostName strasbourg.iot-lab.info
User leone
Host euratech
HostName euratech.iot-lab.info
User leone
Host rennes
HostName rennes.iot-lab.info
User leone
Host grenoble
HostName grenoble.iot-lab.info
User leone
Host rocquencourt
HostName rocquencourt.iot-lab.info
User leone
In [12]:
from fabric.api import env
deploy = False
# We tell to fabric to use the .ssh/config to ease the deployment
env.use_ssh_config = True
# The testbed we want to deploy in. Because it's a simple python variable you can use loops to deploy on several testbeds.
env.host_string = "grenoble"
# Creation of folder just to be sure
testbed_results_folder = pj(experiment_path, "results", "iotlab")
if not os.path.exists(testbed_results_folder):
os.makedirs(testbed_results_folder)
In [8]:
import json
if deploy:
from iotlabcli import experiment
testbed_results = pj(experiment_path, "results", "iotlab")
# Name to give to our run
testbed_name = "ewsn2015_demo_experiment"
# Simulation duration in minutes
testbed_duration = 1
# Experiment id
testbed_experiment_id = None
testbed_client_nodes = [141, 142]
testbed_server_nodes = [143]
testbed_all_nodes = testbed_client_nodes + testbed_server_nodes
# describe the resources
resources = [
{
"nodes": [
"m3-%d.%s.iot-lab.info" % (node, env.host_string) for node in testbed_client_nodes
],
"firmware_path": pj(experiment_path, "udp-client.iotlab-m3"),
"profile_name": "m3_energy_monitoring"
},
{
"nodes": [
"m3-%d.%s.iot-lab.info" % (node, env.host_string) for node in testbed_server_nodes
],
"firmware_path": pj(experiment_path, "udp-server.iotlab-m3"),
"profile_name": "m3_energy_monitoring"
}
]
resources = [experiment.exp_resources(**c) for c in resources]
# Let's save all this configuration in the results folder to keep track of what we ran
with open(pj(testbed_results_folder, "nodes.json"), "w") as f:
f.write(json.dumps(resources,
sort_keys=True, indent=4, separators=(',', ': ')) )
In [ ]:
from fabric.api import run
if deploy:
experiment_finished = False
import iotlabcli
# gets user password from credentials file None, None
# actually submit the experiment
# We move to the folder to have a portable iotlab.json
with lcd(experiment_path):
exp_res = experiment.submit_experiment(
api, testbed_name, testbed_duration, resources)
testbed_experiment_id = exp_res["id"]
with open(pj(testbed_results_folder, "experiment.json"), "w") as f:
f.write(json.dumps({"id": testbed_experiment_id}))
# get the content
print("Exp submited with id: %u" % testbed_experiment_id)
# We wait till the experiment is completed
# note that this step requieres you to have experiment-cli installed on the command line.
# TODO: Replace with a full pythonic way
run("experiment-cli wait -i %d" % testbed_experiment_id)
with open(pj(testbed_results_folder, "serial.log"), 'w') as f:
run("./iot-lab/tools_and_scripts/serial_aggregator.py -i %d" % testbed_experiment_id,
stdout=f, timeout=60 * testbed_duration + 7)
print("Written serial logs to %s" % pj(testbed_results_folder, "serial.log"))
In [ ]:
if deploy:
get(".iot-lab/%d" % testbed_experiment_id, local_path=testbed_results)
Results and logs rarely come ready to be exploited. Python is featured with regexp module and can help you create objects representing the data you want and put them in CSV to have easier exploitation.
Simulation in Cooja can have several output.
All of them can be analyzed by makesense to produce messages
In [44]:
from makesense import parser
In [ ]:
# Transform all logs to a messages object
import pandas as pd
import networkx as nx
import xml.etree.ElementTree as ET
from scipy.spatial import distance
import itertools
def csc_to_graph(name):
tree = ET.parse(name)
l_mote_id = [int(t.text) for t in tree.findall(".//mote/interface_config/id")]
l_mote_type = [t.text for t in tree.findall(".//mote/motetype_identifier")]
l_x = [float(t.text) for t in tree.findall(".//mote/interface_config/x")]
l_y = [float(t.text) for t in tree.findall(".//mote/interface_config/y")]
l_z = [float(t.text) for t in tree.findall(".//mote/interface_config/z")]
g = nx.Graph()
# We first add all nodes with their attributes
for (mote_id, mote_type, x, y, z) in zip(l_mote_id, l_mote_type, l_x, l_y, l_z):
g.add_node(mote_id, mote_type=mote_type, x=x, y=y, z=z)
# We then draw a line between all those nodes if they are within a certain range from
# each others.
for (a, b) in itertools.product(g.nodes(data=True), g.nodes(data=True)):
d_a, d_b = a[1], b[1]
if 0 < distance.euclidean((d_a["x"], d_a["y"], d_a["z"]),
(d_b["x"], d_b["y"], d_b["z"])) <= transmitting_range:
g.add_edge(a[0], b[0])
return g
#for mote_type in mote_types:
# color = mote_types[mote_type]["color"]
# nodelist = mote_types[mote_type]["nodes"]
# nx.draw_networkx_nodes(net, position,
# nodelist=nodelist,
# node_color=color,
# ax=ax_transmission_graph)
#nx.draw_networkx_edges(net, pos=position, ax=ax_transmission_graph)
# labels
#nx.draw_networkx_labels(net, position, ax=ax_transmission_graph)
messages = parser.message(experiment_path)
g = csc_to_graph(pj(experiment_path, "main.csc"))
import matplotlib
# Force matplotlib to not use any Xwindows backend.
matplotlib.use('Agg')
nx.draw(g, pos={node: [data["x"], data["y"]] for node, data in g.nodes(data=True)})
In [164]:
# We were interested in the energy consumption
import csv
import numpy as np
import pandas as pd
if deploy:
testbed_df = {}
consumption_fields = ['junk', 'junk', 'index', 'timestamp_s', 'timestamp_us', 'power', 'voltage', 'current']
for node in testbed_all_nodes:
oml_name = pj(testbed_results_folder, str(testbed_experiment_id), "consumption", "m3-%d.oml" % node)
csv_name = pj(testbed_results_folder, str(testbed_experiment_id), "consumption", "m3-%d.csv" % node)
df = pd.read_csv(oml_name, skip_blank_lines=True, skiprows=8, names=consumption_fields, delimiter="\t")
df.drop("junk", axis=1, inplace=True)
df["timestamp"] = df.timestamp_s + (10 ** -6) * df.timestamp_us
df["timestamp_diff"] = df.timestamp - df.timestamp[0]
df.set_index("index", inplace=True)
# Saving to a dictionnary
testbed_df[node] = df
# Save the dict to a CSV for easier manipulation
df.to_csv(csv_name)
In [183]:
df_msg = pd.DataFrame([dict(msg._asdict()) for msg in messages])
# Dropna is there to clean up the columns having only None data
df_msg[df_msg.message_type == "dis"].dropna(axis=1)
Out[183]:
In [170]:
if deploy:
%matplotlib inline
import matplotlib.pyplot as plt
for host in testbed_all_nodes:
testbed_df[host].voltage.plot()
In [166]:
if deploy:
%matplotlib inline
for host in testbed_all_nodes:
testbed_df[host].power.plot()
In [167]:
if deploy:
%matplotlib inline
for host in testbed_all_nodes:
testbed_df[host].current.plot()