A ritmo de conga (2)

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.

Parte 3

A ritmo de conga (1)

Actualizado: varios comandos de servidor a aspiradora terminaban con un LF (o sea, \n). Añadidos.

Recientemente decidí comprarme un aspirador robótico, así que empecé a ver opciones. Es cierto que, siendo el primero, no quería gastarme mucho por si no me convencía, pero a la vez no quería tampoco comprar algo cutre que luego no funcionase. Así que al final, después de mucho pensar, decidí comprarme un Conga de Cecotec. Mi intención era comprar el más bajo de gama, pero estaba agotado, así que al final me pillé el Conga 1490, que tiene como característica diferenciadora que se puede controlar desde el móvil.

Y claro, me ponen un caramelito así, delante, y lo primero que se me pasó por la cabeza fue «no estaría mal meter las narices a ver cómo funciona».

Sin embargo, cuando me leí las condiciones de uso de la app y descubrí que envía tu IP, tu correo-e y los mapas que hace la aspiradora no sólo a Cecotec (que, a fin de cuentas, es una empresa española y está sujeta a las normas comunitarias de protección de datos), sino directamente al verdadero fabricante, que es chino (y te dicen claramente que él sí que no está sujeto a esas normas), la cosa empezó a mosquearme, y mis ganas de tocar dentro aumentaron bastante.

Pero no se vayan todavía, que aún hay más… resulta que se me ocurre ver qué permisos pide para funcionar, y me encuentro con que en el manifiesto pide, entre otras cosas:

  • hacer llamadas
  • acceso total a Internet
  • acceso al micrófono
  • acceso a la cámara y al flash
  • acceso de lectura y escritura a todo el almacenamiento del dispositivo
  • obtener datos del propietario
  • acceso a los logs del sistema
  • acceso a la lista de aplicaciones
  • arrancar automáticamente en el inicio del sistema

Esto ya me parecía el colmo: una aplicación china con acceso a TODO, y que encima se lanza automáticamente en el arranque… ni-de-co-ña.

Y sí, se lo que dirán algunos: la mitad de esos permisos (cámara, micrófono, llamadas y almacenamiento) no sólo se pueden desactivar en las propiedades de la aplicación, sino que, de intentar usarlos, el sistema operativo avisaría; y la otra mitad (acceso a datos del propietario y lista de aplicaciones) han sido eliminados en versiones recientes de Android. Pero pese a todo, es una cuestión de que no basta con ser bueno, también hay que parecerlo. Y esta app, por mucho de que en la práctica no haga nada malo, que dejen todo eso así queda, como mínimo, feo.

Pero es que, además, como veremos luego, la app y la aspiradora precisan de una conexión a internet para trabajar, lo que significa que si, por el motivo que sea, el fabricante decide que ya no le compensa tener encendido su servidor, MI aspiradora, por la que he pagado dinero, dejará de funcionar de la manera que me vendieron que funcionaría.

Así que toca arremangarse y empezar a trabajar.

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.

Lo primero que hice fue echar un vistazo al manual, y como cabía suponer, la aspiradora tiene WiFi y hay que emparejarla primero con el router. Para ello hay dos maneras, la automática y la manual. En ambos casos se comienza pulsando el botón de encendido de la aspiradora durante tres segundos, de manera que el piloto de la WiFi empieza a parpadear, y a continuación se mete la clave de la WiFi en la app (el SSID ya aparece, poniendo por defecto el mismo que usa el móvil).

Ahora, en modo automático, simplemente se deja que sea la propia app quien configure todo. Sin embargo, en modo manual hay que ir a Ajustes -> WiFi en el móvil y escoger una red con el identificador CongaGyro_XXXXXX, pulsar el botón de configurar, y volver a la WiFi normal. Efectivamente: al ponerse en modo emparejamiento, la aspiradora se convierte en un punto de acceso sin clave.

El primer paso, por tanto, era obvio: poner la aspiradora en modo emparejamiento, conectarse a esa WiFi desde el portátil (en mi caso me dio la IP 192.168.4.2), y hacer un escaneo de puertos con nmap:

nmap -p 1-49151 192.168.4.1

Y tal y como cabía esperar, el puerto 80 está abierto, y al entrar desde un navegador aparece esta página:

Tras escribir el SSID y la clave de mi WiFi, devuelve una página con un OK y el punto de acceso desaparece. Una simple comprobación con nmap nos devuelve la nueva IP de la aspiradora, ahora sí en la WiFi de verdad.

