En las placas Arduino que controlan el puerto USB desde el propio microcontrolador como Leonardo o Micro hay que esperar a que el puerto serie se inicialice para evitar que se pierdan datos. Esto habitualmente se hace en la función Setup tras Serial.begin() con:
while(!Serial){ }
El problema de hacer esto es que se queda esperando a que el puerto USB este conectado y si no se conecta el programa no avanza. ¿Y si queremos tener un programa que funcione correctamente aunque no este conectado a un puerto USB?. Hay dos maneras de solucionar esto. La más simple reemplazar un delay() que espere el tiempo suficiente para que se haya iniciado (entre 500 y 1000 milisegundos suele ser una buena elección). La otra opción es, dentro del bucle, poner un tiempo limite y cuando pase «romper» el bucle con la instrucción break:
const long breakTime = 1000;//un segundo
unsigned long startTime = millis();
void setup(){
Serial.begin(9600);
while(!Serial){
if (millis() - startTime >= breakTime) {
break; //sale del bucle
}
}
}
En las placas como Arduino UNO que el soporte USB es externo al microcontrolador Serial siempre es valido y nunca entra en el bucle.
Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.
Para simular un ratón con una placa Arduino Leonardo contamos con la librería Mouse.h. Al igual que vimos con el teclado hay que usar las funciones Mouse.begin() y Mouse.end() para indicar que se empieza y se termina la simulación de ratón.
Se distingue entre la acción de pulsar un botón del ratón y la de presionar y luego soltar. Para simular la pulsación de un botón se puede usar la función Mouse.click(button). Mientras que para presionar se usa Mouse.press(button) y para liberar Mouse.release(button) (no existe un releaseAll para el ratón). La variable button hace referencia a una de las siguientes constantes definidas en la libreria Mouse.h :
MOUSE_LEFT
MOUSE_RIGHT
MOUSE_MIDDLE
Para simular el movimiento del ratón podemos usar la función Mouse.move(xVal, yVal, wheel) siendo los dos primeros parámetros la cantidad de movimiento (no, la posición) en el eje X y en el Y de la pantalla. El tercero indica el desplazamiento de la rueda central del ratón. Estos desplazamientos pueden ser positivos o negativos en un rango de -128 y 127 (izquierda-derecha, arriba-abajo). Su valor es un poco confuso ya que se refiere a «lo que se ha movido el ratón en ese eje» y afecta al cursor desde su posición actual. No es fácil trasladar ese valor a pixeles ya que también depende, entre otras cosas, de como el ordenador al que este conectado interprete esos valores. Una de las pegas de esta forma de trabajar es que el movimiento del ratón es relativo a las coordenadas actuales del cursor en la pantalla por lo que es difícil situar el ratón en un punto exacto de la misma. El truco para hacerlo con cierta precisión es llevar el ratón a una esquina de la pantalla y desde ahí tratar de moverlo al punto deseado. Vemoa sun ejemplo muy básico de esta idea:
import "Mouse.h"
void setup() {
Mouse.begin();
}
void loop() {
//Mover a la esquina superior izquierda
for(int i = 0; i < 20; i++){
Mouse.move(-128, -128, 0);
}
//Mover a un punto determinado
for(int i = 0; i < 10; i++){
Mouse.move(50, 20, 0);
}
Mouse.click(MOUSE_LEFT);
delay(5000);
}
Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.
Las placas Arduino basados en los microcontroladores 32u4 o SAMD (Leonardo, Esplora, Zero, Due y MKR) pueden simular ser un teclado conectado al puerto USB. Para ello es necesario usar la librería Keyboard.h. Lo primero para simular un teclado es llamar a la función Keyboard.begin() y para finalizar la simulación Keyboard.end().
Antes de comenzar con el resto de las funciones hay que hacer una aclaración. No es lo mismo pulsar que presionar, presionar es solo el gesto de bajar la tecla sin liberarla mientras que pulsar consiste en presionar y liberar la tecla.
Para simular la pulsación de una tecla podemos usar Keyboard.write(char) a la que se pasa como parámetro el código ASCII del carácter. El código del carácter se puede pasar de varias formas:
//Simular la pulsación de la tecla A
Keyboard.write('A'); //Character
Keyboard.write(65); //Decimal
Keyboard.write(0x41); //Hexadecimal
Keyboard.write(0b01000001); //Binario
Al permitir enviarle el código de la tecla en diversos formatos podremos usarlo para simular teclas que no impriman un carácter como pueden ser la teclas con flechas de dirección. Para ello la librería incluye un listado de constantes que representan el valor de estas teclas.
Si queremos simular la pulsación de varios caracteres alfanuméricos podemos usar la funciones Keyboard.print(string) y Keyboard.println(string) que reciben como parámetro un String y simulan la pulsación de todos sus caracteres, println ademas añade un salto de linea al final.
Para simular una tecla presionada se puede usar Keyboard.press(char) se pueden pulsar varias teclas a la vez. Incluso combinar funciones, por ejemplo el siguiente código seria como teclear «hola» con la tecla «Mayús» pulsada.
Para liberar una tecla tenemos dos funciones: Keyboard.release(char) que libera la tecla que le pases como parámetro y Keyboard.releaseAll() que libera todas las teclas que estén presionadas.
Cada vez hay más integración entre la web y la domática, «internet of things» o «web of things» son ejemplos de ello. Uno de los problemas que te puedes encontrar es que a nivel de programación son dos mundos muy dispares. En los dispositivos electrónicos reina C/C++ y similares, mientras que en la web el más usado es JavaScript. Aunque ambos lenguajes no trabajan directamente uno con otro lo hacen a través de API a veces surge el problema tratar los datos y es que mientras que C tiene un tipado de datos muy específico el de JS es menos concreto. ¿Qué podemos hacer cuando necesitamos un tipado de datos muy concreto que nos permita incluso trabajar a nivel de bits?. Simplemente usar datos tipados en JS.
Datos tipados en JavaScript
Pese a su fama JS tiene más tipos de datos que los conocidos String, boolean, number.
En este caso vamos a centrarnos en los siguientes tipos que podemos usar para imitar los tipos habituales de C/C++. Podemos elegir el tamaño en bits, si es con signo o sin signo incluso la forma en que se comporta cuando hay un desbordamiento. La única pega, solo funciona con arrays lo que nos supone una incomodidad a la hora de trabajar con un único elemento (tendremos que usar un array de tamaño 1).
Lo valores en punto flotante siguen el estándar IEEE.
Veamos algunos ejemplos de como usarlos y alguna propiedades interesantes (BYTES_PER_ELEMENT, byte que ocupa cada elemento; byteLength, longitud total del array en bytes):
var int8 = new Int8Array(3);
int8[0] = 42;
console.log(int8[0]); //42
console.log(int8.length); //3
console.log(int8.BYTES_PER_ELEMENT); //1
console.log(int8.byteLength); //3 = length * BYTES_PER_ELEMENT
var int16 = new Int16Array(3);
int16[0] = 42;
console.log(int16[0]); //42
console.log(int16.length); //3
console.log(int16.BYTES_PER_ELEMENT); //2
console.log(int16.byteLength); //6 = length * BYTES_PER_ELEMENT
Un dato clamped (acotado) es un dato que cuando se produce un desbordamiento de su valor por arriba o por abajo el dato toma su mayor y menor valor. O más simplemente explicado si intentas asignarle un valor mayor de 255 o menor que 0 se le asignará el valor 255 y 0 respectivamente. Lo datos que no son de tipo clamped simplemente ignoran los bits del desbordamiento. Podemos verlo mejor con un ejemplo:
var uInt8Clamped = new Uint8ClampedArray(1);
var uInt8 = new Uint8Array(1);
uInt8Clamped[0] = 256;
uInt8[0] = 256;
console.log(uInt8Clamped[0]); //255
console.log(uInt8[0]); //0
uInt8Clamped[0] = 265;
uInt8[0] = 265;
console.log(uInt8Clamped[0]); //255
console.log(uInt8[0]); //9
ArrayBuffer y Dataview
Aún hay un truco más que permite JS para trabajar con estos datos. Crear un ArrayBuffer que permite reservar un «espacio de memoria» indicando el tamaño del mismo en bytes. Desde un ArrayBuffer no puedes ni leer ni escribir esta memoria, para ello tienes que asignarlo a un array tipado como los uqe hemos visto antes. Puedes asignar el mismo ArrayBuffer a varios arrays tipados que lo compartirán:
var buffer = new ArrayBuffer(2);
var view8 = new Uint8Array(buffer);
var view16 = new Uint16Array(buffer);
view16[0] = 258;
console.log(view16[0]);// 258 -> 00000001 00000010
console.log(view8[0]); // 2 -> 00000010
console.log(view8[1]); // 1 -> 00000001
En el ejemplo se ve como un ArrayBuffer de 2 bytes se puede leer desde un array de 1 elemento de 16 bits o un array de un array de 2 elementos de 8 bits y como los valores se solapan.
Hay otra forma de hacerlo, con un DataView. Las ventajas de los DataView son dos:
Permiten escribir y leer en el ArrayBuffer con cualquier tipo de datos usando el métodoset y get correspondiente getUint8(), setUint8(), getInt8(), setInt8(), getUint16(), setUint16(), …
El orden en que se recuperan los bytes es más intuitivo
La gamificación consiste en aplicar mecánicas del juego a otros entornos para lograr un refuerzo de los comportamientos deseados del usuario. Es útil conocer sus mecanismos para poder aplicarlos al diseño de software al que puede aportar varios beneficios:
Aumenta el engagement.
Reduce el estrés del usuario.
Motiva el uso del software.
Reduce el esfuerzo de aprender a usarlo.
Motiva que los usuarios compartan su experiencia y avances entre sus conocidos ayudando a la difusión.
Tipos de jugadores:
Segun Richard Bartle:
Achievers (10%): tienen como objetivo resolver retos con éxito y conseguir una recompensa por ello.
Explorers (10%): quieren descubrir y aprender cualquier cosa nueva o desconocida del sistema.
Socializers (80%): sienten atracción por los aspectos sociales por encima de la misma estrategia del juego.
Killers (1%): buscan competir con otros jugadores
Jugadores VS. Mundo: algunos usuarios (Socializers y Killers) que buscan relacionarse, sea del modo que sea, con otros usuarios, mientras que otros (Explorers y Achievers) prefieren dinámicas que les permitan relacionarse con el mundo del sistema.
Interacción VS. Acción: algunos usuarios (Killers y Achievers) quieren actuar directamente sobre algún elemento, ya sea otro usuario o el propio sistema, mientras que otros (Socializers y Explorers) prefieren dinámicas de interacción mutua.
Mecánicas de juego:
Las mecánicas de juego son aquellas reglas que consiguen que la actividad se asimile a un juego o a una actividad lúdica, pues consiguen la participación y el compromiso por parte de los usuarios a través de una sucesión de retos y barreras que han de superar. Existen muchas mecánicas de juego distintas, pero cabe destacar:
Recolección: se usa la afición de coleccionar de los usuarios y la posibilidad de presumir ante nuestras amistades de estas colecciones.
Puntos: trata de incentivar al usuario mediante un sistema de puntos con el que conseguir algo, como prestigio o premios.
Comparativas y clasificaciones: someten a los usuarios a un sistema de clasificación que tiene en cuenta su implicación en la actividad. De esta manera se explota el espíritu competitivo de los usuarios.
Niveles: con este sistema se premia la implicación del usuario en la actividad otorgándole un nivel o descripción con el que distinguirse del resto, y que anima a los usuarios nuevos a igualarlos.
Realimentación o feedback: cuando el sistema responde a las actividades del usuario, éste valora que el trabajo que ha hecho tiene una implicación relevante.
Reconocimiento social: Se da a conocer públicamente que logros o acciones ha realizado el usuario
Prácticas para aumentar el engagement:
Adaptación hedónica: premiar cada vez que se entra de alguna forma. Se crea la sensación de que es necesario entrar a diario. Sentimiento de beneficio si se entra de pérdida si no se entra.
Generar tareas incompletas (Efecto Zeigarnik): el usuario tiene tendencia a recordar las tareas no terminadas y sentir la necesidad de darles fin. La forma es estructurar la gamificación por “capítulos” que indiquen, u obliguen, cuando parar pero que dejen tareas pendientes.
Sensación de control: el usuario debe de tener la sensación de que sus acciones son las que causan todo.
Captar la atención rápidamente: un usuario prueba entre entre 90 segundos y 3 minutos antes de decidir si sigue. Hay que engancharle pronto y darle alguna recompensa por hacerlo bien lo antes posible. No perder tiempo, no dejarle “explorar” ir directos a engancharle.
Interactividad: el usuario debe de estar realizando acciones, no es un ente pasivo.
Perseguir al usuario: notificaciones, emails, insistir sin resultar pesado.
Refuerzo positivo: hacer que el usuario sienta que es realmente bueno en su labor. Alabar su desempeño.
Interacciones personalizadas: usar el nombre del usuario, un trato cercano y el humor aumenta el éxito de las interacciones.
Crear competición: compararse con los demás incentiva a avanzar para superarles.
Que hace a una gamificación buena:
No ha de ser intrusiva, se tiene que poder usar el software sin participar en el juego.
Orientada a unir al equipo más que a la competición.
Permite socializar (el 80% de los jugadores son sociales).
No es cuestión de hacer que el trabajo sea un juego es cuestión de dar un extra para incentivar.
Buscar el refuerzo positivo y premiar. Evitar el castigo y las penalizaciones.
Hay tareas o momento mentalmente desagradables, la gamificación puede hacer el trance más fácil.
Hay que tratar de integrarla dentro de “una historia” que motive al usuario a seguir a delante.
Tener objetivos personalizados.
Permitir compararse con los demás promoviendo la sana competencia.
Ser útil, no ha de ser simplemente dar puntos o premios, los objetivos propuestos deben de tener un fin más allá de simplemente “enganchar” al usuario.
Se puede usar el watchdog de la placa para que en lugar de reiniciar el programa ejecute una función que nosotros le definamos. Para cambiar el modo hay que modificar el registro WDTCSR que almacena la configuración del watchdog. Este registro ocupa un byte donde cada bit tiene el siguiente significado:
Bit
7
6
5
4
3
2
1
0
nombre
WDIF
WDIE
WDP3
WDCE
WDE
WDP2
WDP1
WDP0
WDIF: Es un flag que indica si el watchdog ha lanzado la interrupción.
WDIE: Si se pone a 1 cuando el tiempo de espera pase el watchdog ejecutara la ISR asociada
WDCE: Tiene que ponerse a 1 para poder cambiar al valorde WDE o WDP3..0. Pasados cuatro ciclos de reloj su estado vuelve a 0.
WDE: Si se pone a 1 cuando el tiempo de espera pase el watchdog reiniciara la placa
WDP3..0: Fija el valor de tiempo de espera del watchdog
Para fijar el valor del tiempo de espera se pueden usar los siguientes valores:
WDP3
WDP2
WDP1
WDP0
Tiempo
0
0
0
0
16 ms
0
0
0
1
32 ms
0
0
1
0
64 ms
0
0
1
1
125 ms
0
1
0
0
250 ms
0
1
0
1
500 ms
0
1
1
0
1 sg
0
1
1
1
2 sg
1
0
0
0
4 sg
1
0
0
1
8 sg
Veamos como configurar WDTCSR correctamente. El funcionamiento es algo extraño, primero hay que habilitar al escritura de los registros poniendo los bits WDCE y WDE a 1:
WDTCSR = (1 << WDCE) | (1 << WDE);
Luego inmediatamente hay que poner WDIE a 1, WDE a 0 y los bits WDP3, WDP2, WDP1, WDP0 con el valor de tiempo que deseemos mirando la tabla. Por ejemplo supongamos que queremos poner 2 segundos de tiempo de espera, miramos la tabla (0111) y colocamos a 1 los bits que correspondan:
Cada constante contiene el número de bit al que representa por ejemplo WDP3 es 5 al hacer 1<<WDP3 estamos poniendo a 1 el bit 5 y hacemos la operación | (or) para unir todos esos bits en un solo byte. Todo esto hay que hacerlo deshabilitando todas las interrupciones.
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:
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.
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
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:
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:
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:
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.