Archivo de la categoría: tutoriales

Reemplazando Android

(La segunda parte de este artículo está en https://blog.rastersoft.com/?p=1386)

Hace unas semanas me compré un cacharro de tipo AndroidTV (este en concreto). Sin embargo ya tenía claro que no quería tener Android, sino un linux como $DEITY manda. Por eso busqué uno con el mismo chip que mi tablet. Por desgracia esta vez no conseguí que arrancase desde una tarjeta externa (parece que han cambiado algo en el cargador). Pero como a cabezota no me gana nadie, decidí buscar alguna alternativa.

Por supuesto no quería eliminar el Android que ya trae, más que nada por si me lo cargo y acabo con un bonito pisapapeles. Por eso decidí intentar una ruta intermedia: una vez arrancado el sistema, lanzaría un programa que mataría todos los procesos de Android y lanzaría una Debian almacenada en un disco externo, desde un entorno chroot, con su entorno gráfico y todo. De esta manera, si no enchufo el disco el sistema arrancaría Android normalmente, y si lo enchufo, arrancaría Linux.

Obviamente la cosa no es tan sencilla. Para empezar, los diversos procesos de Android se vuelven a lanzar automáticamente si mueren, por lo que no podemos utilizar un simple kill pid. Afortunadamente existen los comandos start y stop que lanzan y detienen todo el entorno gráfico. Para probarlos utilicé adb, disponible en el entorno de desarrollo de Android. Para ello primero lancé el servidor con

sudo ./adb start server

Y luego cada vez que necesitaba una consola en el dispositivo, lo conectaba con el cable USB y lanzaba:

./adb shell

Y efectivamente, si ejecutaba stop el entorno de Android desaparecía, quedando la pantalla congelada. Ejecutando start volvía a arrancar todo. Afortunadamente, aunque mi dispositivo no está completamente rooteado, si es lo suficientemente libre como para que, por defecto, entre como root desde esta shell, además de incluir un busybox con chroot. Con esto ya tengo resuelto el primer gran problema. Es cierto que todavía quedan varios servicios en marcha, como el gestor de red, pero es un problema secundario que ya resolveremos luego.

OJO: el servicio adb en el dispositivo Android tarda un poco en ser lanzado. Si acabáis de arrancarlo, sed pacientes. Por otro lado, a veces la lista de dispositivos USB con Android no está actualizada en Linux, así que es posible que haya que tocar un poco en udev para que reconozca el nuestro.

Autoarranque

Si sólo quisiese lanzar manualmente el nuevo entorno ya tendría el problema resuelto; pero yo quiero un dispositivo que arranque Debian incluso si no estoy (por ejemplo si se va la luz y luego vuelve), por lo que no queda más remedio que meter las narices en el proceso de arranque de Android.

Alguno puede pensar que ir a tan bajo nivel es exagerado, pero hay un motivo: la primera idea que se me ocurrió fue buscar algún programa nativo de Android que en el arranque lance automáticamente un script que yo le pase. Encontré varios, pero fue entonces cuando descubrí que mi cacharro no está rooteado del todo, por lo que ese script no podía lanzar un chroot ni nada por el estilo. Así pues, no queda más remedio que meter las narices más abajo.

Como en cualquier sistema Linux, una vez arrancado el núcleo se ejecuta el proceso init. Pero aquí acaban los paralelismos, porque en Android este proceso es muy diferente del SysV Init: aquí hay un conjunto de ficheros .rc con una serie de indicaciones de qué procesos hay que lanzar, en qué orden, con qué permisos, de qué dependen, y, sobre todo, si es necesario volver a lanzarlos si mueren. Estos ficheros .rc se encuentran en el raíz del sistema de ficheros, y ese es primer problema serio con el que nos encontramos: en general, el sistema raíz es de tipo rootfs. Este sistema de ficheros es un tipo de ramfs, que se inicializa durante el arranque con unos valores predeterminados, y además está montado como sólo lectura. Aunque en principio podemos hacer mount -o remount,rw / para permitir escritura, cualquier cambio que hagamos desaparecerá al reiniciar. La única manera de modificar el sistema de archivos es recompilarlo y grabar la partición entera, y eso es algo que no quiero hacer por el riesgo de cascar el sistema. Sin embargo, en /system tenemos una partición que sí es modificable, porque es de tipo ext4, así que intentaremos meter nuestro autoarranque ahí. En caso de que, por defecto, dicha partición esté montada como sólo lectura, podemos volverla de lectura/escritura con:

mount -o remount,rw /system

y devolverla a sólo lectura con

mount -o remount,ro /system

El formato de los ficheros .rc es relativamente sencillo: si una línea comienza por on XXXXX, las líneas siguientes definen los comandos a realizar cuando ocurra el evento XXXXX. Pero si empieza por service XXXXX, entonces las siguientes líneas definen a dicho servicio XXXXX, que se debe lanzar al inicio. Un ejemplo de servicio sería este:

service console /system/bin/sh
    class core
    console
    disabled
    user shell
    group log

Estas líneas definen el servicio console, que se lanza ejecutando el binario /system/bin/sh. Además, debe lanzarse como usuario shell y grupo log. Si estos dos parámetros no se incluyen se lanza como root.

Una línea especial, que nos interesa especialmente, es oneshot. Cuando un servicio la incluye significa que debe lanzarse una única vez durante el arranque, y si se muere no debe lanzarse de nuevo. En cambio, aquellos servicios que no incluyen oneshot se vuelven a lanzar si mueren. El motivo de explicar esto es que lo que vamos a hacer es buscar un servicio de tipo oneshot, y reemplazar su binario por un script que primero llame al binario original, y luego haga lo que a nosotros nos interese. En mi caso tuve más suerte, y encontré este servicio:

service flash_recovery /system/etc/install-recovery.sh
    class main
    oneshot

Por el nombre, tiene pinta de ser lo que se lanza cuando se borran las preferencias y se quiere dejar el sistema como recién comprado. Si vamos a /system/etc, vemos que no existe el fichero install-recovery.sh, supongo que porque sólo se creará cuando se quieran borrar los datos; pero eso significa que nosotros podemos meter en su lugar un ejecutable o un script y éste se ejecutará como root cada vez que arranque el sistema. Y además, al estar en /system podemos editarlo a nuestro antojo ¡Justo lo que estábamos buscando!

Lanzamiento condicional

Ahora que ya tenemos donde meter nuestro código, la primera idea que se nos viene a la cabeza es montar un sistema Debian en, por ejemplo, /system/debian, escribir un script que monte una copia de proc, sys y dev, y meterlo directamente en /system/etc/install-recovery.sh. Esto tiene varios problemas:

  • Las unidades en formato Ext2/3/4 no se montan automáticamente desde Android, por lo que tendríamos que hacerlo a mano.
  • Además, si hay varias unidades USB, tendríamos que ver cual es la correcta de todas ellas
  • Según como lo hagamos podemos perder por completo el acceso a Android.
  • Tendremos el sistema hardcoded, grabado en piedra, y para cambiar cualquier cosa tendremos que volver a entrar en la flash de nuestro dispositivo.

La opción por la que me he decantado es la de disponer de un disco duro externo con el sistema Debian, e incluir en él un pequeño fichero ejecutable con un nombre específico. De esa manera el único cambio que hay que hacer en nuestro sistema Android es añadir un pequeño programa que se lance durante el inicio y que compruebe todas las unidades USB en busca de dicho ejecutable, y si lo encuentra, que lo lance. De esa manera podemos meter ahí toda la magia y reducir al mínimo los cambios a realizar en nuestro dispositivo Android.

Lo primero que hice fue crear un pequeño programa que comprobase constantemente si aparecía un nuevo dispositivo USB; en caso de que así fuese, intentaría montarlo como una partición EXT4 en un punto concreto y ejecutar un fichero runlinux.sh en él. Al principio lo hice todo con comandos shell, pero el resultado era bastante chapucero porque lo que hacía era comprobar de manera periódica (cada cinco segundos) si había dispositivos. Además, cada comprobación implicaba ejecutar varios grep, sed y otros comandos, lo que aumentaba el consumo de CPU. Aunque una vez lanzado el sistema Debian esta comprobación se detenía, sí seguía en marcha mientras se estuviese trabajando en Android. Por eso al final lo reescribí en C y utilizando inotify, de manera que no hace falta realizar una espera activa, sino que el programa sólo se despierta cuando se inserta un nuevo dispositivo. El código de este programa se puede bajar desde aquí. Para compilarlo basta con utilizar el comando

arm-linux-androideabi-gcc launch_debian.c -o launch_debian

El compilador arm-linux-androideabi está disponible en Debian y derivadas en el paquete gcc-arm-linux-androideabi. A continuación hay que copiar el binario a /system/bin en nuestro dispositivo Android, y añadir el siguiente script en /system/etc/install-recovery.sh:

#!/system/bin/sh

/system/bin/launch_debian &

Este script lo que hace es lanzar launch_debian en segundo plano, y éste se quedará esperando a que se conecte un nuevo dispositivo USB con alguna partición EXT4. En caso de que eso ocurra, montará dicha partición (con las opciones noatime y nodiratime; para discos duros normales no es necesario, pero es interesante para unidades flash) e intentará ejecutar el fichero runlinux.sh, pasándole como primer parámetro la ruta donde está montada la partición (por defecto /system/debian), y como segundo parámetro el dispositivo que se ha montado (por ejemplo, sdb2). Una vez que termine de ejecutarse correctamente dicho fichero, launch_debian morirá. Pero si falla la ejecución o no existe dicho fichero, el programa seguirá probando con el resto de dispositivos.

El motivo de poner un script intermedio en lugar de meter el ejecutable directamente en /system/etc/install-recovery.sh es porque así, si por cualquier motivo el sistema Android sobreescribe dicho fichero, puedo recuperarlo fácilmente desde una consola, en lugar de necesitar transferir un binario.

Generando el sistema de ficheros

Ahora que ya tenemos todo listo en el sistema Android queda generar el entorno Debian, que será el que ejecutemos, así como un script que lo lance mediante una llamada a chroot.

Vamos a empezar con el entorno Debian. Para ello formatearemos un disco en EXT3 o en EXT4 y, desde un sistema Debian o derivado (Ubuntu, por ejemplo) bajamos el paquete debootstrap. A continuación montamos manualmente el disco donde queremos generar el sistema (porque, por defecto, los escritorios lo montan con las opciones nodev y noexec, y debootstrap se niega a trabajar en esas condiciones), y desde una línea de comandos teclearemos (asumiendo que el disco donde queremos instalar el sistema base está en /mnt):

sudo debootstrap --arch=armhf --foreign --variant=minbase wheezy /mnt http://ftp.us.debian.org/debian

En este caso estamos instalando Debian wheezy, que es la rama testing a la hora de escribir este artículo. En el futuro puede ser necesario cambiar a otra rama, según se desee.

Una vez ha terminado de instalarse, editamos el fichero etc/resolv.conf para añadir un DNS por defecto. En mi caso he utilizado los de google, pero se puede utilizar cualquiera. Mi fichero quedó así:

# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
#nameserver 127.0.1.1
nameserver 8.8.8.8
nameserver 8.8.4.4

Ahora desmontamos el disco y lo conectamos a nuestro sistema Android. Luego ejecutamos ./adb shell para entrar en él, y procederemos a montar la unidad donde instalamos el sistema en, por ejemplo, /mnt/usb_storage. Una vez hecho esto, y tras asegurarnos de que tenemos conexión a internet desde nuestro dispositivo Android, ejecutamos los siguientes comandos (también en nuestro dispositivo Android mediante adb shell) para entrar en el entorno chroot y terminar la instalación del sistema base:

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH
export HOME=/root
export LD_LIBRARY_PATH=
mount -o bind /proc /mnt/usb_storage/proc
mount -o bind /sys /mnt/usb_storage/sys
mount -o bind /dev /mnt/usb_storage/dev
mount -o bind /dev/pts /mnt/usb_storage/dev/pts
busybox chroot /mnt/usb_storage /bin/bash -l
debootstrap/debootstrap --second-stage

Con esto terminamos la instalación del sistema base, pero ahora toca configurarlo. En primer lugar vamos a configurar APT para poder bajar paquetes y demás. Para ello salimos del entorno chroot, desmontamos el disco, lo montamos en nuestro ordenador y editamos el fichero /etc/apt/sources.list para añadir las siguientes líneas:

deb http://ftp.de.debian.org/debian wheezy main contrib non-free
deb http://ftp.de.debian.org/debian wheezy-updates main contrib non-free
deb http://security.debian.org/ wheezy/updates main contrib non-free

Ahora podemos volver a montar el disco en nuestro sistema Android y volver a lanzar el entorno chroot. Vamos a instalar ahora las herramientas básicas que nos faltan para terminar de configurar el sistema con autoarranque y poder dedicarnos a jugar con él. Para ello vamos a instalar el servicio de gestión de redes, para tener acceso a internet completo, el servidor SSH para poder gestionar todo de manera remota, ifconfig, ping, el cliente DHCP y un editor de textos (normalmente uso nano pero da problemas desde adb; por eso uso vim de manera temporal) para editar los últimos ficheros necesarios para que todo funcione automáticamente. Para ello, una vez que estamos de nuevo dentro de la jaula chroot (¡¡¡no olvidarse de los exports!!!), ejecutamos los siguientes comandos:

apt-get update
apt-get dist-upgrade -y
apt-get install ifupdown openssh-server net-tools vim iputils-ping isc-dhcp-client

El siguiente paso es crear el fichero runlinux.sh, que será ejecutado desde nuestro entorno Android por launch_debian. Este script preparará el entorno para lanzar una sesión chroot, y debe contener las siguientes líneas:

#!/system/bin/sh

# stop the Android system
stop
sleep 1
# stop the daemons to ensure that
# they don't disturb the debian system
# (can't kill them because INIT would
#  relaunch them)
# Also allows to send them to SWAP
busybox killall -SIGSTOP netd
busybox killall -SIGSTOP vold
busybox killall -SIGSTOP displayd
busybox killall -SIGSTOP ueventd
busybox killall -SIGSTOP debuggerd
busybox killall -SIGSTOP rild
busybox killall -SIGSTOP drmserver
busybox killall -SIGSTOP mediaserver
busybox killall -SIGSTOP installd
busybox killall -SIGSTOP servicemanager
# mount proc, sys, dev and dev/pts
mount -o bind /proc $1/proc
mount -o bind /sys $1/sys
mount -o bind /dev $1/dev
mount -o bind /dev/pts $1/dev/pts
export HOME=/
export LD_LIBRARY_PATH=
export PATH=/sbin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
# set the Framebuffer devices where standard apps expect them
cp -a /dev/graphics/* /dev
# launch our Debian system
/system/bin/busybox chroot $1 /bin/system.sh

Lo primero que hace este script es detener el entorno Android, de manera que liberamos la memoria consumida por éste. A continuación envía una señal SIGSTOP a diversos procesos de Android que siguen en marcha, como el gestor de red. Esto es necesario porque, si no, interferiría con las herramientas de Debian. Por otro lado, como ya expliqué antes, no podemos matarlos porque el sistema init los lanzaría de nuevo.

El siguiente paso consiste en montar proc, sys, dev y dev/pts en el sistema Debian. Vemos que utiliza el primer parámetro, pues launch_debian pasa ahí la ruta donde se montó la partición.

A continuación inicializamos las variables de entorno. Vemos que borramos LD_LIBRARY_PATH. Esto es así porque algunos Android la utilizan para añadir otras rutas con bibliotecas, pero en nuestro caso nos interferiría.

Luego se copian todos los ficheros de dispositivo situados en /dev/graphics a /dev. Esto es así porque en Android los dispositivos framebuffer están en esa ruta alternativa, por lo que tenemos que copiarlos a donde las aplicaciones de Linux esperan encontrarlos.

Por último ejecutamos nuestro chroot. Cabe indicar que es necesario poner la ruta completa de busybox porque hemos sobreescrito la variable de entorno PATH. Vemos que lanzamos el script /bin/system.sh. Este script será quien lance todo lo que queramos lanzar en nuestro sistema Debian (servidor X, demonios…). En el caso actual lo que hice fue poner en él las siguientes líneas:

#!/bin/bash

service networking restart
service ssh restart

Con esto inicializo la red (necesario si el disco estaba enchufado al encender el dispositivo Android, porque launch_debian lo detectará tan rápido que no dará tiempo a que arranque Android y configure la red), y lanzo el servidor ssh, que me permitirá entrar de manera remota. Para que estos dos comandos funcionen, sin embargo, es imprescindible configurar algunas cosas primero:

  • Para que ssh funcione, lo primero que es necesario es ponerle clave al usuario root. Para ello utilizamos el comando passwd. Una vez hecho esto editamos /etc/ssh/sshd_config para ajustar la configuración.

Con todo esto ya tenemos el sistema base configurado y listo. Salimos de nuestra jaula chroot, ejecutamos sync por si acaso, y reiniciamos nuestro dispositivo Android. Si todo va bien, el entorno Android no debería ni siquiera arrancar, y deberíamos poder entrar mediante ssh.

El siguiente paso será añadir BitTorrent, servidor FTP y algún cliente multimedia para utilizarlo como equipo de salón, pero eso lo dejo para futuras entradas.

XCB y Cairo

Actualizado: Por fin he terminado la primera versión usable de TabletWM y TabletLauncher. Con ellos y el driver para la pantalla táctil he conseguido el gran objetivo de poder utilizar aplicaciones GNU/Linux nativas en una tablet sin teclado ni ratón.

Crear TabletWM fue, sin duda, la parte más compleja, porque supuso trabajar con un API completamente nuevo, como es XCB. Tenía claro que no quería utilizar XLib, así que me lié la manta a la cabeza, pero partiendo de un gestor de ventanas muy, pero que muy básico hecho por Cinolt a finales de 2011.

Lo primero que aprendí es que el protocolo X es lento, por lo que en un dispositivo como una tablet, con capacidades limitadas, no es recomendable pedir al servidor X las propiedades de una ventana cada vez que se quiere mostrar en pantalla o cambiar sus dimensiones o posición. Sin embargo no parece existir una manera de que el servidor avise al gestor de ventanas de que una propiedad ha cambiado, y dado que estas propiedades contienen información importante necesaria a la hora de mapear una ventana, la mejor solución que encontré fue hacer una caché de datos que relleno en el momento de hacer visible una ventana (en ese momento las propiedades ya tienen que estar definidas), aunque antes puedo haber almacenado otras operaciones que se hayan hecho en la ventana, como un cambio de tamaño o de posición.

Otra cosa que aprendí es que hay dos grupos de eventos de interés para un gestor de ventanas: request y notification. El primero se emite cuando una aplicación pide realizar alguna operación sobre una ventana suya, y la segunda si el resultado de dicha operación cambia algo realmente en la ventana. Así, si una aplicación pide cambiar el tamaño de una ventana, ejecutará un comando xcb_configure_window(). Si no existe un gestor de ventanas, dicho comando se ejecuta normalmente; pero si lo hay, se genera un evento xcb_configure_request en el gestor. Este puede decidir si ignorarlo, ejecutarlo tal cual o modificarlo. Si decide ejecutarlo y el tamaño de la ventana cambia realmente, se emite un evento xcb_configure_notification, que llegará a la aplicación.

Y aquí encontré otro de los problemas que tuve: mi intención es que las ventanas estén maximizadas siempre, por lo que cada vez que recibía un evento xcb_configure_request, emitía un comando xcb_configure_window() con el tamaño máximo de la pantalla. El problema ocurría con algunas ventanas que pedían ser más grandes que la pantalla, por ejemplo la ventana de configuración de Firefox. En este caso, se mostraba la ventana en la primera pestaña, y la pantalla es lo suficientemente grande como para contener todo; al recibir el evento, TabletWM modifica los valores pedidos por el navegador, ajustándolos al tamaño de la pantalla, y cambia las dimensiones de la ventana. Se emite el evento xcb_configure_notification y todo sigue perfectamente.

Pero cuando se escoge la pestaña de Seguridad, el nuevo contenido no entra en la pantalla, por lo que Firefox pide un tamaño más grande. El gestor de ventanas recibe el evento y cambia los parámetros por las dimensiones de la pantalla (que es el mismo tamaño que ya tiene la ventana), y da la orden de cambiar el tamaño. Pero como es el mismo, nunca se genera un evento xcb_configure_notification, pero Firefox espera que le llegue. El resultado es que la ventana queda sin refrescar.

La solución que apliqué fue ejecutar primero el comando de cambio de tamaño tal cual llega, y ejecutar luego un segundo comando con el tamaño que el gestor de ventanas desea. De esa manera la aplicación siempre recibe el evento que espera, y todos felices.

Otra curiosidad fue a la hora de leer ciertas cadenas de texto en las propiedades. En algunos casos, una propiedad (como por ejemplo _XKB_RULES_NAMES) contiene varias cadenas separadas por NUL (o sea, un byte a cero). Si pedimos el tamaño de ésta en la cookie (cookie->length) nos devolverá la longitud de la primera cadena exclusivamente. Si queremos obtener todas tenemos que utilizar xcb_get_property_value_length(), que sí nos dará el tamaño total. Por si fuera poco, no se garantiza que al final de la cadena haya un NUL, por lo que debemos tener en cuenta el tamaño para no pasarnos, en lugar de usar strcpy().

El teclado fue otro de los problemas serios que tuve: el método normal de entrada por teclado de X es relativamente rudimentario, por lo que hoy en día se utiliza la extensión XKB. Por desgracia esta extensión no está portada tal cual a XCB, sino que existe un proyecto separado, XCB-COMMON, que contiene aquellas partes no dependientes de un servidor X. Por si fuera poco, no puedo emular la pulsación de un carácter concreto, sino sólo la pulsación de una tecla en sí. Esto significa que los códigos que tengo que enviar a las X dependerán del idioma del teclado escogido por el usuario, además de tener que emular pulsaciones múltiples como la tecla mayúsculas, AltGR, etc.

Encima, esta biblioteca está orientada a obtener un carácter a partir de una pulsación, cuando yo necesitaba el proceso inverso. Aunque parte del trabajo lo hace (dado un carácter devuelve el código de la tecla), no devuelve el código de los modificadores necesarios. Así, si pido la arroba, me devuelve el código de la tecla ‘2’, pero no me dice si necesito pulsar también las mayúsculas (caso del teclado norteamericano) o AtlGR (caso del teclado español). Esto me obligó a realizar una pequeña chapuza, consistente en probar todas las posibles combinaciones de teclas normales y teclas modificadoras (shift, control, etc) para ver qué caracteres produce cada una.

Y para rizar el rizo, no es posible siempre obtener la combinación correcta, por lo que en algunos casos no queda más remedio que redefinir un código de tecla que no se utilice con un carácter determinado (por ejemplo, la letra Ñ). Esto hay que hacerlo con las funciones de entrada clásicas, no con las de XKB, por lo que el resultado final es algo caótico, pero funciona, y permite definir un teclado cualquiera en pantalla a partir de los caracteres que se quieren mostrar en lugar de las teclas físicas que se deben pulsar.

Pasando a Cairo, decidí utilizar esta biblioteca para los elementos gráficos (como el teclado o la ventana de apagado) en lugar de las funciones de X por varios motivos:

  • Cairo ofrece antialiasing, tanto en fuentes como en primitivas gráficas.
  • Cairo es una biblioteca moderna con un API sencillo y potente.
  • Ya conozco Cairo, pero no las funciones gráficas de X.
  • No supone una carga extra porque el lanzador de aplicaciones utiliza GTK, que por debajo trabaja también con Cairo.

Trabajar con Cairo directamente desde XCB no es muy complicado. Para ello, primero se debe crear una ventana, y a continuación una superficie Cairo con cairo_xcb_surface_create(). La principal complicación es conocer el visual_type de la ventana, para poder pasárselo a la función. Esto lo podemos saber mediante el siguiente código (la variable conn es el handler de la conexión con el servidor X):

    xcb_screen_t *scr=xcb_setup_roots_iterator(xcb_get_setup(conn)).data;
    xcb_visualtype_t *visual_type = NULL;
    xcb_depth_iterator_t depth_iter;

    for (depth_iter = xcb_screen_allowed_depths_iterator (scr); depth_iter.rem; xcb_depth_next (&depth_iter)) {
        xcb_visualtype_iterator_t visual_iter;

        visual_iter = xcb_depth_visuals_iterator (depth_iter.data);
        for (; visual_iter.rem; xcb_visualtype_next (&visual_iter)) {
            if (scr->root_visual == visual_iter.data->visual_id) {
                visual_type = visual_iter.data;
                break;
            }
        }
    }

Luego no tenemos más que engancharnos al evento expose de la ventana (XCB_EVENT_MASK_EXPOSURE) y, cada vez que se reciba, generar un contexto Cairo y repintar la ventana. Un detalle extra a tener en cuenta es que cada vez que se redimensione la ventana es necesario llamar a cairo_xcb_surface_set_size(), para cambiar el tamaño de la superficie Cairo.

Ah, y no olvidar hacer un xcb_flush() al terminar de pintar algo, para que efectivamente lo pinte en la ventana.

Pantalla tactil

Por fin he conseguido acceder a la pantalla táctil. Esta tablet lleva un controlador GSL1680, para el cual no hay casi documentación. Sin embargo, pude encontrar un presunto driver, y de él sacar mucha información.

Después de muchas pruebas fui capaz de leer las coordenadas, pero sólo si arrancaba Linux en caliente (o sea, si arrancaba primero Android y luego reiniciaba). Si apagaba a machete la tablet y encendía directamente en Linux, no funcionaba.

Tras investigar, descubrí que el motivo es que el chip necesita que le envíen el firmware para funcionar. Si arrancaba Android, éste le metía dicho firmware, y al reiniciar todo seguía funcionando. Pero si apagaba la tablet, al quedarse sin corriente, el firmware se borraba, y es necesario volver a cargarlo.

Encima, parece que el firmware es específico para cada modelo. Afortunadamente, en el caso de mi tablet éste estaba disponible en un fichero de texto, en /system/etc.

Los detalles específicos de programación los he escrito en la página sobre el GSL1680 en linux-sunxi, y como no me apetece repetirlo todo, me limitaré a poner un enlace.

Wifi, sonido y mas

Actualizado. Después de mucho pelearme con mi tablet, finalmente he conseguido que funcionen casi todos los dispositivos. También he pulido el proceso de instalación de Linux; de hecho hay bastantes cosas que cambiar del artículo anterior, así que vamos allá.

El fichero SCRIPT.BIN

En primer lugar quería disponer del fichero script.bin original de mi tablet, y no de uno creado para otro dispositivo, porque obviamente podía haber algún parámetro (velocidad de memoria, puertos de dispositivos, etc) que tuviese un valor diferente, evitando que funcionase algún dispositivo. De hecho, una vez que conseguí utilizarlo, el sonido funcionó automáticamente (antes no detectaba ninguna tarjeta de sonido).

El proceso para obtener dicho fichero bin no fue sencillo: dicho fichero está en la flash interna de la tablet, pero por desgracia no tenía acceso a ella desde Linux (no detecta dicho dispositivo) ni desde Android (lo detecta, obviamente, pero no tengo permisos de acceso por no tener la tablet rooteada. Además, al ser la partición de arranque, no se monta una vez lanzado Android, por lo que tampoco está disponible desde éste). Tras muchos intentos encontré la solución: utilizar el ADB (Android Debug Bridge). Esto permite acceder a un dispositivo Android desde un PC. Para ello, una vez instalado el kit de desarrollo de Android, basta conectar la tablet al PC a través de un puerto USB, ir al directorio sdk/platform-tools y ejecutar:

    sudo ./adb start-server
    ./adb shell
    cat /dev/block/mmcblk0p1 > punto_de_montaje_de_tarjeta_SD/particion1.img
    exit
    sudo ./adb kill-server

El primer comando lanzará como root el servidor ADB, que se conectará a nuestro dispositivo, y el segundo lanzará una shell como root en él. El tercero, ejecutado ya desde la shell del dispositivo, volcará la primera partición de la flash en el fichero particion1.img de la tarjeta SD. Por último, salimos de la shell y desactivamos el servidor ADB.

En ocasiones adb no encuentra nuestro dispositivo. Si tecleando ./adb devices no aparece ningún dispositivo detectado, es posible que adb no tenga a nuestro fabricante en su lista. Para añadirlo manualmente hacemos un lsusb, y vemos el identificador de nuestra tablet. Digamos que es AAAA:BBBB (dos números en hexadecimal de cuatro cifras cada uno). Sólo tenemos que editar ~/.android/adb_usb.ini y añadir en él 0xAAAA, reiniciar el servidor adb, y listo.

Ahora sólo tenemos que llevar el fichero particion1.img a un PC con linux y montar dicha partición, por ejemplo en /mnt:

    sudo mount -o loop particion1.img /mnt

Ahí encontraremos el fichero script.bin, que podremos convertir en un fichero .fex mediante la utilidad bin2fex, que encontraremos en el repositorio sunxi-tools. De hecho es necesario hacerlo para poder hacer un par de modificaciones, sin las que nuestro Linux no arrancaría desde la tarjeta SD. En concreto éstos son los cambios:

@@ -12,7 +12,7 @@
 pll4_freq = 960
 pll6_freq = 720
 power_start = 0
-storage_type = 2
+storage_type = 1

 [pm_para]
 standby_mode = 0
@@ -445,7 +445,7 @@
 sdc_wp =

 [mmc2_para]
-sdc_used = 1
+sdc_used = 0
 sdc_detmode = 3
 bus_width = 8
 sdc_cmd = port:PC06<3><1><3><default>
@@ -510,7 +510,7 @@
 usb_id_gpio =
 usb_det_vbus_gpio =
 usb_drv_vbus_gpio = port:power203<1><0><default><0>
-usb_host_init_state = 0
+usb_host_init_state = 1

 [port_pm]
 restrict_1a = 0

El primero cambia el tipo de almacenamiento de Flash a SD. El segundo especifica que no se active la memoria Flash interna (si se activa, no arranca el sistema). Por último, el tercero activa desde el principio el segundo USB, necesario para que funcione la red WiFi.

Existe una lista con todas las opciones de un fichero FEX, útil si se quieren hacer más cambios manuales.

Una vez hechos estos cambios ya podemos generar el fichero script.bin final, siguiendo el método indicado en el artículo anterior.

El kernel

En el núcleo también hay que hacer algunos cambios. Para empezar, hay que utilizar la versión 3.4, en lugar de la 3.0, porque es la que tiene soporte para la tarjeta de red:

    git clone -b sunxi-3.4 https://github.com/linux-sunxi/linux-sunxi linux-3.4-sunxi
    cd linux-3.4-sunxi

De todas formas, el chip que lleva esta tablet es el Realtek RTL8188etv. Aunque en teoría es compatible con el driver para la familia RTL8188, en la práctica hay que añadir una línea para que reconozca el identificador USB de este chip concreto. Aunque ya envié el parche a la lista de correo, lo añado aquí porque no se cuanto tardarán en añadirlo:

diff --git a/drivers/net/wireless/rtl8188eu/os_dep/linux/usb_intf.c b/drivers/net/wireless/rtl8188eu/os_dep/linux/usb_intf.c
index 5f4390a..df9d488 100644
--- a/drivers/net/wireless/rtl8188eu/os_dep/linux/usb_intf.c
+++ b/drivers/net/wireless/rtl8188eu/os_dep/linux/usb_intf.c
@@ -185,7 +185,8 @@ static struct usb_device_id rtw_usb_id_tbl[] ={
 #endif
 #ifdef CONFIG_RTL8188E
        /*=== Realtek demoboard ===*/           
        {USB_DEVICE(USB_VENDER_ID_REALTEK, 0x8179)},//Default ID        
+       {USB_DEVICE(USB_VENDER_ID_REALTEK, 0x0179)},//rtl8188etv ID (szenio 1207 tablet)
 #endif
        {}      /* Terminating entry */
 };

