Archivo por días: 19 julio, 2020

A ritmo de conga (9)

No esperaba escribir esta entrada, pero al final me animé. Resulta que, aunque estoy muy contento con mi robot aspirador, tiene un defectillo que no me convence nada, y es que cuando hay una alfombra y tiene que girar estando encima del borde, se suele atascar.

El motivo es que en la parte trasera, justo en el borde del depósito de suciedad/tanque de agua, hay una rueda para apoyarse. Ésta en concreto:

El problema con ella es que si la aspiradora está justo medio subida en una alfombra, con la mitad del cuerpo encima y la otra mitad fuera, y en ese momento gira sobre sí misma en lugar de avanzar, esa rueda tropezará contra el borde de la alfombra y no dejará que la aspiradora gire, sino que la empujará fuera. En un mundo ideal sería una rueda loca y no habría ese problema.

Pero como yo no soy de quedarme de brazos cruzados, decidí intentar solucionarlo.

Las primeras búsquedas de ruedas locas en internet no dieron ninguna solución adecuada: o eran muy grandes, o eran carísimas (¿en serio más de diez euros por una bola de acero y un soporte impreso en 3D?). Así que decidí buscar una solución más artesanal, al menos para intentar probar el concepto y ver si lo resuelve o no.

Al principio decidí buscar dos bolas de rodamiento de 15 milímetros de diámetro y hacer dos soportes con impresión 3D. Algo parecido a la rueda frontal del mClon. Esos soportes con las bolas irían montados a cada lado de la rueda trasera y sobresaldrían un milímetro más que ésta. Eso debería ser suficiente para salvar cualquier alfombra.

Sin embargo, tras hablar con mis compañeros de A industriosa, me indicaron que, realmente, esa rueda loca no gira demasiado y que además es bastante pesada, y que para lo que yo quiero, algo estático (un simple vástago redondeado) haría la misma función y sería más sencillo de construir.

Ante esto, me acerqué a mi Todo a 100 de confianza y empecé a buscar «cosas esféricas» que me pudiesen servir, y encontré unas pelotas de una goma dura que eran perfectas:

Lo primero fue cortarla a la mitad, de manera que midiese 18 milímetros. Esto lo hice fácilmente con un cutter.

El siguiente paso fue recortar un lateral para que encajase en la zona de la rueda actual, cubriéndola por completo.

Y por último, un poco de cola térmica para fijarla. La ventaja de la cola térmica es que se disuelve con un poco de alcohol, lo que me permitirá quitarla sin dejar rastro en caso de que no funcione.

Y el resultado es, sencillamente, perfecto: ya no se atasca cuando tiene que girar en el borde de una alfombra, sino que sube perfectamente sobre ella como si estuviese bañada en mantequilla. Aunque sí es cierto que se desgasta un poco, no creo que sea un problema, pues en último caso, cuando llegue a la rueda, empezará a apoyar en ella y se acabará el desgaste, pero con la ventaja de que estará rodeada por la goma esférica y seguirá cumpliendo la función de ayudar a sortear alfombras al girar.

A ritmo de conga (8)

Esta será, en principio, la última entrada. En ella voy a describir el servidor/app que escribí para controlar mi aspiradora robot.

DISCLAIMER: no seré responsable si alguien decide seguir mis pasos y se carga su aspiradora. En principio todo lo que cuento debería ser seguro, pero por motivos obvios no me puedo responsabilizar de lo que hagan otras personas, sólo de lo que haga yo.

El código está escrito en python, está disponible en mi cuenta de Gitlab en servidor para robots Conga 1490, y se llama OpenDoñita. Esta descripción complementa a la del capítulo 5.

Para empezar, está cómo hacerlo funcionar. Yo lo he puesto en una Raspberry Pi 4 que tenía muerta de risa por casa (de momento OSMC no funciona en ella), la misma con la que configuré la red WiFi interna para analizar el tráfico. Sin embargo es posible ponerla en cualquier ordenador, siempre y cuando se configure un DNS adecuado. En efecto, si recordamos, la aspiradora se conectará siempre a dos dominios concretos, bl-app-eu.robotbona.com y bl-im-eu.robotbona.com, por lo que necesitamos engañarla de alguna manera para que se conecte a nuestro servidor. La manera más sencilla es entrar en la configuración de nuestro router Wifi y y configurar la IP de nuestra Raspberry Pi como la IP del servidor DNS de nuestra red. Luego, en dicha Raspberry instalamos dnsmasq, pero no configuramos la parte de DHCP, sólo el servidor DNS. Una vez hecho esto editamos el fichero /etc/hosts y añadimos estas dos entradas:

192.168.X.Y bl-app-eu.robotbona.com
192.168.X.Y bl-im-eu.robotbona.com

Siendo 192.168.X.Y la IP del equipo en donde vayamos a poner a correr el servidor (que, normalmente, será la misma Raspberry Pi).

Un detalle importante es que mi código necesita Python 3.6 o superior, lo que significa que funcionará en Raspbian, pero no en la versión actual de OSMC pues está basada en la versión stretch de Debian (lo he probado; quería meter todo en la misma Raspberry pero no ha podido ser). Hasta que actualicen el sistema base a buster (y me consta que están en ello) no se podrá hacer.

Una vez que está hecho reiniciamos dnsmasq para que refresque los valores, y ya podemos lanzar nuestro servidor. Para ello, desde un terminal, vamos al directorio donde esté el código y escribimos:

sudo screen ./congaserver

Es importante estar en el directorio correspondiente pues, por defecto, es ahí donde el servidor buscará el directorio html con las páginas de la aplicación web que se utiliza para controlar a las aspiradoras. Si lo lanzamos desde otro directorio lo más probable es que no funcione. Para más información sobre screen, recomiendo leer la documentación. Es necesario usarlo (o bien nohup en su lugar) para poder salir de la sesión SSH de la RPi y que el programa siga corriendo.

Podemos probar si funciona todo correctamente simplemente abriendo un navegador y escribiendo en un terminal

ping bl-app-eu.robotbona.com

Si todo está correcto, debería resolver a la IP de nuestra Raspberry Pi.

Y ahora que está todo listo y corriendo, ya podemos conectar la aspiradora. Para ello tenemos tres opciones:

  • Apagar la WiFi y volverla a encender. En este caso, la aspiradora volverá a conectarse a ella, buscará el servidor de nuevo y se conectará al nuestro, pues recibirá la IP desde nuestro servidor DNS.
  • Apagar la aspiradora y volverla a encender. Para ello es necesario retirarla de la base de carga y apagar el interruptor situado en el lado derecho, esperar unos segundos y volver a encenderlo.
  • Volver a emparejar la aspiradora. Es el más lioso, pues con el DNS en marcha la app oficial no podrá conectarse al servidor (usa un puerto diferente) y se negará a arrancar, por lo que tendremos que utilizar el script configconga.py, que explicaré más adelante.

En cualquiera de los tres casos sabremos si hemos tenido éxito porque en la pantalla de nuestro servidor aparecerán mensajes que indican que se están haciendo peticiones.

Una vez conectada, ya está todo listo para empezar a trabajar. Si ahora abrimos un navegador y metemos la IP del equipo de nuestro servidor, nos aparecerá la siguiente pantalla:

En la parte blanca de la derecha es donde aparecerá el mapa, pero de momento estará en blanco. Por otro lado, en la parte superior izquierda tenemos un indicador de carga de la batería. Cuando esta completamente negra estará cargada, y si está blanca estará descargada. Ahí se ve que está casi cargada del todo.

El símbolo del rayo indica que está cargando. Cuando esté cargada del todo (o cuando no esté en la base) dicho símbolo desaparecerá.

A continuación vemos los cuatro botones que podemos utilizar. El primero, con forma de casa, ordena a la aspiradora volver a la base. Sin embargo, si ya está en la base aparecerá sólo en forma de líneas, sin relleno (tal y como se ve en la captura).

El siguiente botón, con forma de «play», le indica a la aspiradora que comience a trabajar. Al hacerlo cambiará a un cuadrado, o sea, «stop».

El tercer botón, con forma de altavoz, permite activar o desactivar el pitido de aviso de la aspiradora. Tal y como está en la captura, está desactivado. Al activarlo sonará un pitido de aviso y el icono pasará a estar relleno.

El último botón, el engranaje, muestra la pantalla de configuración de modo de limpieza. Al pulsarlo aparece esta imagen:

Los cuatro superiores, con forma de ventilador, representan las cuatro potencias de aspiración (nada, eco, normal y turbo). Las cuatro siguientes, con forma de gota de agua, la cantidad de líquido que se utiliza para humedecer la fregona (nada, poco, normal, mucho). Un detalle importante es que no es posible escoger a la vez los modos «nada» de aspiración y fregona. Por último, los siete inferiores representan los modos de aspirado y fregado disponibles, aunque lo normal es utilizar el modo auto y no complicarse.

