Arquitectura de un bot que interactúa por voz

Vamos a ver como crear nuestro propio bot que interactue usando la voz. Algo, salvando las distancias, al estilo Siri, Alexa o Cortana. En esta entrada voy a hablar solo de la arquitectura y no del procesamiento del lenguaje natural o de la síntesis del mismo.

Lo primero que necesita todo bot es estar escuchando a la espera de oír su nombre o alguna frase que pueda reconocer como su orden. Aunque hay implementaciones por hardware que esperan oír el nombre del bot para activar la captura de audio, en nuestro caso deberemos estar escuchando constantemente. Así que nuestro primer componente va ser un proceso que escucha constantemente y transforma el audio en texto.

Posteriormente ese texto ha de ser analizado por un sistema de reglas que determine si el audio contiene algún comando valido, en nuestro caso el sistema de reglas estará basado en expresiones regulares.

Una vez tengamos una coincidencia valida se tienen que extraer los datos del comando y ejecutar el código asociado.

El resultado de la ejecución de ese código ha de ser transformado a una frase de texto que sea entendible por el usuario.

Finalmente ese texto será leído en voz alta por nuestro bot. Por lo que necesitaremos un sistema que sintetice la voz a partir del texto.

Los navegadores modernos incluyen una API tanto de reconocimiento del habla como de síntesis del habla . Aunque la verdad que el único que les da un soporte decente es Chrome, de hecho es el único en que logré que funcionara en español.  Con eso tenemos parte de lo que necesita un bot, falta una forma de definir los comando validos y otra de componer las frases que el bot necesite decir. Para ello, y alguna cosa más, se ha creado la librería jsBotVoice

En resumen:

  1. Captura del audio
  2. Conversión a texto
  3. Análisis del texto en busca de coincidencias con los comandos de texto definidos
  4. Extracción de datos y ejecución del código asociado al comando
  5. Creación del texto en respuesta
  6. Síntesis de voz a partir del texto
botVoice

Diagrama de funcionamiento de un bot que se comunica por voz

 

Los puntos del 1 al 4 están explicados en esta entrada del blog y del 5 al 6 en esta otra.

Síntesis de voz y lenguaje natural en un bot

En una entrada anterior veíamos como interpretar los comandos de voz. Ahora vamos a ver hacer que el bot de respuesta lo más naturales posibles en lenguaje hablado.

La forma más sencilla y rápida es tener respuestas predefinidas. Lo malo de este sistema es que la respuesta es siempre igual y queda muy poco natural. Usando la librería jsBotVoice  es tan sencillo como usar el modificador * delante de la cadena y esa cadena será enunciada literalmente.

voice.talk("*Este texto se lee literal");

Para aportar algo de variedad a la respuesta vamos a utilizar un modelo muy simple basado en diccionarios. La idea es que cada palabra de la frase pueda reemplazarse por varias expresiones similares, eligiéndose una al azar lo cual dota al bot de variedad en las respuestas. Lo primero será definir el diccionario.

var spanish_dictionary = {
  "hi": ["hola", "saludos"],
  "person": ["humano","ser"],
  "yes": ["si","afirmativo"],
  "no": ["no","negativo"],
  "ok": ["vale", "de acuerdo", "hecho"]
};

Una vez definido para usarlo solo hemos de crear una frase con esos tokens

voice.talk("hi person");

Esta linea pueda dar varios posibles resultados:

hola humano
saludos humano
hola ser
saludos ser

Si queremos aladir alguna palabra que se lea de forma literal en lugar de interpretarla como un token podemos ponerle delante el modificador #

voice.talk("#Buenas person");

Posibles resultados:
buenas humano
buenas ser

Por otro lado podemos personalizar aun más las respuestas usando variables, para ello usamos el nombre de la variable precedido de un $ . Las variables han de ir almacenadas en el mapa voice.data

voice.data["name"] = "Cubiwan";
voice.talk("hi $name");

Posibles resultados:
hola cubiwan
saludos cubiwan

Más documentación y ejemplos en jsBotVoice .

Alarma con nodeMCU y un radar HW-MS03

La razón oficial de este montaje era crear un sistema de alarma con sensor de movimiento que cuando detectase algo me enviara un notificación al movil. La razón real es que me habia comprado un nodeMCU y un HW-MS03 y algo tenia que hacer con ellos.

