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.

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.