Archivo por días: 31 agosto, 2020

A ritmo de conga (14)

Esta creo que por fin será la última entrada en una serie que se ha prolongado muchísimo más de lo que esperaba. En esta hablaré de los últimos comandos que quedan por analizar.

En primer lugar está el comando 143. Este comando es el que se emite cuando se pulsa una especie de diana que hay en la parte superior derecha de la app oficial, y hace que el robot emita un pitido. En el manual no dice para qué sirve, por lo que lo único que se me ocurre es para localizarlo cuando no sabes donde anda. Sin embargo lo veo poco útil, pues si está en marcha lo escucharás perfectamente, y si está en pausa, al cabo de un minuto más o menos se apagará y dejará de responder a los comandos enviados por WiFi.

Por otro lado, ya he descubierto para qué sirve el comando 400, y no es para indicar que se ha abierto la app. Resulta que, al contrario de lo que pensaba, es la propia aspiradora la que recuerda las tareas cuando la programamos para que aspire tal día a la semana a tal hora. Así, si por ejemplo tenemos dos tareas programadas, una para que limpie de lunes a viernes a las 19:00 horas, y otra para que limpie sábados y domingos a las 16:30 horas, cuando emitimos el comando 400 nos devolverá esto:

{
  "version":"1.0",
  "control":{
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3",
    "broadcast":"0"
  },
  "value":{
    "transitCmd":"401",
    "maxRecord":"15",
    "result":"0",
    "orders":[
      {
        "sign":"1",
        "orderIds":"1,2,3,4,5",
        "valid":"1",
        "hour":"19",
        "minute":"0",
        "mode":"11",
        "fan":"1",
        "waterTank":"255"
      },{
        "sign":"2",
        "orderIds":"0,6",
        "valid":"1",
        "hour":"16",
        "minute":"30",
        "mode":"11",
        "fan":"2",
        "waterTank":"255"
      }
    ]
  }
}

En orders es donde viene toda la información clave: es una lista con tantos diccionarios como programaciones haya. En ellos, el campo sign es el identificador de cada programa. En orderIds vienen los días de la semana en los que se activará, siendo 0 el domingo, 1 el lunes, etc. El campo valid vale 1 si el programa está activo, y 0 si está desactivado. Los campos hour y minute especifican, como cabe suponer, la hora a la que se quiere que comience. Por último, los campos mode, fan y watertank indican el modo de limpieza, la potencia del ventilador y el modo de fregado que se quieren utilizar para ese programa concreto.

El siguiente comando importante es el 402, que permite añadir un nuevo programa a la lista o modificar uno ya existente. El formato es el siguiente:

{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value": {
    "orders":[
      {
        "fan":"3",
        "hour":"19",
        "minute":"30",
        "mode":"11",
        "orderIds":"2,0,6,5,4,3,1",
        "sign":"1",
        "valid":"1",
        "waterTank":"255"
      }
    ],
    "transitCmd":"402"
  }
}\n

Vemos que en el campo orders van los valores del programa que queremos añadir o modificar (lo que se sabe gracias al campo sign.

Por último, el comando 404 permite borrar un programa. Su formato es el siguiente:

{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "orderIds":"3,6",
    "signs":"2",
    "transitCmd":"404"
  }
}\n

El formato es aún más sencillo: junto con el comando y el campo signs, que indica qué programa se quiere borrar, se incluye el campo orderIds con los días del programa a borrar. Este campo tiene que coincidir con lo que contenga en ese momento el campo del mismo nombre del programa a borrar. No he probado qué ocurre si contiene menos o más valores.

Un ultimo detalle curioso es que hace un par de días actualicé el firmware de la WiFi, y curiosamente el contenido del tercer entero de las cabeceras enviadas por el servidor cambió de 0x01090000 a 0x01F20000. Lo mismo para el PONG de respuesta a un PING, que pasó de 0x01080001 a 0x01F10001. Sin embargo, si utilizamos los valores viejos todo sigue funcionando exactamente igual, lo que es curioso.

A ritmo de conga (13)

Una funcionalidad que echaba en falta es el control manual: poder controlar el robot para mandarlo de un lado a otro con el teléfono, en lugar de tener que cogerlo físicamente.

El problema es que, tras analizar el protocolo de control manual a partir de las capturas que había hecho, me encontré con que era ligeramente diferente del que ya estaba utilizando. Para empezar, los comandos no iban a través del servidor, sino que se enviaban directamente desde el móvil a la aspiradora (por fin tenía sentido el puerto 8888). Por otro lado, los «valores misteriosos» no seguían el mismo patrón. Para empezar, es la tablet quien envía los PINGs, y siempre con el mismo número de secuencia: 1a 27 00 00. Por otro lado, estas son varias cabeceras de comandos enviados desde la tablet a la aspiradora:

d1 00 00 00 | fa 00 c8 00 | 00 00 29 27 | 28 27 00 00 | 00 00 00 00
d1 00 00 00 | fa 00 c8 00 | 00 00 2c 27 | 2b 27 00 00 | 00 00 00 00
d1 00 00 00 | fa 00 c8 00 | 00 00 2f 27 | 2e 27 00 00 | 00 00 00 00
d1 00 00 00 | fa 00 c8 00 | 00 00 32 27 | 31 27 00 00 | 00 00 00 00
db 00 00 00 | fa 00 c8 00 | 00 00 35 27 | 34 27 00 00 | 00 00 00 00

Vemos que el primer campo sigue siendo el tamaño, el cuarto sigue siendo un número de secuencia, y el segundo y el cuarto tienen los mismos valores que en el protocolo con el servidor; pero el tercer campo es diferente; de hecho es el valor del número de secuencia pero con los bytes invertidos y sumándole uno al tercer byte.