En principio el proyecto es sencillo. Conectar el radar al nodeMCU y cuadno detecte movmiento enviar una notificación através de un webhook de IFTTT para que apararezca una notificacion en el movil. Pero resulta que nada es tan sencillo como parece, pero asi es más divertido.

Lo primero es concetar el HW-MS03 al nodeMCU lo cual es facil porque solo tiene tres pines.

HW-MS03 nodeMCU Funcion
GND GND Tierra
Vin 3V Alimentacion +3V (ojo no soporta +5V)
Out A0 Señal del HW-MS03 al nodeMCU

Cuando el HW-MS03 detecta algo que se mueve delante suyo (en teoria hasta 4 metros de distancia) manda la señal por el pin out. Basta con detectarla y realizar un http get contra la direccion que facilita IFTTT, facil. He usado al entrada analogica (A0) del nodeMCU porque no estaba segura si el voltaje de la patilla Out bastaria para activar las entradas digitales. Tras varias pruebas he fiajado el valor de umbral en 800.

if(analogRead(A0) > 800){
    ...
}

Pero volvamos un paso atras. resulta que concetar juntos ambas placas fue una mala idea, ambas trabajan en la misma frecuencia. Asi que cuando el nodeMCU esta usando el WiFi el HW-MS03 se vuelve loco. Tras realizar varias pruebas puede ver que esto solo pasa cuando la distancia entre ambos era de unaos 50 cm, pero con cables de esa longituda tambien tenia problemas para leer el estado del pin. La unica opción que me quedaba era encender el WiFi solo para enviar la notificación cuando detectase movimiento. Para ellos hice dos funciones una aque apaga el WiFi y otra que lo conecta. (En realida si que tenia otra opción usar un PIR en lugar de un radar y ya no tendria problemas con el WiFi pero seria menos divertido)

void onWiFi(){  
  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP(ssid, password);

  // Wait for connection
  while((WiFiMulti.run() != WL_CONNECTED)) {
    delay(500);
  }
}

void offWiFi(){
  WiFi.disconnect(); 
  WiFi.mode(WIFI_OFF);
  WiFi.forceSleepBegin();
}

Por lo que caundo se detecte movimiento habra que conectar el WiFi, enviarle mensaje y deconectarlo para volver a tener la alarma lista.

if(analogRead(A0) > 800){
  Serial.println("Alarm!!!!!!");      
  onWiFi();
  sendMsg();        
  offWiFi();
  firstStart = true;
}

firstStart es una variable booleana cuya funcion es dar tiempo «a correr» una vez conectada la alarma ya que si no saltaria al detectarte a ti mismo nada más activar la alarma. Tambien permite un tiempo de «descanso» tras el disparo de la alarma para evitar estar dando alarmas sin para y saturar el movil de notificaciones.

if(firstStart){
  Serial.println("Time to hide");    
  delay(60000);
  Serial.println("Watching...");
  firstStart = false;    
}  

Vamos aver como es el codigo que envia la notificacion. He optado por usar el servicio de IFTTT para ello hay que crearse una cuenta alli y activar un webhook, luego se crea una regla que lance una notificacion en el movil. O si se prefiere cualquiera de la opciones que disponibles.

void sendMsg(){
   http.begin("http://maker.ifttt.com/[tu codigo]/alarm/with/key/[pon aqui tu key]"); //HTTP

  int httpCode = http.GET();

  if(httpCode > 0){
    Serial.println("Alarm send");      
  } else {
    Serial.println("Error send alarm");     
  }

  delay(1000);
}

Mientras el nodeMCU este transmitiendo el radar queda inutilizado.

El codigo completo es:

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>

#include <ESP8266HTTPClient.h>

const char* ssid = "[tu ssid]";
const char* password = "[password]";

bool firstStart = true;
unsigned long timeNewMeasure = 0;

HTTPClient http;
ESP8266WiFiMulti WiFiMulti;

void setup(void){  
  Serial.begin(115200);
  offWiFi();
  //timeNewMeasure = millis()+1000;
}

void offWiFi(){
  WiFi.disconnect(); 
  WiFi.mode(WIFI_OFF);
  WiFi.forceSleepBegin();
}

