Usar KDEConnect para crear una botonera y controlar acciones en el PC desde el móvil

Vamos a ver como usar KDEConnect para crear una botonera en el móvil para interactuar con el ordenador y las aplicaciones. En principio es fácil ya que KDEConnect da la opción de «ejecutar órdenes» en el ordenador desde el móvil, basta con dar de alta el comando en el ordenador y aparecerá un botón en la aplicación para móvil que permita lanzarlo. Para ello hay que ir a la configuración de KDEConnect y luego a la configuración del plugin «Ejecutar órdenes». Ahí podemos dar de alta las nuevas órdenes indicando el texto que se mostrará en el botón en el móvil. Tenemos la limitación de que solo se puede introducir ordenes en una línea, si queremos lanzar varios comando uno tras otro tenemos dos opciones: crear un fichero de script y llamar a ese fichero, introducirlas en esa línea separada por «&&».

Ejecutar comandos en segundo plano:

Es el caso más sencillo, para ello podemos usar la opción «Ejecutar órdenes» que hemos comentado antes. Los comando se lanzan en segundo plano.

Ejecutar acciones en programas:

En este caso queremos realizar acciones sobre aplicaciones. Lo que significa que no siempre tendremos un comando que lo haga. Si por ejemplo queremos que al pulsar sobre un comando de la pantalla de nuestro movil se cambie el pincel de nuestra herramienta de dibujo seŕa necesario simular pulsaciones de teclado. Para casos más complicados es posible que tengamos que simular clicks de ratón. Para ello en Linux tenemos la herramienta xdotool que permite simular el teclado, el ratón, actuar sobre las ventanas del sistema y el escritorio.

Supongamos que queremos tener un comando que cada vez que pulsemos su botón correspondiente en el móvil simule que se teclea la fecha:

xdotool type $(date +"%d/%m/%y %H:%M")

O que pulse control + s para guardar el archivo que este editando:

xdotool key ctrl+s

Abrir una aplicación

Tenemos dos opciones usar xdotool exec o directamente usar el comando que lanza el programa. Por ejemplo para lanzar VLC:

vlc

xdotool exec vlc

Control multimedia

No hay que hacer nada, KDEConnect ya tiene un plugin para controlar la reproducción multimedia del PC desde el móvil.

Teclado y ratón remotos

Tampoco hay que hacer nada, KDEConnect ya permite usar la pantalla del móvil como ratón y el teclado remotos

Mostrar la respuesta en el móvil

Es posible que queramos que el comando lanzado nos devuelva algún mensaje diciéndonos si ah terminado y ha sido con esxito.Ya hay un post sobre este tema, puedes leerlo aquí.

Como ejemplo vamos a usar un comando que nos permite ver cuanto espacio nos queda en el disco:

kdeconnect-cli -d $(kdeconnect-cli -l --id-only) --ping-msg "$(df -h)"

Iconos

Para que el resultado sea al más visual puedes añadir emojis como si fueran iconos. Puedes usar algún teclado de emojis web para copiarlos y pegarlos o instalar uno como emoji-keyboard. Puedes usar varios emojis y combinarlos con otros caracteres alfanuméricos.

El resultado

Ejemplo de configuración en el PC

Configuración de KDEConnect en el PC

Resultado en la pantalla del movil

Resultado en la pantalla del movil

Enviar datos desde el ordenador al móvil con KDEConnect

Vamos a ver como mandar desde un script datos al móvil usando la herramienta KDEconnect. Para ello teneos dos comandos: kdeconnect-cli -d <device-id> –ping-msg «mensaje»que envía una notificación al movil con el texto del «mensaje» y kdeconnect-cli -d <device-id> –share <ruta> que envía el fichero que este en la ruta indicada.

Obtener device-id

Para obtener el listado de ids de todos los dispositivos que han sido vinculados a tu ordenador basta con ejecutar el comando:

kdeconnect-cli -l

El resultado es un listado con el nombre del dispostivo, su device-id y el estado. Si se desea obtener solo el el id se puede usar el parámetro –id-only :

kdeconnect-cli -l --id-only

