Parar la ejecución o finalizar un programa en Arduino

Puede ser que sea lleguemos a un punto en que por seguridad sea necesario parar la ejecución del programa o que simplemente deseemos “apagar” la placa. Para ello se puede llamar a las funciones abort() o exit(0). Que es prácticamente lo mismo ya que abort lo único que hace es llamar a exit(1). Esto tiene sentido en aplicaciones de ordenador donde exit(0) indica la finalización del programa correctamente mientras que cualquier parámetro distinto de 0 indica que ocurrió un error. En Arduino ese valor que se pasa es ignorado, aunque tenemos que añadirlo si no queremos errores de compilación.

Puesta en forma de código la función exit seria similar a esta:

void exit(int ignored){
  cli(); //desactiva las interrupciones
  while(1); //bucle infinito
}

Básicamente suspende la interrupciones y luego entra en un bucle infinito. Si por algún motivo queremos hacer nuestra propia función de stop es fácil imitarla:

void stop(){
  noInterrupts();
  while(1);
}

En caso de que se use el watchdog es necesario deshabilitarlo o reiniciará la placa.

void stop(){
  wdt_disable(); //solo si esta activado el watchdog
  noInterrupts();
  while(1);
}

Las funciones anteriores son recomendables para casos en que la parada se produce por un error grave y donde menos acciones se realicen mejor. Sin embargo si queremos “apagar” la placa podemos dormirla profundamente, no llegara al consumo cero pero si al mínimo.

#include <avr/sleep.h>

void stop(){
  set_sleep_mode( SLEEP_MODE_PWR_DOWN );
  sleep_enable();
  noInterrupts(); //nunca será despertada
  sleep_cpu();
}

Por supuesto en todos estos casos basta con dejar la placa sin alimentación para poder volver a usarla.

Usar el led de las placa Arduino. LED_BUILTIN

Muchas placas de Arduino tienen un pin digital conectado a un led de la propia placa de tal forma que se puede controlar ese pin desde el código. En la placa el led suele ir etiquetado con una “L”. En Arduino UNO es el pin digital 13. Es una forma cómoda de tener un led de señalización sin tener que montar nada. No es necesario saber que pin es, para ello se puede usar la constante LED_BUILTIN que hace referencia a ese pin.

Para poder usarlo lo primero es estar seguro que no se usa ese pin para otra cosa. Es necesario configurarlo como OUTPUT. Basta con poner el pin a HIGH para encenderlo y a LOW para apagarlo.

Aunque es algo muy rudimentario se puede usar para indicar estados del software o errores. Y puede ser útil cuando no podemos tener el ordenador con el monitor serie conectado todo el tiempo al Arduino.

Su uso puede ser tan simple como encenderlo o llegar a cosas más complicadas como usar parpadeos para indicar distintos errores.

En este ejemplo la función blink hace parpadear el led las veces que se le pasa como parámetro

void setup(){
   pinMode(LED_BUILTIN, OUTPUT);
}

void loop(){
  blink(1);
  delay(2000);
  blink(2);
  delay(2000);
  blink(3);
  delay(2000);
}

void blink(byte n){
  for(; n > 0; n--){
    digitalWrite(LED_BUILTIN, HIGH);//Encender led
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);//Apagar led
    delay(500);
  } 
}

Normalizar un valor entre 0 y 1en Arduino.

Cuando quieres comparar distintos datos en distintos rangos es habitual el tener que normalizarlos entre 0 y 1, es decir, convertir una variable de un rango de valores [in_min, in_max] a otro que va de 0 a 1. Para ello podemos usar la función map.

map(x, in_min, in_max, 0.0, 1.0);

En esta otra entrada de blog hay más información sobre la función map y como optimizarla, vamos a usar esos trucos para crear funciones de normalización más optimas.

Lo primero es que como los valores sobre los que se “mapea” son siempre los mismos 0 y 1 podemos crear nuestra propia función a partir del código de la función map.