void sendMsg(){
   http.begin("http://maker.ifttt.com/[tu codigo]/alarm/with/key/[pon aqui tu key]"); //HTTP

  int httpCode = http.GET();

  if(httpCode > 0){
    Serial.println("Alarm send");      
  } else {
    Serial.println("Error send alarm");     
  }

  delay(1000);
}

void onWiFi(){  
  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP(ssid, password);

  // Wait for connection
  while((WiFiMulti.run() != WL_CONNECTED)) {
    delay(500);
  }
}

void loop(void){
  if(firstStart){
    Serial.println("Time to hide");    
    delay(60000);
    Serial.println("Watching...");
    firstStart = false;    
  }  
  if(millis() > timeNewMeasure){
    timeNewMeasure = millis()+100;
    if(analogRead(A0) > 800){
      Serial.println("Alarm!!!!!!");      
      onWiFi();
      sendMsg();        
      offWiFi();
      firstStart = true;
    }
  }
}

Test de código en Arduino

Aprovechando lo que hemos creado en la entrada sobre el debug en arduino vamos a usarlo para para realizar test en el código de nuestros programas en arduino.

Al igual que el código debug, todo el código de test ha de desaparecer del código compilado cuando se quite el #define TEST. Por ejemplo en este caso cuando hacemos tests en lugar de devolver el valor de la entrada analógica fija uno por defecto, sin embargo cuando no hagamos test funcionara de forma correcta.

#ifdef TEST
  //código que se ejecutara para los test
  sensor = 128;
#else
  //Código que se ejecutara cuando no haya test
  sensor = analogRead(sensorPin);
#endif

También necesitamos una función que nos envíe por el puerto en serie si un test ha sido correcto o incorrecto. Como verificar le null a veces tiene su complejidad se ha creado otra función propia para ello.

void test(bool t){
  if(!t){
    Serial.println("False");
  } else {
    Serial.println("True");
  }
}

void testNull(void* o){
  if(o == ((void *)0)){
    Serial.println("is null");
   } else {
    Serial.println("is no null");
  }
}

En lugar de llamarlas directamente se hace usando las macros TEST(X) y TESTNULL(X). Para que desaparezcan cuando se desactiven los test.

¿Pero que pasa si cuando un test falla queremos para la ejecución? Por ejemplo porque puede ser peligroso para el circuito o porque algún elemento no se ha inicializado correctamente. Para eso se han definido dos funciones idénticas pero dentro de la etiqueta ERROR ya que pueden ser utiles en la ejecución habitual del programa cuando ya no se hacen test. ASSERT(X) y ASSERTNONULL(X)*. La diferencia con TEST es que si son ciertas paran la ejecución del programa llamando a la función stop() definida en la librería.

void stop(){
  Serial.print("STOP!!!");
  Serial.flush();
  noInterrupts();
  while(1){
    delay(1000);
  };
}

Esta función bloquea la ejecución del código creando un bucle infinito y bloqueando las interrupciones. Es una salida segura cuando no sabes como continuar y hacerlo podría dañar algo. También se le puede llamar directamente usando la macro STOP

Un ejemplo de uso:

#define DEBUG
#define TRACE
#define INFO
#define ERROR
#define TEST

#include <Debug.h>

int c = 0;

void(* reset) (void) = 0;

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

void loop() {
  TRACEMSG("Start loop");
  inc();
  TEST(c 40)
      STOP
  #endif

  delay(500);
}

void inc(){
  ENTER
    c++;
  EXIT
}

La librería puede encontrarse en mi github

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

Debug en Arduino

La forma habitual de hacer debug en arduino usar la instrucción Serial.print() para mostrar en la consola de monitorización del IDE de arduino los datos.

Este sistema tiene varias pegas:

  • Aumenta el tamaño del codigo y el espacio que ocupa el mismo
  • Consume tiempo de ejecución
  • Interfiere con el uso del puerto USB o de la comunicacion serie
  • No aporta ninguna informacion para saber en que parte del codigo ocurre.
  • No distingue entre mensajes de log, traza, debug, error…..

Una forma de resolver parte de estos problemas es usando el preprocesador de C.La idea es definir una serie de macros cuyo valor dependa de que se haya definido una variable del preprocesador. De tal forma que si la variable no esta definida la macro se reemplazada por nada asi evitamos que cuando no consuma espacio en memoria, que interfiera con el uso del puerto serie cuando no estamos depurando.

