Trabajar con datos tipados de un dispositivo desde una página web

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).

TipoRangoTamaño (bytes)DescripciónTipo en C/C++
Int8Array -128…12718-bit con signoint8_t
Uint8Array 0…25518-bit sin signouint8_t
Uint8ClampedArray 0…25518-bit sin signo (clamped)uint8_t
Int16Array -32768…32767216-bit entero con signoint16_t
Uint16Array 0…65535216-bit entero sin signouint16_t
Int32Array -2147483648… 2147483647432-bit entero con signoint32_t
Uint32Array 0…4294967295432-bit entero sin signouint32_t
Float32Array 1.2E-38…3.4E38432-bit punto flotantefloat
Float64Array 5E-324…1.8E308864-bit punto flotantedouble
BigInt64Array -2^63…2^63 – 1864-bit entero con signoint64_t
BigUint64Array 0…2^64 – 1864-bit entero sin signouint64_t

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

El ejemplo anterior con DataView

const buffer = new ArrayBuffer(2);
var view = new DataView(buffer, 0);
view.setUint16(0, 258); // (max unsigned 16-bit integer)
console.log(view.getUint16(0));// 258 -> 00000001 00000010
console.log(view.getUint8(0)); // 1 -> 00000001
console.log(view.getUint8(1)); // 2 -> 00000010

Gamificación en el diseño del software

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.

Usar el watchdog de Arduino como temporizador

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 alnzado 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:

WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1) | (1<<WDP0)

Si fuera 8 segundos (1001):

WDTCSR = (1<<WDIE) | (1<<WDP3) | (1<<WDP0);

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.

noInterrupts();
WDTCSR = (1 << WDCE) | (1 << WDE);
WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1) | (1<<WDP0);
interrupts();

La función que selanzará se define usando la macro ISR:

ISR (WDT_vect){ 
//aqui va el codigo de tu ISR para el watchdog
}

Veamos un ejemplo:

#include <avr/wdt.h>
 
int count = 0;

void setup() {
  noInterrupts();
  WDTCSR = (1 << WDCE) | (1 << WDE);
  WDTCSR = (1<<WDIE) | (1<<WDP2) | (1<<WDP1) | (1<<WDP0);
  interrupts();
  Serial.begin(9600);
}
 
void loop() {
  Serial.println(count);
  delay(400);
}
 
ISR(WDT_vect) { // WDT interrupt vector
  count++;  
}

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.

Lanzar tareas en una web cuando el navegador está desocupado. RequestIdleCallback

Un problema típico cuando desarrollas webs con mucho javascript es tener que ejecutar ciertas funciones (precarga, envío de estadísticas, actualizar los datos almacenados en local,…) pero que esta ejecución ocurra cuando la carga de la web haya terminado. Y con carga no me refiero únicamente a que haya terminado de cargar el documentos, si no de inicia izar todas las librerías para no afectar al rendimiento de la web. Para ello existe una solución en los navegadores: RequestIdleCallback

RequestIdleCallback lo que hace es encolar las funciones que se le pasan y llamarlas cuando el el navegador tenga poca carga de trabajo, así se mejora el rendimiento de la web y la experiencia de usuario. Su uso es muy sencillo:

requestIdleCallback(nonEssentialFunction);

Donde nonEssentialFunction es la función a la que se llamará cuando el navegador quede inactivo. La función recibe un parámetro un objeto deadline que tiene dos propiedades que podemos usar para planificar la ejecución de nuestro código:

  • timeRemaining(), una función que devuelve cuanto tiempo tenemos para ejecutar nuestro código.
  • didTimeout, que indica si el tiempo fijado como limite para ejecutar nuestra función (más adelante lo veremos) ha expirado. Si su valor es true, timeRemaining() devolverá 0.

Es decir que consultando deadline.timeRemaining() podemos saber si tenemos tiempo suficiente para ejecutar nuestro código. Hay que decir que es un valor orientativo y que si realmente nuestro código tarda más tiempo en terminar no pasara nada.

Veamos el ejemplo más simple, ejecutar una solo función:

function nonEssentialFunction(deadline) {
  task();
}