nmap -sP 192.168.18.0/24

Por supuesto, cada uno tiene que cambiar la IP por el valor de las IPs internas de su red. Una vez encontrada la IP, un nuevo escaneo de puertos me dejó descolocado: no había absolutamente ninguno, todos estaban cerrados a cal y canto.

Decidí que poco más podría hacer sin instalar la aplicación y esnifar los datos que intercambiaba con el aspirador, pero no me hacía ninguna gracia meterla en mi móvil, así que cogí una vieja tablet que no usaba para nada, la formateé, la registré con una cuenta de correo creada ex profeso, y me fui a la Play Store a instalar la app

Y no estaba.

Probé con todo tipo de combinaciones, pero aunque estaban algunas otras apps de Cecotec, la que había visto para la Conga 1490 y 1590 no aparecía por ninguna parte. Curiosamente fue lo mismo que me ocurrió cuando probé con un Android X86 corriendo en una máquina virtual… Mi conclusión es que, dado que en el manifiesto de la app pide permitir hacer llamadas, la Play Store sólo deja instalarla en dispositivos con tarjeta SIM.

Sin embargo, si nos bajamos el APK desde otra fuente, lo copiamos a la tablet y lo instalamos (tras permitir la instalación desde fuentes no confiables, claro) la aplicación funciona perfectamente.

Con ella ya instalada, llega la segunda parte: configurar un punto de acceso que nos permita esnifar todo el tráfico. El problema surge cuando nos damos cuenta de que es muy difícil hacer eso en WiFi, así que mi solución fue usar una Raspberry Pi configurada como punto de acceso, y correr en ella tcpdump para capturar todo el tráfico que pasase.

Para configurar una Raspberry Pi como punto de acceso basta con seguir estas instrucciones:

https://www.raspberrypi.org/documentation/configuration/wireless/access-point-routed.md

Yo utilicé la res 192.168.18.X para la WiFi, y así asegurarme de que no haya interferencias con nada más. Luego, para capturar todo el tráfico, uso el comando:

sudo nohup tcpdump -ni wlan0 '(not dst 192.168.18.4) and (not src 192.168.18.4)' -s0 -w datos.pcap

Siendo 192.168.18.4 la dirección IP de mi ordenador de sobremesa, para que no capture los paquetes que se intercambian cuando entro por SSH, y así tener una captura limpia. El parámetro w almacena los paquetes en bruto en el fichero datos.pcap, lo que nos permite luego abrirlo en, por ejemplo, WireShark, para analizarlos con más detenimiento.

Y con esto está todo listo para empezar, así que puse en marcha la captura de paquetes, y en la tablet lancé la aplicación, rellené los campos con una dirección de correo nueva, creada para la ocasión, emparejé la aspiradora, y lancé un escaneo de puertos… ¡y ahora la aspiradora tenía uno abierto, el 8888!

Buena cosa, pues, pero ahora tocaba parar la captura y empezar a analizar qué habían hecho la aspiradora y la app.

Una vez copiado el fichero con el volcado, procedí a abrirlo con el Wireshark. Lo primero que hace la aspiradora, como cabe esperar, es obtener una IP, pero luego resulta que lo siguiente que hace es conectarse a Ibl-app-eu.robotbona.com, y enviar una petición POST a baole-web/common/sumbitClearTime.do con un formulario con el siguiente contenido (en realidad va en formato URLEncode:

appKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
deviceId=yyyyyyyyyyyyyyy
deviceType=1
clearTime=0

Donde xxxxxxx e yyyyyyy son unas ristras de números hexadecimales que, probablemente, identifiquen a mi aspiradora. La respuesta es un OK con el siguiente texto:

2b
{"msg":"ok","result":"0","version":"1.0.0"}
0

Que resulta ser un bloque en formato chunked con un resultado en formato JSON. A continuación otra conexión al mismo servidor, pero esta vez pidiendo:

appKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
authCode=zzzzz
deviceId=yyyyyyyyyyyyyy
deviceType=1
funDefine=11101
nonce_str=AAAAA
version={"wifi":"1.0.48","mcu":"3.9.1714(513)"}
sign=SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS

Una vez más lo he anonimizado just in case. La respuesta es la misma que antes:

2b
{"msg":"ok","result":"0","version":"1.0.0"}
0

Hasta aquí la aspiradora se ha conectado al puerto 80 del destino, lo que es consistente con el uso de REST. Sin embargo, ahora la aspiradora se conecta al puerto 20008 del destino, y comienza un intercambio de mensajes diferente. La siguiente petición desde la aspiradora es un bloque que comienza con una secuencia de 20 bytes binarios, seguidos por 349 bytes en ASCII en formato JSON (el JSON, en realidad, se transmite en una única línea y sin formato, pero lo he embellecido para mejorar la lectura. Si en algún punto incluyese un LF, incluiría el símbolo \n):

71 01 00 00 | 10 00 00 00 | 01 00 00 00 | 02 00 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control":{
    "targetId":"0",
    "targetType":"6",
    "broadcast":"0"
  },
  "value":{
    "token":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "deviceId":"yyyyyyyyyyyyyy",
    "appKey":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "deviceType":"1",
    "authCode":"zzzzz",
    "deviceIp":"192.168.18.3",
    "devicePort":"8888",
    "firmwareVer":"{\"wifi\":\"1.0.48\",\"mcu\":\"3.9.1714(513)\"}"
  }
}