Una vez aplicado este cambio de una única línea, podemos ya proceder a configurar el núcleo. Para ello partimos de la configuración genérica para el A13:

    make ARCH=arm a13_defconfig
    make ARCH=arm menuconfig

Estas son las opciones que he modificado:

    General Setup
        POSIX message queues=Y
    Device Drivers
        Network Device Support
            Ethernet (10 or 100Mbit)=N
            Wireless LAN
                Realtek 8188E USB WiFi=M
        Character Devices
            Non-standard serial port support=N
            Serial drivers
                8250/16550 and compatible serial support=N
        Graphics support
            Console Display driver support
                Map the console to the primary display device=Y
            Bootup logo=Y
    Security Options
        Enable the securityfs filesystem=N

Una vez hecho, podemos ya proceder a compilarlo, pero con el compilador que utilice la gnueabihf (que utiliza coma flotante por hardware, en lugar de la gnueabi, como hacía en el artículo anterior). Para ello primero necesitamos instalar esta versión del compilador:

    sudo apt-get install gcc-4.7-arm-linux-gnueabihf gcc-arm-linux-gnueabihf

y ya podemos compilar el núcleo en sí:

    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- uImage
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=out modules
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=out modules_install

Los ficheros boot.cmd y boot.scr

El último cambio respecto al artículo anterior está en el fichero boot.cmd (y su fichero hijo boot.scr). Este fichero contiene los parámetros de arranque del núcleo, y es básicamente un script para U-Boot. En nuestro caso, crearemos el fichero boot.cmd en la partición VFAT de nuestra tarjeta SD con este contenido:

    setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait panic=10 ${extra}
    fatload mmc 0 0x43000000 script.bin
    fatload mmc 0 0x48000000 uImage
    bootm 0x48000000

