Archivo de la categoría: Funciones

Funciones en Node-RED

El nodo Function te permite ejecutar cualquier código JavaScript contra el mensaje. Esto te da una completa flexibilidad en lo que haces con el mensaje, pero requiere familiaridad con JavaScript y es innecesario para muchos casos simples. 

Muchas veces existen nodos ya hechos para alguna función que deseemos hacer, si eres novato, antes de hacer una función busca en https://flows.nodered.org/ si hay algún nodo o flujo que lo haga.

El nodo función es un bloque de funciones de JavaScript que se ejecuta contra los mensajes que recibe el nodo. Los mensajes se pasan como un objeto JavaScript llamado msg.

Por convención, tendrá una propiedad msg.payload que contiene el cuerpo del mensaje, además de otras propiedades opcionales como msg.topic.

Se espera que la función devuelva un objeto de mensaje (o varios objetos de mensaje), pero puede optar por no devolver nada para detener un flujo.

La pestaña Setup contiene código que se ejecutará siempre que se inicie el nodo. La pestaña Close contiene código que se ejecutará cuando se detenga el nodo.

Desde la versión 1.1.0, el nodo Función proporciona una pestaña Configuración donde puede proporcionar código que se ejecutará siempre que se inicie el nodo. Esto se puede utilizar para configurar cualquier estado que requiera el nodo Función.

Por ejemplo, puede inicializar valores en contexto local que usará la función principal:

 if (context.get("counter") === undefined) {
     context.set("counter", 0)
 } 

Tenga en cuenta que cada fragmento de código está en un ámbito separado; no puede declarar variables en uno y acceder a ellas en los demás. Necesitas usar el contexto para pasar cosas entre ellos.

La función de Setup puede devolver una Promise (promesa) si necesita completar un trabajo asincrónico antes de que la función principal pueda comenzar a procesar mensajes. Cualquier mensaje que llegue antes de que se complete la función de configuración se pondrá en cola y se manejará cuando esté listo.

Si usa código callback asíncrono en sus funciones, es posible que deba limpiar las solicitudes pendientes o cerrar cualquier conexión, cuando el nodo sea re-deployed. Puede hacer esto de dos formas diferentes.

O agregando un controlador de eventos en caso de close:

 node.on('close', function() {
     // tidy up any async code here - shutdown connections and so on.
 }); 

O, desde Node-RED 1.1.0, puede agregar código a la pestaña Close en el cuadro de diálogo de edición del nodo.

Las pestañas Setup y Close se ejecutan cuando se hace un deploy o al reiniciar Node-RED.

Mas información sobre escribir funciones en Node-RED:

Pros y contras de usar funciones en lugar de nodos: https://discourse.nodered.org/t/pros-and-cons-about-using-custom-functions-vs-the-basic-set-of-nodes/2959 

Otro nodo alternativo con procesos hijos es func-exec: https://flows.nodered.org/node/node-red-contrib-func-exec

Vídeo: https://www.youtube.com/watch?v=8XL3Zq1HjCo 

Escribir una Función

El código dentro del nodo Función representa el cuerpo de la función. La función más simple simplemente devuelve el mensaje exactamente como está: return msg;

Si la función devuelve un valor nulo, no se transmite ningún mensaje y el flujo finaliza.

La función siempre debe devolver un objeto msg. Devolver un número o una cadena resultará en un error.

El objeto del mensaje devuelto no necesita ser el mismo objeto que se pasó; la función puede construir un objeto completamente nuevo antes de devolverlo. La construcción de un nuevo objeto de mensaje perderá todas las propiedades del mensaje recibido. Esto interrumpirá algunos flujos, por ejemplo, el flujo de entrada/respuesta HTTP requiere que las propiedades msg.req y msg.res se conserven de un extremo a otro. En general, los nodos de función deben devolver el objeto de mensaje que se les pasó después de haber realizado cambios en sus propiedades.

Función que devuelve un nuevo mensaje eliminando las propiedades del mensaje original:

 var newMsg = { payload: msg.payload.length };
 return newMsg; 

Ejemplo, crear una función que pase el payload y el topic a mayúsculas:

 var payload=msg.payload; //get payload
 msg.payload=payload.toUpperCase(); //convert to uppercase
 var topic=msg.topic; //get topic
 msg.topic=topic.toUpperCase();//convert to uppercase
 return msg; 

Enviando mensajes

La función puede devolver los mensajes que quiere pasar a los siguientes nodos del flujo con return o puede llamar a node.send (mensajes).

Puede devolver o enviar:

  • un objeto de mensaje único: se pasa a los nodos conectados a la primera salida
  • una matriz de objetos de mensaje: se pasa a los nodos conectados a las salidas correspondientes (múltiples salidas)

Si algún elemento de la matriz es en sí mismo una matriz de mensajes, se envían varios mensajes a la salida correspondiente.

Si se devuelve un valor nulo, ya sea por sí mismo o como un elemento de la matriz, no se transmite ningún mensaje.

Múltiples Salidas

El diálogo de edición de funciones permite cambiar el número de salidas. Si hay más de una salida, la función puede devolver una matriz de mensajes para enviar a las salidas.

Esto facilita escribir una función que envíe el mensaje a diferentes salidas dependiendo de alguna condición. Por ejemplo, esta función enviaría cualquier cosa sobre el tema banana a la segunda salida en lugar de a la primera:

 if (msg.topic === "banana") {
    return [ null, msg ];
 } else {
    return [ msg, null ];
 } 

El siguiente ejemplo pasa el mensaje original tal cual en la primera salida y un mensaje que contiene la longitud de la carga útil se pasa a la segunda salida:

 var newMsg = { payload: msg.payload.length };
 return [msg, newMsg]; 

Ejemplo. Usamos dos nodos inject para inyectar un mensaje sobre dos temas diferentes en un nodo de función con dos salidas. La función envía el mensaje a la salida según el nombre del tema. El tema test1 va a output1 y test2 va a output2.

Código:

 var topic=msg.topic;
 if (topic=="test1"){
     return [msg,null];
 }
 if (topic=="test2"){
     return [null,msg];
 } 

Múltiples Mensajes