Aquí ya empieza a haber cosas interesantes: la aspiradora comunica al servidor cual es la IP interna y el puerto que ha abierto para comunicarse. Estos datos son importantes para la app. Se repiten los campos deviceId, appKey y authCode de mensajes anteriores, pero ahora se añade un token.

La respuesta también es complicada. En este caso es de 99 bytes, y comienza también con una ristra de 20 valores binarios, seguido de 79 bytes con un JSON normal:

63 00 00 00 | 11 00 c8 00 | 01 00 00 00 | 02 00 00 00 | 00 00 00 00
{
   "msg":"login succeed",
   "result":0,
   "version":"1.0",
   "time":"2020-06-21-04-59-39"
}

En el primer mensaje, los dos primeros bytes en formato little endian forman el número 369, que coincide con la suma de los 20 bytes binarios y los 349 bytes de JSON. En el segundo mensaje, los dos primeros bytes coincide con 99 en decimal, por lo que parece que, como mínimo, los dos primeros bytes indican el tamaño de la respuesta, probablemente para poder reservar la cantidad de memoria que necesite en el momento, y no tener que reservar de más, o ampliando bloques. ¿Qué significan el resto de bytes? De momento no tenemos información suficiente para saberlo.

Siguiente envío desde la aspiradora 444 bytes:

bc 01 00 00 | 18 00 00 00 | 01 00 00 00 | 03 00 00 00 | 00 00 00 00
{
  "version":"1.0",
  "control": {
    "targetId":"0",
    "targetType":"6",
    "broadcast":"0"
  },
  "value": {
    "noteCmd":"102",
    "workState":"6",
    "workMode":"0",
    "fan":"1",
    "direction":"0",
    "brush":"2",
    "battery":"100",
    "voice":"2",
    "error":"0",
    "standbyMode":"1",
    "waterTank":"40",
    "clearComponent":"0",
    "waterMark":"0",
    "version":"3.9.1714(513)",
    "attract":"0",
    "deviceIp":"192.168.18.14",
    "devicePort":"8888",
    "cleanGoon":"2",
    "extParam":"{\"cleanModule\":\"3\"}"
  }
}

Y otra vez tenemos una mezcla binaria y ASCII, en este caso de 60 bytes:

3c 00 00 00 | 19 00 c8 00 | 01 00 00 00 | 03 00 00 00 | 01 00 00 00
{
  "msg":"OK",
  "result":0,
  "version":"1.0"
}\n

Esto nos confirma que, efectivamente, lo primero que recibimos es el tamaño de la respuesta completa.

Editado: faltaba un \n al final del JSON.

Ahora la aspiradora envía sólo 20 bytes, lo que es sólo la cabecera:

14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 04 00 00 00 | e7 03 00 00

Y la respuesta del servidor es

14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 04 00 00 00 | e7 03 00 00

En este punto, la app se conecta a la aspiradora y empieza a intercambiar datos con este mismo formato, pero de momento vamos a dejar esa parte de lado, pues esta conexión con el servidor remoto sigue estando activa. De hecho, 30 segundos después del envío anterior, la aspiradora envía de nuevo otra secuencia, en concreto:

14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 05 00 00 00 | e7 03 00 00

Y la respuesta del servidor es