#ifdef DEBUG
#define DEBUGPRINT(X) Serial.print(X);
#else
#define DEBUGPRINT(X) // nothing
#endif

De tal forma que cuadno queremos depurar definimos la siguiente variable

#define DEBUG

Y todas los sitios dodne aparezca DEBUGPRINT(X) seran sustituidos por Serial.print(X); Pero cuando la quitamos todas las lineas son reemplazadas por nada y desaparecen del programa no ocupando ni esapcio ni tiempo de ejecución.

Hay que definir otro más, DEBUGPRINTLN(X) para el caso de que en lugar de quere un Serial.print(X) queramos usar un Serial.println(X)

Ademas vamos a permitir incluir alguna informacion de en que funcion, archivo y linea del codigo estmos.

#define DEBUGMSG(X) \
  Serial.print("DEBUG: ");  \
  Serial.print(__PRETTY_FUNCTION__); \
  Serial.print(' '); \
  Serial.print(__FILE__); \
  Serial.print(':'); \
  Serial.print(__LINE__); \
  Serial.print(' '); \
  Serial.println(X);

Podemos definir varias macros según el los «niveles» de log mostrando solo los que nos interesen, en nuestro caso vamos a definir cinco:
DEBUG
TRACE
INFO
ERROR
TEST

Al final nos quedan quince macros, a las que se han añadido dos más ENTER y EXIT para indicar la entrada y salida de una función que resultan muy útiles para la traza del código.

DEBUG:

DEBUGPRINT(X) Serial.print(X);
DEBUGPRINTLN(X) Serial.println(X);
DEBUGMSG(X) Serial.println(«DEBUG function file.ino:line X»);

TRACE:

TRACEPRINTLN(X) Serial.println(X);
TRACEPRINT(X) Serial.print(X);
TRACEMSG(X) Serial.println(«TRACE function file.ino:line X»);
ENTER Serial.print(«TRACE: ENTER -> function»);
EXIT Serial.print(«TRACE: EXIT -> function»);

INFO:

INFOPRINTLN(X) Serial.println(X);
INFOPRINT(X) Serial.print(X);
INFOMSG(X) Serial.println(«INFO function file.ino:line X»);

ERROR:

ERRORPRINTLN(X) Serial.println(X);
ERRORPRINT(X) Serial.print(X);
ERRORMSG(X) Serial.println(«ERROR function file.ino:line X»);

TEST:

TESTPRINTLN(X) Serial.println(X);
TESTPRINT(X) Serial.print(X);
TESTMSG(X) Serial.println(«TEST function file.ino:line X»);

Todo estas macros y alguna funcionalidad más se puden encontrar en la libreria debugino.

En un ejemplo de su uso podemos ver como funciona. Hay que señalar la necesidad de inicializar la comunicación serie con Serial.begin(9600);. Para configurar que mensajes se muestran y cuales no basta con quitar el #define correspondiente y esos mensajes desaparecerán.

#define DEBUG
#define TRACE
#define INFO
#define ERROR
#define TEST

#include <Debug.h>

int c = 0;

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

void loop() {
  TRACEMSG("Start loop");
  inc();
  TEST(c < 30);
  DEBUGPRINT("Value of C ")
  DEBUGPRINTLN(c);
  #ifdef ERROR
    if(c > 40)
      STOP
  #endif

  delay(500);
}

void inc(){
  ENTER
    c++;
  EXIT
}

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

También puedes ver el vídeo sobre este post en mi canal:

Haz click para ver le vídeo en mi canal de youtube

Interpretar lenguaje natural en un bot usando expresiones regulares

En una entrada anterior hemos visto como capturar voz en el navegador y convertirla en mensajes de texto. Ahora nos toca tratar esos mensajes para poder lanzar el comando adecuado. Cómo realmente lo único que necesitamos entender son comandos muy simples y predefinidos el sistema va a ser relativamente sencillo. Para procesar textos más complejos exiten sistemas más avanzados que requiren procesamiento del lenguaje natural. Estos sistemas requieren un conocimiento previo del lenguaje que van a procesar tanto por parte de la libreria como por parte del programador. Las expresiones regulares son sencillas de definir por el programador y para un sistema tan simple funcionan perfectamente.