y lo compilaremos con:

    mkimage -C none -A arm -T script -d boot.cmd boot.scr

Y listo. Si en algún momento queremos cambiar las opciones de arranque de nuestro núcleo, sólo tendremos que editar el fichero boot.cmd, añadirlas en la primera línea y recompilarlo. Existe una página con todas las opciones del núcleo específicas de sunXi.

Encender y apagar el sistema

De momento no está activa la gestión de energía, por lo que si se hace un halt o un shutdown -h now, el sistema parecerá apagarse, pero en realidad seguirá encendido. En estos casos, para apagarlo «de verdad» es necesario mantener pulsado el botón de encendido durante seis o más segundos.

Arrancando Linux en la tablet

Actualizado: Hoy me puse a intentar meter Linux en la tablet de la que hablé en la anterior entrada. Y lo he conseguido, he aquí la prueba:

tablet_linux

Dado que esta tablet está equipada con un Allwinner A13, necesitamos un kernel y un cargador adecuados. Lo interesante de este chip es que no hace falta tocar la flash interna, sino que basta con ponerlos en una tarjeta SD, y arrancará desde ésta directamente. Y si luego queremos volver al sistema original, basta con retirar la tarjeta y reiniciar.

El núcleo y el cargador los podemos obtener del repositorio GIT linux-sunxi, orientado precisamente hacia este tipo de SoCs. Las instrucciones básicas para compilar y generar todo las saqué de la página de Olimex, y las he adaptado a este modelo.