14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 05 00 00 00 | e7 03 00 00

Otra vez inactividad hasta que 30 segundos después volvemos a tener otro paquete desde la aspiradora:

14 00 00 00 | 00 01 c8 00 | 01 00 00 00 | 06 00 00 00 | e7 03 00 00

Y la respuesta del servidor es

14 00 00 00 | 11 01 c8 00 | 01 00 08 01 | 06 00 00 00 | e7 03 00 00

Lo interesante es que empieza a haber un patrón aquí. Si asumimos que son cinco números de cuatro bytes cada uno, el cuarto número es un número de secuencia: en efecto, comienza en 2 en la primera petición, y su respuesta tiene el 2 también. La siguiente petición tiene el valor 3, y su respuesta también. Luego viene el 4, y otra vez la respuesta, el 5 y otra vez la respuesta, el 6 y su respuesta… Además, el resto de los números de las peticiones y respuestas 4, 5 y 6 son iguales, lo que hace suponer que se utilizan para mantener la conexión activa. Por otro lado, en las peticiones, el quinto byte tiene el bit 0 a cero, y en las respuestas están a uno. En el sexto byte, además, el bit 0 está a cero en peticiones con datos, y a 1 en las que son sólo para mantener la conexión.

Veamos ahora qué pasa con la app en sí. Como ya dije, se conecta al puerto 8888, pero lo raro es que establece dos conexiones en vez de una. ¿Por qué? Ni idea. Pero veamos con calma qué hace.

En la primera conexión tenemos este intercambio, antes de que la aspiradora la cierre (t es tablet, a es aspiradora):

t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00

En la segunda conexión se intercambian estos datos:

t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00
a->t: 14 00 00 00 | 11 01 c8 00 | 01 00 00 00 | 1f 27 00 00 | e7 03 00 00
t->a: 14 00 00 00 | 00 01 c8 00 | 00 00 01 00 | 1f 27 00 00 | 00 00 00 00

O sea, más o menos lo mismo. De aquí no puedo sacar mucha información.

Hasta aquí la primera parte. Ahora toca empezar a hacer pruebas para intentar descubrir cómo dar órdenes a la aspiradora.

Parte 2

Memes animados de hoy y de siempre

Estoy preparando una entrada nueva y me apeteció añadir memes en el texto, porque ayudan a leerlo al permitir descansar la vista entre párrafo y párrafo, y le dan un toque más divertido y agradable.

Por desgracia, la cosa no es tan sencilla. Una primera opción es buscar GIFs animados, pero esto tiene varios problemas:

  • El tamaño de un GIF es bastante grande
  • El número de colores es limitado

La solución obvia es utilizar vídeos en su lugar, algo que WordPress soporta perfectamente. Para ello hay que embeberlo, activar la reproducción automática, el bucle, quitar los controles y, sobre todo, quitar el sonido. Esto último es muy importante, pues si no, el navegador se negará a reproducirlo hasta que el usuario haya interaccionado con la página (lo que obliga a haber hecho click en ella, pero no sirve con hacer scroll). El motivo para esto es evitar que una página recién cargada empiece a reproducir sonido por sorpresa, pues es algo muy molesto. Si no se hace, el vídeo no se reproducirá hasta que el usuario lo ponga en marcha a mano.

Aunque es muy fácil, por desgracia tiene el problema de que obliga a acordarse de todos los pasos, con lo que es bastante sencillo que, por despiste, se nos cuele alguno y nos quede el meme mal. Por eso decidí hacer un poco de JavaScript para solucionar esto de manera sencilla.

Lo primero es bajarse algún plugin que permita añadir JavaScript a nuestra plantilla. Yo uso WordPress, por lo que escogí Simple Custom CSS and JS. Una vez instalado y activado, sólo tenemos que añadir en nuestra plantilla este trozo de código:

let memeList = [];
function setPlayStatus(element) {
// check if it is visible
var rect = element.getBoundingClientRect();
let isVisible = (
rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.left <= (window.innerWidth || document.documentElement.clientWidth)
);
if (isVisible != element.memeData.isVisible) {
if (isVisible) {
element.memeData.video.play();
} else {
element.memeData.video.pause();
}
element.memeData.isVisible = isVisible;
}
}