En esta entrada usaremos la librería jsBotVoice que ayuda a usar expresiones regulares para interpretar los comandos.

Por lo general los bots habitualmente funcionan definiendo varias combinaciones de frases por cada comando precedidas del nombre del bot, «chispas» en nuestro caso. Por ejemplo las frases para preguntarnos la hora pueden ser:

«¿Qué hora es?»
«Dime la hora»
«Di la hora»
«¿Me dices la hora?»
«Hora»

Sin contar que puede ir precedida de cosas como. «Por favor».

Para facilitar el trabajo la mayoría de las librerías llevan herramientas para reducir el número de reglas necesarias permitiendo agrupar varias expresiones bajo una misma regla. En este caso vamos a usar expresiones regulares. Para ello dividiremos cada expresión en tokens que van asociados a expresiones regulares que describen su parte de la frase.

En nuestro caso el modelo sería:

bot please# tell time any

Cada token se traduciría en las siguientes expresiones regulares:

bot: chispa|chispas
please:por favor|porfa
tell: que|di[a-z]|me dices
time: la hora|hora
any: .

El problema es que al usar expresiones regulares tan poco concretas reconoceremos como válidas expresiones incorrectas como:

«chispas divaga hora y media»

En este caso podriamos añadir di|dim|dinos en lugar de di[a-z]*.

Hay que buscar un equilibrio entre flexibilidad de las expresiones y concretar los resultados.

Hemos logrado traducir todas las maneras de decirlo en una sola expresión pero si necesitáramos más no habría problema. Por ejemplo supongamos que queremos añadir:

«¿En que punto de la corriente temporal me encuentro?»

Podemos añadirla directamente por ejemplo:

bot: chispa|chispas|chispita
please:por favor|porfa
tell: en que|que|di[a-z]|me dices
time: la hora|hora
any: .

temporalPoint: punto de la corriente temporal

Que se traduciría en un nuevo comando de voz:

bot please# tell temporalPoint any

Vamos a ponernos con el código, primero definir los tokens:

var tokens = {
  'bot': 'chispa|chispas|chispita',
  'please': 'por favor|porfa',
  'tell': 'que|di[a-z]*|me dices',
  'time': 'la hora|hora',
  'any': '.*',
  'temporalPoint': 'punto de la corriente temporal'
}

Ahora con esos tokens podemos definir las expresiones que el bot va a entender


var vc1 = new VoiceCommand();
vc1.name="tell me time";
vc1.expressions[0] = "bot please# tell time any";
vc1.expressions[1] = "bot please# tell TemporalPoint any";
vc1.execute = function(exp, m, voice) {
 sayTime();
};

Cuando nuestro bot oiga un comando de voz que encaje con las expresiones definidas ejecutara el comando definido en execute al que se le pasa como parámetros:

  • exp: el indice de la expresión que ha coincidido
  • m: array con la coincidencia de texto de cada token de la expresión. m[0] incluye todo el texto
  • voice: referencia al al objeto Voice

Para iniciar el bot a escuchar basta con ejecutar init.

voice.init();

Pero si se quiere probar como reconoce las frases sin iniciar la parte de reconocer el audio puede usarse la función analyze pasando el texto a analizar.

voice.analyze(text);

Hay que tener en cuenta que hay que realizar pruebas reales para detectar errores habituales en el reconocimiento del habla. Por ejemplo en el caso del nombre del bot (bot: chispa|chispas) en se ha incluido chispa porque una gran cantidad de veces se reconocia la palabra «chispas» como si fuera «chispa».

En la web de la librería se puede encontrar más información.

Reducir ruido usando umbrales

Los umbrales son la forma más intuitiva de filtrado de errores. Consiste en establecer valores a partir de los cuales no confiamos en las medidas de nuestros sensores. Estos umbrales pueden establecerse por varios motivos.

  • Limite de funcionamiento de nuestro sensor. Son los limites más alla de los cuales sabemos que nuestro sensor no trabaja correctamente. Por ejemplo muchos sensores de distancia comienzan a dar lecturas muy poco confiables a partir de ciertas distancias tanto de muy cerca como de muy lejos.

  • Limites del entorno. Limites esperados del entorno donde nuestro sensor esta situado. Por ejemplo un sensor de teperatura colocado en una habitación normal de una vivienda se pueden fijar como limites como límite inferior 0° y como limite superior 60° aunque el sensor trabaje en un rango mayor de temperaturas.