Si solo tenemos un dispositivo vinculado podemos usar este comando para evitarnos tener que apuntar el device-id, en lugar de poner el device-id podemos usar:

$(kdeconnect-cli -l --id-only)

Más adelante veremos su uso dentro de un comando.

Enviar una notificación

Supongamos que queremos volcar el resultado de un comando en una notificación, podemos usar:

kdeconnect-cli -d <device-id> --ping-msg "$(ls -al)"

Como ya hemos dicho si solo tienes un dispositivo vinculado puedes usar:

kdeconnect-cli -d $(kdeconnect-cli -l --id-only) --ping-msg "$(ls -al)"

Podemos mandar un fichero corto:

kdeconnect-cli -d $(kdeconnect-cli -l --id-only) --ping-msg "$(cat fichero.txt)"

Este sistema tiene la ventaja de que la notificación aparece de forma inmediata en el teléfono, pero presenta la desventaja de que no se pueden enviar grandes cantidades de texto porque no «caben» y se cortan.

Enviar un fichero

Para enviar un fichero basta con conocer su ruta y usar el siguiente comando:

kdeconnect-cli -d <device-id> --share <ruta>

Podemos usarlo para enviar la salida de un comando:

ls -al > out.txt && kdeconnect-cli -d <device-id> --share out.txt

Lo podemos combinar con el «truco» de antes del device-id:

ls -al > out.txt && kdeconnect-cli -d $(kdeconnect-cli -l --id-only) --share out.txt

En el móvil aparece una notificación de la descarga del fichero.

Memoization y recursividad

Ya hemos visto como usar la memoization para mejorar el rendimiento del código memorizando los resultados de las llamadas a funciones. sin embargo este sistema tiene un punto débil. Las funciones recursivas. Veamos un ejemplo con la función factorial:

function factorial(n){
    console.log("factorial "+n);
    if(n < 2){
        return 1;
    } else {
        return n*factorial(n-1);
    }
}

Ahora crearemos un objeto Memo donde almacenar las llamadas y los resultados:

let memof = new Memo(factorial);
memof.call(4);
factorial 4
factorial 3
factorial 2
factorial 1
24

memof.call(4);
24

memof.call(5);
factorial 5
factorial 4
factorial 3
factorial 2
factorial 1
120

Se puede ver que tras calcular el valor de factorial(4), cuando se llama a factorial(5) este vuelve a llamar a factorial con valores que factorial(4) ya ha llamado y que no han sido memorizadas. Esto se debe a que dentro de la función factorial se llama a factorial(n-1) en lugar de a memof.call(n-1) por lo que esa llamada no pasa por el proceso de memorización y no se guardan esos resultados. Esto le quita mucha eficacia a la memoization. Sin embargo hay un truco que no es muy elegante pero que puede ayudarnos.

Cuando declaras una función en javascript es como si declararas una variable y le asignaras una función:

function factorial(n){
...
}

Es lo mismo que:

let factorial = function(n){
...
}

Que pasaría si le asignáramos a la variable factorial otra función distinta, pues que el código que llama a factorial llamaría ahora a esa nueva función. Y si esa función llama a memof.call() las llamadas recursivas se memorizarían:

factorial = (n) => memof.call(n);
factorial(4);
factorial 4
factorial 3
factorial 2
factorial 1
24

factorial(4);
24

factorial(5);
factorial 5
120

Pero si la función factorial ya no apunta al código de factorial ¿Como es que sigue funcionando y calculando el valor del factorial?. La respuesta es que la función original sigue referenciada:

class Memo {

    constructor (func, keyFunc) {
        this.func = func;
        this.cache = {}; 
        this.calculateKey = keyFunc || this.calculateKey;
    }
....
}

El código anterior sigue siendo accesible desde la variable func de la clase memo.

Podéis encontrar todo el código y ejemplos en la librería memo.js

Memoization y persistencia

Ya hemos visto como crear nuestra propia librería de memoization. Pero sus beneficios duran solo mientras la aplicación está en memoria. Si por ejemplo recargamos la web donde la usamos perdemos todos los resultados memorizados. Para evitar eso podríamos exportar el mapa de clave valor que guarda los parámetros y su valor. Una vez exportado podemos persistirlo como queramos. En el servidor, en el almacenamiento local del navegador donde se quiera.

