viernes, 30 de agosto de 2013

Migración de Proyectos de Anjuta 1.x a 2.x

(english version)

Anjuta es una herramienta que durante años me ha sido de gran utilidad. Si bien la herramienta fundamental para el diseño de interfaces que utilizan Gtk+ es Glade, el hecho de que Anjuta provea cierto nivel de integración entre los distintos elementos necesarios para el desarrollo (glade, el editor scintilla) facilita aún más las cosas.

Dicho esto, tenemos claro entonces que nada impide desarrollar los distintos componentes de nuestro programa por separado: podemos mantener una sesión de glade para ir modificando las ventanas de nuestra interfaz; paralelamente utilizamos algún editor de texto para ir modificando nuestro código. Con una terminal adicional podemos ejecutar y depurar las compilaciones parciales de nuestro programa. Anjuta provee todas estas operaciones de manera integrada.

Para facilitar aún más esa integración, Anjuta 1.x proveía un conjunto funciones que facilitaban algunas tareas (lookup_widget(), p. e.). En versiones más recientes de Anjuta ese soporte dejó de existir, porque se prioriza la utilización directa de la librería libglade. Esta librería permite enlazar el programa y la pantalla desarrollada con glade en tiempo de ejecución. En Anjuta 1.x, la relación entre la interfaz y el código se establece durante la compilación, mediante un conjunto de funciones adicionales provistas por el mismo Anjuta (definidas en el archivo support.c).



Familiarizándonos con la interfaz de Anjuta 2.x

Una vez que instalamos la nueva versión de Anjuta, saltan a la vista algunas diferencias con la versión anterior. La más significativa quizás sea que la combinación de teclas "Alt+G" no arranca Glade como antes. A no desesperarse, la forma de interactuar con Glade ahora es mucho más natural: simplemente hay que dar doble click al archivo que define la interfaz de nuestro programa.


Otra diferencia notoria, es que Anjuta dispone ahora de varios perfiles de compilación predefinidos. Esto no es otra cosa que la combinación de flags del compilador necesarias para generar un proyecto optimizado para su ejecución (-O3), optimizado para debug (-g), etc. Al momento de construir nuestro proyecto, elegimos el tipo de perfil que deseamos.

Abriendo un proyecto "viejo" con Anjuta 2.x


De entre todas las opciones que disponemos para abrir un proyecto viejo, personalmente prefiero la opción "Archivo -> Nuevo -> 5. Proyecto desde fuentes existentes".



Una vez seleccionada esa opción, buscamos el directorio que contiene el proyecto "viejo":

El nombre sugerido para el nuevo proyecto, es el mismo que el directorio seleccionado. Podemos modificarlo si así lo deseamos. A continuación, nos consulta por el tipo de backend que queremos para el proyecto que estamos importando. En mi caso, elijo "Backend de autotools".



Finalmente Anjuta genera el nuevo proyecto: 



puede notarse que a la izquierda figuran todos los archivos heredados del proyecto viejo.

Si intentamos la combinación de teclas "Mayus+F7", nos pregunta por el tipo de perfil de compilación que queremos utilizar:



Elegido el tipo de perfil, intenta construir el binario. No lo logra. Intentaremos superar esta situación.



Acomodando el proyecto recién importado

Como comenté anteriormente, muchas funciones de soporte que proveía Anjuta 1.x, ya no son necesarias. Básicamente se puede prescindir de los archivos: "suport.c", "suport.h", "interface.c" e "interface.h". Se pueden eliminar de varias formas. En mi caso, el directorio importado forma parte de un directorio de trabajo de un proyecto almacenado mediante "subversion", así que utilizo la siguiente opción:



de esta forma, eliminamos todos los archivos mencionados. Además, se deben eliminar las referencias a estos archivos que ya no estarán; en particular hay que editar el archivo "src/Makefile.am", para eliminar las apariciones de los archivos arriba mencionados. Luego debemos posicionarnos desde una consola en el directorio, y ejecutamos el comando "./autogen.sh".

Este último comando no solo regenera el archivo "Makefile", para que no tenga más en cuenta los archivos eliminados. Es probable que muchos utilitarios empleados por nuestro proyecto viejo hayan cambiado desde la última vez que lo modificamos, no solo Anjuta y Glade ("automake", "autotools", etc). "./autogen.sh" se encarga de acomodar las versiones de estos programas en el proyecto importado.

Con un poco de suerte, el intento de generación del binario empezará a fallar por el contenido de los archivos fuente, y no por el armado del proyecto en sí. Para empezar, los programas fuente todavía harán referencia a los archivos de cabecera eliminados ("suport.h" e "interface.h"). Podemos eliminar esas líneas en nuestro código.

