Creando un nuevo módulo en NS3

[Actualizado a cmake, versión 3.36.1 (documentación oficial ns3.36)]
Crear un nuevo protocolo o algoritmo en NS3 es un paso que implica la creación de un nuevo módulo. Obviamente, a partir del tipo de protocolo o algoritmo a implementar, los pasos a seguir y los protocolos que nos servirán como referencia cambiarán. En cualquier caso, siempre necesitaremos generar un nuevo módulo e incluirlo en la compilación de NS3 para poder hacer simulaciones con posterioridad.

Para crear un nuevo módulo, NS3 nos proporciona un script python, create-module.py, dentro de la carpeta src de nuestra instalación de ns3.
Si por ejemplo queremos crear un nuevo protocolo inalámbrico, pongamos el protocolo weightlessp, si ejecutamos:

$utils/create-module.py weightless
Creating module /home/felix/tools/ns-3-dev-git/contrib/weightless
Successfully created new modules
Run './ns3 configure' to include them in the build

Se nos crea la plantilla dentro de src:

$cd contrib/weightless/
felix@homer-esi:~/tools/ns-3-dev-git/contrib/weightless$ ls
CMakeLists.txt doc examples helper model test

donde vemos las carpetas habituales para meter la documentación, los ejemplos, los asistentes, el modelo propiamente dicho y los test. También me genera el archivo CMakeList.txt que utiliza ns3 para compilar los módulos. Para ver que se incluye correctamente en el proceso de compilación:

$./ns3 configure --enable-example --enable-test
$./ns3 build
$./test.py -s weightlessp
.
PASS: TestSuite weightlessp
1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)
.

Ahora falta, obviamente, lo difícil, que es programar el modelo propiamente dicho identificando cada una de las partes. Lo más recomendable es estudiar cuidadosamente un protocolo existente de la misma capa que el que queremos programar y copiar su estructura adaptando el comportamiento a la funcionalidad deseada.

Simulando consumo y recolección de energía con LoraWan en NS3 (y III)

Vamos a ver un ejemplo completo de simulación de consumo y recolección de energía con LoraWan en NS3. La idea es simular un nodo LoraWan al cual le añadiremos un módulo recolector de energía (BasicEnergyHarvesterHelper). El ejemplo que usaremos en esta entrada se encuentra en lorawan-energy.cc. Haremos especial énfasis en la parte de la energía. No obstante, vamos a ir describiendo paso a paso, cada bloque en el ejemplo.
Lo primero que tenemos es la creación de los nodos. Crearemos un dispositivo final en LoraWan y una pasarela. El ejemplo está preparado para ampliar sus capacidades por lo que podríamos poner el número de nodos finales que queramos como argumento. Tal y como vimos en la entrada relativa a los argumentos de entrada.
Volviendo a la creación de nodos:

NodeContainer endDevices;
endDevices.Create(numendDevices);
MobilityHelper scenario = createscenario(radio);
scenario.Install (endDevices);
//put the hub in 0.0 0.0 0.0 center
NodeContainer hub;
hub.Create(1);
MobilityHelper hubposition = createstarcenter();
hubposition.Install (hub);

Se crea un contenedor de nodos con numendDevices, por defecto igual a uno, y se posicionan con un radio igual a 20. Utilizamos para ello la función createscenario. A continuación creamos otro contenedor de nodos para los nodos que hacen las funciones de pasarela o gateway/hub. En este caso, también uno, y lo posicionamos en el centro. La función createstarcenter y la función createscenario se encuentran al final del archivo. Ambas devuelven un MobilityHelper que, utilizando los contenedores, instalamos en los nodos finales endDevices y en los hub.
A continuación debemos poner un interfaz LoraWan en cada dispositivo final y en cada Hub o pasarela.
Para ello, creamos un canal LoraWan de acuerdo a la documentación del módulo:

Ptr <logdistancepropagationlossmodel> loss = CreateObject<logdistancepropagationlossmodel> ();
loss->SetPathLossExponent (3.76);
loss->SetReference (1, 7.7);
Ptr<propagationdelaymodel> delay = CreateObject<constantspeedpropagationdelaymodel> ();
Ptr<lorachannel> channel = CreateObject<lorachannel> (loss, delay);

Estos parámetros de retardo y pérdida simulan la propagación de un canal usando la codificación LoRa. Ahora hay que crear las interfaces Lora, conectadas al canal y las añadiremos a los nodos ya creados. Primero la interfaz LoRa para los dispositivos finales usando los asistentes:

LoraPhyHelper phyHelper = LoraPhyHelper ();
phyHelper.SetChannel (channel);
LorawanMacHelper macHelper = LorawanMacHelper ();
LoraHelper helper = LoraHelper ();
helper.EnablePacketTracking();
phyHelper.SetDeviceType(LoraPhyHelper::ED);
macHelper.SetDeviceType(LorawanMacHelper::ED_A);
NetDeviceContainer endDevicesNetDevices = helper.Install(phyHelper, macHelper, endDevices);

Como se puede observar, creamos la capa física y MAC, indicándole que es un dispositivo final mediante ED y ED_A respectivamente. Adicionalmente, hacemos lo mismo con la pasarela.

LoraHelper helperHub = LoraHelper ();
phyHelper.SetDeviceType (LoraPhyHelper::GW);
macHelper.SetDeviceType (LorawanMacHelper::GW);
helperHub.Install (phyHelper, macHelper, hub);
macHelper.SetSpreadingFactorsUp (endDevices, hub, channel);

Ahora indicando que es una pasarela.
Bien, el siguiente paso es configurar una aplicación, que instalaremos en los nodos finales y que mandará un paquete cada 5 segundos de un tamaño de 12 bytes:

PeriodicSenderHelper periodicSenderHelper;
periodicSenderHelper.SetPeriod (Seconds (5));
periodicSenderHelper.SetPacketSize (12);
ApplicationContainer appContainer = periodicSenderHelper.Install (endDevices);
double simulationTime = 3600;
Time appStopTime = Seconds (simulationTime);
appContainer.Start (Seconds (0));
appContainer.Stop (appStopTime);

Como podemos ver, configuramos el periodo y el tamaño de paquete para instalarlo en los dispositivos finales. A continuación indicamos que inicie en el segundo 0 y termine a la hora de comienzo mediante una variable, appStopTime que utilizaremos para configurar la simulación.
El siguiente paso que vamos añadir a nuestro dispositivo final es una fuente de energía, una pila de 200 mAh y luego configuraremos el consumo de acuerdo a un circuito final. Primero la pila:

BasicEnergySourceHelper basicSourceHelper;
LoraRadioEnergyModelHelper radioEnergyHelper;


// Bateria PD2032 200 mAh 3.7V
basicSourceHelper.Set ("BasicEnergySourceInitialEnergyJ", DoubleValue (2664)); // Energy in J
basicSourceHelper.Set ("BasicEnergySupplyVoltageV", DoubleValue (3.7));

Como podemos ver, 2664 julios a 3.7 voltios.
El consumo lo obtenemos de un modem semtech sx1276:

radioEnergyHelper.Set ("StandbyCurrentA", DoubleValue (0.0016));
radioEnergyHelper.Set ("TxCurrentA", DoubleValue (0.120)); //20 dbm
radioEnergyHelper.Set ("SleepCurrentA", DoubleValue (0.0000002));
radioEnergyHelper.Set ("RxCurrentA", DoubleValue (0.0115));
radioEnergyHelper.SetTxCurrentModel ("ns3::ConstantLoraTxCurrentModel","TxCurrent", DoubleValue (0.12)); //+20dbm
EnergySourceContainer sources = basicSourceHelper.Install (endDevices);
DeviceEnergyModelContainer deviceModels = radioEnergyHelper.Install
(endDevicesNetDevices, sources);