Esto ya plantea algunas dudas; por ejemplo ¿qué pasa si el número de secuencia es mayor de 0xFFFF? ¿Está prohibido tal vez? Si está permitido ¿el campo tercero será el cuarto más 256 y con los bytes en orden inverso? De hecho ¿realmente el número de secuencia es de cuatro bytes también en el protocolo original, o puede que sean sólo dos bytes?

De hecho, para probar esto último decidí ver hasta qué valor devolvía la aspiradora, y tras varias pruebas me encontré con que 10.000 es el número de secuencia más grande que envía en el protocolo original, tras el cual vuelve al 1. Pero el servidor sí envía números más grandes de 10.000.

Aparte de este problema, está la cuestión de que en el código tendría que añadir un nuevo socket y gestionarlo… no es difícil, pero sí un rollo. Sin embargo… ¿Qué pasaría si se pudiese controlar desde la conexión original? ¿Puede el servidor enviar comandos de control manual?

La pregunta es legítima, pues cabe suponer que el control manual utiliza la conexión directa para reducir la latencia (a fin de cuentas, es un control interactivo), y además, si estamos en el bar no tiene sentido querer controlar manualmente una aspiradora que no podemos ver. Pero aún así hay casos en los que puede ser necesario, por ejemplo que tengamos varias WiFis en nuestra casa y que el teléfono esté conectada a una y la aspiradora a otra. Así que decidí probar justo eso y… ¡¡¡Funcionó!!! ¡¡¡Es posible enviar comandos de control manual a través del socket del servidor!!! Eso simplifica la tarea enormemente.

Ahora toca analizar el formato. Veamos un primer caso: ordenamos al aspirador ponerse a girar alrededor de sí mismo hacia la derecha durante tres segundos (las respuestas de la aspiradora son, simplemente, el estado actual, así que he borrado casi todo el contenido y lo he reemplazado por unos puntos suspensivos para no alargar demasiado el bloque):

1315.019054889679
s->a e1 00 00 00 | fa 00 c8 00 | 00 00 f2 01 | 2d 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "direction":"4",
    "transitCmd":"108"
  }
}\n

1315.3824479579926
a->s f6 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2d 27 00 00 | 00 00 00 00
[...]



1317.020054889679
s->a e1 00 00 00 | fa 00 c8 00 | 00 00 f2 03 | 2d 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "direction":"4",
    "transitCmd":"108"
  }
}\n

1317.3924479579926
a->s f6 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2d 27 00 00 | 00 00 00 00
[...]



1318.9394080638885
s->a eb 00 00 00 | fa 00 c8 00 | 00 00 f2 01 | 2f 27 00 00 | 00 00 00 00
{
  "cmd":0,
  "control":{
    "authCode":"xxxxxx",
    "deviceIp":"192.168.3.56",
    "devicePort":"8888",
    "targetId":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
    "targetType":"3"
  },
  "seq":0,
  "value":{
    "direction":"5",
    "tag":"4",
    "transitCmd":"108"
  }
}\n

1319.4073688983917
a->s f6 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2f 27 00 00 | 00 00 00 00
[...]

Y ya vemos cómo va: el comando es el 108 , y luego hay un campo direction que vale 4. ¿Será el comando 108 para girar a la derecha, y habrá otros para el resto de direcciones, o será un único comando para todo y el campo direction indica cual de los cuatro posibles movimientos se desea? Además, al cabo de dos segundos vemos que se vuelve a repetir el comando. ¿Por qué?

Finalmente, al cabo de tres segundos se emite el comando con la dirección 5, por lo que cabe suponer que eso significa detente. ¿Pero qué significa el campo tag?

Para resolver todas estas preguntas, veamos todos los comandos emitidos para los cuatro movimientos posibles (adelante, atrás, izquierda y derecha):

adelante:
  "value":{
    "direction":"1",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "1",
    "transitCmd":"108"
  }

------------------------
atrás:
  "value":{
    "direction":"2",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "2",
    "transitCmd":"108"
  }

------------------------
izquierda:
  "value":{
    "direction":"3",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "3",
    "transitCmd":"108"
  }

------------------------
derecha:
  "value":{
    "direction":"4",
    "transitCmd":"108"
  }

  "value":{
    "direction":"5",
    "tag": "4",
    "transitCmd":"108"
  }

¡Ajá! Ahora tiene sentido: se utiliza un único comando, el 108, para los movimientos manuales, con 1, 2, 3 y 4 para moverse adelante, atrás, izquierda y derecha respectivamente. Cuando se quiere parar se emite el mismo comando pero con la dirección 5, y el campo tag especifica qué movimiento es el que se cancela (probablemente por si los comandos llegan fuera de orden).

Además, si probamos con movimientos que duren más tiempo se ve que el servidor vuelve a enviar el comando de movimiento cada dos segundos. Todo apunta a que es una manera de asegurar que el servidor «sigue ahí», y que si pasa mucho tiempo sin recibir un comando de refresco, la aspiradora dejará de ejecutar el último comando. Esto tiene sentido: si se pierde la conexión es importante que el robot no se quede ejecutando una orden de movimiento manual…

Y con esto ya tenemos todo lo necesario para implementar el control manual, y así de bonito se ve en la app:

Como se ve, hay un nuevo icono que permite alternar entre modo mapa y modo control manual, y pulsando las flechas el robot se moverá. Eso sí, por desgracia la aspiradora no puede estar en la base para que este modo funcione, parece una limitación del propio firmware del robot, no de la app original.

Parte 14