Por supuesto, no alcanza solo con eliminar estas líneas. Todavía quedan muchas referencias a las funciones incluídas en esos archivos eliminados que deberemos reemplazar. El siguiente punto enumera algunas de ellas.



Modificando el código fuente para una compilación exitosa

El primer intento de compilar nuestro código nos muestra una pantalla como la que sigue:




vemos una seguidilla de mensajes de alarma, que nos avisa sobre ciertas funciones que dejaron de estar definidas para el código, a saber: "add_pixmap_directory", "lookup_widget", "create_window", etc. Deberemos reemplazarlas.

En mi caso, he abusado bastante de la función "lookup_widget" en mis proyectos. Básicamente esta función obtenía el widget a partir del nombre del mismo , y para ello barría todo el árbol de dependencias del widget del programa hasta que lo encontraba, en un mecanismo poco eficiente. En cambio, ahora utilizaré la librería "libglade", que analiza el contenido del archivo ".glade" en tiempo de ejecución y realiza las ligaduras necesarias entre los nombres de los widgets, las funciones de callback asociadas, etc. La forma de utilizar libglade, es mediante el uso de GtkBuilder, que parsea el archivo "xml" en tiempo de ejecución, crea los widgets, configura sus propiedades, y asocia las funciones de callback que están definidas en nuestro código. Además, se crea una estructura con punteros a todos aquellos widgets que deban ser accedidos en las funciones de callback (que no sean aquellos widgets cuya acción está asociada a la función), los cuales se inicializan al momento de crear el builder, y de esa manera pueden ser accedidos en cualquier momento.

La primer corrección que realizaré, es la reformulación de la función genéricamente conocida como "create_window()" ("create_ventana_principal()", en mi ejemplo), que desapareció al eliminarse el archivo "interface.c":

La linea:

ventana_principal = create_window ();

Se convierte en:

item = g_slice_new(ItemStruct);

if (create_window(item) == FALSE) {
      logmsg("Error al crear pantalla");
      return(-1);
}


El argumento de la función "create_window()", es un puntero a la estructura que agrupa los widgets que deban ser accedidos en las funciones de callback. A priori no conozco el contenido de la estructura, sino que la iré armando a medida que avance en el proceso de recompilación exitosa del programa. Por lo pronto, incluye al menos un elemento: el puntero al widget "ventana_principal". Un buen lugar para definir esta estructura es el archivo "callbacks.h":

typedef struct {
   GtkWidget       *ventana_principal;
} ItemStruct;

ItemStruct *item;

Podemos definir la función "create_window()" en el mismo archivo "main.c":

gboolean create_window (ItemStruct *item)
{
   GtkBuilder *builder;
   GError* error = NULL;

   builder = gtk_builder_new ();
   if (!gtk_builder_add_from_file (builder, UI_FILE, &error)) {
      g_warning ("No puedo crear builder a partir del archivo: %s", error->message);
      g_error_free (error);
      return FALSE;
   }

   /* Asocia el widget cuyo nombre es "ventana_principal", con el elemento de la estructura que lo va a representar */
   item->ventana_principal = GTK_WIDGET (gtk_builder_get_object (builder, "ventana_principal"));

   /* Asocia las señales */
   gtk_builder_connect_signals (builder, item);

   g_object_unref (builder);

   return TRUE;
}

Esta función también es el lugar propicio para definir cualquier cuestión relacionada a la definición estética de nuestra interfaz, que no esté ya contemplada por el archivo ".xml" de glade, o en el archivo "rc" que estemos usando. UI_FILE es un macro que define el path al archivo ".xml" de glade.

Al comentar o eliminar aquellas lineas donde utilicé la función "
lookup_widget()", se generan los siguientes mensajes en los sucesivos intentos de compilación:

main.c:XXX: error: 'my_variable' undeclared (first use in this function)

La variable "my_variable", que es un puntero a un widget cuyo valor se completaba con el uso de la función "lookup_widget()", ahora debe ser parte de la estructura mencionada anteriormente, y debe inicializarse durante la invocación de la función "create_window()". La estructura quedaría conformada de esta manera:


typedef struct {
   GtkWidget       *ventana_principal;
   GtkWidget       *my_variable;
} ItemStruct;

ItemStruct *item;



Y en la función "create_window()" agregamos la asociación a este nuevo puntero de widget:

      ...

   /* Asocia el widget cuyo nombre es "ventana_principal", con el elemento de la estructura que lo va a representar */
   item->ventana_principal = GTK_WIDGET (gtk_builder_get_object (builder, "ventana_principal"));



   item->my_variable = GTK_WIDGET (gtk_builder_get_object (builder, "my_variable"));


      ...