Todo lo que se comenta en este texto se puede encontrar en la librería memo.js

Para exportado vamos a convertirlo en una cadena de texto que contenga los datos en formato JSON. En nuestro caso los datos están guardados dentro de la variable cache, la cual convertiremos a cadena usando JSON.stringify()

    getCache(){
        return JSON.stringify(this.cache);
    }

Una vez tenemos la cadena de texto para poder persistirla es necesario poder hacer el paso contrario. Recuperar los datos de la cache partiendo de la cadena para ellos emplearemos JSON.parse()

    loadCache(cache){
        this.cache = JSON.parse(cache);
    }

Vamos a ver un ejemplo sacado de la librería memo.js de como usar estas dos funciones para «clonar» un objeto que guarda la memoization de la función sum:

<html>

<script src="memo.js"> </script>

<body>
Abre la consola!!!!
</body>

<script>
function sum(a,b){
    console.log("calculando "+a+" + "+b);
    return a+b;
}

let memo = new Memo(sum);

console.log(memo.call(1,2)); //invoca sum
console.log(memo.call(1,2)); //no invoca sum
console.log(memo.call(2,3)); //invoca sum
console.log(memo.call(2,1)); //invoca sum

let storeMemoCache = memo.getCache(); //exporta la cache a String

//si se desea persisitir la memoization basta con persistir storeMemoCache

let memo2 = new Memo(sum);
memo2.loadCache(storeMemoCache); //carga la cache
console.log(memo2.call(1,2)); //no invoca sum

</script>

</html>

Uso de memoization para mejorar el rendimiento

Memoization, o memoización en español, es una técnica que permite ahorrar tiempo y recursos en el caso de que tengamos funciones cuyo tiempo de ejecución sea muy largo y se le llame varias veces con los mismos parámetros.

La idea es muy sencilla. En cada llamada a la función realizamos los siguientes pasos:

  1. Con los parámetros calculamos una clave única
  2. Comprobamos si ya tenemos un resultado asociado a esa clave
  3. Si lo tenemos devolvemos ese valor.
  4. Si no lo tenemos llamamos a la función
  5. Almacenamos el resultado de la función asociados a la clave calculada

La idea es calcular una clave única a partir de los parámetros que se pasan a la función y almacenar el resultado de la primera vez que se le llama para luego las las siguientes veces poder devolverlo sin calcularlo recuperándolo de la memoria.

Hay que ser consiente de que esta técnica permite una ventaja: reducir el tiempo de ejecución y pero con dos penalizaciones: la primera es que la mejora solo se consigue a partir de la segunda vez que se llama a la función, la primera vez se incrementa el coste puesto que se añade el tiempo de cálculo de la clave y de almacenar el resultado en memoria. La segunda penalización es que requiere espacio en memoria para guardar los resultados.

Esto solo funciona si la función cumple ciertas condiciones:

  • El resultado de la función depende única y exclusivamente de los parámetros que se le pasan.
  • Siempre devuelve el mismo resultado para los mismos parámetros.
  • La única funcionalidad que realiza la función es calcular el resultado.
  • Se llama varias veces a la función con los mismo parámetros.
  • El coste de calculo del resultado es mayor que el coste de calcular la clave y buscar el resultado almacenado en memoria.
  • Se intenta optimizar el tiempo de ejecución sobre el uso de memoria.

Ejemplo de implementación

Vamos a ver como implementar una clase JS para aplicar memoization.El código completo se puede encontrar en su repositorio de github.

La memoization tiene dos elementos principales, la función sobre la que se va a aplicar y como se calcula clave a partir de los argumentos que se pasan a esta función. En el constructor de la función vamos requerir estos dos argumentos, si bien el segundo tiene un valor por defecto. También es necesario inicializar la cache donde se guardaran los resultados.

    constructor (func, keyFunc) {
        this.func = func;
        this.cache = {}; 
        this.calculateKey = keyFunc || this.calculateKey;
    }

