Archivo de la categoría: tutoriales

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.

DBus en Vala

Actualizado Estoy trabajando en un nuevo proyecto, escrito íntegramente en Vala, y la verdad es que cuantas más cosas aprendo sobre él, más me gusta.

Lo último que descubrí fue como trabajar con DBus en Vala, y resulta que es extremadamente sencillo y elegante, aunque tuve que investigar bastante hasta encontrar como hacer algunas cosas, pues los ejemplos que vienen en la página tratan de como trabajar con un servidor hecho por uno mismo, cuando yo necesitaba acceder a un servicio del sistema.

Así pues, y sin más dilación, veamos como se puede usar DBus para obtener una lista de los discos duros conectados por USB al ordenador, y como formatearlos.

Lo primero que vemos es que toda esa información la gestiona el demonio UDisk, el cual exporta una serie de funciones y objetos a través de DBus para que cualquier programa pueda acceder a ellos. Si echamos un vistazo a la documentación de la API, vemos que se exporta el método EnumerateDevices, el cual nos devuelve un array de objetos.

¿Y como accedemos a este método del objeto UDisk desde Vala? Pues con este código:

[DBus (name = "org.freedesktop.UDisks")]
interface UDisk_if : GLib.Object {
    public abstract ObjectPath[] EnumerateDevices() throws IOError;
}

UDisk_if udisk = Bus.get_proxy_sync<UDisk_if> (BusType.SYSTEM, "org.freedesktop.UDisks","/org/freedesktop/UDisks");
var retval = udisk.EnumerateDevices();

Aquí vemos que primero definimos una interfaz con todos los métodos a los que vamos a querer acceder (en este caso sólo uno), sacando de la documentación los parámetros y demás, y le añadimos antes una cabecera con el nombre del objeto. Luego, creamos un proxy síncrono, conectándonos a través del bus del sistema (BusType.SYSTEM), al servidor UDisk (org.freedesktop.UDisks) y pedimos acceso al objeto deseado (/org/freedesktop/UDisks). Finalmente, llamamos al método deseado, almacenando el resultado en una nueva variable.

Es importante recalcar que cualquier método que se defina puede emitir, siempre, una excepción de tipo IOError, por lo que siempre se debe añadir al final de la definición la coletilla throws IOError. De no hacerlo se producirá un error de compilación. También comentar que dado que esta función sólo devuelve un parámetro, la he escrito en formato resultado metodo(parametros); sin embargo, también es perfectamente válida la sintaxis void metodo (parametros,…, out resultado). Esta segunda forma es obligatoria cuando un método devuelve más de un resultado. Con esa sintaxis, la interfaz tendría esta forma:

[DBus (name = "org.freedesktop.UDisks")]
interface UDisk_if : GLib.Object {
    public abstract void EnumerateDevices(out ObjectPath[] path) throws IOError;
}

En retval tenemos ahora un array de objetos ObjectPath, cada uno de tipo Device, representando una unidad extraíble. ¿Y ahora, qué? Pues ahora vamos a imprimir el punto de montaje de cada uno de ellos. Si miramos la documentación del objeto Device, vemos que dicha información se almacena como una propiedad, así que definimos nuestra interfaz así:

[DBus (name = "org.freedesktop.UDisks.Device")]
interface Device_if : GLib.Object {
    public abstract string IdLabel { owned get; }
    public abstract string[] DeviceMountPaths { owned get; }

    public abstract void FilesystemUnmount(string[] options) throws IOError;
    public abstract void FilesystemCreate(string type, string[] options) throws IOError;
    public abstract void PartitionModify (string type, string label, string[] options) throws IOError;
    public abstract void FilesystemMount(string type, string[] options, out string mount_path) throws IOError;
}

Ponemos { owned get; } en lugar de {owned get; set; } porque los campos son de sólo lectura. También he añadido los cuatro métodos necesarios para poder formatear una partición, aunque en el ejemplo no los voy a utilizar.

Ahora llega el momento del código que accede a los datos, que es éste:

Device_if device2;
foreach (ObjectPath o in retval) {
    device2 = Bus.get_proxy_sync<Device_if> (BusType.SYSTEM, "org.freedesktop.UDisks",o);
    GLib.stdout.printf("Disco %s montado en:n",device2.IdLabel);
    foreach (string s in device2.DeviceMountPaths) {
        GLib.stdout.printf("    %sn",s);
    }
}

Así, primero recorremos la lista de objetos que obtuvimos con la anterior llamada (en retval), y creamos un proxy síncrono para cada uno de ellos, contra el servidor de UDisk (él es quien nos dio la lista de objetos, por lo que estos tienen que estar en él). Luego imprimimos la etiqueta simplemente leyendo la propiedad del objeto, y por último imprimimos todos los puntos de montaje (una misma partición puede estar montada en varios sitios a la vez).

Actualización: Uno de los problemas que tuve fue que la llamada a FilesystemCreate (que formatea la unidad especificada) tarda varios segundos en ejecutarse, lo que hacía que la GUI se quedase colgada. La primera solución que encontré fue ejecutarla en un thread aparte, y usar espera activa para detectar cuando terminó, pero era muy poco elegante. La solución definitiva consiste en realizar llamadas asíncronas a DBus, explicadas en el tutorial de Vala. La idea consiste en añadir la palabra reservada async en la definición de la función en la clase, con lo que la llamada a DBus será no-bloqueante, y definir un callback anónimo.

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

  // Llamamos a la función, añadiéndo como último parámetro
  // una función anónima que se ejecutará al terminar la
  // ejecución
  device2.FilesystemCreate.begin(format,options, (obj,res) => {
    try {
      // La llamada a .end recoge los valores devueltos (de haberlos)
      // y dispara cualquier excepción que se haya producido
      device2.FilesystemCreate.end(res);
      return;
    } catch (IOError e) {
      // Captura de excepciones (en este caso, fallo durante el formateo)
    }
  });

Una vez que hemos hecho la llamada, podemos volver al bucle principal de GTK o llamar a la función run() de un cuadro de diálogo, y éste seguirá funcionando en paralelo a la ejecución de nuestra llamada a DBus.