En la linea donde el compilador acusó el mensaje de error, se debe reemplazar la aparición de "my_variable" por el elemento de la estructura. Ejemplo:

gtk_widget_modify_font(GTK_WIDGET(my_variable), pfd);

se debe reemplazar por:

gtk_widget_modify_font(GTK_WIDGET(item->my_variable), pfd);

De a poco vamos agregando todos los widgets que deben ser accedidos, y que por ende deberán ser incluidos en la estructura, de tal manera que no se obtengan más esos mensajes del compilador. Este tipo de reemplazos deberemos realizarlos tanto en "main.c" como en "callbacks.c".

En relación a la función "add_pixmap_directory()", en principio podemos evitar su uso, por cuanto la relación a los archivos de imagen está definida en el archivo ".glade". De cualquier manera, si necesitamos referirnos a alguna imagen en particular, podemos utilizar la función "glade_xml_get_widget()".


Con un poco de suerte, nuestro compilador dejará de informarnos que existen errores o faltantes en nuestro código. Igualmente, si queremos ejecutar el código, probablemente aparezcan errores o bien se vea horrible. Esto es porque debemos corregir el archivo ".glade" aún.





Adaptando el archivo de interfaz a la nueva versión de Glade

Existen algunas diferencias entre las versiones de glade 2 y 3, en relación a las marcas de xml que definen los distintos widgets. Aquí van algunas de ellas:


  • La marca <glade-interface> ahora es <interface>, y la marca de cierre pasa de ser </glade-interface> a </interface>
  • La marca <widget class= ahora es <object class=, y la marca de cierre pasa de ser </widget> a </object>
  • Además a los widgets tipo "GtkVBox" y "GtkVPane" hay que agregarles la propiedad <property name="orientation">vertical</property> para que los widgets no se vean apaisados.
  • Los widgets de tipo "GtkComboBox" que muestra una lista sencilla de texto presentan cambios en versiones más nuevas de GTK+. Si bien existe un widget "GtkComboBoxText" que se asemeja bastante al anterior, este no es reconocido por glade 3.12.1, por lo cual si queremos seguir utilizando glade, debemos utilizar el modelo de lista e integrarlo al GtkComboBox. Los pasos se detallan a continuación:
  1. Desde anjuta, se debe asociar el widget "GtkComboBox" a un modelo de ComboBox. Este modelo se define por separado, y en él se establecen cuántas columnas va a tener, y que tipo de datos va a incluir cada una. Cada columna se identifica con un nombre distintivo. Además se pueden precargar valores en el listado. En el caso de las listas sencillas de texto, la cantidad de columnas va a ser 1, de tipo "gchararray". Una vez definido el modelo de lista, con el botón derecho del mouse sobre el widget se debe acceder a la opción "Editar...", -> pestaña "jerarquía" -> "añadir" una nueva jerarquía donde se define el nombre, el tipo, y a que atributo estará relacionado (en principio, el atributo elegido es el correspondiente a la única columna definida previamente en el modelo). 
  2. En lo correspondiente al código del programa, se deben realizar los siguientes reemplazos:
          // Bucle para completar el ComboBox
          while ( ... ) {
              gtk_combo_box_append_text(GTK_COMBO_BOX(comboBox), item);
          }

por


          GtkListStore *listaModelo;
          GtkTreeIter iter;


          // Bucle para completar el ComboBox
          while ( ... ) {
              gtk_list_store_append (listaModelo, &iter);
              gtk_list_store_set (listaModelo, &iter, 0, item, -1);
          }
          // se asocia la lista al ComboBox 
          gtk_combo_box_set_model(comboBox, GTK_TREE_MODEL(listaModelo));
          // "elementoLista" es el widget creado cuando se definió la jerarquía
          gtk_cell_layout_add_attribute (comboBox, elementoLista, "text", 0);

Aún puede aparecer un mensaje de error en tiempo de ejecución, del tipo "Could not find signal handler  ... ". Este mensaje indica que no se encuentra la definición de señales definidas y asociadas a algún widget (típicamente definidas en el archivo "callbacks.c"). Para evitar que se presente este error, se debe compilar el proyecto con el flag adicional "-export-dynamic". Esto se puede definir desde el menú "Construir -> Configurar el proyecto ... -> Opciones de configuración:", agregando 'CFLAGS=-export-dynamic'

Referencias:
http://www.micahcarrick.com/gnome-anjuta-programming-tutorial.html
http://www.micahcarrick.com/libglade-anjuta-tutorial.html
http://www.micahcarrick.com/gtk-glade-tutorial-part-1.html
http://www.micahcarrick.com/gtk-glade-tutorial-part-2.html