Supondré que tienes una distribución basada en Debian. Lo primero que hay que hacer es instalar los paquetes de compilación cruzada y otras bibliotecas necesarias:

    sudo apt-get install gcc-4.7-arm-linux-gnueabi gcc-4.7-arm-linux-gnueabi-base gcc-arm-linux-gnueabi libncurses5-dev uboot-mkimage build-essential git libusb-dev libusb-1.0-0-dev

Ahora bajamos el u-boot modificado con:

    git clone -b sunxi https://github.com/linux-sunxi/u-boot-sunxi

Entramos en el directorio u-boot-sunxi y compilamos el código:

    cd u-boot-sunxi
    make CROSS_COMPILE=arm-linux-gnueabi- a13-olinuxino
    ls -l u-boot.bin spl/sunxi-spl.bin
    cd ..

Ahora que tenemos compilado el cargador, vamos con el núcleo. Nos lo bajamos desde el repositorio, preparamos una configuración base y entramos en modo configuración:

    git clone -b sunxi-3.0 https://github.com/linux-sunxi/linux-sunxi
    cd linux-sunxi
    make ARCH=arm a13_defconfig
    make ARCH=arm menuconfig

tablet_config

Entre las opciones que he tocado están las siguientes:

  • General Setup
    • POSIX message queues -> Y
  • System Type
    • Emulate SWP/SWPB instructions -> N
  • Device Drivers
    • Network Device Support
      • Ethernet (10 or 100Mbit) -> N
    • Character Devices
      • Non-standard serial port support -> N
      • Serial drivers
        • 8250/16550 and compatible serial support -> N
    • Console Display driver support
      • Map the console to the primary display device -> Y
    • Bootup logo -> Y
  • Security Options
    • Enable the securityfs filesystem -> N

 