Umbrales para el valor

Se aplican directamente al valor medido por el sensor. Las lecturas que excedan los limites son eliminadas. Estos filtros con los primeros que se comprueban y facilitan el trabajo de los filtros que apliquemos después al eliminar valores erroneos extremos. Su implementación es muy sencilla.

Siendo S el valor medido por el sensor , y Tmin y Tmax los umbrales mínimo y máximo S será válido si:

S > Tmin & S < Tmax

Umbrales para el cambio

Se aplican a la cantidad que cambia el valor medido por el sensor en un tiempo determinado. Para poder usar este filtro es necesario conocer el tiempo que a transcurrido entre medidas del sensor. Se fija un valor máximo de cambio por unidad de tiempo. El valor del sensor se considerará valido si el cambio de valor respecto de la lectura anterior es menor que el tiempo transcurrido por el valor máximo de la unidad de cambio

Siendo S el valor medido por el sensor, t el momento actual, dt el periodo de tiempo transcurrido desde la anterior medida de valores del sensor,  T el umbral de cambio máximo permitido S será válido si:

S(t) < S(t-dt) + (T * dt) & S(t) > S(t-dt) – (T * dt)

Hay que tener en cuenta que S puede ser un valor correcto dentro de los umbrales Tmin y Tmax, pero que ha cambiado demasiado rápido para considerar la lectura correcta. por ejemplo tenemos un sensor de temperatura dentro de una habitación y en un segundo la temperatura pasa de 12º a 35º, obviamente hay algo mal.

Generador justo de números aleatorios en Arduino

Partimos de que tenemos una fuente aleatoria de bits (como el generador del mi otro post) pero desconocemos si es justa. ¿Que significa justa?. Significa que ambos valores (0,1) tiene la misma probabilidad (50%). Un bit puede ser aleatorio, pero no por ello sus posible valores han de tener la misma probabilidad. Vamos a usar como ejemplo un generador de bits cuyas probabilidades son:

P(0) = 0,1 = 10%
P(1) = 0,9 = 90%

si usamos esta fuente para generar bytes, números con muchos bit a 1 (11111101, 11110011,….) son más probables que el resto. Para evitar esto podemos convertir el generador de números en uno justo.

Para transformar la salida de ese generador en un salida justa necesitamos obtener dos bits. Si son iguales los desechamos, si son distintos devolvemos el valor del primero (también se podría usar el del segundo). ¿donde esta el truco? Que las probabilidad de que la pareja de bits sea [0,1] o [1,0] son siempre las mismas.

[0,1] = P(0) * P(1)
[1, 0] = P(1) * P(0)

Podemos ver una tabla con todas las opciones

Bit1Bit2ResultadoProbabilidadEjemplo
00 P(0) * P(0)0,1 * 0,1 = 0,01
010P(0) * P(1)0,1 * 0,9 = 0,09
101P(1) * P(0)0,9 * 0,1 = 0,09
11 P(1) * P(1)0,9 * 0.9 = 0,81

La desventaja de este sistema es que si las probabilidades están muy desparejadas puede costar tiempo encontrar una pareja distinta, en este caso el 82% de la veces habrá que descartar la pareja de bits.

Sin embargo cuenta con la ventaja de que no hace falta que conozcamos las probabilidades si tenemos dudas y necesitamos que nuestra cadena aleatoria, simplemente podemos aplicar este método y listo.

El código de ejemplo:

byte randomFairAnalog(int analogInput){
  byte rnd = 0;

  for(int i = 0; i < 8; ){
    delay(5);
    int aux1 = analogRead(analogInput)%2;
    delay(5);
    int aux2 = analogRead(analogInput)%2;

    if(aux1 == aux2)
      continue;
 
    rnd += (aux1 << i);
    i++;
  }

  return rnd;
}

Cuidado con este código en caso de que por algún motivo las lecturas del puerto dejen de ser aleatorias y sean todo el rato la misma la función caerá en un  bucle infinito.

Todo el código puede verse aqui.

Puedes ver el vídeo de como generar números aleatorios con Arduino, donde se aplica este método:

Haz click para ver el vídeo en mi canal de Youtube

Programación Lógica