http://www.micahcarrick.com/gtk-glade-tutorial-part-3.html
http://www.micahcarrick.com/gtk-glade-tutorial-part-3.html
http://www.micahcarrick.com/gtk-glade-tutorial-part-4.html
http://old.nabble.com/please-help:-bug:-glade2-file-conversion-to-glade3-failed-td26467085.html
http://ubuntuforums.org/showthread.php?t=1377005

"Creating GtkComboBox using Glade":
http://www.youtube.com/watch?v=Z5_F-rW2cL8

"gtk_combo_box_append_text() problem":
http://permalink.gmane.org/gmane.comp.gnome.glade.user/3774




jueves, 28 de marzo de 2013

Agregando tareas propietarias a cámaras Ubiquiti

(english version)

Luego de utilizar cámaras IP Axis durante bastantes años (con resultados no tan satisfactorios en el largo plazo), comencé a considerar alternativas. Así es como pasaron por mis manos distintas marcas y modelos que no alcanzaban a cubrir las mínimas espectativas que pretendía cubrir para dejar definitivamente de lado las cámaras Axis. Parece una broma de mal gusto que la mayoría de estas cámaras cuyo firmware suele estar basado en Linux, poseen una interfaz demasiado orientada a navegadores como Internet Explorer, mientras que la inspección desde un navegador en Linux suele verse mal, con las pantallas deformadas, las opciones de menú inutilizables, entre otros defectos.

Eventualmente comencé a realizar algunas pruebas con una cámara Ubiquiti AirCam mini. Desde un principio parecía poseer las características necesarias para cubrir el uso pretendido. Además me interesaron algunas funcionalidades extras, tales como que posee un agente de SNMP, e inclusive posee un servidor de SSH, el cual permite ingresar a la cámara en modo consola.

Al momento de configurar el servicio de dns dinámico me encontré con un problema: aparentemente está preparado para funcionar solo con el servicio de dyndns.org, que lamentablemente dejó de poseer cuentas gratuitas. El manual de la cámara dice lo contrario, en cuanto a que afirma soportar una amplia gama de servicios de dns dinámico, pero lo cierto es que el archivo de log de la conexión invalida esta afirmación. Al configurar el servicio de dns dinámico con una cuenta de no-ip.com, el log de la cámara muestra que el cliente de dns dinámico sigue intentando conectarse al sitio http://members.dyndns.org. Indagando sobre el tema, encontré una forma de agregar tareas o servicios a la cámara sin necesidad de rehacer el firmware (aunque esta última también es una opción válida, ya que ubiquiti provee del código fuente del firmware).

Investigando el entorno

Accediendo por SSH a la cámara, nos encontramos con un sistema de archivos típico de una distribución Linux en un sistema embedido (la referencia de los binarios a busybox, por poner un ejemplo). Indagando un poco en el directorio /etc/rc.d, nos encontramos con el script rc.sysinit encargado de configurar parámetros del kernel de la cámara, y el script rc, encargado de levantar los distintos servicios que corren en ella.

Este último script hace uso de funciones definidas en el archivo /usr/etc/rc.d/rc.funcs, particularmente la función rc_start(), donde podemos encontrar referencias a scripts externos de nombre /etc/persistent/rc.prestart y /etc/persistent/rc.poststart. Tales como su nombre lo indica, estos scripts son ejecutados (si existen) en la etapa previa y posterior del arranque de los servicios, respectivamente. El directorio /etc/persistent permite almacenar otros scripts y archivos propios, y solo debemos acordarnos de grabar la configuración luego de realizado los cambios, para que las modificaciones no se pierdan en el siguiente reinicio de la cámara.

Entre los servicios soportados por la cámara que no están arrancado desde un comienzo, se encuentra el demonio crond, que permite ejecutar tareas de manera periódica. Vamos a aprovecharnos de ese servicio para ejecutar un script de manera periódica, que se fijará si el IP público de nuestra conexión se ha modificado, y en ese caso actualizará la información en los registros de no-ip.com.


Generando los archivos necesarios

Tal como se mencionó anteriormente, deberemos generar el archivo de nombre /etc/persistent/rc.poststart. El contenido de ese archivo es el siguiente:


#!/usr/bin/sh
mkdir /etc/crontabs
cp -a /etc/persistent/no-ip.crontab /etc/crontabs/root
echo "root" > /etc/crontabs/cron.update
crond