Una vez configurado, procedemos a compilar el núcleo:

    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- uImage
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- INSTALL_MOD_PATH=out modules
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- INSTALL_MOD_PATH=out modules_install

Ahora llega el momento de generar la tarjeta SD con el sistema básico. Para ello, primero necesitamos dividirla en, al menos, dos particiones, una en formato VFAT (donde irá el núcleo) y otra en formato EXT2/3/4 con el sistema en sí. Así que arrancamos fdisk y empezamos. Es importante que la primera partición empiece, al menos, en el sector 2048, para dejar sitio para el arranque.

Una vez formateadas ambas particiones, descargamos un sistema Debian base, lo descomprimimos en la partición EXT2/3/4 (que supondremos que está montada en /media/particion2) y copiamos los módulos (asumiendo, también, que la versión de núcleo que tenemos es la 3.0.76):

    wget http://rcn-ee.net/deb/rootfs/wheezy/debian-7.1-console-armhf-2013-07-22.tar.xz
    tar xvf debian-7.1-console-armhf-2013-07-22.tar.xz
    sudo tar -C /media/particion2 -xvf debian-7.1-console-armhf-2013-07-22/armhf-rootfs-debian-wheezy.tar
    cd ../linux-sunxi
    sudo cp -a linux-allwinner/out/lib/modules/3.0.76/ /media/particion2/lib/modules/

Es importante descomprimirlo como root para que conserve los nombres de usuario correctos.

Ahora bajamos los ficheros de definición de placas y las utilidades, y las compilamos:

    git clone https://github.com/linux-sunxi/sunxi-boards
    git clone https://github.com/linux-sunxi/sunxi-tools
    cd sunxi-tools
    make