La programación lógica está inspirada en la lógica de primer orden. El lenguaje más conocido es Prolog y si no lo conocéis os recomiendo que le echéis un ojo. Es una forma de programar bastante diferente a la habitual y aunque para cosas genéricas pueden ser más «cómodos» otros paradigmas de programación para ciertas ramas de la I.A. facilita el desarrollo de forma espectacular.

En programación lógica por lo general hay tres tipos de sentencias:

  • Hechos (facts): Serían los conocimientos de los que se parte. Son sentencias del estilo «Juan es padre de Pepe» que se escribiría como algo así: padre(juan, pepe).
  • Reglas (rules): Calculan nuevos hechos estableciendo relaciones entre ellos.Para ello se utilizan variables. Por ejemplo «Si X es padre de Y e Y es padre de Z entonces X es abuelo de Z» que en Prolog sería: abuelo(X,Z) :- padre(X,Y), padre(Y,Z).
  • Consultas (queries): Se usan para extraer información. Hay de dos tipos, la primera dice si un hecho es verdadero o falso. Por ejemplo «¿Es padre Juan de Pepe?» Que se escribiría (depende el intérprete de Prolog) padre(juan, pepe). El otro tipo son las que tiene que completar un hecho. Por ejemplo «¿Quien es el padre de Pepe?» padre(X, pepe).

Si queréis jugar con él sin tener que instalar nada podéis probar aquí.

Guardando el conocimiento

Supongo que ya os imaginareis como va a aprender este sistema. Convertiremos los datos a un conjunto de hechos y reglas. Para obtener la respuesta de la base de datos usaremos consultas.

¿Qué ventajas tiene este sistema?. Que no solo «aprende» datos, si no que también las relaciones entre ellos y la forma de calcularlas.

En el siguiente ejemplo vemos qué se define el concepto de abuelo y en lugar de añadir abuelo(juan, javi) añadimos la regla genérica para calcularlo.

padre(juan, jose). # juan padre de jose
padre(jose, javi). # jose pare de javi
abuelo(X,Y):-padre(X,Z), padre(Z,Y).

Probamos la query:

abuelo(juan, X)

Se comporta igual que si hubiéramos introducido el dato directamente abuelo(juan, javi).

Otra ventaja de la programación lógica es que es comprensible por los seres humanos, bueno al menos por algunos de ellos.

Usando Prolog desde Javascript

La cosa se complicó en este punto. Si quería que Chispas pudiera usar prolog necesitaba que se pudiera interpretar desde JS. El problema es que no encontré ningún intérprete de programación lógica que encajara con mi idea. O no tenían una sintaxis que me convenciera o no funcionaban bien cuando las reglas se complicaban o lo hacían pero eran muy lentos. Al final el que más me gustaba era el de la página que os he enlazado antes. El único problema es que la web era monolítica y tendría que separar la parte del interprete, encapsularlo y dotarle de una forma de introducir las reglas y leer los resultados que fuera amigable para el desarrollador. Pero eso era fácil de resolverlo programado. Al final el resultado podéis verlo aquí.

Histéresis

El ruido puede causar comportamiento errático. En este caso vamos a ver que problemas puede causar cuando hay un umbral de activación en un sistema. Por ejemplo una célula fotoeléctrica que determina cuando encender o apagar unas luces. Para ello hemos programado un microcontrolador «lea» el valor de la célula y cuando sea menor de 100 encienda la luz. Cuando el valor se aproxime a este punto puede devolver lecturas como estas:

100, 99, 98, 101, 102, 99, 100, 97

Que traducido a la luz que controla sería:

Off, On, On, Off, Off, On, Off, On

La bombilla parpadea sin parar, va parecer que los espíritus tratan de comunicarse con nosotros a través de ella. Para evitar eso se puede separar el umbral en dos. Uno para encender la luz y otro para apagarla. A este hueco se le llama histéresis. Por ejemplo se podría decidir que por debajo de 100 se enciende pero no se apaga hasta que el valor sea 105. Esta distancia sirve para evitar que la luz actúe erraticamente encendiéndose y apagándose sin parar cuando el valor es próximo al del umbral de activación.

El resultado seria:

100, 99, 98, 101, 102, 99, 100, 97

Off, On, On, On, On, On, On, On

Evitando que la luz parpadee todo el rato.