Con esta configuración, nuestro dispositivo transmitiría hasta que terminara la simulación o hasta que la batería no suministrara suficiente energía.
Añadimos un simulador de un recolector de energía, que de forma periódica recolecta un valor aleatorio de energía:

BasicEnergyHarvesterHelper basicHarvesterHelper;
basicHarvesterHelper.Set ("PeriodicHarvestedPowerUpdateInterval", TimeValue (Seconds (1.0)));
basicHarvesterHelper.Set ("HarvestablePower", StringValue ("ns3::UniformRandomVariable[Min=0.0|Max=0.009]"));
EnergyHarvesterContainer harvesters = basicHarvesterHelper.Install (sources);

Podemos ver, que se configura una recolección periódica de un segundo con un valor aleatorio entre 0 y 0.09 julios.
Finalmente configuramos el navegador como en cualquier otra simulación:

Simulator::Stop (appStopTime);
Simulator::Run ();
Simulator::Destroy ();

El ejemplo tiene mas código relacionado con extraer resultados por línea de comando y por gnuplot, esta parte la veremos en otra entrada. Si ejecutamos el ejemplo, vemos que hay una serie de resultados relacionados con la energía restante en la pila.

$ ./waf --run lorawan-energy

Una vez ejecutada la simulación, tenemos un archivo gnuplot-energy-example.sh que se ha generado junto con los datos de la simulación, le damos permisos de ejecución, lo ejecutamos y nos genera una gráfica con la evolución de la energía restante en el nodo final durante la hora de simulación:
Energía restante con recolector de energía
Donde podemos ver cómo la recolección de energía mitiga el gasto energético asociado al envío de paquetes. Si vemos cómo sería la gráfica sin el recolector de energía, basta con comentar esa parte, volver a simular y obtener la gráfica:

Energía restante con recolector de energía

Donde podemos ver cómo la energía restante no se repone en ningún momento.
Es una buena técnica jugar con los valores para observar su influencia en la energía restante en la pila del dispositivo final.

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.

Simulando consumo de energía en NS3

El consumo de energía es un parámetro importante sobre todo en escenarios de nodos alimentados por batería. Tener el perfil de consumo de un nodo no es tarea fácil y menos estimar su tiempo de vida conforme a unos parámetros de transmisión, encendido, apagado, etc.
Por ello hacer simulaciones de energía puede ser una buena práctica para muchos proyectos.

En NS3 se modela el consumo de energía haciendo una abstracción de los elementos que existen en la realidad. En este enlace puedes consultar la documentación oficial.

Los tres elementos principales son, las fuentes de energía, el modelo de energía de un dispositivo y un recolector de energía.

Una fuente de energía (Energy Source) es, como su propio nombre indica, una batería o elemento proveedor de energía que se conecta a un dispositivo para suministrarle energía de acuerdo a un modelo de energía de ese dispositivo.
Un modelo de energía de un dispositivo es un modelo de consumo de acuerdo a una serie de estados en los cuales un dispositivo puede estar. Por ejemplo, un dispositivo inalámbrico puede estar dormido, enviando o recibiendo información y tendrá un consumo distinto en función de cada uno de esos tres estados estado. Por lo tanto, debemos conectar ese modelo de energía con los estados del dispositivo.

Finalmente el recolector de energía sirve para modelar un dispositivo (e.g un panel solar) y el entorno (e.g. radiación solar) para proveer energía a una fuente de energía de cara a su recarga.

Cada uno de esos tres elementos se configuran mediante su correspondiente asistente (Energy source helper, Device Energy Model Helper y Energy Harvesting Helper) que te ayuda a configurar los parámetros básicos mediante atributos.