long map(long x, long in_min, long in_max, long out_min, long out_max) {
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

Se reemplaza out_min por 0 y out_max por 1 y se simplifican los cálculos:

long map(long x, long in_min, long in_max, long out_min, long out_max) {
    return (x - in_min) * (1 - 0) / (in_max - in_min) + 0;
}

Obteniendo:

long normalize(long x, long in_min, long in_max) {
    return (x - in_min) / (in_max - in_min);
}

Vamos a crear también funciones para los float y los double:

float floatNormalize(float x, float in_min, float in_max) {
    return (x - in_min) / (in_max - in_min);
}

double doubleNormalize(double x, double in_min, double in_max) {
    return (x - in_min) / (in_max - in_min);
}

Genial, pero aun podemos hacer otra mejora, la función anterior es genérica, si vamos a aplicar la normalización siempre a valores en el mismo rango podemos crear una nueva función donde remplacemos in_min e in_max por los valores de este rango. Por ejemplo en un rango de 0 a 1023:

//in: 0..1023
//in_min = 0
//in_max = 1023

long customNormalize(long x){
    return (x - 0) / (1023 - 0);
}

Simplificando:

long customNormalize(long x){
    return x / 1023;
}

Hemos reducido el coste de la normalización a una solo división, en el peor de los casos (cuando el in_min no sea 0) será una resta y una división.

Transformar un valor de una escala a otra en Arduino. Map

Hay veces que tenemos un valor expresado en una escala y queremos pasarla a otra. Por ejemplo uno de los casos más habituales es el calcular un porcentaje. Que pasamos un número expresado en una escala a otra que va de 0 a 100. En Arduino para pasar un valor de un rango o escala [in_min, in_max] a otro [out_min, out_max] podemos usar la función map.

long map(long x, long in_min, long in_max, long out_min, long out_max)

La función map toma 5 parámetros:

  • x: Valor a transformar
  • in_min: Valor más bajo de la escala origen
  • in_max: Valor más alto de la escala origen
  • out_min: Valor más bajo de la escala destino
  • out_max: Valor más alto de la escala destino.

La función tiene la siguiente implementación interna:

long map(long x, long in_min, long in_max, long out_min, long out_max) { 
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 
}

Os habréis dado cuenta de que trabajo con long ¿Qué pasa si queremos otro tipo de datos?. Muy fácil, nos hacemos nosotros mismos la función:

float floatMap(float x, float in_min, float in_max, float out_min, float out_max) { 
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 
}


double doubleMap(double x, double in_min, double in_max, double out_min, double out_max) { 
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; 
}

Optimizar la función map

Ya que nos hemos puesto a crear nuestras funciones veamos como podemos optimizar los cálculos, si siempre tenemos que usar la función map con los mismos rango de números podemos crear nuestra versión personalizarla para que se ejecute más rápido. Es tan sencillo como sustituir los valores min y max de cada rango y simplificar. Veamoslo con un ejemplo, vamos a crearnos nuestro propio map que transforme de la escala de 0 a 1023 a otra escala de 0 a 255.

in: 0..1023
out: 0..255
int_min = 0
int_max = 1023
out_min = 0
out_max = 255

Reemplazamos:

long customMap(long x) { 
    return ((x - 0) * (255 - 0) / (1023 - 0)) + 0; 
}

Simplificamos donde podamos:

long customMap(long x) { 
    return (x * 255) / 1023; 
}

Ahora tenemos nuestra función personalizada que realiza solo dos operaciones. Como no es necesario pesarle los rangos usa solo un parámetro.

Microcontroladores e inteligencia artificial embebida

En los últimos años hay dos áreas que han avanzado rápidamente. La inteligencia artificial y todo lo que rodea los microcontraladores y sensores. Sin embargo los avances de una y otra parece separarlas. La I.A. se mueve hacia grandes redes neuronales que requieren grandes máquinas con mucha potencia y memoria. Si bien los microcontroladores están bajando de precio y aumentando sus capacidades quedan muy atrás de lo que necesitan las I.A. actuales.

Ambos están condenados a entenderse. Los sensores y actuadores  son la forma en que la I.A. interactúa con el mundo físico. Muchas aplicaciones de la I.A. viven cómodamente en mundos virtuales procesando y generando datos que no provienen directamente del mundo físico. Pero en muchos otros casos las grandes inteligencias en la nube y los pequeños microcontroladores están condenados a entenderse.

Muchas veces ese problema se soluciona añadiendo conectividad a los microcontroladores y que se comuniquen con la nube. Eso da lugar a dispositivos extremadamente tontos que necesitan conexión a un servidor a miles de kilómetros para encender o apagar una luz o poner la calefacción.

No hay nada malo en que los dispositivos estén conectados. Permite su control y monitorización de forma remota. Lo que no tiene sentido es que el dispositivo no pueda actuar sin conexión a internet. Internet debe de ser una fuente de datos más para el dispositivo. Un termostato puede mirar en internet la previsión del tiempo o el precio de la luz y usarlos como complemento para ajustar la temperatura de la casa pero sin ellos debes de seguir funcionando.

La inteligencia artificial es un área que se avergüenza de sus pasado. Tanto como para dejar de llamar “inteligencia artificial” a sus éxitos anteriores en cuanto aparece un nuevo enfoque. Por ejemplo la visión por computador de hace unos años está llena de algoritmos para reconocer formas, seguir objetos, calcular bordes….llegaron las redes neuronales convolucionales y todo eso quedó en un segundo plano.

Son algoritmos válidos y eficaces, no tan flexibles como el deep learning pero lo poco que hacen lo hacen suficientemente bien. Muchos de esos algoritmos se usan en entornos industriales. Lo mejor de todo es que los microcontroladores actuales pueden ejecutarlos. Podemos tener chips diminutos, de bajo consumo y barato ejecutando algoritmos que llevan décadas siendo usados (y por tanto mejorados) para realizar tareas que requieran cierta cantidad de inteligencia o de aprendizaje.

Si los microcontroladores no son suficientemente potentes los ordenadores también han bajado de precio y hay SOC que pueden actuar de cerebro cuando se requiera más potencia de calculo sin necesidad de abandonar tu red local. Y aún si no fuera suficiente, por ejemplo para reconocimiento de voz, se puede consumir como si fuera un servicio.

Lo ideal seria una arquitectura en la que los micontroladores fueran capaces de realizar las tareas básicas sin ayuda externa, por encima de ellos un hub que conecta y coordina los dispositivos y si aun asi se requiriera alguna tarea demasiado pesada para el hub se puede consumir como servicio. La idea de este modelo es que se “quede en casa” tanta parte del sistema como sea posible.

Ahora que tenemos una idea de la arquitectura ideal centrémonos en la inteligencia embebida dentro los microntroladores, se enfrenta a la siguientes limitaciones:

  • Memoria RAM, suele ser poca y hay que gestionarla bien.
  • Potencia de cálculo, los microcontroladores no están diseñados para grandes cálculos por lo que tienen importantes limitaciones.
  • Tiempo real, por lo general no pueden esperar demasiado a dar respuesta. Un sistema de seguridad no puede tardar un cuarto de hora en decidir si da la alarma.
  • Bateria, muchos dispositivos funcionan con baterías lo cual complica todo un poco más y que obliga a aplicar medidas para reducir el consumo y alargar su duración.
  • Poco espacio para modelos, los microcontroladores no tienen “un disco duro” donde almacenar grandes cantidades de datos. Eso limita el tamaño de los modelos y bases de datos a usar.

En resumen los algoritmos de inteligencia artificial que se llevan usando décadas pueden seguir siendo útiles en dispositivos de domótica en lugar de depender de sistemas en la nube.

Como evitar ataques de repetición en tus proyectos de IoT con OTP (Arduino)

Los ataques de repetición son un habitual en los dispositivos de IoT y muchas grandes empresas los han sufrido. Se producen cuando alguien intenta securizar las comunicaciones entre dispositivos pero no tiene muy claro como se hace.

Realmente un ataque de repetición es un ataque de inyección de comandos pero con algunas limitaciones. Vamos a plantear un escenario muy sencillo. Una casa con una bombilla inteligente conectada por WiFi (o de cualquier otra manera la capa física de la conexión da igual). Esta bombilla recibe comandos para encenderse y apagarse. Ahora conectamos un dispositivo “malvado” a esta red (o no lo era cuando lo conectamos pero un atacante se hace con el control). Este dispositivo podría enviar comandos de encendido y apagado a la bombilla. Lo que sería el equivalente a un niño jugando con el interruptor. Esto sería un ataque de inyección de comandos.

Para solucionarlo el fabricante piensa que puede cifrar los comandos con un cifrado superseguro e irrompible, cada bombilla tendrá una clave única así que aunque el atacante haga ingeniería inversa no le va servir de nada. Ahora el fabricante anuncia que su dispositivo es seguro y comprador se siente protegido.

Pero la realidad es que no basta con cifrar la comunicación. La lámpara recibe comandos de encendido y apagado cifrados. Podríamos decir que es lo mismo, un conjunto de bytes enciende la lámpara y otro la apaga, solo que esta vez ese conjunto es diferente para cada lámpara del mundo. La clave de este ataque es que si siempre se cifra el mismo comando con la misma clave el conjunto de bytes siempre es el mismo por lo que al atacante le basta con capturarlo para poder reenviarlo tantas veces como quiera.

¿Y si añade una parte del mensaje que sea aleatoria? Tampoco sirve, aunque ahora cada mensaje sea un grupo de bytes diferente, se puede reutilizar el mismo mensaje varias veces ya que el dispositivo no puede verificar su validez.

Lo mismo ocurre si el mensaje va firmado digitalmente. Nada impide al atacante copiar ese mensaje y reenviarlo.

El truco esta en usar una parte del mensaje que sea predecible y no repetible. Predecible para que el receptor del mensaje pueda saber que es correcto y no repetible para descartar los ya utilizados.

Para ello vamos a usar passwords de un solo uso (OTP) para saber más sobre el tema se puede ver en este articulo. En el articulo se emplea un contador o un timestamp dependiendo de que tipo de OTP se use, lo ideal seria usar TOTP. En nuestro caso eso da igual. No hay que olvidar que aquí estamos hablando de proyectos IoT lo cual limita técnicamente las soluciones que podemos adoptar.

Antes de ver posibles soluciones vamos a empezar explicando el concepto de “ventana de tiempo del ataque”, se podría definir como “tiempo durante el cual somos vulnerables al ataque”. En nuestro caso es el tiempo durante el que un mensaje capturado y reenviado por un atacante es considerado válido por nuestro dispositivo. Cuanto más pequeña sea esta ventana más seguros estaremos.

Volviendo a nuestro OTP tenemos: una función que lo calcula (OTP), un contador o timestamp (c) y un secreto compartido entre ambos dispositivos (k) y un comando o mensaje a enviar (msg). En lugar de calcular un token OTP con el contador uniremos el mensaje y el contador y calcularemos un OTP de ambos.

Token = OTP(msg+c, k)

Truco, si el mensaje es demasiado grande para procesarlo ya sea por memoria o por tiempo se puede usar cualquier función que lo resuma (una función hash o algo menos costoso), aunque hay que tener cuidado de que dos comandos distintos generen dos resúmenes diferentes.

Ahora enviamos el mensaje y el token para que el cliente pueda validarlo.

Si un tercero interceptara nuestro comando este solo le seria valido durante un breve periodo de tiempo.

Si queremos añadir un extra de seguridad podemos hacer que nuestro sistema no acepte dos veces el mismo comando durante un periodo de tiempo igualo mayor a la ventana de ataque.

En el esquema de debajo se puede ver como es el proceso, para calcular c se usa el timestamp (t) actual dividido por la ventana de tiempo (vt).

Un detalle a tener en cuanta es que nunca se ha hablado de cifrar la comunicación, este sistema es seguro aunque los mensajes se distribuyan de forma abierta.

Password de un solo uso (OTP) en Arduino (HMAC, HTOP, TOTP)

Los password de un solo uso también llamados OTP (One Time Password) son passwords que solo se pueden usar una vez o durante un periodo de tiempo breve. Es una buena medida de seguridad, aunque estas contraseñas sean robadas o interceptadas se reduce el tiempo que el atacante puede acceder al sistema. Uno de sus usos es como segundo factor de autenticación. Casi todos habremos tenido que usar alguna vez códigos que nos envían al móvil o que genera algún aplicación y cuyo tiempo de vida es breve. Eso es un OTP.

Cuando hay un canal seguro para notificar el OTP, por ejemplo un mensaje por SMS. Es sencillo crear un OTP, se genera un código aleatorio asociado a ese usuario y listo.

Pero no siempre hay un canal seguro. En ese caso la solución es algo más complicada. Necesitamos tener dos programas que generen exactamente la misma contraseña en el mismo momento. Para estos sistemas necesitaremos dos cosas.

  • Un secreto o clave, que es compartido por ambos sistemas y debe de permanecer en secreto para que sea seguro. La llamaremos k
  • Una secuencia o contador, este dato puede ser publico sin ningún problema (en teoría) sirve para que ambos programas generadores creen la misma contraseña. Lo llamaremos c

Con random

Una implementación muy sencilla y de muy baja seguridad (suficiente para proyectos caseros) es usar el generado de números pseudoaleatorios de Arduino. En este caso el secreto es la semilla con la que se inicializa el generador. En este caso la secuencia o contador no es explicito, no se le pasa es implícito al numero de veces que se ha llamado a la función random. Lo que añade la dificultad añadida de tener ambas secuencias sincronizadas.

long randNumber;
void setup() {
  randomSeed(k);
}
void calculateOTP() {
  return random(10000, 99999);//contraseña de 5 digitos
}

Otro problema es que cada vez que se reinicia perdemos el punto donde estábamos de la secuencia y esta vuelve a empezar. Seria necesario guardar en la EEPROM el número de veces que hemos generado un OTP y luego llamar a la función ese numero de veces para resincronizar el estado del generador de números pseudoaletaorios.

Con una función hash

Otra táctica parecida es usar una función de hash. Para calcular la primera contraseña se llama a la función de hash con el secreto k, luego se usa el password anteriror, añadiendole el secreto k para calcular el siguiente:

k0 = H(k)

k1 = H(k0+k)

k2 = H(k1+k)

k3 = H(k2+k)

….

kn = H(kn-1+k)

La ventaja de este sistema es que basta con guardar en la EEPROM el último password generado para mantener la sincronización.

HTOP

Estos dos sistemas nos sirven para desarrollos caseros que no requieren demasiada seguridad para hacer una implementación segura podemos usar HMAC (hash-based message authentication code). HMAC usa una función de hash, un secreto o clave y un mensaje, en este caso el mensaje será el contador. A esta implementación se le conoce como HTOP (HMAC-based one-time password).

Empecemos viendo como funciona HMAC

HTOP(k, c) = HMAC(k, c) = H( (k ^ opad) || H(k ^ ipad) || c )

  • H es la función e hash
  • k es la clave secreta
  • c es el contador
  • opad es un bloque formado por el valor 0x5c repetido
  • opad es un bloque formado por el valor 0x36 repetido
  • || es la operación OR
  • ^ es la operación XOR

En este caso no necesitamos tener el contador sincronizado. El contador se puede pasar en la petición. Supongamos que el dispositivo B intenta contactar con el dispositivo A, el proceso puede ocurrir de dos maneras:

  • B usa su contador interno y la pasa el password (TokenB ) con el contador usado para calcularlo. A calcula el password con el contador que le pasa B y verifica si coinciden
  • B le pide a A un contador y A le pasa el contador con el que B tiene que generar el password (tokenB) y se lo devuelve a A que verifica si coinciden

El segundo caso es más seguro ya que evitamos que un atacante

TOTP

Lo ideal seria que ninguno tuviera que intercambiar el contador, que ambos tuvieran un contador sincronizado. Una opción seria usar la hora actual. O un timestamp que exprese la hora actual como milisegundos transcurridos desde el 1 de enero de 1970 a las 00:00:000. Es decir que TOTP (Time-based One-time Password) es HMAC usando como mensaje el tiempo o lo que es lo mismo usar HTOP usando como contador el tiempo.

El único problema aquí es que cada milisegundo la contraseña cambia y puede ser muy poco tiempo para realizar el intercambio de password. La solución es dejar una ventana de tiempo durante la cual el password es válido. Si usamos t para indicar el tiempo en milisegundo y vt el tiempo (en milisegundos) durante el cual el password es válido.

c = Trunc(t/vt)

TOTP(k, c) = HMAC(k, c)

Así nos ahorramos la parte de tener que intercambiar el contador entre dispositivos. Ambos usan la hora actual con una ventana de tiempo vt.

El problema es que Arduino no tiene un RTC (real time clock) por lo que no tiene forma de saber directamente qué hora es, necesita un elemento externo que le informe de ello.

“TOTP” sin RTC

Hay una forma un poco tramposa de implementar TOTP sin usar un RTC. En este caso partimos de HTOP, usaremos la segunda forma de HTOP en la que A le pasa el contador a B. El funcionamiento es exactamente igual que antes solo que tras un periodo de tiempo t la variable contadorA se incrementa automáticamente dejando a B con un OTP que ya no es valido.

No es un sistema tan cómodo como tener ambos sistemas sincronizados por su reloj. Ademas de que si varios dispositivos quieren conectarse al dispositivo A puede ser complicado de gestionar y necesitarías un contador y un secreto para cada uno.

¿Cual elegir?

Las soluciones basadas en HMAC son las más seguras pero también las más exigentes en tiempo y memoria. Y aunque TOTP es la segura y cómoda implica tener un RTC y que todos los dispositivos lo tengan sincronizados. Cada uno tiene que elegir el método según el balance de seguridad y coste.

Código

Para implementar todo esto podemos usar la librería Cryptosuite para Arduino. Permite usar la funciones hash SHA-1 y SHA-256.

Una vez copiada a nuestro directorio de librerías de Arduino puede usarse añadiendo el include correspondiente

#include "sha1.h"
#include "sha256.h"

Un ejemplo de uso de HMAC es:

 uint8_t *hash;
 Sha256.initHmac(key,keyLength); //secreto y su longitud
 Sha256.print(counter); //contador
 hash = Sha256.resultHmac();

Regresión lineal con incertidumbre en Arduino

Vamos a empezar este texto develando el truco que usaremos para representar incertidumbre con la regresión lineal para Arduino y que se basa en emplear la versión con pesos del algoritmo de regresión lineal. La incertidumbre estará representada como valores con una variación de pesos según la certeza que tengamos de su valor. Usaremos el peso como porcentaje de certeza de ese dato

Esta no es la mejor ni la única manera de hacerlo. No hay que olvidar que aquí se trata de hacerlo en algo tan limitado en memoria y potencia como pude ser un Arduino UNO.

Usaremos la librería regressino, en concreto su librería para regresión lineal:

#include <LinearRegression.h>

LinearRegression lr = LinearRegression();

Una forma de incertidumbre es cuando directamente tenemos valores de los que “nos fiamos” menos que de otros. Por ejemplo, porque vienen de dos fuentes distintas. En este caso los datos menos fiables tendrán que tener un peso más bajo que los más fiables para que su influencia sobre el resultado final sea menor.

//datos fuente no fiable
lr.learn(1, 3, 0.5);
lr.learn(2, 5, 0.5);
lr.learn(3, 6, 0.5);

//datos fuente fiable
lr.learn(2, 4, 1);
lr.learn(4, 5, 1);
lr.learn(5, 6, 1);

Pero hay otro caso de incertidumbre, cuando no conocemos el valor del dato con seguridad, lo que conocemos son los valores entre los que está comprendido. Generalmente tenemos dos valores, un mínimo y un máximo o tres valores con uno más probable y un mínimo y un máximo (a veces representados como errores) entre los que ese valor puede variar. En este caso tenemos que definir cómo se distribuye el peso (probabilidad) entre estos valores. Hay que recordar que la suma total de los pesos tiene que ser igual a 1.

Una vez definida la forma en que se distribuye la probabilidad hay que descuartizarla en puntos. La idea es que esto funcione en un Arduino UNO y no podemos trabajar directamente con funciones de probabilidad.

Supongamos que para x = 10 sabemos que el valor de y está comprendido entre 2 y 3. Ahora hay que saber cómo está distribuida la probabilidad entre esos dos valores. Veamos algunas posibilidades:

  • Toda la probabilidad se concentra en cada uno de esos valores por lo tanto el 2 tendría un 50% de certeza y el 3 otro 50%. O lo que es lo mismo un peso de 0.5
lr.learn(10, 2, 0.5);
lr.learn(10, 3, 0.5);
  • La probabilidad se distribuye de forma uniforme por todo el espacio entre esos dos puntos. Para representarlo tómanos varios puntos entre 2 y 3 y les asignamos a todos la misma probabilidad.
lr.learn(10, 2, 0.2);
lr.learn(10, 2.25, 0.2);
lr.learn(10, 2.5, 0.2);
lr.learn(10, 2.75, 0.2);
lr.learn(10, 3, 0.2);
  • El punto central es mucho más probable que los extremos. Un ejemplo de como podemos hacerlo.
lr.learn(10, 2, 0.25);
lr.learn(10, 2.5, 0.5);
lr.learn(10, 3, 0.25);
  • Si por ejemplo queremos simular una distribución en campana de media u y con desviación estándar s
lr.learn(10, u, 0.682);
lr.learn(10, u-s, 0.136);
lr.learn(10, u-(2*s), 0.023);
lr.learn(10, u+s, 0.136);
lr.learn(10, u+(2*s), 0.023);

Regresión lineal con pesos en Arduino

Ya hemos visto varias formas de extender las capacidades de la regresión lineal en Arduino. En este caso vamos a asociar pesos a los valores para que no todos los casos aporten los mismo al resultado final. Pero qué significa “aportar” más al resultado, de forma gráfica podríamos imaginarnos que los puntos de mayor peso atraen más a la recta de la regresión lineal por lo que esta tiende a acercarse más a estos. La utilidad de este sistema es cuando tenemos resultados que por algún motivo valoramos más que otros.

Vamos a modelar el peso como un valor entre 0 y 1. De tal forma que el peso máximo corresponda con 1 y el mínimo con 0. A mayor peso más “aporta” ese valor al resultado. Entre dos valores uno con peso 1 y otro con peso 0.5 el primer cuenta el doble que el segundo. Cuando el peso es 1 no hay diferencia con la regresión lineal sin pesos. Y cuando el peso es cero el valor no va a aportar nada al resultado.

Partimos de la función de regresión lineal que ya vimos en otro post:

void LinearRegression::learn(double x, double y){
    n++;
    meanX = meanX + ((x-meanX)/n);
    meanX2 = meanX2 + (((x*x)-meanX2)/n);
    varX = meanX2 - (meanX*meanX);

    meanY = meanY + ((y-meanY)/n);
    meanY2 = meanY2 + (((y*y)-meanY2)/n);
    varY = meanY2 - (meanY*meanY);

    meanXY = meanXY + (((x*y)-meanXY)/n);

    covarXY = meanXY - (meanX*meanY);

    m = covarXY / varX;
    b = meanY-(m*meanX);
}

Como ya hemos visto en otro post vamos a usar “el truco” que consiste en transformar el valor de x e y que se le pasa a la función que calcula la regresión lineal (learn). Podríamos verlo como que el valor de x se usan para modificar el valor de meanX y meanX2 y el de y para meanY y meanY2. Entonces si w es el peso:

  • Si el peso es 1 x e y no cambian su valor
  • Si es 0 x e y no tienen que afectar a los valores de meanX y meanY para que pase eso el valor de x ha de ser igual que meanX y el de y igual a meanY.
  • Si el peso este entre 0 y 1 el valor ha de componerse con el valor de x e y y el valor de las medias de cada uno.

Para conseguir eso vamos a ponderar el valor de la x y el de meanX según w

x = x*w + meanX*(1-w);
y = y*w + meanY*(1-w);

Solo queda añadir un par de comprobaciones para evitar que el valor pueda ser mayor de 1 o menor de 0:

void LinearRegression::learn(double x, double y, double w){
    if(w >= 1) { 
        learn(double x, double y);
    } else if(w <= 0) {
        return;
    } 

    x = x*w + meanX*(1-w);
    y = y*w + meanY*(1-w);
    learn(double x, double y);
}

Todo esto se puede ver en la librería Regressino.

Por último comentar que hay dos posible optimizaciones que se podrían aplicar si fuera necesario y que nos permiten ahorrar algunos cálculos si hay muchos pesos cercanos a 0 o a 1:

  • Los pesos muy cercanos a 0 se ignoran
  • Los pesos muy cercanos a 1 se tratan como si no tuvieran peso


Hay una pequeña pérdida de precisión pero en el caso de tener muchos datos puede compensar al reducir el tiempo de cálculo.

Excepciones y control de errores en Arduino

Uno de los puntos débiles de las placas más básicas de Arduino es la gestión de errores, siguen la política de “nunca me equivoco” de tal forma que acciones o código que en otros sistemas lanzarían error no producen ninguno en Arduino.

Un excepción es un mecanismo en el software que “salta” cuando se produce un error y permite que el software ejecute una rutina de gestión de errores que se ocupa de gestionarlo para después recuperar su funcionamiento normal, si es posible. Arduino no permite usar el mecanismo habitual de C++, las instrucciones try y catch. Es posible usarlas en el código sin que den error al compilar pero durante el proceso de compilación se desactiva su funcionamiento. No es algo que se haga de forma arbitraria, simplemente no funcionan en Arduino. Si embargo vamos a tratar de conseguir un mecanismo similar que nos permite una gestión de errores muy básica . Para ello emplearemos dos funciones (setjmp y longjmp) y un tipo de variable (jmp_buf). esta funciones permiten establecer un punto de salto dentro de una función al que se puede ir desde cualquier parte del programa:

  • setjmp – establece el punto donde se va recuperar la ejecución del programa hay que pasarle como parámetro una variable de tipo jmp_buf.
  • jmp_buf – es un tipo de variable que almacena la información necesaria para restaurar la ejecución del código en el punto indicado por setjmp.
  • longjmp – salta al punto establecido por setjmp. Requiere dos parámetros uno de tipo jmp_buf que indica a que setjmp va a saltar y otro que indica el código de error que va devolver setjmp tras el salto. Este código nunca puede ser 0 si se usa 0 devolverá 1.

Ahora veamos como se usa, para entenderlo bien el código hay que saber los siguientes detalles:

  • Es necesario incluir la librería setjmp.h
  • setjmp la primera vez que se llama se usa para fijar el punto de salto y devuelve el valor cero
  • Cuando se llama a longjmp es como si el programa continuara desde donde se llamo a setjmp pero en lugar de devolver 0 devuelve otro valor
  • Para que esto funcione correctamente setjmp tiene que estar dentro de un if o un switch.
  • Para que todo funcione correctamente setjmp ha de fijar el punto de salto dentro del loop, si lleva a otras funciones puede fallar al llegar al final de la función donde ha saltado

En el caso más simple solo tenemos un tipo de errores y por tanto cada vez que se produce un error la forma de gestionarlo es siempre la misma. Para ello basta con usar un if.

#include <setjmp.h>

jmp_buf exception_mng;
int a = 5;
int b = 1;
int c;

void setup() {
  Serial.begin(9600);
}

void loop() { 
  if (setjmp(exception_mng)) { //si se produce error
		Serial.println("EXCEPTION");
  }
  a--;
  divide();
  delay(1000);    
}

void divide(){
  if(a == 0){
    longjmp(exception_mng, 1);
  }
  c = b/a;  
  Serial.print(b);
  Serial.print("/");
  Serial.print(a);
  Serial.print(" = ");
  Serial.println(c);
}

Una versión más avanzada permite gestionar distintos tipos de incidencias según el valor que se le pase a longjmp como segundo parámetro

#include <setjmp.h>

jmp_buf exception_mng;
int a = 5;
int b = 1;
int c;

void setup() {
  Serial.begin(9600);
}

void loop() { 
  switch (setjmp(exception_mng)) {
	case 0: //sin errores
		break;
  case 1: //division por cero
		Serial.println("EXCEPTION DIVISION BY 0");
    break;
  case 2: //divisor negativo
		Serial.println("EXCEPTION DIVISION BY NEGATIVE NUMBER");
    break;
  default: //se ejecuta cuando no se cumple ninguno de los casos anteriores
		Serial.println("GENERIC EXCEPTION");
    break;

  }
  a--;
  divide();
  delay(1000);    
}

void divide(){
  if(a == 0){
    longjmp(exception_mng, 1);
  }
  if(a < 0){
    longjmp(exception_mng, 2);
  }
  c = b/a;  
  Serial.print(b);
  Serial.print("/");
  Serial.print(a);
  Serial.print(" = ");
  Serial.println(c);
}

Este sistema nos dota de un mecanismo básico de gestión de errores aunque nos obliga a nosotros a realizar la comprobación para lanzar la “excepción”.