function checkForMemes() {
let elements = document.getElementsByTagName(«figure»);
for (let element of elements) {
if (!element.classList.contains(‘meme’)) {
continue;
}
memeList.push(element);
element.memeData = {};
for (let child of element.childNodes) {
if (‘controls’ in child) {
element.memeData.video = child;
element.memeData.isVisible = false;
child.muted = true;
child.controls = false;
child.loop = true;
child.autoplay = true;
setPlayStatus(element);
}
}
}
}

function refreshMemesStatus() {
for (let meme of memeList) {
setPlayStatus(meme);
}
return true;
}

window.addEventListener(«load», checkForMemes);
window.addEventListener(«DOMContentLoaded», refreshMemesStatus, false);
window.addEventListener(«scroll», refreshMemesStatus, false);
window.addEventListener(«resize», refreshMemesStatus, false);

Con esto lo único que tenemos que hacer es añadir al elemento video la clase CSS meme, y automáticamente se establecerán los parámetros necesarios para que el vídeo funcione a la primera. A mayores, cuando el meme/vídeo se sale de la zona visible de la pantalla, se le da la orden de pausa para que no consuma nada de procesador. Aunque es probable que los navegadores lo tengan en cuenta, no es mala idea dejar indicado de manera explícita que nos da igual que el vídeo no siga avanzando mientras no lo vemos.

Resolución nativa en QEmu

Como ya comenté anteriormente, estoy pasando de VirtualBox a Gnome-Boxes, el cual utiliza KVMQEmu por debajo. Esto lo hice en origen porque tenía problemas con los drivers gráficos de VirtualBox en Fedora, mientras que en Boxes, Fedora funcionaba a resolución nativa (esto es, ajustando la resolución de la máquina virtual al tamaño de la ventana) sin necesidad de instalar nada. Sin embargo, algo raro ocurría con Debian Buster (la actual estable), pues no conseguía dicho efecto.

La solución, después de buscar por ahí, fue instalar el paquete spice-vdagent y reiniciar. Con él, la resolución del sistema gráfico se ajusta automáticamente al tamaño de la ventana, con lo que trabajar en dichas máquinas virtuales es tan cómodo como en las de VirtualBox.

Multipackager, Fedora y Debian

Desde hace tiempo puedo generar paquetes fácilmente para varias distribuciones gracias a un pequeño programa que me hice, multipackager, que a partir de las fuentes, me hace paquetes para Debian estable y SID, Ubuntu LTS y actual, Fedora y Arch. Para ello genera un contenedor con la distribución concreta, añade los paquetes necesarios y ejecuta el programa que genera el paquete.

Por desgracia, ya llevaba una temporada con problemas con Fedora: no tenía manera de generar una imagen para Fedora 31. Afortunadamente, echando un vistazo a cómo lo hacía LXC pude encontrar una manera, algo chapucera, para qué negarlo.

Por desgracia, hace unos días terminó de romper del todo: hasta ahora, para generar las imágenes de contenedores utilizaba yum, la herramienta de paquetería de Fedora/Red Hat, que, afortunadamente, estaba disponible en Debian SID. Pero por desgracia, hace unos días fue eliminada de los repositorios. Algunos dirán que esa herramienta está obsoleta, y que hoy en día se utiliza dnf, y es verdad, pero por desgracia esa nunca estuvo disponible en Debian, y no fui capaz de instalarla a partir de las fuentes.

Ante esto, necesitaba una solución. Decidí echar un vistazo de nuevo a ver qué hacía LXC, y me encontré con que partía de la imagen Live ISO de Fedora, montándola, extrayendo el fichero con la imagen squashfs, extrayendo de ella el sistema de archivos, y configurando un sistema nuevo con ellos. Lo veía bastante lioso, pero no veía otra opción, así que empecé a ver cómo automatizar la descarga de la ISO… y de pronto encontré un interesante directorio en el servidor web llamado Containers… y efectivamente, contenía imágenes del sistema de archivos básicas, listas para ser utilizadas. Lo único que tenía que hacer era procesar la página web que listaba el contenido de la carpeta para obtener el nombre del fichero correcto, y a correr.

Y gracias a eso, multipackager ya vuelve a funcionar a pleno rendimiento, y he podido sacar un nuevo paquete de Cronopete que funciona en Fedora 32.

Marcadores en una nueva pestaña, y portando todo a una nueva sesión de Firefox