Vamos a ver un ejemplo de función que calcula la clave para almacenar el resultado. En la versión por defecto lo que se hace es generar un array con todos los parámetros y convertirlos a un cadena de texto JSON.

    calculateKey(){
        let args = Array.from(arguments);
        return JSON.stringify(args);
    }

Tenemos, la función y la forma de calcular la clave. Nos queda como gestionamos la cache. Los pasos son:

  1. Calcular la clave
  2. Recuperar el valor asociado a esa clave
  3. Si el valor existe se devuelve
  4. Si no existe se calcula
    call(){ 
        let key = this.calculateKey(...arguments);
        if(this.isInCache(key)){
            return this.cache[key];
        } else {
            let result = this.func(...arguments);
            this.add(key, result);
            return result;           
        }
    }

El código completo:

class Memo {
    constructor (func, keyFunc) {
        this.func = func;
        this.cache = {}; 
        this.calculateKey = keyFunc || this.calculateKey;
    }
    call(){ 
        let key = this.calculateKey(...arguments);
        if(this.isInCache(key)){
            return this.cache[key];
        } else {
            let result = this.func(...arguments);
            this.add(key, result);
            return result;           
        }
    }
    calculateKey(){
        let args = Array.from(arguments);
        return JSON.stringify(args);
    }
    isInCache(key){
        return key in this.cache;
    }
    add(key, value){
        this.cache[key] = value;
    }
}

Estrategias para generar la clave de la cache

Hay casos en que varias configuraciones distintas de parámetros son equivalentes. Cuando estos casos se conocen a priori, sin necesidad de calcular el resultado, una mejora puede ser cambiar la función que genera la clave para que tenga en cuenta esta particularidad. Principalmente hay dos estrategias para hacerlo: que la clave que se genera sea la misma para todos estos casos o generar todas las claves que se sabe que tienen el mismo resultado y añadirlas a la cache. Veamos algunos ejemplos.

Propiedad conmutativa: Hay casos en que puede intercambiar el valor de algunos parámetros sin que afecte al resultado. En estos casos una buena optimización es una vez calculado uno de estos casos se aplique a todos los que son equivalentes.

Por ejemplo si la función sum(a,b) realiza la suma de a y b. El resultado será el mismo aunque intercambien valores. sum(2,5) = sum(5,2). Podríamos generar ambas claves «[2,5]» y «[5,2]» y asignarles el mismo resultado.

Otra forma de hacerlo seria ordenar los parámetros de menor a mayor así sum(2,5) y sum(5,2) generarían la misma clave: «[2,5]».

Aproximaciones de números con decimales: En muchos casos la diferencia entre dos números decimales es tan pequeña que prácticamente no va a afectar al resultado. Para evitar calcular un resultado cuando no es necesario se puede calcular la clave limitando el numero de decimales.

Por ejemplo si fijamos que número máximo de decimales sean 2, tanto sum(3.141592 , 2) como sum(3.1416 , 2) generarían la misma clave «[3.14,2]»

Cadenas de texto: Otro de los casos habituales es que haya cadenas de texto que son equivalentes. Un caso habitual son las mayúsculas y minúsculas, en muchos casos no influye en el resultado. En un caso así search(«juan») y search(«Juan») deberían de generar las dos la misma clave: «[juan]»

Datos estructurados: Un caso especial es el de los argumentos que son alguna estructura de datos y que pueden tener muchas formas equivalentes. Por ejemplo, si uno de los parámetros que recibe es un JSON con dos valores «a» y «b» estas dos formas serian la misma: {«a»:1,»b»:2} y {«b»:2,»a»:1}. Llevando el ejemplo más lejos, si la funciona solo va mirar los campos «a» y «b» el caso {«a»:1,»b»:2,»c»:3} seria idéntico. Otro caso son los arrays cuando el orden de los elementos dentro de él da igual. Estos casos son mucho más difíciles de tratar ya que no se puede poner una regla que siempre sea válida y habrá que buscar soluciones propias para cada caso.

Como ejemplo podemos ver un par de ejemplos de como calcular la clave para la cache.

