Crear un kiosko digital con chromium o firefox en Raspberry Pi

Vamos a ver como crear un “kiosko” a partir de una web con Raspberry Pi, una pantalla táctil y una pagina web que es la que queremos mostrar en nuestro kiosko. Lo que deseamos es que la web aparezca a pantalla completa y sin que el usuario pueda “escapar” de ella. En mi caso lo que deseaba mostrar era una web con el tiempo, un calendario y alguna información del ayuntamiento de mi ciudad. Para ellos bastaba con mostrar una pagina web alojada en la propia Raspberry con iframes para mostrar parte de la información. El problema era que se viera en toda la pantalla y no se pudiera salir accidentalmente (o intencionadamente) de la web.

Para ello existe en chromium y firefox (tambien en chrome aunque para Raspberry no este) el modo kiosko que muestra una página web a pantalla completa, sin barra de herramienta, direcciones, menú o cabecera. La forma de lanzarlo es asi:

chromium-browser --kiosk direccionweb

firefox --kiosk direcionweb

google-chrome --kiosk direccionweb

La única forma de salir es con un teclado y pulsado alt+f4. En mi caso como no había teclado ya tenia mi kiosko seguro pero hay algunas cosas que hay que tener en cuenta según su uso para que el kiosko sea seguro:

  • Desactivar el recordar y autocompletar de campos
  • Nada de teclados
  • Si se tiene que poner un teclado mejor si es un teclado en pantalla, mejor aun si el teclado es parte de la web, por ejemplo este
  • El teclado ha de tener solo las teclas necesarias. No tiene que tener teclas “especiales” como control, alt, f1, f2,…
  • Asegurarse de que la web que se muestra no tiene enlaces que lleven fuera de la misma.
  • Bloquear los puertos USB para evitar que nadie los use. Si la conexión de red es por WiFi aquí se puede ver como hacerlo

Estas medidas hay que tenerlas en cuenta según el entorno del kiosko, no es lo mismo en una casa que en una tienda o en un lugar publico.

Aun así hay varios puntos de ataque:

  • Quitar la alimentación.
  • Quitar la conexión de red.

Quitar la alimentación eléctrica dejaría nuestra Raspberry apagada, el problema esta cuando vuelve, que por defecto se reinicia. Aquí tenemos que decidir porque opción optamos. Si mostrar una pantalla de login (mejor si es consola en lugar de interface gráfica) con usuario y password (ambos deben de ser seguros, nada de dejar los por defecto) y dejar el kiosko sin servicio. La otra opción es configurar el autologin y hacer que el kiosko se lance automáticamente cuando el desktop se inicie. Aquí se explica como hacerlo.

Al quitar la conexión de red (red de datos) el navegador mostrará una pagina de error. Al recuperar la conexión es posible que se quede esa página en lugar de volver a la que debería mostrar el kiosko. Hay una solución que es en lugar de cargar directamente la web remota poner un servidor web en la propia Raspberry Pi, que el navegador en modo kiosko cargue la pagina que sirva este servidor local y ella sea la que se muestren las distintas fuentes de datos. Al falla la conexión el servidor local sigue funcionando y permite tener cierto control sobre lo que ocurra.

Raspberry Pi, ejecutar una aplicación o script al inicio

Cuando queremos “ejecutar una aplicación al inicio” nos podemos referir a dos momentos distintos: al iniciar la Raspberry, al iniciar el escritorio. En el primer caso solo se pueden lanzar aplicaciones o script que no necesiten una “ventana” para funcionar mientras que el segundo se suele usar más para aplicaciones de escritorio.

Ejecutar una aplicación al iniciar la Raspberry Pi

Para esta tarea podemos recurrir a la herramienta crontab. Ya vimos su uso aqui. Siguiendo las indicaciones de ese posta bastaria con usar el comando:

crontab -e

Y luego añadir la entrada:

@reboot aplicacion

Donde “aplicacion” es la ruta y e lnombre de la aplicación, script o comando a lanzar.

Ejecutar una aplicación al iniciar el escritorio de Raspberry Pi

Hay que ir al directorio /etc/xdg/autostart y crear un fichero con el nombre que queramos pero terminado en .desktop el contenido del mismo ha de ser:

[Desktop Entry]
Name=Lanzo mi aplicacion
Exec=aplicacion
Terminal=false
Type=Application

El campo name puede ser una pequeña descripción de la aplicación y el campo exec contiene la ruta y el nombre de la aplicación a lanzar.

Debo señalar que hay más formas de realizar estas dos tareas y estas solo son dos de las más habituales y sencillas.

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

Reiniciar Arduino por software

