Simulando consumo de energía en NS3 (II)

Seguimos explicando el modelo de simulación del consumo de energía. Ya explicamos en la primera entrada relativa a la energía los conceptos básicos modelados en NS3. Seguimos trabajando con el ejemplo básico examples/energy/energy-model-example.cc

Si observamos el ejemplo, existen dos funciones añadidas justo antes de la función principal, RemainingEnergy y TotalEnergy:

/// Trace function for remaining energy at node.
void
RemainingEnergy (double oldValue, double remainingEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Current remaining energy = " << remainingEnergy << "J");
}

/// Trace function for total energy consumption at node.
void
TotalEnergy (double oldValue, double totalEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Total energy consumed by radio = " << totalEnergy << "J");
}

Estas funciones, como podemos ver, imprimen la energía que queda en la fuente de energía y el total de energía consumida por la radio.

Para invocar estas funciones cuando existe un cambio en la energía restante y/o energía consumida, debemos enlazar las funciones anteriores a los cambios de estado de la fuente de energía y del modelo de energía consumido respectivamente. De esta forma, obtenemos primero una referencia a la fuente de energía con ID 1. A continuación, mediante la función TraceConnectWithoutContext, vinculamos el cambio de estado en RemainingEnergy a una llamada a la misma función, mediante MakeCallBack y un puntero a la función deseada, en este caso RemainingEnergy también:

Ptr basicSourcePtr = DynamicCast (sources.Get (1));
basicSourcePtr->TraceConnectWithoutContext ("RemainingEnergy", MakeCallback (&RemainingEnergy));

A continuación se sigue el mismo proceso obteniendo el modelo de consumo de energía de un dispositivo:

// device energy model
Ptr basicRadioModelPtr =
basicSourcePtr->FindDeviceEnergyModels ("ns3::WifiRadioEnergyModel").Get (0);
NS_ASSERT (basicRadioModelPtr != NULL);
basicRadioModelPtr->TraceConnectWithoutContext ("TotalEnergyConsumption", MakeCallback (&TotalEnergy));

De esta forma se tracean los cambios de estado en las fuentes de energía, que simulan baterías, modelos de energía, que simulan el consumo de las tarjetas de red, y los recolectores de energía que simularían paneles solares, viento, etc.

La función TraceconnectWithoutContext conecta una fuente de trazas (es decir, algo que representa un estado dentro de NS3 y que puede cambiar) con una función. Identificaremos estas fuentes de trazas por que son variables de la plantilla TracedValue, por ejemplo, puedes ver en la definición BasicEnergySource tiene una variable TracedValue m_remainingEnergyJ; que es susceptible de ser traceada mediante una función asociada con TraceconnectWithoutContext asocíandole una función que tenga como argumentos el valor antiguo y nuevo. El tipo de argumentos en este caso será double. La documentación de cada clase proporciona qué TracedSources tiene para usar este mecanismo (e.g. BasicEnergySource).

Argumentos de entrada en simulaciones de NS3

Para ahorrarte trabajo, puedes configurar mediante argumentos de entrada muchos parámetros o atributos de tu simulación, tanto de los módulos que usas de NS3, como definir tus propios parámetros. Por ejemplo, si vas a simular el tráfico en una red lorawan con topología en estrella, con la pasarela en el medio, puedes poner como parámetro configurable el número de nodos o el radio de la estrella. Este ejemplo es el que vamos a utilizar para explicar cómo gestionar los argumentos de entrada. El archivo que usaremos es el lorawan.cc del repositorio. Necesitarás instalar el módulo de lorawan como indicamos aquí

Si copiamos el archivo lorawan.cc al directorio scratch de nuestra instalación NS3 podemos ver qué argumentos podemos definir por la linea de comandos mediante la opción –PrintHelp, esto es:

$ ./waf --run "lorawan --PrintHelp"
lorawan [Program Options] [General Arguments]
[...]
Program Options:
--radio: Radio of the disc where random put the nodes [20]
--numnodes: Num. nodes in the grid for simulating [20]


General Arguments:
--PrintGlobals: Print the list of globals.
--PrintGroups: Print the list of groups.
--PrintGroup=[group]: Print all TypeIds of group.
--PrintTypeIds: Print all TypeIds.
--PrintAttributes=[typeid]: Print all attributes of typeid.
--PrintHelp: Print this help message.