Este script crea el directorio /etc/crontabs (que es consultado por el demonio crond), copia un archivo (cuyo contenido se muestra en breve) a ese directorio con el nombre del usuario que correrá la tarea periódica (root en nuestro caso), y genera un archivo de nombre cron.update cuyo contenido es el nombre del usuario que ha visto modificada su entrada de cron (esto permite avisarle al demonio crond que relea los archivos de configuración). Como último paso, lanza el servicio de cron al ejecutar el demonio crond. El script rc.poststart debe tener permisos de ejecución.

El archivo /etc/persistent/no-ip.crontab posee una sola línea, y es la siguiente:

*/5 * * * * /etc/persistent/no-ip.client >/dev/null 2>&1

Esta línea le indica a crond, que ejecute el script /etc/persistent/no-ip.client (cuyo contenido se muestra en breve) cada 5 minutos, y que descarte la salida por pantalla cuando lo ejecute.

Por último, el script /etc/persistent/no-ip.client ejecutado periódicamente por crond, contiene lo siguiente:


#!/usr/bin/sh

DDNSUSER="UsuarioNOIP"
DDNSPASS="PasswordNOIP"
DDNSHOST="HostnameNOIP.no-ip.org"

IPADDRESS=`wget http://ipecho.net/plain -O - -q ; echo`
if [ "${IPADDRESS}" = "" ]; then
exit 1
fi

IPSAVED=""
if [ -f /tmp/ip.publico ]; then
IPSAVED=`cat /tmp/ip.publico`
fi

if [ "${IPADDRESS}" != "${IPSAVED}" ]; then
echo ${IPADDRESS} > /tmp/ip.publico
logger Se detecta IP publico ${IPADDRESS}
wget -q -O /dev/null "http://${DDNSUSER}:${DDNSPASS}@dynupdate.no-ip.com/nic/update?hostname=${DDNSHOST}&myip=${IPADDRESS}" true
fi


Este script posee definida tres variables:
DDNSUSER: usuario con que definimos la entrada del host en no-ip.com
DDNSPASS: contraseña del mismo
DDNSHOST: nombre del host definido en no-ip.com

El script obtiene el IP público de la conexión con la ayuda del sitio http://ipecho.net, y lo almacena en la variable IPADDRESS. Además compara este valor con la dirección IP que posee almacenada en el archivo /tmp/ip.publico donde almacenó el valor de la dirección pública en el último cambio (luego del arranque de la cámara, este archivo no existe por lo que la actualización en no-ip.com se ejecuta por lo menos una vez). Además genera una entrada en el log, informando del cambio de IP público detectado. Este script también debe poseer permisos de ejecución.

Una vez que generamos los archivos necesarios, podemos grabar estas modificaciones con el comando:

cfgmtd -w -p /etc/

Este comando graba las modificaciones a la memoria flash.

Luego podemos reiniciar el equipo (con el comando reboot, por ejemplo), y controlar si las modificaciones se mantienen.


Controlando los cambios

Si las modificaciones se hicieron sin errores, veremos que:


  1. Los archivos agregados en /etc/persistent se encuentran allí
  2. El proceso crond está corriendo.
  3. Aparecen mensajes de crond en el log del sistema, tales como:
Mar 28 14:27:19 192.168.1.20 crond[292]: time disparity of 586527 minutes detected
Mar 28 14:30:01 192.168.1.20 crond[292]: crond: USER root pid 314 cmd /etc/persistent/no-ip.client >/dev/null 2>&1
Mar 28 14:30:02 192.168.1.20 root: Se detecta IP publico 1.2.3.4
Mar 28 14:35:01 192.168.1.20 crond[292]: crond: USER root pid 323 cmd /etc/persistent/no-ip.client >/dev/null 2>&1

El primer mensaje es un aviso por parte de crond, respecto a que hubo un corrimiento importante de la hora. Eso se debe a que la cámara no posee reloj, y arranca con una fecha y hora errónea, hasta que sincroniza con los relojes de Internet vía ntp.

En la primera ejecución del script, vemos que la dirección IP es detectada correctamente, lo que implica que se produjo la actualización en los registros de no-ip. El valor de esta dirección IP debería estar correctamente almacenada en el archivo /tmp/ip.publico

Quedará para otra ocasión la revisión de los scripts de la página de gestión de la cámara, para detectar la causa por la cual pareciera que el único servicio de DNS dinámico que contempla es el de dyndns.org. De todas formas, si el aparente bug se debiera a una mala interpretación de mi parte, no deja de ser útil que se conozcan mecanismos que permiten la modificación o el agregado de funcionalidades a la cámara.

Referencias:
Adding FTP To The Ubiquiti AirCam Mini
Integrate No-IP