Ahora vamos a generar el fichero script.bin, necesario para configurar el núcleo durante el arranque. Para ello utilizaremos las utilidades que acabamos de compilar y el fichero de configuración a13_mid.fex, situado en el repo sunxi-boards, por ser el más parecido a nuestra tablet:

    ./fex2bin ../sunxi-boards/sys_config/a13/a13_mid.fex script.bin

El siguiente paso consiste en copiar el fichero uImage y script.bin en la partición VFAT de nuestra tarjeta SD:

    cp script.bin /media/particion1
    cd ../linux-sunxi
    cp arch/arm/boot/uImage /media/particion1

Por último sólo queda instalar u-Boot en la tarjeta, para lo cual hacemos (asumiendo que la tarjeta está en /dev/sdX):

    cd ../u-boot-sunxi
    sudo dd if=spl/sunxi-spl.bin of=/dev/sdX bs=1024 seek=8
    sudo dd if=u-boot.bin of=/dev/sdX bs=1024 seek=32

Y con esto ya está. Sólo queda meter la tarjeta SD en la tablet, encenderla, y conectar un teclado USB.

El próximo paso será intentar activar la WiFi, algo que me está dando bastantes quebraderos de cabeza.

Hackeando cuelgatazas

Mi cocina es algo pequeña, así que cualquier cosa que ayude a tener más espacio siempre es bienvenida. Una idea que tuve hace tiempo fue aprovechar que los muebles tienen una moldura por debajo, que deja un hueco de unos tres centímetros, para añadir unos cuelgatazas disimulados en la base, y así tenerlas más a mano y aprovechar el hueco liberado para otras cosas.

Por desgracia, no encontré en ninguna parte un modelo que me convenciese. El más parecido fue éste de CASA, diseñado para enganchar en la balda de un estante. Pese a todo, es más un cuelgapocillos, porque el metal de la zona curva, que es la que sostiene el peso, se dobla demasiado si pongo una taza de desayuno (pulsar en las imágenes para verlas a tamaño completo):

IMG_20130616_192040

Sin embargo, si sujetaba el colgador de manera adecuada todavía podría utilizarlo, así que me puse manos a la obra. Lo primero fue buscar un trozo de tablero (en mi caso, de 15×75 cm) donde fijar los colgadores, y marcar en él los puntos en donde se cruzan las barras que lo forman:

IMG_20130616_164828 IMG_20130616_164847 IMG_20130616_165024

El siguiente paso fue fijar cada uno de los tres cuelgatazas al tablero mediante unas alcayatas, montadas de tal manera que impidan que se mueva en cualquier sentido:

IMG_20130616_165846 IMG_20130616_165904

A continuación había que fijarlo al mueble, debajo de la rejilla de los platos. Para ello puse dos pequeñas escuadras en uno de los laterales; en el otro, como hay un mueble «normal», decidí atornillarlo directamente a su base:

IMG_20130616_170102

Una vez colocado el tablero sobre las escuadras y atornillado a éstas, hay que recortar lo que sobresalga de los tornillos, obviamente:

IMG_20130616_171132

¡Y listo! Ya puedo colgar hasta 18 tazas cómodamente.

IMG_20130616_192103

Funciones asíncronas en Vala

Actualizado. Acabo de modificar un poco el código de Cronopete para que haga uso de las funciones asíncronas de Vala. El motivo es eliminar, en la medida de lo posible, el uso de threads.

Las funciones asíncronas de Vala son una ayuda muy interesante a la hora de realizar tareas que requieren mucho tiempo, a la vez que se quiere evitar que la interfaz se bloquee. La solución más inmediata en estos casos consiste en crear un thread y realizar en él todo el proceso, pero en ocasiones es una solución muy compleja, pues se necesita intercambiar datos entre éste y el thread principal, o sincronizar ambos. La solución está en las funciones asíncronas. Estas son funciones que pueden interrumpirse y reanudarse a voluntad en cualquier punto, lo que permite realizar una forma de multitarea cooperativa de manera sencilla.

Imaginemos que definimos una función o método asíncronos así:

public async int AsyncFnc(int p1, int p2, out int p3, out String p4) {
    // código de la función asíncrona (también puede ser un método de una clase)
    [...]
}

Para llamar a esta función, lo haremos de esta manera:

AsyncFnc.begin(p1, p2, callback_finalizacion);

Vemos que sólo incluimos los parámetros de entrada, pero no los de salida. Por otro lado, tampoco recogemos el valor devuelto. Por último, tenemos un misterioso callback_finalizacion. Este parámetro es opcional, y sólo hay que añadirlo cuando se desea ejecutar algún código cuando la función asíncrona finalice. Es en dicha función donde recogeremos los valores de salida y cualquier excepción que se produjese durante la ejecución de la función asíncrona. Dicho callback es de tipo GLib.AsyncReadyCallback, y su formato es el siguiente:

public delegate void AsyncReadyCallback (Object? source_object, AsyncResult res)

Sin embargo, Vala nos ofrece algunas ventajas, como son las funciones anónimas y los closures, que nos dan como ventaja el poder acceder desde el callback a variables externas a éstas. Así pues, utilizando una función anónima podemos llamar a nuestra función asíncrona así:

AsyncFnc.begin(p1, p2, (obj,res) => {

    int retval;
    retval=AsyncFnc.end(res, out p3, out p4);

    // resto del código del callback
    [...]
});

De esta manera, cuando la función asíncrona llegue al final y retorne, se ejecutará el código de callback contenido en el closure. Vemos también que llamamos a AsyncFnc.end; esta es, precisamente, la manera que tenemos de obtener el valor devuelto por la función asíncrona, así como los parámetros de salida. También es esta llamada la que debemos rodear con un try/catch() si queremos capturar cualquier excepción lanzada desde dentro de la función asíncrona.

Así pues, ya sabemos llamar a una función asíncrona, y sabemos como recibir sus resultados. ¿Pero como hacemos para que ceda el control y devolvérselo? No olvidemos que NO se tratan de varios threads paralelos, sino de un único hilo de ejecución.

La respuesta está en yield y .callback(). La primera es una llamada que, al realizarla dentro de una función asíncrona, devuelve el control a la función llamante. La segunda es un método de la función asíncrona (igual que .begin y .end; podemos comparar una función asíncrona con un objeto que ofrece esos tres métodos públicos) que lo que hace es continuar la ejecución de dicha función justo después del último yield ejecutado. Un detalle interesante es que las llamadas a yield se pueden situar absolutamente en cualquier parte de una función asíncrona, incluso dentro de un bucle, lo que nos permite, de una manera sencilla, devolver el control en cada iteración para que se vayan ejecutando otras partes del código, y recuperarlo para la siguiente iteración.

Ahora que ya tenemos la imagen completa podemos ver como se ejecuta el código: cuando una función normal llama a una función asíncrona, le cede el control y ésta comienza a ejecutarse hasta alcanzar el primer yield. En ese punto la llamada retorna a la función normal, y sigue ejecutándose el código situado a continuación de la llamada. Cuando, en algún punto posterior, se llame al .callback() de la función asíncrona, el control pasará al código situado justo después de dicho primer yield, y seguirá ejecutándose hasta que encuentre otro yield, momento en que el control de la ejecución pasará al código situado justo después de la llamada a .callback(). Este juego de ping-pong continuará hasta que la función asíncrona llegue al final y retorne. En ese momento se ejecutará el código del callback y continuará la ejecución después de la última llamada a .callback().

Un ejemplo gráfico

Para entender mejor este proceso, veámoslo en forma gráfica. Por cuestiones de claridad lo dividiremos en dos imágenes: en la primera veremos el flujo desde que se inicia la función asíncrona hasta que está a punto de acabar, y en la segunda desde que se ejecuta el return al final.

En la imagen anterior vemos la primera parte del ciclo de vida de una función asíncrona. Cada función está representada por un rectángulo, y dentro de ella, el flujo de ejecución natural es de arriba a abajo.