Hay veces que necesitamos reiniciar la placa Arduino desde el propio software. Por ejemplo cuando se produce un error que no podemos gestionar. Sin embargo Arduino no trae ninguna función “reset” para hacerlos. Si recordamos lo que dijimos del watchdog precisamente esa es su función, reiniciar la placa. Podemos aprovechar eso para hacer una función de reset, configuramos el watchdog con el tiempo más corto posible y entramos en un bucle infinito, obligándolo a que pasado el tiempo reinicie la placa.

void reset(){
  wdt_enable(WDTO_15MS);
  while(1){};
}

Puede ser que hayamos sobrescrito el ISR del watchdog o que no queramos incluir la librería del watchdog solo para hacer un reset. La solución es sencilla, imitar lo que hace el watchdog, llamar a la función en la posición 0 de la tabla de vectores de interrupción. Da igual si no sabes lo que es la traducción es que salta la ejecución del código a la posición 0 de memoria. Podemos usar un puntero a función que apunte a esa dirección.

void(* reset) (void) = 0;

Y luego llamar a esa función.

reset();

Es recomendable usar el primer método siempre que se pueda ya que reiniciar la placa es la función del watchdog y seguro que funciona en todas las placas Arduino, la segunda solución depende más del microcontrolador de la placa y aunque deberia de funcionar las placas con microntroladores AVR no es seguro que funcione con otros microcontroladores.

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.

Inicializar los servos en Arduino

Cuando se empieza con Arduino y se hace algún proyecto con servoa se llega el desesperante momento en que se reinicia la placa Arduino y los servos comienzan a dar bandazos hasta que llegan a la posición inicial que les marca el programa. ¿Como resolver esto?

Lo primero que hay que saber es que la mayoría de los ejemplos para aprender a usar la librería servo son incorrectos. No inicializan los servos y por se vuelvan locos. Veamoslo con un ejemplo sacado de la web de Arduino.

/* Sweep
 by BARRAGAN <http://barraganstudio.com>
 This example code is in the public domain.

 modified 8 Nov 2013
 by Scott Fitzgerald
 http://www.arduino.cc/en/Tutorial/Sweep
*/

#include <Servo.h>

Servo myservo; 

int pos = 0;

void setup() {
  myservo.attach(9);  
}

void loop() {
  for (pos = 0; pos <= 180; pos += 1) { 
    myservo.write(pos);
    delay(15);         
  }
  for (pos = 180; pos >= 0; pos -= 1) {
    myservo.write(pos);
    delay(15);  
  }
}

Lo primero es que el servo se vuelve “loco” porque toma los valores por defecto de la librería servo. Se pueden encontrar en el archivo Servo.h

#define MIN_PULSE_WIDTH       544     // the shortest pulse sent to a servo  
#define MAX_PULSE_WIDTH      2400     // the longest pulse sent to a servo 
#define DEFAULT_PULSE_WIDTH  1500     // default pulse width when servo is attached

Veamos lo que significan. Los servos ajustan su posición en base a la duración de un pulso que se les envía. En este caso se ajusta el pulso para que tenga una duración mínima de 544 microsegundos y una duración máxima de 2400 microsegundos. Si estáis acostumbrados a posicionar los servos usando grados de 0 a 180 el valor de 0 grados corresponde con un pulso de duración 544 y 180 con uno de 2400. Para los valores intermedios se interpola el valor, de tal forma que para G grados el pulso tiene que durar P microsegundos:

(G * (MAX_PULSE_WIDTH – MIN_PULSE_WIDTH) / 180 ) + MIN_PULSE_WIDTH = P

Sacando cuentas podemos ver que DEFAULT_PULSE_WIDTH corresponde más o menos con 90 grados.

Lo que ocurre es lo siguiente:

  1. Al ejecutar myservo.attach(9) el servo se mueve hasta los 90 grados
  2. Al ejecutar myservo.write(pos) el servo se mueve a la posición indicada, en este caso 0 grados.

No es que el servo se vuelva “loco” es que le decimos que de esos bandazos.

La solución es cambiar el setup para que antes de ejecutar myservo.attach(9) le ponga el valor con el que queremos inicializar el servo.

void setup() {
  myservo.write(pos); //inicializamos la posicion del servo
  myservo.attach(9);  
}

Conservar el valor anterior del servo