Si tenemos que realizar varias tareas podemos ir haciéndolas de una en una hasta que se termine el tiempo. na vez terminado el tiempo no se han acabado todas las tareas se puede volver a llamar a requestIdleCallback:

function nonEssentialFunction(deadline) {
    while (deadline.timeRemaining() > 0){ //mientras quede tiempo
        task(n); //ejecutar tarea n
        n--;
    }

    if(n > 0) { //si aun quedan tareas se encola la función otra vez
        requestIdleCallback(nonEssentialFunction);
    }
}

Veamos un ejemplo completo:

<html>
<script>
    function nonEssentialFunction(deadline) {
        while (deadline.timeRemaining() > 0)
            console.log(deadline.timeRemaining());
    }

    //Se prepara la función cuando este libre el procesador
    requestIdleCallback(nonEssentialFunction);
</script>

<body>
</body>

<script>
    //esto se ejecuta antes que nonEssentialFunction
    for(let i = 0; i < 100; i++){
        console.log(i);
    }    
</script>
</html>

Poner un limite de tiempo

Puede ser que no queramos que la función que queremos llamar se retrase demasiado, para ellos podemos pasar otro parámetro que establece el tiempo de expiación, pasado ese tiempo se lanza la ejecución de la función:

requestIdleCallback(nonEssentialFunction, { timeout: 2000 });

El tiempo viene expresado en milisegundos.

Cuando una función es ejecuta porque expira el tiempo fijado en timeout se puede ver en que deadline.didTimeout es true.

function nonEssentialFunction(deadline) {
    while (deadline.timeRemaining() > 0 || deadline.didTimeout )
        console.log(deadline.timeRemaining());
}


requestIdleCallback(nonEssentialFunction, {timeout, 3000});

Como saber si un elemento HTML se ve en pantalla en una web con scroll. IntersectionObserver

Todos los que llevamos un tiempo programando webs nos hemos tenido que enfrentar al scroll. ¿Qué vel el usuario en su pantalla? ¿Por que punto del div (párrafo, imagen,…) está?. Muchas veces la solución es un enrevesado conjunto de cálculos. Por suerte los navegadores han implementado un solución en el javascripr que permite hacerlo fácilmente: IntersectionObserver

Explicado de forma muy resumida, IntersectionObserver invoca a una función cuando el elemento html que le indiquemos se muestre en la pantalla en distintos porcentajes.

Primero veamos como se crea un IntersectionObserver:

let observer = new IntersectionObserver(handleIntersect, options);
observer.observe(element);

handleIntersect es la función a la que llamara el observer. Se le pasa un objeto entries que es un array de objetos entry correspondiente a vrios eventos de intersección asociados (por lo general es solo uno). Cada uno de los objetos entry tiene las siguientes propiedades:

entry.boundingClientRect
entry.intersectionRatio
entry.intersectionRect
entry.isIntersecting
entry.rootBounds
entry.target
entry.time

option es un objeto con tres posibles campos:

  • root: Indica que elemento que es usado como viewport para comprobar la visibilidad de nuestro elemento. Debe ser ancestro del nuestro. Por defecto es el viewport del navegador.
  • rootMargin: Delimita los margenes del elemento root que se ha de tener en cuanta al calcular la intersecciones. Por defecto son cero.
  • threshold: Es un número o un array que indican a que porcentaje de visibilidad se llama a la función asociada al observer. Su valor debe estar definido entre 0 (0%) y 1 (100%)

element es el elemento que el observer “estará vigilando”

Veamos un ejemplo completo:

<html>
<style>
    p {
        font-size: 120px;
        border: 2px solid black;    
    }
</style>

<body>
    <p id="p0">0</p>
    <p id="p1">1</p>
    <p id="p2">2</p>
    <p id="p3">3</p>
    <p id="p4">4</p>
    <p id="p5">5</p>
    <p id="p6">6</p>
    <p id="p7">7</p>
    <p id="p8">8</p>
    <p id="p9">9</p>    
</body>

<script>
    let element = document.getElementById("p5");

    let options = {
        root: null, //document.getElementById()
        rootMargin: '0px',
        threshold: [0, 0.5, 1.0]
    }

    let observer = new IntersectionObserver(handleIntersect, options);
    observer.observe(element);

    function handleIntersect(entries){
        entries.forEach(entry => {
            console.log("Elemento con id "+entry.target.id+"se muestra en pantalla en un porcentaje de: "+(entry.intersectionRatio*100));
        });
    }