Lo mejor de todo es que recuerda el modo escogido incluso si se apaga la aspiradora, pues la configuración se guarda en nuestro servidor.

Para cerrar esta ventana basta con pulsar la flecha situada abajo a la derecha.

Si ponemos a funcionar la aspiradora, se irá generando poco a poco el mapa, cuyo tamaño se irá ajustando poco a poco a medida que se va calculando. Por eso al principio estará formado por círculos gigantescos, pero luego irán haciéndose más pequeños a medida que la superficie descubierta aumente. Así, al principio veremos algo así:

El protocolo REST

Y tras esto, paso a describir el protocolo REST utilizado por el servidor. Porque, efectivamente, la nueva app es una aplicación web, por lo que se puede actualizar y tunear sin necesidad de parar el servidor y volver a lanzarlo (lo que, además, exigiría volver a hacer que la aspiradora se conectase a él usando uno de los tres métodos comentados antes).

Los comandos que acepta son los siguientes. Para empezar, están los tres específicos del emulador del servidor de Robot Bona, que son:

/baole-web/common/sumbitClearTime.do
/baole-web/common/getToken.do
/baole-web/common/*

Los dos primeros devuelven los datos necesarios para emparejar la aspiradora con el servidor, y el tercero devuelve simplemente un OK.

Ahora vienen los comandos específicos del servidor. Estos son comandos que no afectan a la aspiradora, sino que son útiles para implementar la app.

/robot/list
/robot/XXXXX/getStatus
/robot/XXXXX/setStatus
/robot/XXXXX/getProperty?key=YYYYYY
/robot/XXXXX/setProperty?key=YYYYYY&value=ZZZZZZ

El primero devuelve una lista con el identificador de cada robot que está conectado ahora mismo al servidor. Este identificador se puede utilizar para enviar comandos a un robot específico si tenemos varios en casa.

Los otros cuatro son comandos de servidor. El parámetro XXXXX sería un identificador del robot al que va dirigido (obtenido con list), o bien puede ser all si queremos dirigirnos a todos. De hecho, actualmente la app no soporta discernir entre robots, y siempre envía los comandos a all. Si en el futuro me compro otro robot puede que lo implemente.

getStatus devuelve el estado actual de la aspiradora. Se trata de un diccionario con los datos enviados por la aspiradora al servidor, tales como el mapa, el nivel de batería, el modo actual de trabajo… Cada vez que la aspiradora envía una actualización, el servidor la almacena internamente, y es esta caché la que se envía aquí. Eso significa, por ejemplo, que si no estamos aspirando, el mapa que recibamos será el último que se envió, aunque haya sido hace horas. También si hubo algún error aparecerá su código en el campo correspondiente, aunque haga mucho que no se produjeron más errores.

setStatus permite modificar alguno de los valores anteriores, pero sólo en la caché del servidor. No afecta a la aspiradora. Existe para poder resetear algunos campos que se activan en casos muy concretos, como por ejemplo el campo error, y así poder detectar cuando se ha producido uno nuevo. Actualmente no se utiliza para nada.

getProperty y setProperty se utilizan para almacenar datos personalizados en el servidor, referidos a una aspiradora concreta. Actualmente se utilizan para almacenar la configuración deseada (potencia de succión, cantidad de agua en modo fregado, y modo de limpieza), de manera que aunque se apague la aspiradora, el sistema recordará la configuración deseada por el usuario. La forma de almacenarlo es como pares clave/valor, y siempre son strings.

Por último, la lista de comandos que van a la aspiradora. Son:

/robot/XXXXX/clean
/robot/XXXXX/stop
/robot/XXXXX/return
/robot/XXXXX/updateMap
/robot/XXXXX/sound?status=0|1
/robot/XXXXX/fan?speed=0|1|2|3
/robot/XXXXX/watertank?speed=0|1|2|3
/robot/XXXXX/mode?type=auto|gyro|random|borders|area|x2|scrub
/robot/XXXXX/notifyConnection
/robot/XXXXX/askStatus

clean, stop y return controlan la operación básica de la aspiradora: comenzar un ciclo de limpieza, parar, y volver a la base. No tiene más complicación.

updateMap envía un comando 131, de manera que en torno a una décima de segundo después tendremos la variable map actualizada, y podremos obtener su nuevo valor con /robot/XXXXX/getStatus.

sound permite activar (1) o desactivar (0) el pitido de aviso de la aspiradora. Teniendo en cuenta que cada vez que termina de cargar emite un pitido, yo personalmente prefiero apagarlo.

fan y watertank permiten escoger la potencia de aspiración y la cantidad de agua que se utilizarán durante el ciclo de limpieza.

mode permite escoger entre los siete modos de limpieza, aunque lo normal es utilizar auto.

notifyConnection emite un comando 400. En la app actual se emite nada más cargar la página, para avisar de que hay una app abierta.

askStatus emite un comando 98, el cual hace que unos milisegundos más tarde la aspiradora envíe una actualización de su estado. El servidor, al recibirlo, actualizará su caché y podremos leer los cambios con robot/XXXXX/getStatus.

Por último, cualquier petición que no coincida con este formato se asumirá que está pidiendo un fichero, por lo que se buscará en la carpeta html. Es así como se sirven las páginas HTML y las imágenes de la app.

Emparejando la conga a una red WiFi

Una vez que hemos cambiado el DNS veremos que la app oficial, la del móvil, ya no funciona. Esto es porque intenta conectarse al servidor oficial, y al no poder por apuntar ahora al nuestro, se niega a abrirse siquiera. Eso significa que no podemos utilizarla si necesitamos emparejar nuestro aspirador con otra red WiFi.

Afortunadamente, para eso está configconga.py. Es un programita que permite realizar la operación de emparejado. Para ello necesitamos un ordenador con WiFi.

Lo primero es poner la aspiradora en modo emparejamiento. Para ello hay que mantener pulsado el botón de encendido hasta que el piloto de WiFi empiece a parpadear y suene un pitido.

En este momento la aspiradora se ha convertido en un punto de acceso WiFi, con un nombre CongaGyro_XXXXXX (siendo XXXXXX el identificador único de la aspiradora). Ahora tienes que conectarte desde tu ordenador a esa WiFi (no tiene contraseña), y ejecutar el programa anterior con:

./configconga.py    WIFI_SSID    WIFI_PASSWORD

Siendo SSID el identificador de la red WiFi a la que quieres conectar tu aspiradora, y PASSWORD la clave de dicha red WiFi. Una vez hecho esto la aspiradora dejará de ser un punto de acceso, y pasará a conectarse a la WiFi indicada.

¡Ahora, a disfrutarlo!

Parte 9

A ritmo de conga (7)

DISCLAIMER: no seré responsable si alguien decide seguir mis pasos y se carga su aspiradora. En principio todo lo que cuento debería ser seguro, pero por motivos obvios no me puedo responsabilizar de lo que hagan otras personas, sólo de lo que haga yo.

En la anterior entrada paré tras comentar los distintos comandos de que dispone la aspiradora y del formato del mensaje de estado. Ahora llega el momento de explicar el formato de los mapas. Claro que debo ser sincero: hace unos días encontré un repositorio de Git donde Felix Engelmann explica ese formato para la aspiradora Proscenic 790T, la cual parece ser la misma que la Conga 1490, y aunque pasa muy por encima del protocolo en general, sí explica en detalle cómo funcionan los mapas, por lo que me ahorré el trabajo de descubrirlo por mi cuenta. Paso a limitarme a describir con mis palabras lo que explica ahí.

Cada vez que enviamos un comando 131 a la aspiradora, o bien cada 30 segundos aproximadamente si no hacemos nada (pero en ambos casos sólo si está aspirando), nos llegará un mensaje de la aspiradora con este formato:

{
  "version": "1.0",
  "control": {
    "targetId": "0",
    "targetType": "6",
    "broadcast": "0"
  },
  "value": {
    "noteCmd": "101",
    "clearArea": "0",
    "clearTime": "10",
    "clearSign": "2020-06-24-01-31-41-2",
    "clearModule": "11",
    "isFinish": "1",
    "chargerPos": "8,12",
    "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==",
    "track": "AQAEADIxMzExMTEy"
  }
}

Aquí hay tres elementos que nos interesan: los campos chargerPos, map y track. Para entenderlos hay que saber primero que la aspiradora genera un mapa interno en forma de una cuadrícula, donde cada cuadrado que la compone puede estar en uno de varios estados (zona sin explorar, zona libre y obstáculo).

Tras hacer algunos cálculos y ver cómo trabaja la aspiradora, parece que cada elemento de la cuadrícula es un cuadrado de unos 20cm en el mundo real(tm). Esto es consistente con el modo area, en el que la aspiradora va limpiando cuadrados de 2×2 metros (o sea, bloques de 10×10 cuadrados de la cuadrícula). Y como la aspiradora tiene un diámetro de 32cm, vemos que hay cierto solape, lo cual es perfectamente lógico si queremos garantizar que aspire todo el polvo y suciedad. Por defecto, la cuadrícula que envía la aspiradora es de 100×100 elementos, lo que nos da un tamaño máximo de 20×20 metros para la casa, aunque también es cierto que el tamaño viene en la cabecera del bloque map, por lo que supongo que en casas grandes lo ajustará dinámicamente.

Sabiendo esto podemos empezar a analizar los campos. El más sencillo es chargerPos. Éste contiene las coordenadas de la cuadrícula del mapa en donde está situado el cargador, lo que permite a la aspiradora volver hasta él para recargar la batería.

El siguiente en complejidad es map: es fácil darse cuenta de que es una ristra de bytes codificado con base64, por lo que lo primero que hay que hacer es convertirlo a bytes. En Javascript se puede hacer con atob, y en Python con el módulo base64. Una vez hecho esto, tiramos los primeros 4 bytes (que no sabemos qué significan), y luego cada par de bytes son las coordenadas x e y del recorrido de la aspiradora. Así, el primer par de bytes son las coordenadas del mapa en donde empezó a limpiar, el siguiente par es la siguiente cuadrícula a la que se ha movido, etc. Dado que la cuadrícula, por defecto, es de 100×100, parece que no tendremos que preocuparnos de si son números en complemento a 2 (desde -128 hasta 127) o normales (desde 0 hasta 255).

Con este campo podremos saber la ruta que ha ido siguiendo la aspiradora por la cuadrícula.

El tercer campo es el más complejo, pues está comprimido utilizando una técnica Run-Length Encoding, que, afortunadamente, es relativamente sencilla. Para ello tenemos que fijarnos en los dos primeros bits de cada byte. Si ambos están a 1, entonces es un contador de repeticiones y el número de veces que se repetirá el siguiente byte viene indicado por los seis bits inferiores. Ojo, porque si el siguiente byte también tiene ambos bits superiores a 1 entonces también se debe tener en cuenta para el número de repeticiones, tomando los seis bits del primer byte y moviéndolos seis posiciones a la izquierda, para luego anexionar los seis bits del segundo byte. Y así sucesivamente, anexionando tantos bytes seguidos como haya con los dos bits superiores a 1. Así, si tenemos los bytes 0xC2 0xDA 0xAA, nos está diciendo que tenemos que repetir el byte 0xAA un total de 0x21A veces (0xC2 & 0x3F = 0x02; 0xDA & 0x3F = 1A).

¿Y qué pasa con los bytes que no tienen sus dos bits superiores a uno? Esos contienen el estado de cuatro cuadrados consecutivos de la cuadrícula del mapa. El formato es el siguiente:

  • 00: zona sin explorar
  • 01: zona explorada libre (o sea, suelo)
  • 10: obstáculo (una pared, un mueble, una pata de una silla, un programador tocando las narices…)

Así, un byte 0x16 sería 00010110 en binario, lo que se traduce en cuatro elementos de la cuadrícula: 00 01 01 10. Por tanto aquí tendríamos un bloque sin explorar, los dos siguientes a su derecha serían bloques explorados libres (suelo), y finalmente una pared.

Por otro lado, la secuencia 0xC8 0x55 se descomprimiría como 0x55 0x55 0x55 0x55 0x55 0x55 0x55 0x55, u ocho veces 0x55; o sea 4 x 8 = 32 bloques de suelo libre.

Entonces, si tenemos estas dos secuencias (mapa arriba y track abajo):

AAAAAAAAZABk0fIAAqnWAAqqqdYABqqp1QABJqqp1gDCqqnVAAEqqqnT0wA=
AQIKADIxOjE6MDMwMy86LzouNC40MTAx

Tras procesarlas tendremos esto:

donde vemos que el cargador está en el punto verde, vemos también largas zonas exploradas que están libres (puntos rojos), paredes que delimitan la habitación (puntos azules), y el recorrido que ha seguido la aspiradora (línea negra).

Y con esto se ha terminado el análisis del protocolo, y ya tenemos todo lo suficiente para hacer nuestro propio servidor y app, y es justo lo que he hecho. Pero mejor eso en la siguiente entrada.

Parte 8