Como podemos ver, después de los mensajes de compilación (sustituidos por […]), podemos ver que el programa lorawan tiene definidos dos opciones, el radio y el numnodes, indicando el radio de la estrella y el número de nodos respectivamente. El número por defecto en ambos casos y si no indicamos esas opciones es 20. Si queremos simular 3 nodos en una topología en estrella con un radio de 10 metros, solo tendremos que indicarlo en línea de argumentos:
$ ./waf --run "lorawan --numnodes=3 --radio=10"
[...]

Si analizamos el archivo lorawan.cc, la definición de argumentos es relativamente sencilla, en primer lugar se indica que vamos a parsear la línea de comandos en busca de atributos y se definen las variables que almacenarán los valores que se pasen por línea de comandos:

#include "ns3/command-line.h"
[...]
double radio = 20.0;
int numnodes = 20;

A continuación se definen los valores esperados en línea de argumentos, en nuestro ejemplo, lo hacemos mediante una función que definimos antes del main y añadimos al final. En esta función creamos la variable que almacenará y parseará los argumentos y añadimos nuestros dos argumentos esperados mediante AddValue:

CommandLine setupcmd(){
CommandLine cmd;
cmd.AddValue ("radio", "Radio of the disc where random put the nodes", radio);
cmd.AddValue ("numnodes", "Num. nodes in the grid for simulating",numnodes);
return cmd;
}

como puedes ver indicamos el nombre, una pequeña ayuda y las variables que van a almacenar los valores.
Finalmente, parseamos la línea de comandos:

[..]
CommandLine cmd =setupcmd();
cmd.Parse (argc, argv);
[..]

Con lo que en las variables definidas a tal efecto, guardaremos los valores que le pasemos.

Aparte de poder definir nuestros propios argumentos de entrada para configurar nuestras simulaciones, muchos módulos que usamos tienen sus propios atributos configurables por líneas de comandos. Por ejemplo, si queremos ver un listado de todos los módulos disponibles, podemos verlos con la opción –PrintTypeIds

./waf --run "lorawan --PrintTypeIds"
Waf: Entering directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.112s)
Registered TypeIds:
ns3::A2A4RsrqHandoverAlgorithm
ns3::A3RsrpHandoverAlgorithm
[..]
ns3::LoraChannel
ns3::LoraInterferenceHelper
ns3::LoraNetDevice
ns3::LoraPhy
ns3::LoraRadioEnergyModel
ns3::LoraTag
ns3::LoraTxCurrentModel
[...]

En esa larga lista, podemos explorar qué atributos son configurables mediante la opción –PrintAttributes y el identificador o nombre del módulo. Por ejemplo, si queremos saber qué atributos puedo especificar mediante línea de comandos del módulo de ns3 LoraRadioEnergyModel, podemos ejecutar:

$ ./waf --run "lorawan --PrintAttributes=ns3::LoraRadioEnergyModel"
Waf: Entering directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.063s)
Attributes for TypeId ns3::LoraRadioEnergyModel
--ns3::LoraRadioEnergyModel::RxCurrentA=[0.0112]
The radio Rx current in Ampere.
--ns3::LoraRadioEnergyModel::SleepCurrentA=[1.5e-06]
The radio Sleep current in Ampere.
--ns3::LoraRadioEnergyModel::StandbyCurrentA=[0.0014]
The default radio Standby current in Ampere.
--ns3::LoraRadioEnergyModel::TxCurrentA=[0.028]
The radio Tx current in Ampere.
--ns3::LoraRadioEnergyModel::TxCurrentModel=[0]
A pointer to the attached tx current model.



Donde podemos ver que podemos definir la corriente de envío, recepción, dormir, etc.

Instalar el módulo LoRaWAN en NS3

[Adaptado a la versión 3.36 con cmake]

LoRaWAN es una tecnología especialmente diseñada para IoT en dispositivos alimentados por baterías, con necesidades de comunicación bidireccional y un ancho de banda limitado (entre 0.3kbps y 50kbps). La topología por excelencia es la de estrella, con la pasarela en el centro de la estrella (que conecta la red LoRaWAN a una red IP convencional).