Hay casos en que no tenemos un valor por defecto para un servo, tiene que conservar el valor con el que se quedo antes del reinicio. Como no podemos leer el valor del servo (read/readMicroseconds no leen el valor del servo devuelve el último valor pasado a write/writeMicroseconds). ¿Qué podemos hacer? Usar la EEPROM para almacenar los valores de los servos. Podemos almacenar el valor del servo con EEPROM.put() y recuperarlos con EEPROM.get(). Sin embargo esto tiene un problema, la memoria EEPROM del Arduino tiene una vida de unas 100.000 escrituras, que pueden ser poco para algunos casos. Hay algunas posibles soluciones:

  • Crear una posición de apagado de tal manera que antes de apagarse los servos vuelvan a esa posición. No sirve para fallos/reinicios repentinos.
  • Guardar la posición de los servos cada cierto tiempo. En casos donde los servos cambian rápidamente de posición no sirve.
  • Ir cambiando la dirección de memoria EEPROM donde se almacena el estado de los servos. Si cada byte de EEPROM se puede escribir 100000 veces, si rotamos la dirección de byte a lo largo de toda la memoria alargamos esa vida.
  • Guardar solo la posición si cambia lo suficiente. Se compara el valor almacenado con el valor del servo si la diferencia es pequeña no se cambia.

Veamos un ejemplo del ultimo caso.

Mover un servo.

void moveServo(Servo servo, byte angle, int addressEEPROM) {
    byte savedAngle = EEPROM.read(addressEEPROM, angle);//recuperamos el valor 
    if(abs(angle - savedAngle) > 10){//si más de 10 grados de diferencia se guarda
        EEPROM.update(addressEEPROM, angle);
    }
    servo.write(angle);
}

Se incian los servos con el valor recuparado de memoria.

void initServo(Servo servo, int addressEEPROM, uint8_t pin) {
    int savedAngle;
    EEPROM.get(addressEEPROM, saveAngle);//recuperamos el valor guardado
    servo.write(savedAngle);
    servo.attach(pin);
}

Obtener el código en ensamblador de un sketch de Arduino

Una de las cosas que echaba de menos al programar un Arduino era la posibilidad de ver un desemsamblado del código generado. Hay veces que puede ayudarte a entender por qué falla algo o saciar tu curiosidad de cómo funciona. Aunque el IDE de Arduino no trae una opción cómoda para hacerlo es muy sencillo.

Hay que realizar el proceso en dos pasos:

Obtener el volcado binario del sketch de Arduino

Primero vamos a obtener un volcado binario del sketch. Para ello recurrimos a la opción “Exportar Binarios compilados” del menú “Programa”.

Programa -> Exportar Binarios compilados

Una vez exportados podemos acceder a ellos con la opción “Mostrar Carpeta de Programa” del menú “Programa”.

Como resultado de la exportación obtenemos dos archivos .hex.

Ficheros binarios obtenidos

Se corresponden al binario con el bootloader de Arduino o sin el bootloader. Podemos abrirlo con un editor hexadecimal y ver que contiene.

Primeras lineas de ejemplo.ino.with_bootloader.standard.hex

El fichero con el bootloader es el que se copia a la placa Arduino cuando “cargamos” el sketch

Convertir el volcado vinario a ensamblador

Para realizar esta parte necesitaremos el programa avr-objdump el cual se puede encontrar en el directorio donde este instalado el IDE de Arduino en la ruta hardware\tools\avr\bin\avr-objdump. Una vez localizado podemos usar el siguiente comando para obtener el desensamblado del binario:

<ruta-avr-objdump>avr-objdump -j .sec1 -d -m avr5 fichero.hex > fichero.asm

Tras ejecutar ese comando la salida se vuelca en el fichero de nombre ejemplo.asm que podemos abrir con cualquier editor de textos.

Primeras lineas de ejemplo.asm

Como curiosidad las primeras lineas del fichero corresponde a la tabla de vectores de interrupción del microcontrolador, por eso son todo saltos al código que gestiona cada una de las interrupciones.

Watchdog en Arduino

Un watchdog es un sistema de seguridad que usan muchos microcontroladores y sistemas embebidos. Su funcionamiento es simple, cada vez que se reinicia el watchdog empieza una cuenta atrás hasta cero, si antes de llegar a cero el watchdog se reinicia la cuenta atrás vuelve a empezar. Si no se reinicia y el contador alcanza el valor 0 el sistema se reinicia automáticamente. Así se evita que se quede colgado.

Con el reinicio se pierden los datos que están en la memoria SRAM por lo que todo dato que queramos conservar ha de guardarse en la EEPROM.

Para usar el watchdog hemos de incluir la librería del fabricante del microcontrolador:

#include <avr/wdt.h>

Para deshabilitar el watchdog se puede usar la función:

wdt_disable();

Empezamos aprendiendo a deshabilitarlo porque es lo primero que hay que hacer para evitar problemas durante el inicio de la placa o puede interferir en el proceso de grabación de un nuevo código en la placa. Cuando se vuelca un nuevo código hay que dar tiempo para que se pueda volcar un nuevo programa o se perderá la posibilidad de grabar nuevos programas. La causa de esto es que cuando se empieza a subir un nuevo código desde el IDE se paraliza la ejecución del programa, por lo que no se reinicia el contador del watchdog si este llega a cero antes de que se termine de subir el código la placa se reinicia y la grabación se corta.