El primero ordena los argumentos que se le pasan;

function calculateKeySorted(){
    let args = Array.from(arguments).sort();    
    return JSON.stringify(args);
}

El segundo convierte los argumentos a minúsculas:

function calculateKeyToLowerCase(){
    let args = Array.from(arguments);    
    return JSON.stringify(args).toLowerCase();
}

Comparar pantallas para encontrar errores visuales durante los test del software

Uno de los problemas de los test automáticos para probar aplicaciones es que no se guían por el aspecto visual de la aplicación. Puedes tener un botón torcido, un texto que no se lee o un color que no corresponde y los test serán correctos, sin embargo para el usuario es importante que el botón que tiene que pulsar sea visible o que pueda leer ese texto ilegible.

Es una tarea difícil de automatizar. Programar código para comprobar la correcta disposición de todos los elementos es algo muy costoso. Y tener una IA que detecte posible elementos erróneos puede sonar muy interesante pero es aun más costoso y complicado. Así que vamos a optar por una solución más sencilla, comparar una imagen que sabemos que esta bien con una captura de pantalla de la aplicación durante las pruebas. Luego calcularemos las diferencias entre ambas imágenes.

Hay múltiples librerías para realizar la comparación de la imagen como pixelmatch o Resemble.js. Aunque estas librerías tienen bastante funciones el algoritmo básico de comparación es muy sencillo. Se comparan uno a uno los pixel de la imagen de referencia con los de la imagen capturada, se fija un valor de umbral. Si la diferencia entre ambos pixel es mayor que el valor umbral se considera que ese pixel es distinto y se marca.

Para que funcione bien la imagen que se usa como modelo ha de ser idéntica a la que se obtiene de la captura de pantalla. Podéis estar pensando que siempre se puede reescalar una de la dos imágenes pero generalmente eso mete «ruido» en los bordes de los elementos de la imagen que el algoritmo de comparación detecta como diferencias.

Los pasos a segir son los siguientes:

  1. Captura de pantalla
  2. Cargar imagen modelo correspondiente
  3. Crear una tercera imagen comparando los pixeles de las dos anteriores. Cada pixel cuya diferencia supere cierto valor fijado se marcara como «diferente», por ejemplo poniéndolo en rojo.
  4. Buscar si existe alguna diferencia en la imagen resultado de la comparación
  5. Si existe alguna diferencia se deja la imagen con el resultado de la comparación y se avisa al usuario para que la revise.

El principal problemas son los datos que cambian cada vez que se realiza el test. Por ejemplo: ids de elementos, fechas, horas, elementos generados en parte o completamente por procesos (pseudo)aleatorios. Estos elementos dejan «una mancha» en el resultado de la comparación. Una solución para evitar estas manchas es «taparlas» para ello vamos a usar plantillas con una zona de un color especial que cuando el algoritmo las lee ignora.

Otra técnica para ignorar «pequeñas cantidades de error» es dividir la imagen en cuadrados, por ejemplo de 16×16. En cada cuadrado hay un total de 256 pixeles, solo lanzaremos una advertencia si la cantidad de pixeles marcados como diferentes supera cierto número. Por ejemplo el 20%, o lo que es lo mismo 51 pixeles. Con esto logramos que solo diferencias «notables» se consideren.

Este es un sistema realmente simple para ayudar a resolver el problema de comprobar el aspecto visual de la aplicación o la web durante los test. Si bien no resuelve todo el problema y sigue siendo necesario un humano para verificar las imágenes marcadas como errores. Agiliza mucho el proceso de comprobar el correcto aspecto visual de la aplicación o web.

Calcular la media aritmética, media geométrica, media armónica y media cuadrática en Arduino

Vamos a ver como implementar más funcione estadísticas en un entorno tan limitado como Arduino. Para ello necesitamos usar formas acumulativas de cálculo. En este caso acumulativas se refiere a que no tengan que calcularse de nuevo todos los valores cada vez que se añade uno nuevo, esto nos ahorra gran cantidad de cálculos y de espacio en memoria.

Media aritmética

Es o que normalmente llamamos «media». Corresponde con la suma de cada uno de los valores de muestra dividido entre el numero de valores:

\frac{1}{n} \sum_{} x

Ya la vimos como calcularla de forma acumulativa, vamos a recordarlo rápidamente:

mean = mean + (x-mean)/n);

Media geométrica

Es la raiz enesima del producto de cada uno de los valores:

\sqrt[n]{\prod_{} x}

Vamos a desarrollar nuestro cálculo acumulativo a partir del modelo acumulativo para calcularla que desarrollan en este articulo.

Resumiendo, calculamos la media de ln(x) usando la formula de la media acumulada vista antes:

meanLn = meanLn + ((log(x)-meanLn)/n);

Para calcula la media geométrica a partir de este valor vasta con elevar el numero e al valor calculado:

Necesitaras declarar el número e:

const double e=2.71828;

Media armónica

Se calcula dividiendo el numero de muestras entre el sumatorio de uno partido por el valor de cada muestra. (Si no te has enterado, tranquilo, no me he enterado ni yo y soy el que lo ha escrito). Se ve mejor con la fórmula:

\frac{n}{\sum_{} 1/x}

En lugar de usar la versión acumulativa vamos a optar por aprovecharnos de la relación entre las distintas medias:

harmonica = \frac{geometrica^2}{aritmetica}

En código:

harmonicMean =  pow(geometricMean(), 2)/mean();

Media Cuadrática

Es la raíz cuadra del sumatorio del cuadrado de los valores:

\sqrt{\frac{1}{n} \sum_{} x^2}

Para calcularlo usamos la misma formula que para la media aritmética pero aplicada al cuadrado del valor:

mean2 = mean2 + (((x*x)-mean2)/n);

Luego para obtener el valor final solo hemos de calcular la raíz cuadrada de la misma:

sqrt(mean2):

Puede encontrar el código de la implementación de todo esto en este proyecto de github.

De regresión lineal a regresión logística en Arduino

Ya hemos visto como calcular la regresión lineal en Arduino y como a partir de esta calcular diversos tipos de regresiones. Lo que vamos a ver aquí es usar un truco para convertir la regresión lineal en regresión logística basandonos en la función sigmoide.

La regresión lineal se usa como clasificador binario entre dos conjuntos. En el caso ideal de regresion logística para cualqueir valor de x devuelve un valor de y que es 0 o 1 dependiendo de a que clase pertenezca. Pero en la vida real rara vez suele ser un «caso ideal» y hay valores para los que devolverá un valor comprendido entre 0 y 1. Este resultado puede interpretarse como la probabilidad de que sea del grupo representado por el valor 1 o cuadno esto carezca de sentido simplemente tomar cualqueir valor mayor de 0,5 como del grupo del 1 y cualquie valor por debajo como del grupo del 0.

La función sigmoide se define como:

1 / 1 + e^{-y}

El valor de y lo podemos sacar de la regresión lineal:

y = mx +b

Juntandolo todo:

1 / 1 + e^{-(mx+b)}

Veamos las diferencias entre ambas fórmulas:

Regresión lineal (verde) comparada con regresión logística (naranja)

Regresión Lineal:

  • Su fórmula define una linea
  • No está acotada, no tiene un valor máximo ni mínimo
  • Se usa para estimar valores.
  • Devuelve un valor numérico

Regresión logística:

  • Su fórmula define una "S"
  • Esta acotada entre 1 y 0
  • Se usa para clasificar un valor en uno de dos grupos. Clasificador binario.
  • El resultado que devuelve se puede interpretar de dos maneras: como probabilidad de pertenecer a un grupo si se toma el valor directamente o como pertenencia absoluta a un grupo u otro si se considera que cuando el valor obtenido este por encima de 0,5 se pertenece a uno y por debajo al otro.

Forma de implementarlo

La forma de implementar esto en un Arduino es aprovechar la librería que ya tenemos de regresión lineal y que nos soluciona los problemas de memoria y tiempo de cálculo que tienen los cálculos estadísticos en Arduino. Simplemente una vez que nuestro sistema aprenda el modelo lineal basta con transformar el resultado que devuelve este modelo para convertir su respuesta a la de una regresión logística.

    double exp = linealRegression.calculate(x)*-1; 
    return 1/1+pow(e, exp);