Desde el Main Loop de la aplicación se llama a un callback (por ejemplo, porque el usuario ha pulsado un botón en la interfaz gráfica), llamado Function en este ejemplo, y en éste se llama a nuestra función asíncrona (llamada AsyncFnc en el ejemplo), que comienza a ejecutarse. En el momento en el que se alcanza el primer yield, el control vuelve al callback que la lanzó, que sigue su ejecución hasta que, en un punto, éste llama a AsyncFnc.callback(). En ese momento el flujo de ejecución vuelve a la función asíncrona, que continúa justo en la instrucción que sigue al primer yield. Un poco después se llega a un segundo yield, que devuelve el control al callback original.

Cuando el callback termina, se devuelve el control al Main Loop, como siempre. Pero la función asíncrona aún no ha terminado. Vemos entonces que en dos puntos determinados se llama a AsyncFnc.callback(). Estas llamadas no son automáticas, sino que tuvieron que ser preprogramadas (por ejemplo con un temporizador, o con una llamada a Idle.add(AsyncFnc.callback) para que se ejecuten en un momento en que no hay eventos por despachar). Cada vez que se llama a dicha función, la ejecución de la función asíncrona continúa justo después del último yield, hasta que encuentra otro, o bien hasta que finaliza la función.

En el siguiente esquema, las flechas en rojo muestran el flujo de ejecución cuando la función asíncrona llega al final: en ese momento se ejecuta el callback registrado cuando se lanzó la función (si es que existe, claro; si no se definió, se retorna directamente), y cuando se termina éste, se retorna al punto de llamada.

Llamar a una función asíncrona desde otra función asíncrona

A la hora de llamar a una función asíncrona desde otra función asíncrona, la primera idea que nos puede venir a la cabeza es hacer

// ATENCION, ESTE CODIGO NO FUNCIONARA CORRECTAMENTE

async void AsyncFnc1() {
    int retval;

    [...]
    AsyncFnc2( (ob,res) => {
        retval=AsyncFnc2.end();
        AsyncFnc1.callback();
    });
    yield;
    [...]
}

Aparentemente, este código lanzaría AsyncFnc2 y suspendería la ejecución de AsyncFnc1, pero de manera que, al terminarse la ejecución de AsyncFnc2, continuaría la ejecución de AsyncFnc1.

Por desgracia, ese código es probable que no funcione correctamente, como mínimo en un caso concreto: si, por la razón que sea, la función AsyncFnc2 finaliza sin llamar a yield al menos una vez, se llamará antes a AsyncFnc1.callback() que al yield, por lo que el código fallará. Una solución a este problema sería utilizar Idle.add(AsyncFnc1.callback) en su lugar, pero sólo funcionará si tenemos un Main Loop en nuestro programa, pues será éste quien llame al callback después de que la función asíncrona retorne tras ejecutar el yield.

Afortunadamente, los diseñadores de Vala eran conscientes de que es muy común llamar a una función asíncrona desde otra, por lo que simplificaron notablemente este caso concreto. Así, para llamar a AsyncFnc2 desde AsyncFnc1, de manera que ésta última continúe su ejecución cuando acabe la otra, sólo hay que hacer

async void AsyncFnc1() {
    int retval;

    [...]
    retval=yield AsyncFnc2();
    [...]
}

Otra ventaja es que, en este caso, no necesitamos llamar a AsyncFnc2.end() para obtener los resultados, porque se hace directamente.

El flujo de ejecución sería así:

Vemos que desde el Main Loop llamamos a nuestra primera función asíncrona, AsyncFnc1, y después de un trozo de código llamamos, mediante yield, a nuestra segunda función asíncrona, AsyncFnc2. En ésta ejecutamos algo de código y llegamos a un yield. Sin embargo, éste no retorna a AsyncFnc1, sino a la llamada anterior. Esto es así porque AsyncFnc1 lo que quiere es dormir hasta que AsyncFnc2 termine.

También vemos que para reanudar la ejecución es preciso llamar a AsyncFnc2.callback(). Esto debe tenerse muy en cuenta: en este instante es incorrecto llamar a AsyncFnc1.callback(). Por eso es fundamental que cada función asíncrona se encargue de programar su llamada para recuperar el control, y no delegarlo a funciones externas, pues es muy fácil cometer errores.

Cuando AsyncFnc2 llega al final, se cede el control al código de AsyncFnc1, que sigue ejecutándose, y al terminar, se llamaría a la función de callback de finalización (si la hubiera), y seguiría la ejecución justo a continuación de la llamada a AsyncFnc2.callback() (que, en este caso, simplemente seguiría con la ejecución del Main Loop).

Casos de uso de funciones asíncronas

El primer caso de uso de las funciones asíncronas es para llamar a otras funciones asíncronas. Esto, que puede parecer una perogrullada, toma sentido en cuanto descubrimos las funciones asíncronas de Gio. Un ejemplo es el de File.copy_async: este método de la clase File copia un fichero de manera asíncrona. De no existir, tendríamos que crear un hilo y ejecutar en él la función síncrona de copia, porque de no hacerlo así, si el fichero fuese muy grande la interfaz gráfica de nuestro programa se quedaría congelada durante la operación.

Hay dos maneras de llamar a estas funciones asíncronas: la primera es la clásica, incluyendo un callback (bien como puntero a función, bien como función anónima o closure) que se llame cuando termine la operación; la segunda, más cómoda en según que casos, es llamarla mediante yield desde una función asíncrona de Vala. En este segundo caso hay que saltarse el parámetro del callback.

El segundo caso de uso es para llamar a servicios externos mediante DBus. De esta manera podemos aprovechar el tiempo entre el envío de la petición y la llegada de la respuesta. Para ello, basta con añadir async en la definición de la interfaz DBus. Un ejemplo de como definir la interfaz DBus para formatear un disco, mediante el demonio UDisks:

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
	public abstract async void FilesystemCreate(string type, string[] options) throws IOError;
}

Por desgracia, en este caso concreto no podemos aprovechar la funcionalidad de la llamada mediante yield, debido a que existe un timeout en las llamadas a métodos DBus de, aproximadamente, 25 segundos, y se necesita más tiempo para formatear algunos soportes. Se supone que se puede ajustar este timeout mediante un modificador de Vala, pero después de consultar en la lista de correo y de buscar en internet, no he conseguido que funcione. Si alguna alma caritativa nos puede iluminar…

El siguiente caso de uso es cuando se quiere realizar una tarea larga y que consuma CPU, pero no se quieren usar threads o lanzar un proceso paralelo, sino ejecutarlo en los momentos en los que el proceso actual esté inactivo (en el caso de una aplicación gráfica, cuando no hay eventos que procesar en la cola). La solución consiste en crear una función asíncrona que realice la tarea, insertando llamadas a yield de manera regular. Para que la función se despierte cuando la cola está vacía, usamos Idle.add() para llamar al método .callback() de nuestra función asíncrona, así:

public async void AsyncFnc() {

    // Inicialización de variables y demás
    [...]

    // Comienza la tarea
    while(cond==true) {

        // Realizamos una iteración de la tarea
        [....]

        // Avisamos al Main Loop que queremos que nos despierte
        // cuando no haya eventos pendientes
        Idle.add(AsyncFnc.callback);

        // Y devolvemos el control al búcle principal o Main Loop
        yield;
    }

    // La tarea ha finalizado
    // Limpiamos todo
    [...]
}

Por supuesto, podemos utilizar también un temporizador en lugar de Idle, o cualquier otro sistema que decidamos.

Un detalle interesante que se puede ver en este ejemplo es que la llamada a yield puede ir dentro de un bucle while o for sin ningún problema.

Por último, también son útiles para sincronizar threads. El truco en este caso está en almacenar un puntero al método de callback de la función asíncrona que queremos sincronizar, lanzar el thread, y al final de éste, añadir dicho callback a la cola de mensajes. El esquema es éste:

async void AsyncFunc() {

    // Aquí almacenamos el método .callback() de nuestra función asíncrona
    SourceFunc callback = AsyncFunc.callback;

    // Creamos la función para el thread. La hacemos anónima (closure) para que tenga acceso a la
    // variable local callback.
    ThreadFunc<void*> run = () => {
        // Código del thread. Aquí hacemos las operaciones deseadas
        [...]

        // Aquí ya hemos terminado las operaciones, así que
        // añadimos el método .callback() a la cola para que se llame inmediatamente
        Idle.add((owned) callback);
        return null;
    };

    // Lanzamos el thread
    Thread.create<void*>(run, false);

    // Y esperamos a que se llame a nuestra función callback
    // que será cuando termine el thread
    yield;
}

En la documentación oficial se encuentran tres ejemplos de funciones asíncronas, muy útiles para entender aún mejor esta útil característica de Vala. En concreto aparece un ejemplo del método de sincronización de threads, donde se puede ver mucho mejor la sintaxis concreta.

Entrada de línea en mi C2

Cuando, hace años, me compré mi Citroen C2, me conformé con el autorradio que traía de serie: un modelo relativamente sencillo de la marca Clarion. Este equipo trae radio y lector de CDs, además de permitir la conexión de un cargador de CDs externo. Por desgracia, sólo admite CDs normales, nada de MP3, por lo que es bastante limitado.

En varias ocasiones sopesé el cambiarlo por otro modelo, pero eso me obligaba a renunciar a los controles de volumen en el volante, o bien a comprar un (caro) cable adaptador. Encima, la entrada de cargador externo de CDs tienen que activarla en un concesionario para que funcione, por lo que conectar un reproductor externo de manera directa parecía una misión imposible.

Durante un tiempo utilicé un emisor de FM, pero el resultado era bastante pobre, en buena medida porque no conseguía encontrar una frecuencia que se mantuviese libre desde mi casa hasta el trabajo.

Todo parecía perdido cuando, hace unos días, encontré una entrada de Panito69 en el foro del Citroen C2 donde explica como añadir una entrada de línea a esta radio. Básicamente hay que conectarla en los puntos que se ven en esta foto que el propio Panito69 publica en dicha entrada (pincha en ella para agrandarla):

Una vez hecho, basta con meter un CD de música, y el sonido de lo que metamos por la entrada sonará en los altavoces. Por alguna misteriosa razón, al conectar cualquier dispositivo, el lector de CDs quedará silenciado.

En mi coche monté directamente un cable con un jack macho al extremo, para no tener que andar con más cables y demás. Este es el resultado con el móvil conectado, y con el cable recogido:

Sin embargo, el depender de un CD supone algunos problemillas: para empezar, el sonido de éste se escucha muy levemente entre canciones; por otro lado, cuando el CD se acaba y la radio vuelve al principio, el sonido se corta durante dos segundos, el tiempo que el cabezal tarda en volver al principio.

La solución que encontré fue crear un CD en blanco con la duración máxima posible. Con él, puedo escuchar el máximo tiempo posible la música, y con el mínimo de interferencias. Aquí podéis bajar el fichero WAV de 79 minutos que utilicé, listo para grabarse en un CD-R cualquiera. Ojo, que aunque el fichero RAR ocupa sólo 57 KBytes, el WAV resultante ocupa algo más de 800 MBytes. Para hacerlo, basta con escoger la opción Crear un CD de audio en cualquier programa de grabación de CDs, como Nero, Brasero… y añadir ese WAV.

Ahora sólo espero que los herederos de John Cage no me demanden por plagio 😀

Cronopete 2.1.0: pensando en todos

Acabo de lanzar la versión 2.1.0 de Cronopete, que añade una pequeña modificación en el efecto zoom para intentar que vaya mejor en ordenadores lentos.

Para entender el problema, lo primero es explicar como realicé el efecto de zoom. Al principio quería usar clutter, porque se supone que permite hacer cosas como que una ventana se mueva en tres dimensiones y demás, que es justo lo que pretendía. Por desgracia, para poder meter dentro de un actor de clutter un elemento de GTK (en este caso el navegador de ficheros), tenía que trabajar con GTK3, porque en GTK2 no está soportado.

Probé a usar ventanas auténticas, pero el cambio de tamaño para hacer el efecto era lentísimo, por lo que no resultaba. Después de muchas pruebas, me lié la manta a la cabeza e intenté hacerlo de una forma radicalmente diferente. La solución que encontré era tan «simple» como usar la función gdk_pixbuf_get_from_drawable para hacer una captura de la ventana, y usar el pixmap resultante para hacer el efecto de scroll mediante Cairo. Al ser una imagen estática en lugar de un conjunto de widgets, el redimensionado debería ser rápido, incluso haciéndolo por software.

Pero las cosas, por desgracia, no son tan sencillas en la práctica. Cada vez que hacía una captura, me salía un pixmap negro, o con la ventana tal y como estaba antes de cualquier cambio que quisiese reflejar. El motivo es que el redibujado de una ventana en GTK es asíncrono: yo puedo dar la orden de mostrar algo, de cambiar un texto, o de añadir más elementos a una lista, pero hasta que no retorne al bucle principal de GTK, no se llevará a cabo esa acción.

La primera solución que encontré fue aprovechar el temporizador que uso para las animaciones para sacar la captura: así, tras dar la orden de cambiar de carpeta, por ejemplo, marcaba un flag que indicaba que había que hacer una nueva captura, activaba el temporizador y salía del callback, volviendo al bucle principal. En ese momento GTK repintaba toda la ventana, que quedaba lista para ser capturada en cuanto venciese el temporizador.

Esto funcionaba en mi ordenador sin problemas, así que publiqué la versión 2.0.0; pero más tarde, al tener la oportunidad de probarlo en un equipo lento, me encontré con que no iba bien, pues capturaba antes de tiempo. Era obvio que GTK le daba más prioridad a los temporizadores que al redibujado de ventanas, por lo que, en ese equipo, no le daba tiempo a repintar todo.

Pregunté en los foros si GTK emitía alguna señal cuando terminaba de redibujar una ventana, pero nadie respondió, así que decidí probar yo mismo todas las señales que emite un widget, a ver cual me podía ser útil. Probé con damage-event, composited-changed, expose-event, realize, screen-changed y show, antes de encontrar la que parecía la solución a mis problemas: map-event. Pero aunque en el código de ejemplo que hice funcionaba, cuando la probé en el código de cronópete también falló.

Hasta que, al final, revisando otras funciones, encontré la solución en las funciones idle. Estas se ejecutan cuando no queda ninguna tarea por hacer en el bucle principal, así que la solución fue tan sencilla como añadir una función idle que haga la captura: ésta no será llamada hasta que la cola esté vacía, lo que será cuando se hayan procesado todos los eventos de repintado de widgets dentro de la ventana que queremos capturar, o sea, cuando ésta ya esté completamente lista.

Barras clásicas

No me convencen las nuevas barras de desplazamiento de Ubuntu. Sin duda son geniales cuando tienes una pantalla pequeña, y de hecho pienso conservarlas en mi portátil, pero en un monitor de 24 pulgadas no tienen tanto sentido, y la verdad es que a veces es molesto no acertar para hacer scroll.

Afortunadamente, encontré en el blog de Andrés Gomez una entrada explicando como volver a las barras clásicas, truco que voy a publicar levemente modificado porque tal y como aparece ahí no funciona.

Abrimos un terminal y tecleamos (pulsando Enter después de cada línea, y teniendo en cuenta que nos pedirá la clave):

sudo su
echo "export LIBOVERLAY_SCROLLBAR=0" > /etc/X11/Xsession.d/80overlayscrollbars
exit

Reiniciamos, y listo, ya tenemos nuestras barras clásicas.

Aunque, como comenta Germán Poo en los comentarios, otra opción consiste en desinstalar el paquete overlay-scrollbar.