Una función puede devolver varios mensajes en una salida al devolver una matriz de mensajes dentro de la matriz devuelta. Cuando se devuelven varios mensajes para una salida, los nodos posteriores recibirán los mensajes uno a uno en el orden en que se devolvieron.

En el siguiente ejemplo, msg1, msg2, msg3 se enviarán a la primera salida y msg4 se enviará a la segunda salida.

 var msg1 = { payload:"first out of output 1" };
 var msg2 = { payload:"second out of output 1" };
 var msg3 = { payload:"third out of output 1" };
 var msg4 = { payload:"only message from output 2" };
 return [ [ msg1, msg2, msg3 ], msg4 ]; 

El siguiente ejemplo divide la carga útil recibida en palabras individuales y devuelve un mensaje para cada una de las palabras.

 var outputMsgs = [];
 var words = msg.payload.split(" ");
 for (var w in words) {
     outputMsgs.push({payload:words[w]});
 }
 return [outputMsgs]; 

Ejemplo. Usar un nodo inject para inyectar una cadena de prueba en el nodo de función. El nodo de función toma la cadena, pero en lugar de enviar 1 mensaje, tiene un bucle for que crea 3 mensajes y los coloca en un array.

Código:

 var m_out=[]; //array for message objects
 var message=msg.payload;
 for (i=0;i<3;i++){
     message=message+i; //add count to message
     var newmsg={payload:message,topic:msg.topic}
     m_out.push(newmsg);
 }
 return [m_out]; 

Enviar mensajes de forma asincrónica

Si la función necesita realizar una acción asincrónica antes de enviar un mensaje, no puede devolver el mensaje al final de la función con return.

En su lugar, debe hacer uso de la función node.send(), pasando los mensajes que se enviarán.

Por ejemplo:

 doSomeAsyncWork(msg, function(result) {
     msg.payload = result;
     node.send(msg);
 });
 return; 

Esto es útil, por ejemplo, al recorrer una matriz u objeto y enviar datos a medida que se leen. Por ejemplo:

 count=0;
 for(var i=0;i<10;i++)
 {
 msg.payload=count;
 node.send(msg)
 count+=1; 
 } 

Ejemplo: ver como al mandar send, los mensajes se procesan de forma simultánea al ser asíncrono:

 for(var i=0;i<10;i++) {
     msg.payload=i;
     await new Promise(r => setTimeout(r, 1000)); //sleep(1000)
     node.send(msg);
 } 

Desde la versión Node-RED 1.0, el nodo Función clonará cada objeto de mensaje que pase a node.send para garantizar que no haya modificaciones no intencionales de los objetos de mensaje que se reutilicen en la función.

La función puede solicitar al tiempo de ejecución (runtime) que no clone el primer mensaje pasado a node.send pasando falso como segundo argumento de la función. Haría esto si el mensaje contiene algo que de otra manera no se puede clonar, o por razones de rendimiento para minimizar la sobrecarga de enviar mensajes: node.send(msg,false);

Si un nodo Función realiza un trabajo asincrónico con un mensaje, el tiempo de ejecución no sabrá automáticamente cuándo ha terminado de manejar el mensaje.

Para ayudarlo a hacerlo, el nodo Función debe llamar a node.done() en el momento apropiado. Esto permitirá que el tiempo de ejecución rastree correctamente los mensajes a través del sistema.

 doSomeAsyncWork(msg, function(result) {
     msg.payload = result;
     node.send(msg);
     node.done();
 });
 return; 

Registro y manejo de errores

Para registrar cualquier información o reportar un error, las siguientes funciones están disponibles:

  • node.log(«Log message»)
  • node.warn(«Warning»)
  • node.error(«Error»)

Los mensajes de warn y error también se envían a la pestaña de debug del editor de flujo.

Para un registro más fino, también están disponibles node.trace() y node.debug(). Si no hay ningún logger configurado para capturar esos niveles, no se verán.

Si la función encuentra un error que debería detener el flujo actual, no debería devolver nada. El nodo Catch se puede utilizar para manejar errores. Para invocar un nodo Catch, pase msg como segundo argumento a node.error: node.error(«Error», msg);

Acceder a la información del nodo

En el bloque de funciones, se puede hacer referencia a la identificación y el nombre del nodo mediante las siguientes propiedades:

  • node.id – id del nodo
  • node.name – nombre del nodo

Uso de variables de entorno

Se puede acceder a las variables de entorno usando: env.get («MY_ENV_VAR»).

Variables de entorno: https://nodered.org/docs/user-guide/environment-variables 

Almacenamiento de datos

Aparte del objeto msg, la función también puede almacenar datos en el almacén de contexto: https://nodered.org/docs/user-guide/context 

En el nodo Función hay tres variables predefinidas que se pueden usar para acceder al contexto:

  • context: el contexto local del nodo
  • flow: el contexto del alcance del flujo
  • global: el contexto de alcance global

Estas variables predefinidas son una característica del nodo Función y no se usan en la creación de nodos.

Hay dos modos de acceder al contexto; ya sea sincrónico o asincrónico. Los almacenes de contexto integrados proporcionan ambos modos. Es posible que algunos almacenes solo den acceso asincrónico y arrojará un error si se accede a ellas de manera sincrónica.

Para obtener un valor del contexto: var myCount = flow.get(«count»);

Para establecer un valor: flow.set(«count», 123);

El siguiente ejemplo mantiene un recuento de cuántas veces se ha ejecutado la función:

 // initialise the counter to 0 if it doesn't exist already
 var count = context.get('count')||0;
 count += 1;
 // store the value back
 context.set('count',count);
 // make it part of the outgoing msg object
 msg.count = count;
 return msg; 

get/set multiples valores:

 var values = flow.get(["count", "colour", "temperature"]);
 // values[0] is the 'count' value
 // values[1] is the 'colour' value
 // values[2] is the 'temperature' value
 flow.set(["count", "colour", "temperature"], [123, "red", "12.5"]); 

El contexto global se puede rellenar previamente con objetos cuando se inicia Node-RED. Esto se define en el archivo principal settings.js bajo la propiedad functionGlobalContext.

Esto se puede utilizar para cargar módulos adicionales dentro del nodo Función: https://nodered.org/docs/user-guide/writing-functions#loading-additional-modules 

