Actualizado: varios comandos de servidor a aspiradora terminaban con un LF. Añadidos.
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.
Ahora que ya sabía la estructura básica del protocolo de la aspiradora, decidí escribir un pequeño programa en Python que me extrajese la información de manera más clara, y así no tener que andar con Wireshark, que es bastante peñazo. Con él tengo las secuencias y el orden en que se transmiten disponibles de un vistazo. El código se puede bajar desde mi web.
Lo primero que hice fue apagar completamente la aspiradora para que se borrase todo lo que tuviese en memoria, y emparejarla de nuevo, a ver si cambiaba algo en el proceso. Tras comparar ambas, todo es igual excepto:
- el campo nonce
- el campo sign (aunque esto es, probablemente, porque actualicé el firmware)
- el campo token
- el campo authcode
A mayores, la aspiradora envió una petición HTTP extra a POST /baole-web/common/uploadLog.do HTTP/1.1 con información de un fallo, pero no parece nada importante.
Siguiendo con el análisis, decidí que tocaba empezar a enviar comandos, así que inicié la captura de paquetes y ordené a la aspiradora que comenzase a funcionar. Este fue el resultado (el campo targetId es, en realidad, un valor hexadecimal aparentemente aleatorio, pero único para cada aspiradora, así que lo anonimizo aquí. Es el mismo valor todas las veces que aparece). El número decimal es el instante de tiempo (en segundos) desde que empecé a registrar los datos, y el sentido del paquete se indica con s->a si es de servidor a aspiradora, a->s si es de aspiradora a servidor:
3.5093190670013428 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1a 27 00 00 | 00 00 00 00 { "cmd":0, "control":{ "authCode":"yyyyyy" "deviceIp":"192.168.18.3", "devicePort":"8888", "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3" }, "seq":0, "value":{"transitCmd":"100"} }\n 3.6597180366516113 a->s db 01 00 00 | fa 00 00 00 | 01 00 00 00 | 1a 27 00 00 | 00 00 00 00 { "version":"1.0", "control":{ "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3", "broadcast":"0" },"value":{ "noteCmd":"102", "workState":"5", "workMode":"0", "fan":"2", "direction":"0", "brush":"2", "battery":"100", "voice":"2", "error":"0", "standbyMode":"1", "waterTank":"40", "clearComponent":"0", "waterMark":"0", "version":"3.11.416(513)", "attract":"0", "deviceIp":"192.168.18.3", "devicePort":"8888", "cleanGoon":"2", "extParam":"{\"cleanModule\":\"3\"}" } } 3.7990760803222656 a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 1a 00 00 00 | 00 00 00 00 { "version":"1.0", "control":{ "targetId":"0", "targetType":"6", "broadcast":"0" },"value":{ "noteCmd":"102", "workState":"5", "workMode":"0", "fan":"2", "direction":"0", "brush":"2", "battery":"100", "voice":"2", "error":"0", "standbyMode":"1", "waterTank":"40", "clearComponent":"0", "waterMark":"0", "version":"3.11.416(513)", "attract":"0", "deviceIp":"192.168.18.3", "devicePort":"8888", "cleanGoon":"2", "extParam":"{\"cleanModule\":\"3\"}" } } 3.883021116256714 s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 1a 00 00 00 | 01 00 00 00 { "msg":"OK", "result":0, "version":"1.0" }\n 4.672595024108887 a->s 14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 1b 00 00 00 | e7 03 00 00 4.731273174285889 s->a 14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 1b 00 00 00 | e7 03 00 00 6.818763017654419 a->s bd 01 00 00 | 18 00 00 00 | 01 00 00 00 | 1c 00 00 00 | 00 00 00 00 { "version":"1.0", "control":{ "targetId":"0", "targetType":"6", "broadcast":"0" },"value":{ "noteCmd":"102", "workState":"1", "workMode":"11", "fan":"2", "direction":"0", "brush":"2", "battery":"100", "voice":"2", "error":"0", "standbyMode":"1", "waterTank":"40", "clearComponent":"0", "waterMark":"0", "version":"3.11.416(513)", "attract":"0", "deviceIp":"192.168.18.3", "devicePort":"8888", "cleanGoon":"2", "extParam":"{\"cleanModule\":\"3\"}" } } 6.9016029834747314 s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 1c 00 00 00 | 01 00 00 00 { "msg":"OK", "result":0, "version":"1.0" }\n 8.281097173690796 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1c 27 00 00 | 00 00 00 00 { "cmd":0, "control":{ "authCode":"zzzzzz", "deviceIp":"192.168.18.3", "devicePort":"8888", "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3" }, "seq":0, "value":{"transitCmd":"131"} }\n 8.425811052322388 a->s 54 01 00 00 | fa 00 00 00 | 01 00 00 00 | 1c 27 00 00 | 00 00 00 00 { "version":"1.0", "control":{ "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3", "broadcast":"0" },"value":{ "transitCmd":"132", "doTime":"614", "clearArea":"0", "clearTime":"0", "clearModule":"11", "clearSign":"2020-06-24-01-31-41-2", "chargerPos":"-1,-1", "map":"AAAAAAAAZABk0vwAKtgACtgAKtPVAA==", "track":"AQABADIx" } } 12.955074071884155 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1f 27 00 00 | 00 00 00 00 { "cmd":0, "control":{ "authCode":"zzzzzz", "deviceIp":"192.168.18.3", "devicePort":"8888", "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3" }, "seq":0, "value":{"transitCmd":"131"} }\n 13.06220006942749 a->s 5c 01 00 00 | fa 00 00 00 | 01 00 00 00 | 1f 27 00 00 | 00 00 00 00 { "version":"1.0", "control":{ "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3", "broadcast":"0" },"value":{ "transitCmd":"132", "doTime":"619", "clearArea":"0", "clearTime":"5", "clearModule":"11", "clearSign":"2020-06-24-01-31-41-2", "chargerPos":"-1,-1", "map":"AAAAAAAAZABk0vwAKoDXAApA1wAqgNPUAA==", "track":"AQACADIxMzE=" } }
Bueeeeeeno… menuda parrafada. Sin embargo empezamos a sacar ya cosas en claro: para poner en marcha la aspiradora y que comience a limpiar, la clave parece estar en este bloque, que parece servir para enviar un comando específico: en este caso, el servidor envía un paquete con el comando 100, como se especifica en transitCmd.
s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 1a 27 00 00 | 00 00 00 00 { "cmd":0, "control":{ "authCode":"yyyyyy", "deviceIp":"192.168.18.3", "devicePort":"8888", "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3" }, "seq":0, "value":{"transitCmd":"100"} }\n
Otro detalle interesante es que siempre que la aspiradora envía un paquete, su séptimo byte es 00, pero si lo envía el servidor es C8. Esto también se cumplía en las comunicaciones anteriores, salvo en los paquetes ping, los que aparentemente sólo son necesarios para mantener la comunicación. A pesar de todo, esa parte aún va a requerir más investigación.
Una vez que la aspiradora está en marcha, el servidor envía repetidamente el comando 131, el cual parece que pide a la aspiradora que devuelva información sobre su posición y estado actual.
La darle a la opción de dejar de aspirar, los comandos que se intercambian son:
2.3617970943450928 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 23 27 00 00 | 00 00 00 00 { "cmd":0, "control":{ "authCode":"yyyyyy", "deviceIp":"192.168.18.3", "devicePort":"8888", "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3" }, "seq":0, "value":{"transitCmd":"102"} }\n 2.4898791313171387 a->s dc 01 00 00 | fa 00 00 00 | 01 00 00 00 | 23 27 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3", "broadcast": "0" }, "value": { "noteCmd": "102", "workState": "1", "workMode": "11", "fan": "2", "direction": "0", "brush": "2", "battery": "100", "voice": "2", "error": "0", "standbyMode": "1", "waterTank": "40", "clearComponent": "0", "waterMark": "0", "version": "3.11.416(513)", "attract": "0", "deviceIp": "192.168.18.3", "devicePort": "8888", "cleanGoon": "2", "extParam": "{\"cleanModule\":\"3\"}" } } 2.6665220260620117 a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 1d 00 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "0", "targetType": "6", "broadcast": "0" }, "value": { "noteCmd": "102", "workState": "2", "workMode": "0", "fan": "2", "direction": "0", "brush": "2", "battery": "100", "voice": "2", "error": "0", "standbyMode": "1", "waterTank": "40", "clearComponent": "0", "waterMark": "0", "version": "3.11.416(513)", "attract": "0", "deviceIp": "192.168.18.3", "devicePort": "8888", "cleanGoon": "2", "extParam": "{\"cleanModule\":\"3\"}" } } 2.751024007797241 s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 1d 00 00 00 | 01 00 00 00 { "msg":"OK", "result":0, "version":"1.0" }\n 4.319652080535889 a->s 84 01 00 00 | 14 00 00 00 | 01 00 00 00 | 1e 00 00 00 | 00 00 00 00 { "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": "-1,-1", "extParam": "{\"fan\":\"2\",\"waterTank\":\"40\",\"mode\":\"0\"}", "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==", "track": "AQAEADIxMzExMTEy" } }
El comando para detenerse parece ser el 102, lo que es consistente con que el de iniciar sea el 100.
Por último, si pulsamos la opción de volver a la estación base, ésto es lo que sale:
0.530087947845459 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 d6 4c | 25 27 00 00 | 00 00 00 00 { "cmd": 0, "control": { "authCode": "yyyyyy", "deviceIp": "192.168.18.3", "devicePort": "8888", "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3" }, "seq": 0, "value": { "transitCmd": "104" } }\n 0.6716470718383789 a->s db 01 00 00 | fa 00 00 00 | 01 00 00 00 | 25 27 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3", "broadcast": "0" }, "value": { "noteCmd": "102", "workState": "2", "workMode": "0", "fan": "2", "direction": "0", "brush": "2", "battery": "100", "voice": "2", "error": "0", "standbyMode": "1", "waterTank": "40", "clearComponent": "0", "waterMark": "0", "version": "3.11.416(513)", "attract": "0", "deviceIp": "192.168.18.3", "devicePort": "8888", "cleanGoon": "2", "extParam": "{\"cleanModule\":\"3\"}" } } 0.8249011039733887 a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 20 00 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "0", "targetType": "6", "broadcast": "0" }, "value": { "noteCmd": "102", "workState": "4", "workMode": "0", "fan": "1", "direction": "0", "brush": "2", "battery": "100", "voice": "2", "error": "0", "standbyMode": "1", "waterTank": "40", "clearComponent": "0", "waterMark": "0", "version": "3.11.416(513)", "attract": "0", "deviceIp": "192.168.18.3", "devicePort": "8888", "cleanGoon": "2", "extParam": "{\"cleanModule\":\"3\"}" } } 0.8965139389038086 s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 20 00 00 00 | 01 00 00 00 { "msg":"OK", "result":0, "version":"1.0" }\n 2.895132064819336 a->s 83 01 00 00 | 14 00 00 00 | 01 00 00 00 | 21 00 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "0", "targetType": "6", "broadcast": "0" }, "value": { "noteCmd": "101", "clearArea": "0", "clearTime": "15", "clearSign": "2020-06-24-01-31-41-2", "clearModule": "0", "isFinish": "0", "chargerPos": "49,49", "extParam": "{\"fan\":\"1\",\"waterTank\":\"40\",\"mode\":\"0\"}", "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==", "track": "AQAEADIxMzExMTEy" } } 2.9380459785461426 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 27 27 00 00 | 00 00 00 00 { "cmd":0, "control":{ "authCode":"yyyyyy", "deviceIp":"192.168.18.3", "devicePort":"8888", "targetId":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType":"3" }, "seq":0, "value":{ "transitCmd":"131" } }\n 3.048809051513672 a->s 64 01 00 00 | fa 00 00 00 | 01 00 00 00 | 27 27 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3", "broadcast": "0" }, "value": { "transitCmd": "132", "doTime": "643", "clearArea": "0", "clearTime": "15", "clearModule": "0", "clearSign": "2020-06-24-01-31-41-2", "chargerPos": "49,49", "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==", "track": "AQAEADIxMzExMTEy" } } 7.939983129501343 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 2a 27 00 00 | 00 00 00 00 { "cmd": 0, "control": { "authCode": "yyyyyy", "deviceIp": "192.168.18.3", "devicePort": "8888", "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3" }, "seq": 0, "value": { "transitCmd": "131" } }\n 8.04991602897644 a->s 6c 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2a 27 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3", "broadcast": "0" }, "value": { "transitCmd": "132", "doTime": "648", "clearArea": "0", "clearTime": "20", "clearModule": "0", "clearSign": "2020-06-24-01-31-41-2", "chargerPos": "50,50", "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNL8AA==", "track": "AQAGADIxMzExMTEyMjIyMQ==" } } 12.952444076538086 s->a d1 00 00 00 | fa 00 c8 00 | 00 00 09 01 | 2c 27 00 00 | 00 00 00 00 { "cmd": 0, "control": { "authCode": "yyyy", "deviceIp": "192.168.18.3", "devicePort": "8888", "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3" }, "seq": 0, "value": { "transitCmd": "131" } }\n 13.123134136199951 a->s 70 01 00 00 | fa 00 00 00 | 01 00 00 00 | 2c 27 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "targetType": "3", "broadcast": "0" }, "value": { "transitCmd": "132", "doTime": "653", "clearArea": "0", "clearTime": "25", "clearModule": "0", "clearSign": "2020-06-24-01-31-41-2", "chargerPos": "50,48", "map": "AAAAAAAAZABk0vwAaoDXAGpA1wBqgNcAqNgABdLjAA==", "track": "AQAHADIxMzExMTEyMjIyMTIz" } } 13.785572052001953 a->s bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 22 00 00 00 | 00 00 00 00 { "version": "1.0", "control": { "targetId": "0", "targetType": "6", "broadcast": "0" }, "value": { "noteCmd": "102", "workState": "5", "workMode": "0", "fan": "2", "direction": "0", "brush": "2", "battery": "100", "voice": "2", "error": "0", "standbyMode": "1", "waterTank": "40", "clearComponent": "0", "waterMark": "0", "version": "3.11.416(513)", "attract": "0", "deviceIp": "192.168.18.3", "devicePort": "8888", "cleanGoon": "2", "extParam": "{\"cleanModule\":\"3\"}" } } 13.849518060684204 s->a 3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 22 00 00 00 | 01 00 00 00 { "msg":"OK", "result":0, "version":"1.0" }\n
Y, efectivamente, el comando es el 104. ¿Serán pares todos los comandos?
Además, parece que cuando la aspiradora ha llegado al cargador, emite automáticamente una trama para indicarlo (la del instante de tiempo 13.785572052001953). También vemos que el servidor envía repetidamente el comando 131 durante el trayecto, lo que parece confirmar que, efectivamente, se utiliza para pedir la posición y estado de la aspiradora mientras se está moviendo.
Y con esto ya están analizados los tres comandos más importantes. En la próxima entrega intentaré enviar yo mis propios comandos haciéndome pasar por el servidor, y en la cuarta espero analizaré el resto de comandos: selección del modo de aspirado, potencia del motor, fregona y control manual.