Hace tiempo empecé a utilizar una extensión de Firefox llamada newtab-bookmarks, pues me permitía disponer de los marcadores directamente de fondo de cualquier nueva pestaña. Sin embargo, recientemente me encontré con que mi sesión de Firefox se ha… degradado. Básicamente me ocurrían «cosas raras», cosas que en otro equipo con el mismo sistema operativo (Debian SID) y configuración no ocurrían. Probé a crear una nueva sesión desde cero y los problemas desaparecieron, por lo que decidí migrarlo todo desde la vieja a la nueva. A fin de cuentas, esta configuración fue pasando de versión en versión desde hace más de una década, por lo que tiene sentido que pueda haber algún problemilla. Y aquí empezaron mis problemas: para empezar, al ir a descargar esa extensión me encontré con que ya no está soportada, aunque afortunadamente redirige a Perfect Home, otra extensión que hace más o menos lo mismo… excepto utilizar favicons en los enlaces.

Una vez resuelto este problema, exportar los marcadores fue sencillo. Y entonces llegó el verdadero quebradero de cabeza: ¿como exporto las contraseñas guardadas? Suponía que sería algo tan sencillo como utilizar la opción de exportar, pero, sorprendentemente, dicha opción no existe. Y no es que lo diga yo, es que en Mozilla lo reconocen abiertamente.

Vemos que la opción que dan es utilizar un exportador de terceros, pero es algo que no me convence nada; a fin de cuentas, estamos hablando de dar acceso a mis contraseñas… y siendo más de cien, no me apetecía ponerme a copiarlas a mano. Afortunadamente, tras rebuscar un poco más encontré que basta con copiar los ficheros key4.db y logins.json al nuevo perfil, y dispondremos de todas nuestras contraseñas guardadas.

Nueva versión de ActivityAppLauncher

Acabo de subir una nueva versión de ActivityAppLauncher, la extensión de Gnome Shell que muestra un menú de categorías en el modo Overview. El motivo de actualizarla, después de años sin tener que tocarla prácticamente, es que en Gnome Shell 3.36.0 han hecho varios cambios internos que rompen las cosas que tuve que hacer para que funcionase.

Sin embargo, aproveché para echar un vistazo más en detalle al viejo código (a fin de cuentas, fue una de las primeras extensiones que escribí), y encontré una manera más elegante y, sobre todo, fiable, de añadir la vista con las aplicaciones: ahora es una página «con todas las de la ley» dentro del modo Overview, y no un pegote metido con calzador. Esto tiene la ventaja de que hay una animación al pasar de la lista de ventanas al de aplicaciones y viceversa, pero a cambio no me ha sido posible conservar la lista de escritorios cuando se muestra la lista de aplicaciones.

También aproveché para cambiar el sistema de instalación por uno basado en meson, vilmente fusilado del utilizado en desktop-icons.

En breve estará disponible en la página de extensiones de Gnome Shell, pero mientras tanto, se puede bajar del repositorio GIT.

Compilando Gnome Shell

Estos últimos meses he estado preparando un parche para Mutter, y he necesitado instalar la rama de desarrollo de Gnome Shell. Y aunque hay bastante documentación, hay detalles que están algo incompletos, así que voy a poner aquí lo que he ido aprendiendo.

Para empezar, toda la compilación se hace mediante jhbuild, lo que simplifica muchísimo el trabajo. Y aunque es mucho más sencillo que bajarse a mano todo, aún tiene algunos detalles peliagudos.

Lo primero, si queréis tener éxito en compilar una versión de desarrollo, es mejor que utilicéis Fedora, en concreto la versión de desarrollo (o beta) también. De esta manera no tendréis problemas de versiones con las dependencias. Para ello, nada mejor que utilizar una máquina virtual. Aunque VirtualBox es una buena opción, yo tuve serios problemas con los controladores de pantalla (y es raro… con Debian o Ubuntu siempre fueron perfectamente), por lo que al final me decanté por Gnome Boxes, una herramienta más sencilla, pero con la que no tuve ningún problema.

Una vez instalada, bajamos jhbuild y lo instalamos con:

git clone https://gitlab.gnome.org/GNOME/jhbuild.git
cd jhbuild
./autogen.sh
make
make install

Una vez hecho esto debemos añadir (si no lo tenemos ya) a nuestro PATH el directorio ~/.local/bin. Y ya tendremos jhbuild instalado.

El siguiente paso es pedir que nos construya Gnome Shell. Para ello escribimos:

jhbuild build gnome-shell