La implementación del código se puede encontrar en la librería Regressino

Utilidad

¿Tiene sentido transformar una regresión lineal en un modelo de regresión logística?. Aunque esta conversión se puede realizar para cualquier regresión lineal no tiene sentido hacerlo. Solo tiene sentido usarlos cuando se quiera entrenar un clasificador binario y haya dos grupos de elementos claramente diferenciables. Entonces se puede calcular la recta de regresión y convertirla en una regresión logística que funcione como clasificador.

Tampoco va servir para calsificar cualquier grupo de elementos, han de ser linealmente separables. dicho de forma más intuitiva, tienen que poder separarse trazando una linea recta entre ellos.

En definitiva, sin ser una opción ideal, es suficiente buena y útil como para plantearse su uso.

Estadísticas básicas en Arduino

Como ya vimos en el post sobre regresión lineal en Arduino, el principal problema que plantea Arduino para realizar cálculos estadísticos es la escasa capacidad de memoria y cálculo que tiene. Para ello en lugar de guardar todos los datos vamos a usar formulas que permiten aproximar los valores estadísticos que vamos a utilizar sin gastar casi recursos, la idea es guardar solo una aproximación.

En el siguiente enlace puedes encontrar la librería SimpleStatisticsArduino de Arduino que implenta lo explicado en este texto.

Para la varianza y la media usaremos las siguientes formulas que tratan de aproximar

numeroDeMuestras++;
media += (nuevoValor – media)/numeroDeMuestras;
media2 += (value^2 – media2)/numeroDeMuestras;
varianza = media2 – media^2;

Con estos valores podemos aproximar la suma de todas los datos:

suma = media*numeroDeMuestras;

La desviación estándar :

desviacionEstandar = sqrt(varianza);

Otros dos valores que podemos almacenar de forma muy sencilla y casi sin costes es el valor mínimo y máximo. Cada nuevo valor se comprueba:

if(minimo > nuevoValor){
minimo = nuevoValor;
}
if(maximo < nuevoValor){
maximo = nuevoValor;
}

Ahora con estos dos valores podemos calcular el valor central, que no es lo mismo que la media:

centro = (maximo – minimo) / 2;

Con esta estrategia solo necesitamos 6 variables para almacenar los datos sobre los que se calcula la estadística.

Estadística con dos variables en Arduino

Tenemos dos variables X e Y, partiendo de los cálculos del apartado anterior para cada una ahora podemos calcular los valores conjuntos, para ello debemos de almacenar dos variables más necesarias para calcular la covarianza:

mediaXY += ((XY)-mediaXY)/numeroDeMuestras;
covarianza = mediaXY – (mediaXmediaY);

Ahora con la covarianza podemos calcular la correlación:

correlacion = covarianza / (desviacionEstandarX * desviacionEstandarY);

Con estos datos podemos calcular los parámetros de la regresión lineal:

m = covarianza / varianzaX;
b = mediaY – m*mediaX;

Y la propia regresión:

y = m*x + b;

Si buscas una implementación de la idea de este post pero optimizada exclusivamente para la regresión lineal puede mirar la librería Regressino.

Por último podemos calcular el centroide que no es nada mas que el centro de cada una de las variables X e Y.

centroide = [centroX, centroY]

De esta forma se pueden calcular bastantes valores sin consumir casi memoria o recursos

Como convertir una aplicación de MS-DOS en una aplicación web.

Seguro que todos los que tenemos cierta edad tenemos programas desarrollados para MS-DOS que siempre hemos querido recuperar pero nunca hemos tenido tiempo de volver a programarlos en algo más moderno. En mi caso eran varios videojuegos que había desarrollado cuando era adolescente.

Para ello vamos a usar dos herramientas: DOSBox, JS-DOS (en este caso la versión 6.22).

Preparando la aplicación

Lo primero de todo es montarse DOSBox en tu ordenador e instalar la aplicación que queramos usar en la web. Vamos a llamarla programa.exe. Una vez instalada la iniciamos y la configuramos tal y como queramos que se ejecute desde la web. Si necesitamos incluir algún fichero para que lo abra la aplicación ahora es el momento.