Los asistentes de modelos concretos heredan de esos asistentes con las particularidades de cada modelo/tecnología. Por ejemplo, hay un asistente para configurar una batería que hereda del Energy source helper y que añade los elementos específicos que caracterizan a una batería de ese tipo partiendo de los parámetros básicos del modelo:

  • Energía inicial(J)
  • Voltage de partida (V)
  • Tiempo de actualización del nivel de la batería

Por ejemplo, una batería del tipo RV añade parámetros como los valores de voltage de corte, valores alfa, beta, etc.
Existen modelos de batería de Litio Y rv.

De igual forma, el modelo de consumo de energía de un dispositivo se realiza asociándolo a una tecnología concreta, modelando cada uno de los estados y su consumo en Amperios. El modelo mas estudiado y modelado es el modelo wifi que define el consumo para hasta 7 estados distintos.
Para los dispositivos con varias interfaces (e.g. pasarelas con varias interfaces inalámbricas), una fuente de energía puede estar conectado a varios modelos de consumo.

Veamos el ejemplo básico que viene con la instalación de ns3, ejecutamos el ejemplo energy-model-example y vemos la salida que nos proporciona:

ns-allinone-3.31/ns-3.31$ ./waf --run energy-model-example
Waf: Entering directory `/home/felix/tools/ns-allinone-3.31/ns-3.31/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.31/ns-3.31/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (2.409s)
+0.000000000s -1 Assign IP Addresses.
0.000574667s Current remaining energy = 0.0995293J
0.000574667s Total energy consumed by radio = 0.000470652J
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
--
Received one packet! Socket: 10.1.1.1 port: 49153 at time = 0.00287467
--
0.106594s Total energy consumed by radio = 0.0875537J
0.106594s Current remaining energy = 0.0124463J
End of simulation (10s) Total energy consumed by radio = 0.0982333J
End of simulation (10s) Total energy consumed by radio = 0.0875537J

en el archivo $/ns-3.31/examples/energy/energy-model-example.cc podemos ver ese ejemplo. Es un ejemplo muy básico wifi donde se configura, mediante asistentes, una fuente de energía y se asocia a un modelo de consumo de wifi. Un aspecto a tener en cuenta es que si instalas el módulo de lorawan, también existe un ejemplo de energía energy-model-example.cc por lo que waf puede ejecutar el ejemplo que se encuentre antes en el orden de búsqueda. Si quieres asegurarte ejecutar el ejemplo citado, pon la ruta completa ./waf --run examples/energy/energy-model-example

Bien, expliquemos el ejemplo wifi, lo primero, creamos un asistente de fuentes de energía y lo configuramos con una energía inicial de 0.1 Julios.

BasicEnergySourceHelper basicSourceHelper;
basicSourceHelper.Set ("BasicEnergySourceInitialEnergyJ", DoubleValue (0.1));

Instalamos esa fuente de energía en todos los nodos de un contenedor previamente configurado (NodeContainer c) con los nodos wifi (2).

EnergySourceContainer sources = basicSourceHelper.Install (c);

A continuación configuramos el modelo de consumo que queremos para dichos dispositivos:

WifiRadioEnergyModelHelper radioEnergyHelper;
radioEnergyHelper.Set ("TxCurrentA", DoubleValue (0.0174));

Vemos que sólo configuramos el consumo cuando se transmite dejando el resto de consumos por defecto.
Finalmente, con las fuentes de energía y los modelos, creamos un contenedor asociando fuentes de energía y modelos de consumo:

DeviceEnergyModelContainer deviceModels = radioEnergyHelper.Install (devices, sources);

El resto de la configuración es exactamente igual a cualquier otro ejemplo de ns3 relacionado con WiFi.
Un aspecto a destacar es cómo se imprime la información de la energía restante en la batería. El mecanismo es algo diferente a lo visto en este tutorial y lo describiremos en detalle en otra entrada. Básicamente se asocia una función al cambio en el estado de la energía remanente de forma que cuando cambia esa variable se llama a esa función que imprime el valor.

Este mecanismo lo describiremos en profundidad en otra entrada.

Instalación del simulador NS3

Instrucciones actualizadas a la versión 3.36 (mayo 2022)

La forma mas simple de instalación del simulador NS3 pasa por bajarse el paquete completo de la web de versiones del simulador y descomprimirla en el directorio de trabajo. El tutorial de instalación completo puede consultarse en la wiki de instalación. En mi sistema Debian Buster, tras bajarme el archivo ns-allinone-3.36.tar.bz2 se descomprime:

$ tar -xf ns-allinone-3.36.tar.bz2
$ ls
ns-allinone-3.36 ns-allinone-3.36.tar.bz2
$ cd ns-allinone-3.36/
ns-allinone-3.36$ ls
bake build.py constants.py netanim-3.108 ns-3.36 pybindgen-0.22.1 README util.py

Atendiendo al sistema operativo donde estés trabajando, necesitas instalar una serie de dependencias y librerías que ns3 utiliza para los diferentes módulos. Hay dependencias que no necesitas instalar si el módulo que depende de esas librerías no lo vas a emplear. Si estás empezando, instala todas las dependencias que se indican en el wiki de instalación para tu sistema operativo. También te indican para qué sirve cada dependencia, mi recomendación es que, como ya he dicho, si estás empezando, las instales todas. En mi caso, las dependencias para Debian Buster vienen especificadas en su sección correspondiente usando la herramienta apt-get.
Una vez instaladas las dependencias existen varias alternativas para la instalación. Nosotros vamos a utilizar el script ns3, dentro del directorio ns-3.36. Este script tiene una funcionalidad parecida al antiguo sistema de compilación y configuración waf. Ese sistema, a partir de la versión 3.36 ha sido cambiado por Cmake. En el directorio donde hemos descomprimido el archivo ns-allinone-3.36.tar.bz2, , dentro de la carpeta ns-3.36, encontramos el archivo ns3. Lo ejecutamos para compilar el simulador:

./ns3 configure --enable-test --enable-examples

Se configura el simulador y al final te informa de qué módulos se compilarán. Las opciones habilitan los test y los ejemplos respectivamente, una opción que, si estás empezando, te servirán de guía en tus simulaciones. En mi caso, la información final es:

Modules configured to be built:
antenna aodv applications
bridge buildings config-store
core csma csma-layout
dsdv dsr energy
fd-net-device flow-monitor internet
internet-apps lr-wpan lte
mesh mobility netanim
network nix-vector-routing olsr
point-to-point point-to-point-layout propagation
sixlowpan spectrum stats
tap-bridge test topology-read
traffic-control uan virtual-net-device
wave wifi wimax

Modules that cannot be built:
brite click mpi
openflow visualizer

-- Configuring done
-- Generating done



Si algún módulo te interesa y no está en la lista, analiza la salida del comando por que posiblemente, el script ns3 no haya encontrado algo que necesita para compilar ese módulo o no lo tiene habilitado por defecto. Mas adelante veremos las opciones para habilitar distintos módulos.
Para compilar el simulador:

./ns3 build

Si todo ha ido bien, de nuevo, debería informarse de los módulos compilados:



Como último paso, comprobamos que todos los test pasan satisfactoriamente:

ns-allinone-3.36$cd ns-3.36
ns-allinone-3.36/ns-3.36$./test.py
..
..
657 of 660 tests passed (657 passed, 3 skipped, 0 failed, 0 crashed, 0 valgrind errors)
List of SKIPped tests:
ns3-tcp-cwnd (requires NSC)
ns3-tcp-interoperability (requires NSC)
nsc-tcp-loss (requires NSC)

y vemos que hemos pasado todos los test excepto tres relacionados con tcp que requieren de Network Simulation Cradle (NSC). Deberemos habilitar esa dependencia (y compilar todo) para pasar esos test.

De esta forma, ya tendríamos el simulador listo para trabajar y poder empezar a simular nuestros escenarios.

El sistema de trazas de NS3

Una vez que hemos creado una simulación necesitamos analizar los resultados de dicha simulación. Cómo sacar los resultados de una simulación es responsabilidad del sistema de trazas de NS3. El sistema de trazas de NS3 genera, como veremos a continuación, una serie de archivos en un formato determinado que nos muestra todos los resultados de nuestra simulación. Estos formatos están definidos en los módulos existentes en NS3 y podemos añadirlos a nuestros nuevos módulos. En esta entrada vamos a centrarnos en sacar información de los módulos existentes mediante un formato determinado (pcap).

Existen dos formatos que podemos generar, archivos con extensión .tr que es heredado del simulador NS2 y archivos con extensión .pcap que es un estándar para guardar paquetes de red. Por supuesto, al ser un archivo en C++ podemos generar resultados en otros formatos, pero supondría acceder a la información directamente lo cual es más complejo e ineficiente.

No obstante, personalmente recomiendo el uso de pcap, ya que es un estándar abierto soportado por multitud de herramientas que podemos utilizar para analizar los resultados de nuestra simulación. Por ejemplo tenemos wireshark si queremos analizar el diálogo de un protocolo en nuestra simulación o scapy si queremos generar gráficas específicas mediante scripts python.

Los módulos mas relevantes de NS3 ya tienen implementados un asistente para generar trazas. Estos asistentes o Helpers nos permiten generar de forma fácil las trazas más relevantes. Vamos a generar trazas de nuestro ejemplo básico para ver cómo funciona. Tomamos nuestro basic-iot-sensors.cc y vamos a generar un archivo pcap por cada interfaz creada de nuestros tres nodos (Recordar que un nodo puede tener varias interfaces).

En principio, con habilitar la traza pcap en la capa física de wifi nos vale con lo que nos basta añadir:

wifiPhy.EnablePcapAll("resultados");

Si compilamos nuestro ejemplo de nuevo veremos que se han generado archivos pcap por cada interfaz con el formato resultados-NodeId-DeviceId.pcap. En nuestro caso tendríamos tres nodos con una única interfaz por lo que se generan los archivos resultados-0-0.pcap, resultados-1-0.pcap y resultados-2-0.pcap.
Captura wireshark

Todas las clases que heredan de la clase PcapHelperForDevice tiene este mecanismo para habilitar el pcap en todas las interfaces (Devices). Estas clases son (imagen extraída de la documentación de la clase PcapHelperForDevice):
Captura wireshark

Si se está interesado en cambios de estado de variables concretas, definidas como traceables dentro del modulo. Esto es, se pueden monitorizar sus cambios de estado, podemos definir funciones que se invoquen cuando hay un cambio en el valor de una variable. En la entrada relativa a observar el consumo de energía vemos un ejemplo de cómo observar este tipo información relativa a variables concretas (e.g. nivel de carga de una batería). Identificarás en las clases qué variables pueden ser traceadas por que son declaradas mediante una plantilla TracedValue

El sistema de log de NS3

Un buen entorno de logging te permite depurar y entender qué está pasando en tu simulación, así como entender cómo está estructurado el simulador NS3 y sus diferentes módulos.
El sistema de logging de NS3 se usa mediante variables de entorno y mediante el propio código de tu simulación si quieres habilitar logging en tu propia simulación.

Se establecen siete niveles de log proporcionando de menos a mas información:

  1. NS_LOG_ERROR: mensajes de error
  2. NS_LOG_WARN: mensajes de aviso
  3. NS_LOG_DEBUG: mensajes específicos de depuración
  4. NS_LOG_INFO: mensajes de información genéricos
  5. NS_LOG_FUNCTION: mensajes de llamadas a funciones para la trazabilidad de llamadas
  6. NS_LOG_LOGIC: mensajes de log con el flujo lógico dentro de cada función
  7. NS_LOGIC_ALL: todos los mensajes

Adicionalmente hay un nivel incondicional que imprime la salida con independencia de los niveles de log activos o no. Este es NS_LOG_UNCOND
¿Cómo visualizamos la información de log en ns3?, bien, vamos a comenzar con nuestro ejemplo IoT básico donde, precisamente, uno de los problemas es que no generábamos ningún tipo de información acerca de la simulación.
Efectivamente si lo ejecutamos, habiendo colocado previamente el archivo basic-iot-sensors.cc en el directorio scratch:

$./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 (2.047s)

Vemos que no sale ningún tipo de información útil acerca de la simulación salvo que se ha compilado y ejecutado sin errores.
Vamos a estudiar la información que nos genera el módulo de YansWifiPhy, para ello:

$ export NS_LOG=YansWifiPhy
felix@homer:~/tools/ns-allinone-3.30.1/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.701s)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x5641614960f0)
+0.000000000s -1 YansWifiPhy:SetChannel(0x5641614960f0, 0x564161490400)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x56416153d640)
+0.000000000s -1 YansWifiPhy:SetChannel(0x56416153d640, 0x564161490400)
+0.000000000s -1 YansWifiPhy:YansWifiPhy(0x564161541e90)
...

vemos que usamos la variable de entorno NS_LOG para fijar el módulo en el que estamos interesados. Si ponemos NS_LOG a un nombre de un módulo que no existe se imprimirá un listado de todos los módulos que NS3 tiene implementados. El nivel de log por defecto en la mayoría de los módulos será todos, podemos especificarlo también a la hora de definir el módulo. Por ejemplo vamos a definir el nivel info para el módulo UdpClient:

$ export NS_LOG=UdpClient=level_info
$ ./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.480s)
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 0 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 1 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 2 Time: 2
TraceDelay TX 32 bytes to 10.1.1.1 Uid: 33 Time: 2.5
....

Vemos que ahora sale la información de las aplicaciones UDP instaladas en los nodos IoT enviando información. Substituye level_info por level_all en el primer comando para que veas como sale mucha más información.
Si algún módulo no se usa en tú simulación y pones NS_LOG a ese módulo no saldrá ninguna información adicional.
Si quieres indicar que el nombre de la función que genera el mensaje también se imprima, puedes hacer un OR con prefix_fund. En bash necesitarás comillas:

$export 'NS_LOG=UdpClient=level_all|prefix_func'

Para habilitar varios módulos, puedes encadenar usando : varios módulos.

$ export 'NS_LOG=UdpClient:UdpServer'

Vamos a crear un módulo para nuestra simulación y a sacar nuestro mensaje de log. Para ello, edita el archivo basic-iot-sensors.cc y añade estas dos líneas, por ejemplo, justo debajo de donde se inicia la función main:

int main (int argc, char *argv[]){
NS_LOG_COMPONENT_DEFINE ("IotEjemplo");
NS_LOG_INFO ("Creando la simulación");

Aunque ya estaba incluido en dicho archivo, si lo utilizas en tu simulación acuerdate incluir los archivos de cabezera de log («ns3/log.h»).
En la primera función definimos el módulo IotEjemplo y en la segunda sacamos nuestro primer mensaje. Si ahora queremos ver esa información:

$export 'NS_LOG=IotEjemplo'
$ ./waf --run basic-iot-sensors
Waf: Entering directory `/ns-allinone-3.30.1/ns-3.30.1/build'
[2876/2963] Compiling scratch/basic-iot-sensors.cc
[2921/2963] Linking build/scratch/basic-iot-sensors
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 (6.339s)
IotEjemplo:main(): [INFO ] Creando la simulación

Donde, como podemos ver, se muestra nuestro mensaje.

Es buena idea usar el mecanismo de log en cualquier simulación de ns3 pero se hace imprescindible si aspiras a crear un nuevo módulo para el simulador destinado a ser usado por la comunidad.

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.