Sin embargo, lo más probable es que no funcione y nos diga que nos faltan un montón de dependencias. Para instalarlas, basta con ejecutar:

jhbuild sysdeps – -install gnome-shell

y tomarse un café (o dos), porque tarda un buen rato en bajar e instalar las dependencias. Cuando termine volvemos a ejecutar el comando previo, y probablemente nos dirá que nos falta aún libavcodec y compañía. Por suerte es posible compilarlo aún así, mediante:

jhbuild build – -nodeps gnome-shell

Ahí empezará a compilar por fin, y tardará, tardará mucho (y requerirá mucho disco duro, por cierto. Aseguraos de que vuestra máquina virtual tiene suficiente). Pero lo importante que debemos saber es que el código lo baja en ~/jhbuild/checkout, por lo que si queremos aplicar parches, es ahí donde hay que tocar el código.

Cuando termina de compilar, instala todo en ~/jhbuild/install, y ya podemos probarlo. Yo lo que suelo hacer es cerrar la sesión gráfica, irme a una consola de texto (con Alt+F2), y ahí ejecutar primero:

jhbuild shell

para tener una shell dentro del entorno, y luego ejecutar:

dbus-run-session – – ~/jhbuild/install/bin/gnome-shell – -wayland – -display-server

y se arranca la sesión. Un problema que yo no he conseguido resolver es que la opción de Cerrar sesión no funciona. Yo lo resuelvo abriendo una terminal y matando el proceso.

Parche para Gnome Boxes

Aunque hasta ahora utilizaba VirtualBox para hacer virtualización, hace unas semanas me dio por probar Gnome Boxes en su lugar, y la verdad es que me ha convenció mucho. Es cierto que no tiene muchas características avanzadas de VirtualBox, como son el portapapeles bidireccional o el acceso directo a USBs, pero la verdad es que, en general, no los necesito. Y, sin embargo, sí tiene algo que me era especialmente útil, que es que la emulación de la tarjeta gráfica funciona de manera nativa en Fedora y en Debian, sin necesidad de instalar nada. El motivo de que esto me resulte especialmente útil es que tengo algunos parches pendientes para mutter, el gestor de ventanas de Gnome, y la única manera de probarlos en condiciones (sobre la versión de desarrollo) es trabajar con Fedora. Por desgracia, no se qué pasa que los drivers de VirtualBox para la pantalla no me funcionan en Fedora. No es que funcionen mal, es que no he conseguido que me funcionen. En alguna ocasión parece que van, pero de pronto dejan de funcionar sin motivo aparente. Y depurar en un sistema en 640×480 es imposible. Por eso estoy utilizando Gnome Boxes para todo esto, porque me funciona perfectamente.

Por desgracia, hace unos días me encontré con que era imposible crear una nueva máquina virtual en Boxes: cada vez que escogía la opción en le menú, el programa se cerraba con un core dump.

Tras indagar un poco descubrí que el problema era que el programa se conectaba a una URL externa para bajar una lista de recomendaciones y mostrar las tres más interesantes; sin embargo, por algún motivo, todas menos dos fallaban a la hora de descargarlas. Y aquí llega el problema: el código no comprobaba cuantas recomendaciones había e intentaba mostrar siempre tres, pero al haber sólo dos, cascaba. Como solucionarlo era trivial, decidí escribir yo mismo el parche y enviárselo, y lo aceptaron de inmediato, por lo que ya vuelve a funcionar perfectamente.

Y aún me preguntan por qué me gusta tanto el software libre…

Ubuntu arranca lento en VirtualBox

De un tiempo a esta parte, cada vez que instalaba una Ubuntu en una máquina de VirtualBox, me encontraba con que a la mitad del arranque se quedaba parada. En una de éstas me dio por probar a esperar, y muuucho tiempo después, terminó de arrancar.

Al principio parecía que tenía que ver con VirtualBox en sí, porque ocurría después de instalar los drivers para máquinas virtuales. Sin embargo, era consciente de que no tenía sentido, porque si no habría reportes por toda internet. Por eso empecé a revisar con calma la configuración, y me di cuenta de que en el procesador estaba desactivado el soporte de PAE. Esto me mosqueó, pues había leído en varios sitios que en Ubuntu al menos ahora era obligatorio, pues el núcleo por defecto viene con dicha opción activa.

Así pues, fui a la configuración, en Sistema -> Procesador, y la activé, y, efectivamente, el problema estuvo resuelto.