</script>
</html>

Primeros pasos SDR. ¿Qué puedo hacer?

Acabas de comprar tu primer equipo de SDR que posiblemente sea un decodificador de TDT (DVB-T) con un chip RTL2832 o similar conectado por USB. ¿Ahora que?

Vamos a revisar rápidamente que herramientas hay que aprender a usar con tu equipo SDR.

Comprobar que todo funciona

Lo primero es comprobar si esta correctamente conectado y las capacidades de tu dispositivo. Para ello podemos usar:

rtl_test

La salida de la aplicación te muestra los datos de los sintonizadores encontrados.

Explorar el espectro de radio

Para ello puedes usar la aplicación gqrx que te permite explorar todo el espectro de radio.

Lo primero que puedes hacer es buscar una lista de emisoras de radio, sintonizar su frecuencia y probar a escucharlas. Puedes ver cómo afecta cambiar el ancho de banda o el modo de decodificación. Luego con cierta práctica puedes ir recorriendo el espectro a ver qué “escuchas”. Por ejemplo puedes buscar en que frecuencia trabajan algunos equipos inalámbricos y analógicos para ver si alguien cerca usa alguno.

Cuando tengas más soltura y conocimiento del tema gqrx puede quedarse corto para ti y puede usar algun software con más opciones como SDR++

Otra herramienta imprescindible que hay que saber usar es rtl_sdr que te permite capturar la señal y guardarla en un fichero. Muchas herramientas trabajan con estas capturas.

rtl_sdr -f frecuencia -g ganancia -s samplerate -n numero_de_muestras archivo

También hay que saber usar rt_power que captura el espectro de la señal.

Vamos a “cazar” aviones. ADS-B

Otra cosa que podemos hacer es “espiar” que aviones pasan por encima nuestro. Para ellos podemos escuchar la señales del sistema ADS-B que los aviones emiten. Para leer estos datos puedes usar el comando:

rtl_adsb

Así lo único que consigues es montón de caracteres alfanuméricos con el parámetro -V la información se muestra de una forma más amigable

rtl_adsb -V

Hay que decir que no solo los aviones emiten este tipo de señales,

El resultado es algo más entendible aunque es difícil saber que significa. Aquí puedes tener una pista de que significa cada cosa. Haciendo un pequeño resumen.

El campo Type Code indica el tipo de mensaje:

Type CodeTipo de mensjae
1-4Identificación de la aeronave
5-8Posición
9-18Altitud (altimetro)
19Velocidad
20-22Altitud (GNSS)
23-27Reservado
28Estado de la aeronave
29Información de estado y estado de destino
31Estado operacional

El campo ICAO Address puede ayudarnos a identificar el avión y por tanto el vuelo. Para ello s epeuden suar webs como flightradar24.

Si lo que quiere es tener la información más clara puedes usar dump1090 (o dump109-mutability en el caso de mi distribución de Linux). Basta con lanzar lo con la opción –interactive y procesara esos datos por ti y te los mostrará de un forma más sencilla de entender.

dump1090 --interactive
dump1090-mutability --interactive

Es posible que te ocurra que rtl_adsb muestre un montón de resultados pero dump1090 no muestre ningún avión, se puede deber a estaciones emisoras en tierra. A menos que vivas cerca de un aeropuerto de una gran ciudad hay que tener paciencia para captar los datos de un avión.

Internet of Thing

Pasemos a capturar y decodificar mensajes de IoT. Para ellos puedes probar con el programa rtl_433.

rtl_433

Los datos son devueltos de una manera muy intuitiva donde cada paquete es decodificado y mostrado en pantalla.

Por defecto escucha la frecuencia 433 MHz pero usando el parámetro -f puedes probar con diferentes frecuencias a ver qué detectas, para probar puedes empezar con 315 MHz, 345 MHz, 868 MHz o 915 MHz. Por ejemplo:

rtl_433 -F F868M

Si deseas volcar los datos a un fichero con el parámeto -F puedes configurar que formato usar de la lista: kv|, json, csv, mqtt, |influx, syslog, |null.