Una vez terminado el paso necesitamos comprimir todos los ficheros necesarios para que funcione la aplicación en un fichero zip. Lo ideal sería que el ejecutable programa.exe quede directamente en la raíz del zip para que al descomprimirlo no quede dentro de ninguna carpeta.

Todo este proceso no es necesario que lo hagamos desde DOSBox, para instalar la aplicación hay que «montar’ una carpeta del ordenador como si fuera una unidad de disco de DOSBox, puedes comprimir allí los ficheros.

Ejecutandola en una web

Vamos a suponer que el fichero se llama programa.zip. Ahora vamos a preparar la web.

Necesitaremos un canvas en el que se ejecutará el emulador de MS-DOS, para referenciarlo vamos a usar el id «jsdos». Para fijar de el tamaño del canvas usaremos CSS.

Debajo se puede ver el código completo:

<!doctype html>

<head>
    <meta charset="utf-8">
    <title>Programa.exe</title>
    <script src="js-dos.js"></script>
    <style>
        #jsdos, .dosbox-container {
            width: 800px;
            height: 600px;
        }
    </style>
</head>

<body>
    <canvas id="jsdos"></canvas>   
    <script>
    Dos(document.getElementById("jsdos"), {
        autolock: true
    }).ready(function (fs, main) {
        fs.extract("programa.zip").then(function () {
            main(["-c", "programa.exe"]).then(function (ci) {              
                window.ci = ci;
            });
        });
    });
    </script>
</body>

</html>

Su funcionamiento es sencillo y se puede entender sin demasiadas explicaciones, algunos puntos que se necesitan aclarar:

fs.extract(«programa.zip») descomprime el fichero zip en el directorio raiz C:

main([«-c», «programa.exe»]) el primer comando va a C: y el segundo ejecuta programa.exe

window.ci = ci; Permite acceder a la API de DosCommandInterface desde cualquier parte de la web.

autolock: true Sirve para capturar el ratón automáticamente cuando se hace click sobre el canvas. Si tu aplicación no usa el ratón para nada puedes prescindir de ponerlo.

Algunas funciones interesantes

  • Entra y salir de modo pantalla completa ci.fullscreen(), ci.exitFullscreen()
  • Realiza una captura de pantalla ci.screenshot()
  • Simular pulsacioenes de teclas ci.simulateKeyPress(keyCode), ci.simulateKeyEvent(keyCode, pressed)
  • Ejecutar comandos ci.shell([cmd1, cmd2, …])

Problemas

Aquí recopilo un listado de cosas que me han dado problemas:

  • Resoluciones raras de pantalla. Forzar la aplicación en formatos que distorsionan mucho el formato 4:3 habitual de los monitores de esa época
  • El sonido no siempre va tan bien como sería deseable
  • El ratón, muchas aplicaciones de MS-DOS usan falsos ratones, imágenes que superponen donde debería estar el ratón. Algunos dan problemas.
  • El uso desde el móvil no está resuelto, aunque no es algo fácil de solucionar. El punto bueno es que te dan la herramientas básicas para que trates de resolverlo.

Ideas interesantes

Algunas ideas interesantes que se me ocurrieron pero no he tenido oportunidad o tiempo de intentaras.

  • Aunque JS-DOS ofrece alguna formas de ejecutar múltiples comandos hay soluciones como los ficheros de procesamiento por lotes .bat
  • Se pueden asociar comandos de MS-DOS, pulsaciones de teclas o del ratón a acciones en la web esto puede dar lugar a interesantes formas de interactuar con la aplicación (incluso de hacerla más accesible)
  • Un gamepad virtual sobre el canvas donde se ejecuta la aplicación. Para usar juegos desde el móvil es imprescindible
  • Usando Apache Cordoba o PhoneGap se podría convertir la web en una aplicación móvil. Aunque no sé si tiene utilidad más allá de entretenerse un rato probando
  • Lo mismo que el punto anterior pero en el escritorio con electron