Existen tres clases de dispositivos en LoRaWAN (clase A, B, C) asociados a tres funcionalidades muy concretas. La clase A está asociado a un dispositivo final que periódicamente o bajo requerimientos de la aplicación, manda la información a la pasarela y espera una ventana de tiempo por si hubiera algún tipo de necesidad de comunicación en el sentido pasarela->dispositivo. Este esquema permite dormir el dispositivo de clase A entre transmisiones y obliga a guardar todas las comunicaciones en el sentido pasarela-> dispositivos y esperar a que se produzca una comunicación en el sentido dispositivo -> pasarela.

La clase B se sincroniza con la red y habilita un periodo de escucha de forma periódica (ventana máxima de 128 segundos) por si hubiera un mensaje hacia el dispositivo. De esta forma se puede hacer determinista cuanto tiempo tarda en recibir un mensaje de la red enviado por la pasarela. No obstante este despertar periódico consume batería y los hace menos eficiente que la clase A desde el punto de vista del consumo energético.

Finalmente, la clase C está siempre escuchando por lo que cualquier transmisión desde la red (pasarela) es inmediatamente recibido a menos que el propio dispositivo esté transmitiendo. Se pueden intercambiar los modos de funcionamiento entre la clase A y la clase C si necesitamos, por ejemplo, enviar una actualización de firmware a través de la interfaz inalámbrica.
En NS3 hay varias implementaciones de un módulo de NS3 para simular la tecnología LoRaWAN. El mas estable está disponible en la App Store de NS3.
La instalación es sencilla, pasa por bajarse el código en el directorio src de la instalación NS3 con git clone y la dirección https://github.com/signetlabdei/lorawan, y volver a configurar y compilar todo:

$ns-allinone-3.36/ns-3.36/src$ git clone https://github.com/signetlabdei/lorawan
Clonando en 'lorawan'...
remote: Enumerating objects: 37, done.
..
..
Resolviendo deltas: 100% (906/906), listo.

A continuación, en el directorio principal, la configuración y compilación habitual, habilitando los test y los ejemplos:

$ns-allinone-3.36/ns-3.36/$./ns3 configure --enable-test --enable-examples
..
..

a la hora de construir, fijarnos que el módulo lorawan aparece en los módulos compilados:

$/ns-allinone-3.36/ns-3.36$ ./ns3 build

Para comprobar que todo ha ido bien, podemos ejecutar sus test específicos del módulo:

$/ns-allinone-3.36/ns-3.36$./test.py -s lorawan
.....
[1/1] PASS: TestSuite lorawan
1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)

y el ejemplo básico:

/ns-allinone-3.36/ns-3.36$ ./ns3 run simple-network-example
SimpleLorawanNetworkExample:main(): Creating the channel...
+0.000000000s -1 SimpleLorawanNetworkExample:main(): Setting up helpers...
+0.000000000s -1 LoraPhyHelper:LoraPhyHelper(0x7fff858a3a70)
+0.000000000s -1 SimpleLorawanNetworkExample:main(): Creating the end device...
+0.000000000s -1 LoraPhyHelper:SetDeviceType(0x7fff858a3a70, 1)
+0.000000000s -1 LorawanMacHelper:SetDeviceType(0x7fff858a3aa0, 1)
+0.000000000s -1 LoraHelper:Install()
+0.000000000s -1 LoraPhyHelper:Create(0x7fff858a3a70, 0, 0x5617411f0970)
+0.000000000s -1 LoraInterferenceHelper:LoraInterferenceHelper(0x5617411f0bf8)
+0.000000000s -1 LoraInterferenceHelper:SetCollisionMatrix(): Setting the GOURSAUD collision matrix
+0.000000000s -1 LoraPhy:SetChannel(0x5617411f0bc0, 0x5617411f0090)
+0.000000000s -1 LoraPhy:SetDevice(0x5617411f0bc0, 0x5617411f0970)
+0.000000000s -1 LoraHelper:Install(): Done creating the PHY
....

También hay un ejemplo de energía, que genera un archivo de texto battery-level.txt que guarda el nivel de batería para un dispositivo de clase A, a lo largo de una simulación:

/ns-allinone-3.36/ns-3.36$ ./ns3 run energy-model-example
+0.000000000s -1 Assign IP Addresses.
0.000574667s Current remaining energy = 0.0995293J
0.000574667s Total energy consumed by radio = 0.000470652J
0.000714667s Current remaining energy = 0.0994147J
0.000714667s Total energy consumed by radio = 0.000585312J
0.000762667s Current remaining energy = 0.0993754J
0.000762667s Total energy consumed by radio = 0.000624624J
0.00287467s Current remaining energy = 0.0973922J
0.00287467s Total energy consumed by radio = 0.00260779J
.....

En futuras entradas, utilizaremos esta tecnología para ir creando una serie de escenarios de IoT con los que poder estudiar aspectos concretos.

Ejemplo básico NS3: IoT y sensores

Como ya hemos comentado en las primeras secciones, hay numerosos ejemplos y test que te sirven de ayuda y como punto de partida, vamos a realizar un ejemplo básico intentando simular una red de sensores que mandan información a una pasarela (Gateway) o nodo central. El objetivo de este ejemplo es ir completándolo con otras funciones muy necesarias en simulación (Log, trazas, configuración, varias tecnologías, energía, etc.) en entradas posteriores. En el repositorio del tutorial, el archivo de inicio es basic-iot-sensors.cc.
Lo primero que vamos a realizar es crear 3 nodos (variable numsensors) y posicionarlos en un punto determinado (aleatorio). Utilizamos una función que crea un escenario con un radio (variable radio) determinado en metros y, a continuación, le pasamos el contenedor de nuestros nodos para que posicione, de forma aleatoria, dichos nodos;

NodeContainer nodes;
nodes.Create(numsensors);
MobilityHelper scenario = createscenario(radio);
scenario.Install (nodes);

Como ya indicamos en la entrada de topología , se crea un escenario mediante el MobilityHelper con la configuración deseada:

MobilityHelper createscenario(double radio)
{
MobilityHelper mobility;
mobility.SetPositionAllocator ("ns3::UniformDiscPositionAllocator",
"rho", DoubleValue (radio),
"X", DoubleValue (0.0),
"Y", DoubleValue (0.0),
"Z", DoubleValue (0.0));

mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel");
return mobility;
}



Del contenedor de nodos, creamos un centro de la estrella donde posicionaremos el primer nodo, que de ahora en adelante representará la pasarela:

MobilityHelper gatewayposition = createstarcenter();
gatewayposition.Install (nodes.Get(0));

Una vez creados los dispositivos y emplazados (la pasarela en la posición x,y,z a 0,0,0 y el resto de forma aleatoria), vamos a seguir configurando nuestros nodos, configurando una interfaz wifi en cada uno de los nodos. La tecnología inalámbrica WIFI está de sobra probada en NS3 y nos sirve de punto de partida para nuestro ejemplo.
Para la creación de NetDevice por cada Nodo e instalarlo en cada uno de los nodos, usaremos los asistentes, nos crearemos un asistente para la capa física (wifiPhy), la capa Mac (wifiMac) y el objeto que modela el canal inalámbrico (wifiChannel) y al cual deberemos conectar todos los objetos que modelan la capa física.

WifiHelper wifi;
YansWifiPhyHelper wifiPhy = YansWifiPhyHelper::Default ();
YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default ();
WifiMacHelper wifiMac;
wifiMac.SetType ("ns3::AdhocWifiMac");
wifiPhy.SetChannel(wifiChannel.Create());
NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

En este caso para el modelado de la capa física y el canal utilizamos el modelo Yans. Es el modelo por defecto que se utiliza pero, obviamente, el El módulo wifi es uno de los más completos y estudiados con varios modelos disponibles.
El tipo de MAC es ns3::AdhocWifiMac que modela relaciones punto a punto entre dos dispositivos con Wifi. A continuación establecemos el canal en el asistente wifiPhy para que todos los objetos creados de la capa física compartan el canal y se puedan comunicar entre ellos. Estas configuraciones, en la parte wifi, son las configuraciones por defecto. Por último, instalamos usando el asistente general del wifi la capa física y la MAC en todos los nodos. El asistente de wifi nos devuelve un contenedor con los NetDevices creados (y asociados a los nodos):

NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