Acceso al contexto asincrónico

Si el almacén de contexto requiere acceso asincrónico, las funciones get y set requieren un parámetro de devolución de llamada adicional.

 // Get single value
 flow.get("count", function(err, myCount) { ... });
 // Get multiple values
 flow.get(["count", "colour"], function(err, count, colour) { ... })
 // Set single value
 flow.set("count", 123, function(err) { ... })
 // Set multiple values
 flow.set(["count", "colour", [123, "red"], function(err) { ... }) 

El primer argumento pasado a la callback, err, solo se establece si ocurrió un error al acceder al contexto.

La versión asincrónica del ejemplo de recuento se convierte en:

 context.get('count', function(err, count) {
     if (err) {
         node.error(err, msg);
     } else {
         // initialise the counter to 0 if it doesn't exist already
         count = count || 0;
         count += 1;
         // store the value back
         context.set('count',count, function(err) {
             if (err) {
                 node.error(err, msg);
             } else {
                 // make it part of the outgoing msg object
                 msg.count = count;
                 // send the message
                 node.send(msg);
             }
         });
     }
 }); 

Múltiples Almacenes de Contexto

Es posible configurar múltiples almacenes de contexto. Por ejemplo, se podría utilizar un almacén basado en memoria y en archivos.

Las funciones de contexto get/set aceptan un parámetro opcional para identificar el almacén a utilizar.

 // Get value - sync
 var myCount = flow.get("count", storeName);
 // Get value - async
 flow.get("count", storeName, function(err, myCount) { ... });
 // Set value - sync
 flow.set("count", 123, storeName);
 // Set value - async
 flow.set("count", 123, storeName, function(err) { ... }) 

Añadir Estado

El nodo de función también puede proporcionar su propia decoración de estado de la misma forma que otros nodos. Para establecer el estado, llame a la función node.status. Por ejemplo:

 node.status({fill:"red",shape:"ring",text:"disconnected"});
 node.status({fill:"green",shape:"dot",text:"connected"});
 node.status({text:"Just text status"});
 node.status({});   // to clear the status 

Para los parámetros aceptados en el estado ver https://nodered.org/docs/creating-nodes/status

  • fill – red, green, yellow, blue or grey
  • shape – Ring or Dot

Las actualizaciones de estado también pueden ser capturadas por el nodo Estado.

Carga de módulos adicionales

Si se necesita usar un módulos de node.js es necesario activarlos previamente. Los módulos de nodo adicionales no se pueden cargar directamente dentro de un nodo de función. Deben cargarse en el archivo settings.js y agregarse a la propiedad functionGlobalContext.

Por ejemplo, el módulo del sistema operativo integrado puede estar disponible para todas las funciones agregando lo siguiente al archivo settings.js.

 functionGlobalContext: {
     osModule:require('os')
 } 

En ese momento, se puede hacer referencia al módulo dentro de una función como global.get (‘osModule’).

Los módulos cargados desde el archivo de configuración deben instalarse en el mismo directorio que el archivo de configuración. Para la mayoría de los usuarios, ese será el directorio de usuario predeterminado: ~/.node-red: 

 cd ~/.node-red
 npm install name_of_3rd_party_module 

Reusar Nodos de Función

Se pueden guardar los nodos de función en la library y reutilizarlos en otros flujos importándolos. Para guardar una función en la library, haga doble clic en la función para editarla y haga clic en el icono de marcador junto al nombre de la función. Aparece un menú desplegable para importar o guardar la función.

También se puede utilizar un subflow para almacenar funciones. El uso de un subflujo los hace disponibles como nodos que puede seleccionar en la paleta de nodos de la izquierda.

Otros Objetos Disponibles

Objetos disponibles en el nodo función:

context

  • context.get(..): get a node-scoped context property
  • context.set(..): set a node-scoped context property
  • context.keys(..): return a list of all node-scoped context property keys
  • context.flow : same as flow
  • context.global : same as global

flow

  • flow.get(..) : get a flow-scoped context property
  • flow.set(..) : set a flow-scoped context property
  • flow.keys(..) : return a list of all flow-scoped context property keys

global

  • global.get(..) : get a global-scoped context property
  • global.set(..) : set a global-scoped context property
  • global.keys(..) : return a list of all global-scoped context property keys

RED

  • RED.util.cloneMessage(..) : safely clones a message object so it can be reused

env

  • env.get(..) : get an environment variable

El nodo Función también pone a disposición los siguientes módulos y funciones:

  • Buffer – the Node.js Buffer module
  • console – the Node.js console module (node.log is the preferred method of logging)
  • util – the Node.js util module
  • setTimeout/clearTimeout – the javascript timeout functions.
  • setInterval/clearInterval – the javascript interval functions.

Iniciación a Arduino en 3 Directos de YouTube

En estos tres directos hechos en marzo de 2020 hago una iniciación a Arduino.

Primer Directo

Hablo de:

Enlaces del vídeo:

Segundo Directo

Hablo de:

  • Uso de una pantalla LCD con Arduino
  • Instalar una librería
  • Funciones Arduino
  • Debug de un Sketch de Arduino

Enlaces del vídeo:

Tercer Directo

Hablo de:

Enlaces del Vídeo:

Prácticas: Funciones Definidas por Usuario

Montaje Arduino UNO:

Montaje Wemos:

Ejercicio15 – Funciones

Hacer un menú interactivo con Arduino. Con todo lo visto anteriormente, hacer un ejemplo de un menú interactivo donde se dan 4 opciones y pulsando cada una de ellas se ejecuta una acción concreta. Si el valor pulsado no es ninguna de las opciones avisar y volver a mostrar el menú hasta que se pulse una opción correcta. Usar funciones para cada una de las opciones.

Opciones:

  • 1 – Encender led siguiente (paso por referencia la posición del led)
  • 2 – Sacar por pantalla el LCD que está encendido
  • 3 – Sonar el buzzer 5 segundos
  • 4 – Fin (entra en un bucle infinito y no sale)

Solución: https://codeshare.io/5NYRvm

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio15-Funciones

Ejercicio16 – Función Detecta flanco

Señales digitales:

Hacer una función que detecte flancos ascendentes y otras flancos descendentes, para ser reutilizada en otros proyectos.

Unificar estas dos funciones en una única función llamada detectaFlanco() donde le paso el pin y devuelve 1 si es flanco ascendente, -1 si es flanco descendente y 0 si no hay cambio de estado.

Ponerla en un ejemplo con alguno de los botones, usando este loop:

 
void loop() {
  int flanco = detectaFlanco(PIN_BOTON_A);
  if (flanco == 1)
    Serial.println("flanco ascendente");
  if (flanco == -1)
    Serial.println("flanco descendente");
}

Solución: https://codeshare.io/amkrV1

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio16-Funcion_Detecta_Flanco

Ejercicio17 – Función Detecta flanco dos pines

Para ejercicio detecta flanco, probar la función con los dos botones en los pines 2 y 3. La función detecta flanco solo funciona con un pulsador, pero cuando se intenta usar con dos pulsadores ya no funciona. Comprobar porqué.

Solución: https://codeshare.io/5NYrqr

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio17-Funcion_Detecta_Flanco_2Pines

La función para detectar flanco es la base para luego entender las clases y objetos y luego las librerías.

La solución es crear un objeto detecta flanco, para ello crear una clase y se puede distribuir mediante una librería como https://github.com/jecrespo/Detecta_Flanco_Libreria que se puede descargar desde https://github.com/jecrespo/Detecta_Flanco_Libreria/releases/tag/Version_1.0:

#include <DetectaFlanco.h>
#define PIN_BOTON_A 2
#define PIN_BOTON_B 3

DetectaFlanco df1(PIN_BOTON_A);
DetectaFlanco df2(PIN_BOTON_B);

void setup() {
  Serial.begin(9600);
  df1.inicio(INPUT_PULLUP);
  df2.inicio(INPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  int flanco1 = df1.comprueba();
  int flanco2 = df2.comprueba();

  if (flanco1 == 1)
    Serial.println("Flanco asc A");

  if (flanco1 == -1)
    Serial.println("Flanco desc A");

  if (flanco2 == 1)
    Serial.println("Flanco asc B");

  if (flanco2 == -1)
    Serial.println("Flanco asc B");

  delay(50); //Evitar rebotes
}

Ejercicio18 – Dado Digital

Usando las funciones de números aleatorios hacer un dado digital que genere un número aleatorio entre 1 y 6 y encienda un led aleatorio cada vez que se pulse el botón A. Usar el montaje del Wemos D1 mini

Usar la función de detección de flanco hecha en el anterior ejercicio.

Random Numbers

  • randomSeed() – Inicializa el generador de número pseudo-aleatorios
  • random() – Genera números pseudo-aleatorios

Paso 1 – Generar un valor aleatorio entre 1 y 6 al pulsar el botón

Paso 2 – Hacer girar el anillo led haciendo el efecto y que baje la velocidad

Paso 2 – Dejar fijo el nuevo número aleatorio

Solución: https://codeshare.io/anmypv

Hacer commit y pull del código en el repositorio “Curso Programacion Arduino 2019” que esté en una carpeta llamada Ejercicio18-Dado

Funciones Definidas por Usuario Avanzado

Una función es un bloque de código que tiene un nombre y un conjunto de instrucciones que son ejecutadas cuando se llama a la función. Son funciones setup() y loop() de las que ya se ha hablado.

Una función tiene un nombre y un conjunto de instrucciones que son ejecutadas cuando se llama a la función. Son funciones setup() y loop() de las que ya se ha hablado anteriormente.

Las funciones de usuario pueden ser escritas para realizar tareas repetitivas y para reducir el tamaño de un programa. Segmentar el código en funciones permite crear piezas de código que hacen una determinada tarea y volver al área del código desde la que han sido llamadas.

Una función puede llamarse múltiples veces e incluso llamarse a sí misma (función recurrente). Las funciones pueden recibir datos desde afuera al ser llamadas a través de los parámetros y puede entregar un resultado.

Las funciones se declaran asociadas a un tipo de valor. Este valor será el que devolverá la función, por ejemplo ‘int’ se utilizará cuando la función devuelva un dato numérico de tipo entero. Si la función no devuelve ningún valor entonces se colocará delante la palabra “void”, que significa “función vacía”

Sintaxis:

 
tipo nombreFunción (parámetros) {
   instrucciones;
}

Para llamar a una función, simplemente:

 
nombreFunción(parámetros);

En una función que devuelve un valor siempre debe tener la instrucción Return, este termina una función y devuelve un valor a quien ha llamado a la función: http://arduino.cc/en/Reference/Return

Si se define una función y no ponemos return el valor devuelto es cero. No da error de compilación.

Ventajas del uso de funciones:

  • Ayuda a tener organizado el código.
  • Una función codifica una tarea en un lugar de nuestro sketch, así que la función solo debe ser pensada una sola vez.
  • Reduce la probabilidad de errores al modificar el código.
  • Hacen que el tamaño del sketch sea menor porque el código de la función es reutilizado.
  • Facilita la lectura del código.
  • Hace más sencillo reutilizar código en otros sketches.

Más información: http://arduino.cc/en/Reference/FunctionDeclaration

Nombres de funciones

Generalmente los nombres de las funciones deben ser en minúscula, con las palabras separadas por un guión bajo, aplicándose éstos tanto como sea necesario para mejorar la legibilidad.

“mixedCase” (primera palabra en minúscula) es aceptado únicamente en contextos en donde éste es el estilo predominante con el objetivo de mantener la compatibilidad con versiones anteriores.

En el caso de las clases, los nombres deben utilizar la convención “CapWords” (palabras que comienzan con mayúsculas).

Las funciones en Arduino pueden estar dentro del mismo fichero .ino o en otro fichero con extensión .ino dentro del directorio del sketch.

Paso por Valor y Paso por Referencia

Hasta ahora siempre hemos declarado los parámetros de nuestras funciones del mismo modo. Sin embargo, éste no es el único modo que existe para pasar parámetros.

La forma en que hemos declarado y pasado los parámetros de las funciones hasta ahora es la que normalmente se conoce como «por valor«. Esto quiere decir que cuando el control pasa a la función, los valores de los parámetros en la llamada se copian a «objetos» locales de la función, estos «objetos» son de hecho los propios parámetros.

 
int funcion(int n, int m) {
  n = n + 2;
  m = m - 5;
  return n+m;
}

int a = 10;
int b = 20;

Serial.println(funcion(a,b));
Serial.println(funcion(10,20));

Empezamos haciendo a = 10 y b = 20, después llamamos a la función «funcion» con las objetos a y b como parámetros. Dentro de «funcion» esos parámetros se llaman n y m, y sus valores son modificados. Sin embargo al retornar al programa que lo llama, a y b conservan sus valores originales. Lo que pasamos no son los objetos a y b, sino que copiamos sus valores a los objetos n y m. Es lo mismo que hacer funcion(10,20), cuando llamamos a la función con parámetros constantes. Si los parámetros por valor no funcionasen así, no sería posible llamar a una función con valores constantes o literales.

Las referencias sirven para definir «alias» o nombres alternativos para un mismo objeto. Para ello se usa el operador de referencia (&).

Por ejemplo: 

 
int a;
int& r = a;  //Referencia a entero y debe siempre inicializarse

a = 10;
Serial.println(r);

En este ejemplo los identificadores a y r se refieren al mismo objeto, cualquier cambio en una de ellos se produce en el otro, ya que son, de hecho, el mismo objeto. El compilador mantiene una tabla en la que se hace corresponder una dirección de memoria para cada identificador de objeto. A cada nuevo objeto declarado se le reserva un espacio de memoria y se almacena su dirección. En el caso de las referencias, se omite ese paso, y se asigna la dirección de otro objeto que ya existía previamente. De ese modo, podemos tener varios identificadores que hacen referencia al mismo objeto, pero sin usar punteros.

El operador de dirección (&) devuelve la dirección de memoria de cualquier objeto

Los punteros se declaran igual que el resto de los objetos, pero precediendo el identificador con un asterisco (*)

 
int *pEntero;
int x = 10;
int y;

pEntero = &y; //pEntero apunta a la variable entera y
*pEntero = x; // y = 10

Más información sobre punteros: https://es.stackoverflow.com/questions/46909/cual-es-la-diferencia-entre-int-e-int

Si queremos que los cambios realizados en los parámetros dentro de la función se conserven al retornar de la llamada, deberemos pasarlos por referencia. Esto se hace declarando los parámetros de la función como referencias a objetos. Por ejemplo:

 
int funcion(int &n, int &m) {
  n = n + 2;
  m = m - 5;
  return n+m;
}

int a = 10;
int b = 20;

Serial.println(funcion(a,b));
Serial.println("a = " + String(a) + " b = " + String(b));
Serial.println(funcion(10,20)); //es ilegal pasar constantes como parámetros cuando estos son referencias

En este caso, los objetos «a» y «b» tendrán valores distintos después de llamar a la función. Cualquier cambio de valor que realicemos en los parámetros dentro de la función, se hará también en los objetos referenciadas. Esto quiere decir que no podremos llamar a la función con parámetros constantes, ya que aunque es posible definir referencias a constantes, en este ejemplo, la función tiene como parámetros referencias a objetos variables. Y si bien es posible hacer un casting implícito de un objeto variable a uno constante, no es posible hacerlo en el sentido inverso. Un objeto constante no puede tratarse como objeto variable.

Una const reference es una referencia a que no permite cambiar la variable a través de esa referencia. Por ejemplo const int& r = a; en r tengo el valor de a pero no puedo cambiar el valor de a usando r.

No confundir este concepto con el modificador de variable static, que es utilizado para crear variables que solo son visibles dentro de una función, sin embargo, al contrario que las variables locales que se crean y destruyen cada vez que se llama a la función, las variables estáticas mantienen sus valores entre las llamadas a las funciones.

Más información:

Sobrecarga de Funciones

Hay operadores que tienen varios usos, como por ejemplo *, &, << o >>. Esto es lo que se conoce en C++ como sobrecarga de operadores. Con las funciones existe un mecanismo análogo, de hecho, en C++, los operadores no son sino un tipo especial de funciones, aunque eso sí, algo peculiares.

En C++ podemos definir varias funciones con el mismo nombre, con la única condición de que el número y/o el tipo de los argumentos sean distintos. El compilador decide cuál de las versiones de la función usará después de analizar el número y el tipo de los parámetros. Si ninguna de las funciones se adapta a los parámetros indicados, se aplicarán las reglas implícitas de conversión de tipos.

Las ventajas son más evidentes cuando debemos hacer las mismas operaciones con objetos de diferentes tipos o con distinto número de objetos. También pueden usarse macros para esto, pero no siempre es posible usarlas, y además las macros tienen la desventaja de que se expanden siempre, y son difíciles de diseñar para funciones complejas. Sin embargo las funciones serán ejecutadas mediante llamadas, y por lo tanto sólo habrá una copia de cada una.

Ejemplo:

 
int mayor(int a, int b);
char mayor(char a, char b);
float mayor(float a, float b);
int mayor(int a, int b, int c, int d);

int mayor(int a, int b) {
  if(a > b) return a; else return b;
}

char mayor(char a, char b) {
  if(a > b) return a; else return b;
}

float mayor(float a, float b) {
  if(a > b) return a; else return b;
}

int mayor(int a, int b, int c, int d) {
  return mayor(mayor(a, b), mayor(c, d));
}

Las llamadas a funciones sobrecargadas se resuelven en la fase de compilación. Es el compilador el que decide qué versión de la función debe ser invocada, después de analizar, y en ciertos casos, tratar los argumentos pasados en la llamadas. A este proceso se le llama resolución de sobrecarga.

Tener en cuenta que el tipo de retorno de la función no se considera en la sobrecarga de funciones. Consideremos el caso en el que desea escribir una función que devuelve un número aleatorio, pero se necesita una versión que devolverá un entero, y otra versión que devolverá un doble.

int getRandomValue();
double getRandomValue();

Pero el compilador toma esto como un error. Estas dos funciones tienen los mismos parámetros (ninguno) y en consecuencia, la segunda getRandomValue () serán tratada como una redeclaración errónea de la primera. En consecuencia, tendrán que ser dado diferentes nombres a estas funciones.

Más información:

Sobrecarga de operadores:

Ámbito de las variables

Una variable puede ser declarada al inicio del programa antes de la parte de configuración setup(), a nivel local dentro de las funciones, y, a veces, dentro de un bloque, como para los bucles del tipo if.. for.., etc. En función del lugar de declaración de la variable así se determinará el ámbito de aplicación, o la capacidad de ciertas partes de un programa para hacer uso de ella.

Una variable global es aquella que puede ser vista y utilizada por cualquier función y estamento de un programa. Esta variable se declara al comienzo del programa, antes de setup().

Recordad que al declarar una variable global, está un espacio en memoria permanente en la zona de static data y el abuso de variables globales supone un uso ineficiente de la memoria.

Una variable local es aquella que se define dentro de una función o como parte de un bucle. Sólo es visible y sólo puede utilizarse dentro de la función en la que se declaró. Por lo tanto, es posible tener dos o más variables del mismo nombre en diferentes partes del mismo programa que pueden contener valores diferentes, pero no es una práctica aconsejable porque complica la lectura de código.

En el reference de Arduino hay una muy buena explicación del ámbito de las variables:http://arduino.cc/en/Reference/Scope

La variables estáticas solo se crean e inicializan la primera vez que la función es llamada. Ver ejemplo en: http://arduino.cc/en/Reference/Static

Más información:

NOTA: Hay una práctica del ámbito de las variables en el capítulo del curso de programación: “Prácticas: Variables y Tipos de Datos en Arduino”, Ejercicio07-Ambito_Variables

Inline

Cuando usamos el nombre de una función, indicando valores para sus argumentos, dentro de un programa, decimos que llamamos o invocamos a esa función. Esto quiere decir que el procesador guarda la dirección actual, «salta» a la dirección donde comienza el código de la función, la ejecuta, recupera la dirección guardada previamente, y retorna al punto desde el que fue llamada.

Esto es cierto para las funciones que hemos usado hasta ahora, pero hay un tipo especial de funciones que trabajan de otro modo. En lugar de existir una única copia de la función dentro del código, si se declara una función como inline, lo que se hace es insertar el código de la función, en el lugar (y cada vez) que sea llamada. Esta indica al compilador que cada llamada a la función inline deberá ser reemplazado por el cuerpo de esta función. En la práctica la función inline es utilizado sólo cuando las funciones son pequeñas para evitar generar un ejecutable de tamaño considerable.

La palabra reservada inline tiene la ventaja de acelerar un programa si éste invoca regularmente a la función inline. Permite reducir considerablemente el código, en particular para los accesadores de una clase. Un accesador de clase es típicamente una función de una línea. 

Ejemplo:

 
inline int mayor(int a, int b) {
  if(a > b) return a;
  return b;
}

Más información de inline:

Prototipos de Funciones

Primero recordar que en el lenguaje de Arduino al contrario que en estandar C, no es necesario declarar los prototipos de las funciones, puesto que de eso se encarga el de incluirlo el arduino builder, al igual que de añadir la función Main.

En C++ es obligatorio usar prototipos. Un prototipo es una declaración de una función. Consiste en una presentación de la función, exactamente con la misma estructura que la definición, pero sin cuerpo y terminada con un «;». 

En general, el prototipo de una función se compone de las siguientes secciones:

  • Opcionalmente, una palabra que especifique el tipo de almacenamiento, puede ser extern o static. Si no se especifica ninguna, por defecto será extern.
  • El tipo del valor de retorno, que puede ser void, si no necesitamos valor de retorno.
  • Modificadores opcionales. 
  • El identificador de la función. Es costumbre, muy útil y muy recomendable, poner nombres que indiquen, lo más claramente posible, qué es lo que hace la función, y que permitan interpretar qué hace el programa con sólo leerlos.
  • Una lista de declaraciones de parámetros entre paréntesis. Los parámetros de una función son los valores de entrada (y en ocasiones también de salida). 

Un prototipo sirve para indicar al compilador los tipos de retorno y los de los parámetros de una función, de modo que compruebe si son del tipo correcto cada vez que se use esta función dentro del programa, o para hacer las conversiones de tipo cuando sea necesario.

Normalmente, los prototipos de las funciones se declaran dentro del fichero del programa, o bien se incluyen desde un fichero externo, llamado fichero de cabecera, (para esto se usa la directiva #include).

Ya lo hemos dicho más arriba, pero las funciones son extern por defecto. Esto quiere decir que son accesibles desde cualquier punto del programa, aunque se encuentren en otros ficheros fuente del mismo programa. En contraposición las funciones declaradas static sólo son accesibles dentro del fichero fuente donde se definen.

Si programamos en Arduino clases o librerías, es posible que debamos utilizar los prototipos de funciones.

Más información: 

Bibliotecas/Librerías en C++

Junto con los compiladores de C y C++, se incluyen ciertos archivos llamados bibliotecas más comúnmente librerías. Las bibliotecas contienen el código objeto de muchos programas que permiten hacer cosas comunes, como leer el teclado, escribir en la pantalla, manejar números, realizar funciones matemáticas, etc.

Las bibliotecas están clasificadas por el tipo de trabajos que hacen, hay bibliotecas de entrada y salida, matemáticas, de manejo de memoria, de manejo de textos y como imaginarás existen muchísimas librerías disponibles y todas con una función específica.

La declaración de librerías, tanto en C como en C++, se debe hacer al principio de todo nuestro código, antes de la declaración de cualquier función o línea de código, debemos indicarle al compilador que librerías usar, para el saber qué términos están correctos en la escritura de nuestro código y cuáles no. La sintaxis es la siguiente: #include <nombre de la librería> o alternativamente #include «nombre de la librería». En tu código puedes declarar todas las librerías que quieras aunque en realidad no tienen sentido declarar una librería que no vas a usar en tu programa, sin embargo no existe límite para esto.

La directiva de preprocesador #include se usa en los lenguajes C y C++ para “incluir” las declaraciones de otro fichero en la compilación. Esta directiva no tiene más misterio para proyectos pequeños. En cambio, puede ayudar aprovechar bien esta directiva en proyectos con un gran número de subdirectorios.

Ejemplo:

 
#include "iostream"
#include "string"
#include <math.h>
using namespace std;

Lo único adicional, es la línea que dice using namespace std; esta línea nos ayuda a declarar un espacio de nombre que evita tener que usarlo cada que accedemos a alguna función específica de una librería. Teniendo este namespace declarado podemos llamar por ejemplo el comando cout >>, que pertenece a la librería iostream, sin embargo sin este namespace sería std::cout >>, imagina tener que hacer esto cada vez que uses algún comando o función de las librerías, sería bastante tedioso.

Algunas de las librerías de uso más común de C++ y que forman parte de las librerías estándar de este lenguaje.

  • fstream: Flujos hacia/desde ficheros. Permite la manipulación de archivos desde el programar, tanto leer como escribir en ellos.
  • iosfwd: Contiene declaraciones adelantadas de todas las plantillas de flujos y sus typedefs estándar. Por ejemplo ostream.
  • iostream: Parte del a STL que contiene los algoritmos estándar, es quizá la más usada e importante (aunque no indispensable).
  • math: Contiene los prototipos de las funciones y otras definiciones para el uso y manipulación de funciones matemáticas.
  • memory: Utilidades relativas a la gestión de memoria, incluyendo asignadores y punteros inteligentes (auto_ptr). «auto_ptr» es una clase que conforma la librería memory y permite un fácil manejo de punteros y su destrucción automáticamente.
  • ostream: Algoritmos estándar para los flujos de salida.
  • Librería stdio: Contiene los prototipos de las funciones, macros, y tipos para manipular datos de entrada y salida.
  • Librería stdlib: Contiene los prototipos de las funciones, macros, y tipos para utilidades de uso general.
  • string: Parte de la STL relativa a contenedores tipo string; una generalización de las cadenas alfanuméricas para albergar cadenas de objetos.
  • vector: Parte de la STL relativa a los contenedores tipo vector; una generalización de las matrices unidimensionales C/C++
  • list: Permite implementar listas doblemente enlazadas (listas enlazadas dobles) fácilmente.
  • iterator: Proporciona un conjunto de clases para iterar elementos.
  • regex: Proporciona fácil acceso al uso de expresiones regulares para la comparación de patrones.
  • thread: Útil para trabajar programación multihilos y crear múltiples hilos en nuestra aplicación.

Arduino ya incluye diversas librerías por defecto sin llamarlas explícitamente, p.e. math.h

Más información:

Software para visualizar dependencias de ficheros:

Funciones Anónimas y Lambda

En la programación, una función anónima (función literal, abstracción lambda o expresión lambda) es una definición de función que no está vinculada a un identificador. Las funciones anónimas son a menudo argumentos que se pasan a funciones de orden superior, o que se utilizan para construir el resultado de una función de orden superior que necesita devolver una función. Si la función sólo se utiliza una vez, o un número limitado de veces, una función anónima puede ser sintácticamente más ligera que la que se utiliza con una función nombrada. Las funciones anónimas se utilizan en casi todos los lenguajes de programación.

En algunos lenguajes de programación, las funciones anónimas se implementan comúnmente para propósitos muy específicos, como la vinculación de eventos funciones de callback, o la instanciación de la función para valores particulares, que pueden ser más eficientes, más legibles y menos propensos a errores que la llamada a una función con nombre más genérico.

C++ soporta funciones anónimas, llamadas expresiones lambda, que tienen la forma::

[capture](parameters) -> return_type { function_body }

Un ejemplo de función lambda se define de la siguiente manera:

 
[ ](int x, int y) -> int { return x + y; }

C++11 también soporta cierres. Los cierres (closures) se definen entre corchetes ([,]) en la declaración de expresión lambda. El mecanismo permite capturar estas variables por valor o por referencia. La siguiente tabla lo demuestra:

  • [ ]        //no variables defined. Attempting to use any external variables in the lambda is an error.
  • [ x, &y ]   //x is captured by value, y is captured by reference
  • [ & ]       //any external variable is implicitly captured by reference if used
  • [ = ]       //any external variable is implicitly captured by value if used
  • [ &, x ]    //x is explicitly captured by value. Other variables will be captured by reference
  • [ =, &z ]   //z is explicitly captured by reference. Other variables will be captured by value

Más información:

Librerías vs Funciones en Arduino

Si en nuestro código tenemos múltiples funciones, podemos incluirlas en un fichero de C++ por ejemplo funciones.h, e incluyéndose en el principal con la instrucción #include “funciones.h”. En C++ el código se organiza en diferentes ficheros con extensiones .h y .cpp a los que se van llamando con #include para añadirlos al fichero que lo llama para poder usar su contenido.

Ejemplo: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio45-Librerias

Como se ha visto anteriormente, las librerías son trozos de código hechas por terceros que usamos en nuestro sketch. Esto nos facilita mucho la programación y hace que nuestro programa sea más sencillo de hacer y luego de entender. Más adelante veremos cómo hacer una librería.

Las librerías en Arduino incluyen los siguientes archivos comprimidos en un archivo ZIP o dentro de un directorio. Estas siempre contienen:

  • Un archivo .cpp (código de C++)
  • Un archivo .h o encabezado de C, que contiene las propiedades y métodos o funciones de la librería.
  • Un archivo Keywords.txt, que contiene las palabras clave que se resaltan en el IDE (opcional).
  • Muy posiblemente la librería incluye un archivo readme con información adicional de lo que hace y con instrucciones de como usarla.
  • Directorio denominado examples con varios sketchs de ejemplo que nos ayudará a entender cómo usar la librería (opcional).

Más información: https://aprendiendoarduino.wordpress.com/2017/06/20/librerias-arduino-3/ 

Una librería a diferencia de las funciones debe estar al menos en un fichero diferente con extensión .h y opcionalmente en otro .cpp y además debe ser llamada con #include desde el sketch de arduino y estar en una ruta accesible desde el IDE de Arduino, ya sea el mismo directorio del sketch o en algunas de las rutas configuradas para librerías. 

La ventaja de usar librerías frente a las funciones es que no es necesario incluir el código cada vez que se va a reutilizar sino que con tener la librería instalada en el IDE y llamarla mediante #include ya la puedo usar en mi código.

Al llamar a una librería desde un sketch, la librería completa es cargada a la placa de Arduino incrementando el tamaño del espacio usado en el microcontrolador, tanto en la memoria flash como en la RAM.

Las librerías que usamos para los sketches tienen una versión, que se suelen actualizar con frecuencia. También tenemos un control de versiones en el nuevo IDE a partir de 1.6.4 que nos facilita la gestión de la versión de las librerías usadas. Este aspecto es importante porque un sketch que funciona con una versión de una librería es posible que al compilarlo con otra versión en otro IDE no funcione. Por ello es importante documentar con que versión de librería está hecho o distribuir el sketch con la librería con la que se ha creado. Generalmente las librerías tienen compatibilidad hacia atrás, pero puede que no ocurra o que el comportamiento de la librería sea diferente.

Al cambiar el IDE también nos podemos encontrar que nuestro sketch no es compatible con la versión de la librería que estemos usando, que es diferente con la que se diseñó originalmente el sketch.

Listado de librerías Arduino:

Funciones Definidas por Usuario

En programación, una función es un grupo de instrucciones con un objetivo particular y que se ejecuta al ser llamada desde otra función o procedimiento. Una función puede llamarse múltiples veces e incluso llamarse a sí misma (función recurrente).

Las funciones pueden recibir datos desde afuera al ser llamadas a través de los parámetros y puede entregar un resultado.

Una función tiene un nombre y un conjunto de instrucciones que son ejecutadas cuando se llama a la función. Son funciones setup() y loop() de las que ya se ha hablado anteriormente.

Las funciones de usuario pueden ser escritas para realizar tareas repetitivas y para reducir el tamaño de un programa. Segmentar el código en funciones permite crear piezas de código que hacen una determinada tarea y volver al área del código desde la que han sido llamadas.

Las funciones se declaran asociadas a un tipo de valor. Este valor será el que devolverá la función, por ejemplo ‘int’ se utilizará cuando la función devuelva un dato numérico de tipo entero. Si la función no devuelve ningún valor entonces se colocará delante la palabra “void”, que significa “función vacía”

Sintaxis:

 
tipo nombreFunción (parámetros) {
   instrucciones;
}

Anatomía de una función en C:

Para llamar a una función, simplemente:

 
nombreFunción(parámetros);

Una función que devuelve un valor siempre debe tener la instrucción Return, esta termina una función y devuelve un valor a quien ha llamado a la función: http://arduino.cc/en/Reference/Return

Si se define una función y no ponemos return el valor devuelto es cero. No da error de compilación.

Ventajas del uso de funciones:

  • Ayuda a tener organizado el código.
  • Una función codifica una tarea en un lugar de nuestro sketch, así que la función solo debe ser pensada una sola vez.
  • Reduce la probabilidad de errores al modificar el código.
  • Hacen que el tamaño del sketch sea menor porque el código de la función es reutilizado.
  • Facilita la lectura del código.
  • Hace más sencillo reutilizar código en otros sketches.

Más información: http://arduino.cc/en/Reference/FunctionDeclaration

Funciones Arduino en playground: http://playground.arduino.cc/Code/Function

En Visualino podemos hacer uso de las funciones en el apartado de “Functions”

En Visualino para multiplicar la lecturas de las entradas analógicas A0 y A1 usando una función multiplicación se puede hacer así:

Solución: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio42-Visualino_Funciones

Nombres de funciones

Generalmente los nombres de las funciones deben ser en minúscula, con las palabras separadas por un guión bajo, aplicándose éstos tanto como sea necesario para mejorar la legibilidad.

“mixedCase” (primera palabra en minúscula) es aceptado únicamente en contextos en donde éste es el estilo predominante con el objetivo de mantener la compatibilidad con versiones anteriores.

En el caso de las clases, los nombres deben utilizar la convención “CapWords” (palabras que comienzan con mayúsculas).

Las funciones en Arduino pueden estar dentro del mismo fichero .ino o en otro fichero con extensión .ino dentro del directorio del sketch principal.

Funciones vs Librerías

Ejemplo de blink usando funciones:

 
void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}

void loop() {
  enciendo();
  delay(1000);  
  apago();
  delay(1000);  
}

void enciendo() {
  Serial.println("Enciendo...");
  digitalWrite(13, HIGH);
}

void apago(){
  Serial.println("Apago...");
  digitalWrite(13, LOW);  
}

Código: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio43-Blink_Funcion

En Visualino se podría hacer de esta forma:

Solución: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio43-Visualino_Blink_Funcion

Este ejercicio también se podría hacer dividiendo el código en dos ficheros uno principal que tiene las funciones setup() y loop() y otro con las funciones enciende() y apaga () en el fichero funciones.ino. Los ficheros están en la misma carpeta y al abrirlos en el IDE de Arduino los vemos en pestañas diferentes.

Por lo tanto podemos organizar nuestro código en varios ficheros .ino en la misma carpeta. Todo ellos los veremos en pestañas en el IDE de Arduino.

Código en: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio44-Funciones

Esto también podría hacerse usando “librerías”  o fichero de C++ con un fichero funciones.h e incluyéndolo en el principal con la instrucción #include «funciones.h».

En C++ el código se organiza en diferentes ficheros con extensiones .h y .cpp a los que se van llamando con #include para añadirlos al fichero que lo llama para poder usar su contenido.

Código en: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio45-Librerias

Práctica Final de Funciones

Con todo lo visto de Strings, operadores, estructuras de control y funciones, hacer un ejemplo de un menú interactivo donde se dan varias opciones y pulsando cada una de ellas se ejecuta una acción concreta. Si el valor pulsado no es ninguna de las opciones avisar y volver a mostrar el menú hasta que se pulse una opción correcta.

Opciones:

  • 1 – Encender Led pin 13
  • 2 – Apagar Ler pin 13
  • 3 – Contar segundos hasta pulsar tecla y mostrar por pantalla
  • 4 – Fin de programa

Solución: https://github.com/jecrespo/aprendiendoarduino-Curso_Arduino_2017/tree/master/Ejercicio46-Estructuras_de_Control