Para activar el watchdog y fijar cuanto tiempo tiene que esperar el watchdog antes de reiniciar la placa se usa la función:

wdt_enable(WDTO_1S);

El parámetro que recibe la función indica el tiempo que esperará el watchdog antes de reiniciar el dispositivo. No puede tomar cualquier valor, tiene que ser una de los siguientes valores predefinidos:

  • WDTO_15MS
  • WDTO_30MS
  • WDTO_60MS
  • WDTO_120MS
  • WDTO_250MS
  • WDTO_500MS
  • WDTO_1S
  • WDTO_2S
  • WDTO_4S
  • WDTO_8S

Para evitar que se reinicie la placa hemos de llamar a la siguiente función ante de que pase el tiempo indicado:

wdt_reset();

Tras llamarla la cuenta atrás comienza otra vez.

Un ejemplo de esqueleto de función seria la siguiente:

#include <avr/wdt.h>

setup(){
  wdt_disable();
  //el resto del setup
  delay(3000); //para evitar que no nos da problemas al cargar nuevos programas
  wdt_enable(WDTO_4S); //se activa el watchdog 
}

loop(){
 wdt_reset(); //reiniciar contador del watchdog
 //código del programa
}

Memoization con tiempo de vida

La memoization ya vimos lo que era y como usarla. Ahora vamos a añadir una característica más que puede resultar útil en algunos casos: “tiempo de vida”. Cada respuesta memorizada solo va a ser válida durante un periodo de tiempo. Pasado ese tiempo el resultado deja de ser válido y si se vuelve a llamar

Todo lo que veamos en este texto esta publicado en la librería memo.js

El primer cambio que vamos a realizar sobre lo que vimos en el anterior post de memoization es que tendremos dos variables para almacenar los datos, una con la cache de los parámetros y sus valores y otra con el momento en que se almacenó esa cache.

La forma de funcionar es la misma que la memoization normal solo que cuando se va recuperar un valor de la cache se comprueba cuanto tiempo hace que se almaceno y si ha transcurrido un tiempo mayor que el tiempo de vida fijado se llama a la función y se actualiza la cache con el valor que devuelva.

Veamos algo de código partiendo de la librería de memoization que creamos.

Lo primero es extender la clase original:

class MemoTime extends Memo {

}

Modificamos el constructor para que cree una variable (lifetime) donde guardar cuando fue la última vez que se actualizo esa clave. También se le tiene que pasar al constructor el tiempo de vida (timeOfLife)

 constructor (func, timeOfLife, keyFunc) {
    super(func, keyFunc);
    this.timeOfLife = timeOfLife || 1000;
    this.lifetime = {};        
 }

También hay que modificar la función que añade una clave – valor para que almacene el momento (new Date()) en que se guarda esa clave.

add(key, value){
    this.lifetime[key] = new Date();
    super.add(key, value);
}

Por último la función que verifica si esta en la cache para que responda ‘false’ en el caso de que este pero haya caducado:

 isInCache(key){
   if(key in this.lifetime) {
      if((new Date() - this.lifetime[key]) < this.timeOfLife){ //¿ha caducado?
         return true;
      }
   } else {
      return false;
   }
}

El resultado final es el siguiente:

class MemoTime extends Memo {
    constructor (func, timeOfLife, keyFunc) {
        super(func, keyFunc);
        this.timeOfLife = timeOfLife || 1000;
        this.lifetime = {};        
    }

    isInCache(key){
        if(key in this.lifetime) {
            if((new Date() - this.lifetime[key]) < this.timeOfLife){
                return true;
            }
        } else {
            return false;
        }
    }

    add(key, value){
        this.lifetime[key] = new Date();
        super.add(key, value);
    }
}

Puede parecer contradictorio tener un mecanismo para almacenar los resultados de una función para no tener que volver a llamarla y que caduquen. Esto es útil cuando el resultado de la función cambia con el tiempo o queremos evitar que a una función se le llame muchas veces seguidas.

Por ejemplo si tenemos un servidor que nos devuelve un dato y no queremos saturarlo de peticiones podemos limitar el número de las mismas usando está técnica. Así cada vez que se llame a la función se tendrá una respuesta pero como mucho se le llamara una vez por cada periodo indicado en el tiempo de vida.

Por último vamos a ver un ejemplo de la clase que hemos creado:

function sum(a,b){  
    console.log("calculating "+a+" + "+b);  
    return a+b;  
}  

let memoTime = new MemoTime(sum, 2000);

console.log(memoTime.call(4,5)); //call sum

console.log(memoTime.call(4,5)); //no call sum
setTimeout(memoTime.call(4,5),1000); //no call sum
setTimeout(memoTime.call(4,5),3000); //call sum

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