Hemos usado la configuración por defecto, este es uno de los puntos más importantes en cuanto a una simulación, tienes que definir claramente las métricas en las cuales estás interesados y modelar de forma simple el resto de elementos. Por ejemplo, si estás interesado en un protocolo de comunicaciones de la capa de aplicación y quieres validar su funcionalidad, la capa física/MAC que utilices debe ser modelada de la forma más simple posible.

Si por contra, estás interesado en modelar un nuevo algoritmo de control de flujo de 802.11ax, es la capa de aplicación la que debe modelar lo mínimo para generar el tráfico necesario para probar la funcionalidad.

Hasta aquí hemos creado el «Hardware» de nuestra simulación, vamos a instalar y configurar el resto de elementos.

Pasemos a la parte de configuración de la pila de protocolos, en este caso vamos a instalar y configurar una pila de protocolos TCP/IP, para ello creamos un asistente y lo instalamos en todos los nodos:

InternetStackHelper internet;
internet.Install (nodes);

Una vez instalada, configuramos la dirección de red y le asignamos direcciones IP a las interfaces de red instaladas en los nodos:

Ipv4AddressHelper ipv4;
ipv4.SetBase ("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer i = ipv4.Assign (devices);

en este caso, usamos la dirección de red 10.1.1.0 con máscara 255.255.255.0 y se les asigna, de forma consecutiva, a cada uno de los NetDevices en el container «devices» creando un contenedor de interfaces ya configuradas.

El último paso de creación de nuestro escenario propiamente dicho es la creación de aplicaciones que creen tráfico. Vamos a instalar un servidor UDP en la pasarela (nodo 0) y lo configuramos para que escuche en el puerto 2000 :

uint16_t port = 2000;
UdpServerHelper server(port);
ApplicationContainer gatewayapps = server.Install(nodes.Get(0));
gatewayapps.Start(Seconds(1.0));
gatewayapps.Stop(Seconds(11.0));

Como vemos en el código de arriba, creamos un asistente de servidor en el puerto deseado y lo instalamos en el nodo 0. Por último indicamos los momentos en que arrancamos y paramos el servidor en este caso sobre el contenedor que se ha creado. Un paso similar hay que hacer en los clientes, debemos crear clientes software configurados para enviar una cantidad de tráfico al servidor que acabamos de instalar en el todo 0 (y que obtenemos con nodes.Get(0)).
Obtenemos la dirección IP del servidor (la 0) del contenedor de interfaces, establecemos el tamaño en 32 bytes, que para la información generada por un sensor es suficiente. En principio generaremos como mucho 10 paquetes:

Address gatewayAddress = Address(i.GetAddress (0));
uint32_t packetSize = 32;
uint32_t maxPacketCount = 10;

Con esta configuración, se crea un asistente de cliente, se establece el intervalo de envío y se configura el cliente:

UdpClientHelper client (gatewayAddress, port);
Time interPacketInterval = Seconds (0.5);
client.SetAttribute("PacketSize", UintegerValue(packetSize));
client.SetAttribute ("Interval", TimeValue (interPacketInterval));
client.SetAttribute ("MaxPackets", UintegerValue(maxPacketCount));

Hay que resaltar cómo se configura, mediante el método SetAttribute, cada una de las características, indicando su nombre y valor, este mecanismo es muy utilizado por los objetos NS3 para la configuración de parámetros.
Por último, instalamos en los nodos un cliente (incluyendo la pasarela que actuará como sensor/servidor) creándose otro contenedor de aplicaciones que debemos arrancar y parar. Hay que asegurarse que el servidor esté arrancado antes que los clientes, por eso arrancamos en el segundo 2 y paramos mas tarde para no perder información.

ApplicationContainer apps = client.Install(nodes);
apps.Start(Seconds(2.0));
apps.Stop(Seconds(10.0));

Por último, arrancamos el simulador:

Simulator::Stop (Hours (24));
Simulator::Run ();
Simulator::Destroy ();

Los detalles del simulador lo veremos en otra entrada.

Con esto terminamos nuestra simulación, colocando el archivo en el directorio scratch lo ejecutamos:

/ns-3.30.1$ ./waf --run basic-iot-sensors
Waf: Entering directory `ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.747s)

Que compila y ejecuta nuestra simulación. Realmente no vemos ningún flujo de información por lo que no sabemos si todo ha ido bien, en próximas entradas veremos cómo obtener información, mediante log y trazas, de qué está pasando en nuestra simulación.