Como Hacer Una Actividad Sugar

Hacer Actividades compartidas

Introducción

Una de las cualidades distintivas de Sugar es que varias Actividades tienen la capacidad de ser utilizadas por más de una persona al mismo tiempo. Más y más computadoras están siendo utilizadas como medio de comunicación.Los más recientes juegos de computadora no sólo ponen al jugador en contra de una máquina; sino que construyen un mundo en el que los jugadores compiten entre si. Websites comoFacebookincrementan su popularidad porque dan a las personas la posibilidad de interactuar e incluso jugar juegos entre sí. Entonces, es natural que un software educativo apoye este tipo de interacciones.

Tengo una sobrina que es miembro entusiasta del sitio web "El Club del Pingüino"creado por Disney. Cuando le di Sugar on a Stick Blueberry como regalo de Navidad le mostré la vista Vecindario (Neighborhood en inglés) y le dije que Sugarpermitía que toda su computadora se comportara como el Club del Pingüino. Ellapensóque era una idea genial. Me gusta mencionarlo.

Ejecutar Sugar como más de un usuario

Antes de empezar a escribir unaporcióndecódigo es necesario pensar como se realizará la prueba de estas Actividades. En el caso de una Actividad compartida es razonable pensar que es necesario tener más de una computadora para realizar tests o pruebas sobre dicha Actividad, pero los diseñadores de Sugar tomaron en cuenta que las Actividades serían compartidas y desarrollaron medios por los cuales se pueden probar las Actividades compartidas utilizando sólo una computadora. Estosmétodoshan ido evolucionando, por lo que existen ligeras variaciones en como probar dependiendo de la versión de Sugar que se esté utilizando. La primera cosa que se debe saber es como correr múltiples instancias de Sugar como usuarios diferentes.

Fedora 10 (Sugar .82)

En Sugar .82 existe una manera práctica para ejecutarmúltiplescopias del emulador Sugar y tener cada instancia como un usuario diferente, sin tener que haber iniciado sesión en la máquina Linux con más de un usuario. En la línea de comando para cada usuario adicional que se quiera ejecutar es necesario añadir una variable de entorno SUGAR_PROFILE de la siguiente forma:

SUGAR_PROFILE=austen sugar-emulator

Cuando haces esto el emulador de Sugar creará un directorio llamado austen bajo ~/.sugar para almacenarinformacióndel perfil en dicho directorio, etc. Se te solicitará introducir un nombre y seleccionar un color para el ícono. Cada vez que arranques usando elSUGAR_PROFILE deausten, serás ese usuario. Si lanzas el emulador sin utilizar un SUGAR_PROFILE, serás el usuario regular que configuraste previamente.

Fedora 11 (Sugar .84)

A pesar de lo práctico de usar el SUGAR_PROFILE, los desarrolladores de Sugar decidieron que tenía limitaciones y por lo tanto dejó funcionar en la version .84 y posteriores. Con Sugar .84 y superiores es necesario crear un usuario Linux adicional y ejecutar el emulador Sugar como dos usuarios Linux independientes. En el entorno GNOME existe una opciónUsuarios y Grupos (Users and Groups) en el submenú Administración (Administration) del menú Sistema (System)desde donde es posible configurar un segundo usuario. Antes se te solicitará que ingreses la contraseña de administrador que has creado cuando has configurado inicialmente el entorno Linux con el que trabajas.

Crear el segundo usuario es bastante simple, pero ¿cómo se consigue tener dos usuarios diferentes registrados al mismo tiempo? De hecho es bastante simple. Es necesario que abras una terminal e ingreses lo siguiente:

ssh -XY jausten@localhost

Vemos que "jausten" es el user_id del segundo usuario. Se hará una consulta para verificar la confiabilidad de la computadora "localhost". "localhost" significa que utilizarás los mecanismos de red para iniciar una nueva sesión dentro la misma computadora, entonces es seguro contestar que si (yes). Entonces se te pedirá introducir su contraseña (password), y desde ese momento todo lo que realices desde esa terminal pertenecerá al usuario con has iniciado la sesión. Puedes iniciar el emulador de Sugar desde esta terminal y la primera vez te pedirá un nombre y color del ícono.

sugar-jhbuild

Con sugar-jhbuild (la última version de Sugar) las cosas nuevamente cambian un poquitín. Debes usar el método de inicio de sesión como multiples usuarios Linux tal como se hace para le versión .84, pero no se te preguntará ningún nombre de usuario. En su lugar Sugar utilizará el nombre de usuario con el que está corriendo el sistema. No podrás cambiar el nombre de usuario, pero si podrás cambiar el color del ícono como siempre.

Necesitas una instalación independiente de sugar-jhbuild para cada usuario. Estas instalaciones adicionales serán bastante rápidas puesto que se instalaron todas las dependencias la primera vez.

Conectar con otros usuarios

Sugar utiliza un software llamado Telepathy, el cual implementa un protocolo de intercambio de mensajes instantáneo llamado XMPP (Extended Messaging and Presence Protocol). Este protocolo solía llamarse Jabber. En esencia Telepathy permite incrustar un cliente de mensajería instantánea dentro de tu Actividad (Activity). Puedes utilizar este recurso para enviar mensajes de usuario a usuario, ejecutar métodos remotamente e incluso para realizar transferencia de archivos.

De hecho existen dos formas para que los usuarios de Sugar puedan interactuar en una red:

Salut

Si dos computadoras están conectadas en el mismo segmento de una red, las mismas deberían ser capaces de encontrarse y compartir Actividades. Si tienes una red doméstica donde todo el mundo utiliza el mismo router puedes compartir con otros en esa red. A este tipo de conexión se lo suele llamar Link-Local XMPP. El software Telepathy que hace esto posible se conoce como Salut.

Las laptops XO cuentan con software y hardware especial que les permite soportar las redes Malla (Mesh Networking), donde las XO que están próximas pueden automáticamente iniciar una red malla sin necesidad de un router. Para Sugar mientres estés conectado no importa el tipo de conexión tengas. Cableada o inalámbrica (llamada también por radio o wireless), Malla o no, todas ellas funcionan de la misma forma.

El servidor Jabber

Otra forma de conectarse a otros usuarios y sus Actividades es a través del uso de un servidor Jabber. La ventaja de usar un servidor Jabber es que es posible conectar y compartir Actividades con personas fuera de la red local. Estas personas incluso podrían estar al otro lado del mundo. Jabber permite que Actividades en diferentes redes se conecten entre si, aún cuando estas redes se encuentren protegidas por un muro de fuego (firewall en inglés - en el mundo informático el término muro de fuego se utiliza para referirse a un software de protección de intrusos sobre la red). La parte de Telepathy que trabaja con un Seridor Jabber se llama Gabble.

En general deberías usar Salut para testear tus aplicaciones si es posible. Esto simplifica el testeo y no utiliza los recursos de un Servidor Jabber.

Realmente no es importante si tu Actividad se conecta con otros usando Gabble o Salut. De hecho, la Actividad no tiene idea que está utilizando. Estos detalles se encuentran escondidos para la Actividad y es directa responsabilidad de Telepathy. Cualquier Actividad que trabaja con Salut trabajará también con Gabble y viceversa.

Para configurar el emulador-sugar para que utilice Salut ve al panel de control de Sugar:

En Sugar .82 esta opción de menú se llama Panel de Control (Control Panel). En versiones más recientes se llama My Settings (Mis parámetros u opciones)

Haz clic en el ícono de la red (Network).

El campo Servidor en esta pantalla debe permanecer vacío para usar Salut. Puedes usar la tecla de borrar (backspace) para borrar cualquier entrada en este campo.

Debes seguir todos estos pasos para cada usuario Sugar que vaya a ser parte del test de la Actividad compartida.

Si por alguna razon deseas testear tu Actividad utilizando un servidor Jabber, en el Wiki de OLPC se mantiene una lista de servidores públicos disponibles en http://wiki.laptop.org/go/Community_Jabber_Servers

Una vez que tienes configurado sea Salut o un Servidor Jabber en ambas instancias de Sugar que ejecutes, debes ir a la vista de Vecindario (Neighborhood) de ambas máquinas para ver si entre ellas fueron capaces de detectarse, y quiza puedas intentar utilizar la actividad Chat entre ellas. Si tienes todo esto funcionando estás listo para intentar programar una Actividad compartida.


La Actividad MiniChat

Tal como tomamos la Actividad Read Etexts y la analizamos al detalle, vamos a hacer lo mismo con la Actividad Chat para crear una nueva Actividad a la que llamaremos MiniChat. La Actividad real Chat (Chat Activity) tiene ciertas características que no necesitamos para demostrar la mensajería de una Actividad compartida:

  • Tiene la habilidad de cargar su código en Pippy para su visualización. Esta es una cualidad que supuestamente tienen todas las actividades de las XO, pero Chat es una de las pocas que lo implementan. Personalmente, si quiero ver el código de una Actividad prefiero ir directamente a git.sugarlabs.org donde tengo la posibilidad de ver tanto el código actual como versiones anteriores.

  • Chat puede mantener una conexión de uno a uno con un cliente convencional XMPP. Esto puede ser útil para el Chat pero no necesariamente útil o deseable para la mayoría de las Actividades compartidas.

  • Si incluyes una URL en un mensaje del Chat, la interfaz de usuario te permite hacer clic en la URL para hacer una entrada en el Diario (Journal) para dicha URL. Entonces puedes usar el Diario (Journal) para abrir la URL con la Actividad Browse (Navegar). (Esto es necesario porque las Actividades no pueden lanzarse unas a otras). Es genial, pero no es necesario para demostrar cómo se hace una Actividad compartida.

  • La sesión de chat se guarda en el Diario (Journal). Cuando retomas el Chat desde el Diario (Journal) restaura los mensajes desde tu sesión previa en la interfaz de usuario. Nosotros ya sabemos como guardar cosas en el Diario y también como restaurar cosas desde el Diario, por lo tanto el MiniChat no hará esto.

El código resultante es aproximadamente la mitad de largo que el original. También hice algunos otros cambios:

  • El campo de entrada está arriba de los mensajes del chat en lugar de abajo. Esto permite tomar capturas de pantalla parciales de la Actividad en acción más fácilmente.

  • He eliminado la barra de herramientas (toolbar) del nuevo estilo y la cambié por una barra de herramientas del viejo estilo. Esto me permite testear la Actividad en Fedora 10 y 11 la cual no soporta las nuevas barras de herramientas (toolbars).

  • Tomé la clase TextChannelWrapper y la puse en un archivo propio diferente. Hice esto porque la clase parecía útil para otros proyectos.

El código y todos los archivos requeridos para la Actividad MiniChat están en el directorio MiniChat del repositorio Git. Para ejecutarlo necesitas correr:

./setup.py dev

en el proyecto para dejarlo listo para el test. La activity.info se ve así:

[Activity]
name = Mini Chat
service_name = net.flossmanuals.MiniChat
icon = chat
exec = sugar-activity minichat.MiniChat
show_launcher = yes
activity_version = 1
license = GPLv2+

Este es el código para textchannel.py:

import logging

from telepathy.client import Connection, Channel
from telepathy.interfaces import (
    CHANNEL_INTERFACE, CHANNEL_INTERFACE_GROUP,
    CHANNEL_TYPE_TEXT, CONN_INTERFACE_ALIASING)
from telepathy.constants import (
    CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES,
    CHANNEL_TEXT_MESSAGE_TYPE_NORMAL)

class TextChannelWrapper(object):
    """Wrap a telepathy Text Channel to make
    usage simpler."""
    def __init__(self, text_chan, conn):
        """Connect to the text channel"""
        self._activity_cb = None
        self._activity_close_cb = None
        self._text_chan = text_chan
        self._conn = conn
        self._logger = logging.getLogger(
            'minichat-activity.TextChannelWrapper')
        self._signal_matches = []
        m = self._text_chan[CHANNEL_INTERFACE].\
            connect_to_signal(
            'Closed', self._closed_cb)
        self._signal_matches.append(m)

    def send(self, text):
        """Send text over the Telepathy text channel."""
        # XXX Implement CHANNEL_TEXT_MESSAGE_TYPE_ACTION
        if self._text_chan is not None:
            self._text_chan[CHANNEL_TYPE_TEXT].Send(
                CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, text)

    def close(self):
        """Close the text channel."""
        self._logger.debug('Closing text channel')
        try:
            self._text_chan[CHANNEL_INTERFACE].Close()
        except:
            self._logger.debug('Channel disappeared!')
            self._closed_cb()

    def _closed_cb(self):
        """Clean up text channel."""
        self._logger.debug('Text channel closed.')
        for match in self._signal_matches:
            match.remove()
        self._signal_matches = []
        self._text_chan = None
        if self._activity_close_cb is not None:
            self._activity_close_cb()

    def set_received_callback(self, callback):
        """Connect the function callback to the signal.

        callback -- callback function taking buddy
        and text args
        """
        if self._text_chan is None:
            return
        self._activity_cb = callback
        m = self._text_chan[CHANNEL_TYPE_TEXT].\
            connect_to_signal(
            'Received', self._received_cb)
        self._signal_matches.append(m)

    def handle_pending_messages(self):
        """Get pending messages and show them as
        received."""
        for id, timestamp, sender, type, flags, text \
            in self._text_chan[
            CHANNEL_TYPE_TEXT].ListPendingMessages(
            False):
            self._received_cb(id, timestamp, sender,
                type, flags, text)

    def _received_cb(self, id, timestamp, sender,
        type, flags, text):
        """Handle received text from the text channel.

        Converts sender to a Buddy.
        Calls self._activity_cb which is a callback
        to the activity.
        """
        if self._activity_cb:
            buddy = self._get_buddy(sender)
            self._activity_cb(buddy, text)
            self._text_chan[
                CHANNEL_TYPE_TEXT].
                AcknowledgePendingMessages([id])
        else:
            self._logger.debug(
                'Throwing received message on the floor'
                ' since there is no callback connected. See '
                'set_received_callback')

    def set_closed_callback(self, callback):
        """Connect a callback for when the text channel
        is closed.

        callback -- callback function taking no args

        """
        self._activity_close_cb = callback

    def _get_buddy(self, cs_handle):
        """Get a Buddy from a (possibly channel-specific)
        handle."""
        # XXX This will be made redundant once Presence
        # Service provides buddy resolution
        from sugar.presence import presenceservice
        # Get the Presence Service
        pservice = presenceservice.get_instance()
        # Get the Telepathy Connection
        tp_name, tp_path = \
            pservice.get_preferred_connection()
        conn = Connection(tp_name, tp_path)
        group = self._text_chan[CHANNEL_INTERFACE_GROUP]
        my_csh = group.GetSelfHandle()
        if my_csh == cs_handle:
            handle = conn.GetSelfHandle()
        elif group.GetGroupFlags() & \
            CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
            handle = group.GetHandleOwners([cs_handle])[0]
        else:
            handle = cs_handle

            # XXX: deal with failure to get the handle owner
            assert handle != 0

        return pservice.get_buddy_by_telepathy_handle(
            tp_name, tp_path, handle)

Y aquí está el código para minichat.py:

from gettext import gettext as _
import hippo
import gtk
import pango
import logging
from sugar.activity.activity import (Activity,
    ActivityToolbox, SCOPE_PRIVATE)
from sugar.graphics.alert import NotifyAlert
from sugar.graphics.style import (Color, COLOR_BLACK,
    COLOR_WHITE, COLOR_BUTTON_GREY, FONT_BOLD,
    FONT_NORMAL)
from sugar.graphics.roundbox import CanvasRoundBox
from sugar.graphics.xocolor import XoColor
from sugar.graphics.palette import Palette, CanvasInvoker

from textchannel import TextChannelWrapper

logger = logging.getLogger('minichat-activity')

class MiniChat(Activity):
    def __init__(self, handle):
        Activity.__init__(self, handle)

        root = self.make_root()
        self.set_canvas(root)
        root.show_all()
        self.entry.grab_focus()

        toolbox = ActivityToolbox(self)
        activity_toolbar = toolbox.get_activity_toolbar()
        activity_toolbar.keep.props.visible = False
        self.set_toolbox(toolbox)
        toolbox.show()

        self.owner = self._pservice.get_owner()
        # Auto vs manual scrolling:
        self._scroll_auto = True
        self._scroll_value = 0.0
        # Track last message, to combine several
        # messages:
        self._last_msg = None
        self._last_msg_sender = None
        self.text_channel = None

        if self._shared_activity:
            # we are joining the activity
            self.connect('joined', self._joined_cb)
            if self.get_shared():
                # we have already joined
                self._joined_cb()
        else:
            # we are creating the activity
            if not self.metadata or self.metadata.get(
                'share-scope',
                SCOPE_PRIVATE) == SCOPE_PRIVATE:
                # if we are in private session
                self._alert(_('Off-line'),
                    _('Share, or invite someone.'))
            self.connect('shared', self._shared_cb)

    def _shared_cb(self, activity):
        logger.debug('Chat was shared')
        self._setup()

    def _setup(self):
        self.text_channel = TextChannelWrapper(
            self._shared_activity.telepathy_text_chan,
            self._shared_activity.telepathy_conn)
        self.text_channel.set_received_callback(
            self._received_cb)
        self._alert(_('On-line'), _('Connected'))
        self._shared_activity.connect('buddy-joined',
            self._buddy_joined_cb)
        self._shared_activity.connect('buddy-left',
            self._buddy_left_cb)
        self.entry.set_sensitive(True)
        self.entry.grab_focus()

    def _joined_cb(self, activity):
        """Joined a shared activity."""
        if not self._shared_activity:
            return
        logger.debug('Joined a shared chat')
        for buddy in \
            self._shared_activity.get_joined_buddies():
            self._buddy_already_exists(buddy)
        self._setup()

    def _received_cb(self, buddy, text):
        """Show message that was received."""
        if buddy:
                nick = buddy.props.nick
        else:
            nick = '???'
        logger.debug(
            'Received message from %s: %s', nick, text)
        self.add_text(buddy, text)

    def _alert(self, title, text=None):
        alert = NotifyAlert(timeout=5)
        alert.props.title = title
        alert.props.msg = text
        self.add_alert(alert)
        alert.connect('response', self._alert_cancel_cb)
        alert.show()

    def _alert_cancel_cb(self, alert, response_id):
        self.remove_alert(alert)

    def _buddy_joined_cb (self, activity, buddy):
        """Show a buddy who joined"""
        if buddy == self.owner:
            return
        if buddy:
            nick = buddy.props.nick
        else:
            nick = '???'
        self.add_text(buddy, buddy.props.nick+'
            '+_('joined the chat'),
            status_message=True)

    def _buddy_left_cb (self, activity, buddy):
        """Show a buddy who joined"""
        if buddy == self.owner:
            return
        if buddy:
            nick = buddy.props.nick
        else:
            nick = '???'
        self.add_text(buddy, buddy.props.nick+'
            '+_('left the chat'),
            status_message=True)

    def _buddy_already_exists(self, buddy):
        """Show a buddy already in the chat."""
        if buddy == self.owner:
            return
        if buddy:
            nick = buddy.props.nick
        else:
            nick = '???'
        self.add_text(buddy, buddy.props.nick+
            ' '+_('is here'),
            status_message=True)

    def make_root(self):
        conversation = hippo.CanvasBox(
            spacing=0,
            background_color=COLOR_WHITE.get_int())
        self.conversation = conversation

        entry = gtk.Entry()
        entry.modify_bg(gtk.STATE_INSENSITIVE,
            COLOR_WHITE.get_gdk_color())
        entry.modify_base(gtk.STATE_INSENSITIVE,
            COLOR_WHITE.get_gdk_color())
        entry.set_sensitive(False)
        entry.connect('activate',
            self.entry_activate_cb)
        self.entry = entry

        hbox = gtk.HBox()
        hbox.add(entry)

        sw = hippo.CanvasScrollbars()
        sw.set_policy(hippo.ORIENTATION_HORIZONTAL,
            hippo.SCROLLBAR_NEVER)
        sw.set_root(conversation)
        self.scrolled_window = sw

        vadj = self.scrolled_window.props.widget.\
            get_vadjustment()
        vadj.connect('changed', self.rescroll)
        vadj.connect('value-changed',
            self.scroll_value_changed_cb)

        canvas = hippo.Canvas()
        canvas.set_root(sw)

        box = gtk.VBox(homogeneous=False)
        box.pack_start(hbox, expand=False)
        box.pack_start(canvas)

        return box

    def rescroll(self, adj, scroll=None):
        """Scroll the chat window to the bottom"""
        if self._scroll_auto:
            adj.set_value(adj.upper-adj.page_size)
            self._scroll_value = adj.get_value()

    def scroll_value_changed_cb(self, adj, scroll=None):
        """Turn auto scrolling on or off.

        If the user scrolled up, turn it off.
        If the user scrolled to the bottom, turn it back on.
        """
        if adj.get_value() < self._scroll_value:
            self._scroll_auto = False
        elif adj.get_value() == adj.upper-adj.page_size:
            self._scroll_auto = True

    def add_text(self, buddy, text, status_message=False):
        """Display text on screen, with name and colors.

        buddy -- buddy object
        text -- string, what the buddy said
        status_message -- boolean
            False: show what buddy said
            True: show what buddy did

        hippo layout:
        .------------- rb ---------------.
        | +name_vbox+ +----msg_vbox----+ |
        | |         | |                | |
        | | nick:   | | +--msg_hbox--+ | |
        | |         | | | text       | | |
        | +---------+ | +------------+ | |
        |             |                | |
        |             | +--msg_hbox--+ | |
        |             | | text       | | |
        |             | +------------+ | |
        |             +----------------+ |
        `--------------------------------'
        """
        if buddy:
            nick = buddy.props.nick
            color = buddy.props.color
            try:
                color_stroke_html, color_fill_html = \
                    color.split(',')
            except ValueError:
                color_stroke_html, color_fill_html = (
                    '#000000', '#888888')
            # Select text color based on fill color:
            color_fill_rgba = Color(
                color_fill_html).get_rgba()
            color_fill_gray = (color_fill_rgba[0] +
                color_fill_rgba[1] +
                color_fill_rgba[2])/3
            color_stroke = Color(
                color_stroke_html).get_int()
            color_fill = Color(color_fill_html).get_int()
            if color_fill_gray < 0.5:
                text_color = COLOR_WHITE.get_int()
            else:
                text_color = COLOR_BLACK.get_int()
        else:
            nick = '???'
            # XXX: should be '' but leave for debugging
            color_stroke = COLOR_BLACK.get_int()
            color_fill = COLOR_WHITE.get_int()
            text_color = COLOR_BLACK.get_int()
            color = '#000000,#FFFFFF'

        # Check for Right-To-Left languages:
        if pango.find_base_dir(nick, -1) == \
            pango.DIRECTION_RTL:
            lang_rtl = True
        else:
            lang_rtl = False

        # Check if new message box or add text to previous:
        new_msg = True
        if self._last_msg_sender:
            if not status_message:
                if buddy == self._last_msg_sender:
                    # Add text to previous message
                    new_msg = False

        if not new_msg:
            rb = self._last_msg
            msg_vbox = rb.get_children()[1]
            msg_hbox = hippo.CanvasBox(
                orientation=hippo.ORIENTATION_HORIZONTAL)
            msg_vbox.append(msg_hbox)
        else:
            rb = CanvasRoundBox(
                background_color=color_fill,
                border_color=color_stroke,
                padding=4)
            rb.props.border_color = color_stroke
            self._last_msg = rb
            self._last_msg_sender = buddy
            if not status_message:
                name = hippo.CanvasText(text=nick+':   ',
                    color=text_color,
                    font_desc=FONT_BOLD.get_pango_desc())
                name_vbox = hippo.CanvasBox(
                    orientation=hippo.ORIENTATION_VERTICAL)
                name_vbox.append(name)
                rb.append(name_vbox)
            msg_vbox = hippo.CanvasBox(
                orientation=hippo.ORIENTATION_VERTICAL)
            rb.append(msg_vbox)
            msg_hbox = hippo.CanvasBox(
                orientation=hippo.ORIENTATION_HORIZONTAL)
            msg_vbox.append(msg_hbox)

        if status_message:
            self._last_msg_sender = None

        if text:
            message = hippo.CanvasText(
                text=text,
                size_mode=hippo.CANVAS_SIZE_WRAP_WORD,
                color=text_color,
                font_desc=FONT_NORMAL.get_pango_desc(),
                xalign=hippo.ALIGNMENT_START)
            msg_hbox.append(message)

        # Order of boxes for RTL languages:
        if lang_rtl:
            msg_hbox.reverse()
            if new_msg:
                rb.reverse()

        if new_msg:
            box = hippo.CanvasBox(padding=2)
            box.append(rb)
            self.conversation.append(box)

    def entry_activate_cb(self, entry):
        text = entry.props.text
        logger.debug('Entry: %s' % text)
        if text:
            self.add_text(self.owner, text)
            entry.props.text = ''
            if self.text_channel:
                self.text_channel.send(text)
            else:
                logger.debug(
                    'Tried to send message but text '
                    'channel not connected.')

Y así es como se ve la Actividad en acción:

Intenta lanzar más de una copia del emulador Sugar, con esta Actividad instalada en cada uno. Si estás utilizando Fedora 10 y SUGAR_PROFILE la Actividad no necesita estar instalada más de una vez, pero si estás utilizando una versión posterior de Sugar que requiere id de Usuario (userid) de Linux independiente para cada instancia, necesitas mantener copias separadas del código para cada usuario. En tu propio proyecto usar un repositorio central Git en git.sugarlabs.org puede hacer esta tarea mucho más sencilla. Sólo tienes que postear una copia de los cambios que realices en el repositorio central y Git enviará (pull) una copia al segundo id de usuario (userid). El segundo id de usuario (userid) puede usar la URL pública del repositorio. No es necesario configurar SSH para otro usuario que el primero.

Puedes haber leído en algún lugar que es posible instalar una Actividad en una máquina y compartir esta Actividad con alguien más que no tenga esa Actividad instalada. En este caso la segunda máquina podría conseguir una copia de la Actividad desde la primer maquina e instalarla automáticamente. Probablemente también leíste que si dos usuarios de una Actividad compartida tienen diferentes versiones de la misma, entonces quien tenga la versión más reciente actualizará a la más antigua. Ninguna de estas afirmaciones es cierta hoy y no parece que lleguen a ser ciertas en un futuro cercano. Estas ideas se discuten en las listas de correo de vez en cuando, pero existen dificultades prácticas que es necesario vencer antes de que cualquiera de estas características pueda funcionar, la mayoría de estas dificultades tienen que ver con la seguridad. Por ahora ambos usuarios de una Actividad compartida deben tener la Actividad instalada. Por otro lado, dependiendo de cómo está escrita la Actividad, dos versiones diferentes de la misma podrían establecer comunicación una con otra. Si los mensajes que se intercambian están en el mismo formato no debería haber problemas de comunicacion entre diferentes versiones.

Una vez que tengas ambas instancias del emulador-sugar ejecutándose, puedes lanzar la Actividad MiniChat en una instancia e invitar al segundo usuario a unirse a la sesión de Chat. Puedes hacer ambas cosas desde la vista Vecindario (Neighborhood).

Las invitaciones se hacen de la siguiente manera:

Aceptar a un amigo se ve así:

Después que hayas jugado con la Actividad MiniChat por unos momentos, hablaremos de los secretos del uso de Telepathy para crear una Actividad compartida.

Conocer a tus amigos (buddies)

Como hemos dicho anteriormente, XMPP es el Protocolo de Mensajería y Presencia Extendido (por sus siglas en inglés Extended Messaging and Presence Protocol). Presencia es lo que parece; te permite saber quién está disponible para compartir una Actividad, así como también que otras Actividades compartidas por otros están disponibles. Existen dos formas de compartir una Actividad. La primera de ellas es cuando cambias el menú desplegable Compartir con (Share with) de la barra de herramientas principal, entonces debería quedar Mi Vecindario (My Neighborhood) en lugar de Privado (Private). Esto significa que cualquiera en la red puede compartir tu Actividad. Otra forma de compartir una Actividad es ir a la vista de Vecindario (Neighborhood) e invitar a alguien específico para compartirla. La persona a la que le llega la invitación no sabe realmente si la invitación fue hecha específicamente para ella o para todos los usuarios que están en la red en el Vecindario. El término técnico que se utiliza para referirse a las personas que comparten tu Actividad es Buddies. El lugar donde los Buddies se encuentran y colaboran se conoce como MUC o Multi User Chatroom. (Sala de Chat de múltiples usuarios)

El código usado por nuestra Actividad para invitar a los Buddies y para unirse como un Buddy a una Actividad iniciada por alguien más, esté en el método __init__():

        if self._shared_activity:
            # we are joining the activity
            self.connect('joined', self._joined_cb)
            if self.get_shared():
                # we have already joined
                self._joined_cb()
        else:
            # we are creating the activity
            if not self.metadata or self.metadata.get(
                'share-scope',
                SCOPE_PRIVATE) == SCOPE_PRIVATE:
                # if we are in private session
                self._alert(_('Off-line'),
                    _('Share, or invite someone.'))
            self.connect('shared', self._shared_cb)

    def _shared_cb(self, activity):
        logger.debug('Chat was shared')
        self._setup()

    def _joined_cb(self, activity):
        """Joined a shared activity."""
        if not self._shared_activity:
            return
        logger.debug('Joined a shared chat')
        for buddy in \
            self._shared_activity.get_joined_buddies():
            self._buddy_already_exists(buddy)
        self._setup()

    def _setup(self):
        self.text_channel = TextChannelWrapper(
            self._shared_activity.telepathy_text_chan,
            self._shared_activity.telepathy_conn)
        self.text_channel.set_received_callback(
            self._received_cb)
        self._alert(_('On-line'), _('Connected'))
        self._shared_activity.connect('buddy-joined',
            self._buddy_joined_cb)
        self._shared_activity.connect('buddy-left',
            self._buddy_left_cb)
        self.entry.set_sensitive(True)
        self.entry.grab_focus()

Existen dos formas de iniciar una Actividad: iniciando la Actividad o uniéndose a una Actividad que alguien más inició. La primera línea arriba en negrita determina si nos estamos uniendo o somos el primer usuario de una Actividad. Entonces invocamos que se ejecute el método _joined_cb()cuando el evento 'joined' ocurre. Este método obtiene una lista de Buddies desde el objeto _shared_activity y crea mensajes en la interfaz del usuario informando al usuario que esos Buddies ya se encuentran en la sala de chat (chat room). Finalmente ejecuta el método _setup().

Si no nos estamos uniendo a una Actividad existente entonces verificamos si estamos compartiendo la Actividad con alguien. Si no lo estamos haciendo desplegamos un mensaje diciéndole al usuario que invite a alguien al chat. También hacemos una solicitud cuando el evento 'shared' sucede para que se ejecute el método _shared_cb(). Este método simplemente invoca al método _setup().

El método _setup() crea un objeto TextChannelWrapper (traducción aproximada: Envoltorio de Canal de Texto) utilizando el código en textchannel.py. También le dice al objeto _shared_activity que se requiere que se invoquen ciertos metodos cuando nuevos Buddies se unan a la Actividad y también cuando algunos Buddies abandonen la Actividad. Todo lo que necesitas saber sobre los Buddies lo puedes encontrar en el código que está arriba, excepto como enviarles mensajes a ellos. Para esto usaremos el Text Channel (Canal de Texto). No es necesario aprender del Canal de Texto en gran detalle porque la clase TextChannelWrapper hace todo lo que podrías necesitar hacer con el Canal de Texto y esconde los detalles de la implementación.

    def entry_activate_cb(self, entry):
        text = entry.props.text
        logger.debug('Entry: %s' % text)
        if text:
            self.add_text(self.owner, text)
            entry.props.text = ''
            if self.text_channel:
                self.text_channel.send(text)
            else:
                logger.debug(
                    'Tried to send message but text '
                    'channel not connected.')

El método add_text()es interesante. Identifica al propietario del mensaje y los colores del entorno (podría decirse el borde del ícono que tiene la XO) que le corresponden y muestra los mensajes en ese color. En el caso de mensajes enviados por la Actividad toma al propietario de la Actividad dentro el método __init__()de esta forma:

 self.owner = self._pservice.get_owner()

En el caso de mensajes recibidos se obtiene el Buddy emisor de esta forma:

    def _received_cb(self, buddy, text):
        """Show message that was received."""
        if buddy:
                nick = buddy.props.nick
        else:
            nick = '???'
        logger.debug('Received message from %s: %s',
            nick, text)
        self.add_text(buddy, text)

Pero, ¿qué sucede si queremos hacer más que sólo enviar mensajes de texto hacia atrás y hacia adelante? ¿Qué deberíamos utilizar para esto?


Es una serie de tubos!

No, no internet. Telepathy tiene un concepto llamado Tuberías (Tubes) que describe cómo diferentes instancias de una Actividad pueden comunicarse entre ellas. Lo que Telepathy hace es tomar el Canal de texto y construir tuberías encima de este. Existen dos tipos de Tuberías:

  • Tuberías D-Bus (D-Bus Tubes)

  • Tuberías de Flujo (Stream Tubes)

Se utiliza una Tubería D-Bus (D-Bus Tube) para que la instancia de una Actividad pueda invocar metodos en una instancia Buddy de la Actividad. Una Tubería de Flujo (Stream Tube) se utiliza para enviar datos por medio de Sockets, por ejemplo para copiar un archivo desde una instancia a otra de una Actividad. Un Socket es una vía de comunicación sobre la red que utiliza Protocolos de Internet. Un socket en informática es un espacio virtual que sirve como entrada y salida para intercambiar comunicaciones entre equipos, por ejemplo la implementación del Protocolo HTTP que se utiliza en Internet (World Wide Web) es implementada con Sockets. En el siguiente ejemplo vamos a usar HTTP para transferir libros desde una instancia de Read Etexts III a otra.

Read Etexts III, Ahora puedes compartir libros!

El repositorio Git con los códigos de ejemplo de este libro tiene un archivo llamado ReadEtextsActivity3.py en el directorio Making_Shared_Activities que se ve de esta manera:

import sys
import os
import logging
import tempfile
import time
import zipfile
import pygtk
import gtk
import pango
import dbus
import gobject
import telepathy
from sugar.activity import activity
from sugar.graphics import style
from sugar import network
from sugar.datastore import datastore
from sugar.graphics.alert import NotifyAlert
from toolbar import ReadToolbar, ViewToolbar
from gettext import gettext as _

page=0
PAGE_SIZE = 45
TOOLBAR_READ = 2

logger = logging.getLogger('read-etexts2-activity')

class ReadHTTPRequestHandler(
    network.ChunkedGlibHTTPRequestHandler):
    """HTTP Request Handler for transferring document
    while collaborating.

    RequestHandler class that integrates with Glib
    mainloop. It writes the specified file to the
    client in chunks, returning control to the
    mainloop between chunks.

    """
    def translate_path(self, path):
        """Return the filepath to the shared document."""
        return self.server.filepath


class ReadHTTPServer(network.GlibTCPServer):
    """HTTP Server for transferring document while
    collaborating."""
    def __init__(self, server_address, filepath):
        """Set up the GlibTCPServer with the
        ReadHTTPRequestHandler.

        filepath -- path to shared document to be served.
        """
        self.filepath = filepath
        network.GlibTCPServer.__init__(self,
            server_address, ReadHTTPRequestHandler)


class ReadURLDownloader(network.GlibURLDownloader):
    """URLDownloader that provides content-length and
    content-type."""

    def get_content_length(self):
        """Return the content-length of the download."""
        if self._info is not None:
            return int(self._info.headers.get(
                'Content-Length'))

    def get_content_type(self):
        """Return the content-type of the download."""
        if self._info is not None:
            return self._info.headers.get('Content-type')
        return None

READ_STREAM_SERVICE = 'read-etexts-activity-http'

class ReadEtextsActivity(activity.Activity):
    def __init__(self, handle):
        "The entry point to the Activity"
        global page
        activity.Activity.__init__(self, handle)

        self.fileserver = None
        self.object_id = handle.object_id

        toolbox = activity.ActivityToolbox(self)
        activity_toolbar = toolbox.get_activity_toolbar()
        activity_toolbar.keep.props.visible = False

        self.edit_toolbar = activity.EditToolbar()
        self.edit_toolbar.undo.props.visible = False
        self.edit_toolbar.redo.props.visible = False
        self.edit_toolbar.separator.props.visible = False
        self.edit_toolbar.copy.set_sensitive(False)
        self.edit_toolbar.copy.connect('clicked',
            self.edit_toolbar_copy_cb)
        self.edit_toolbar.paste.props.visible = False
        toolbox.add_toolbar(_('Edit'), self.edit_toolbar)
        self.edit_toolbar.show()

        self.read_toolbar = ReadToolbar()
        toolbox.add_toolbar(_('Read'), self.read_toolbar)
        self.read_toolbar.back.connect('clicked',
            self.go_back_cb)
        self.read_toolbar.forward.connect('clicked',
            self.go_forward_cb)
        self.read_toolbar.num_page_entry.connect('activate',
            self.num_page_entry_activate_cb)
        self.read_toolbar.show()

        self.view_toolbar = ViewToolbar()
        toolbox.add_toolbar(_('View'), self.view_toolbar)
        self.view_toolbar.connect('go-fullscreen',
                self.view_toolbar_go_fullscreen_cb)
        self.view_toolbar.zoom_in.connect('clicked',
            self.zoom_in_cb)
        self.view_toolbar.zoom_out.connect('clicked',
            self.zoom_out_cb)
        self.view_toolbar.show()

        self.set_toolbox(toolbox)
        toolbox.show()
        self.scrolled_window = gtk.ScrolledWindow()
        self.scrolled_window.set_policy(gtk.POLICY_NEVER,
            gtk.POLICY_AUTOMATIC)
        self.scrolled_window.props.shadow_type = \
            gtk.SHADOW_NONE

        self.textview = gtk.TextView()
        self.textview.set_editable(False)
        self.textview.set_cursor_visible(False)
        self.textview.set_left_margin(50)
        self.textview.connect("key_press_event",
            self.keypress_cb)

        self.progressbar = gtk.ProgressBar()
        self.progressbar.set_orientation(
            gtk.PROGRESS_LEFT_TO_RIGHT)
        self.progressbar.set_fraction(0.0)

        self.scrolled_window.add(self.textview)
        self.textview.show()
        self.scrolled_window.show()

        vbox = gtk.VBox()
        vbox.pack_start(self.progressbar, False,
            False, 10)
        vbox.pack_start(self.scrolled_window)
        self.set_canvas(vbox)
        vbox.show()

        page = 0
        self.clipboard = gtk.Clipboard(
            display=gtk.gdk.display_get_default(),
            selection="CLIPBOARD")
        self.textview.grab_focus()
        self.font_desc = pango.FontDescription("sans %d" %
            style.zoom(10))
        self.textview.modify_font(self.font_desc)

        buffer = self.textview.get_buffer()
        self.markset_id = buffer.connect("mark-set",
            self.mark_set_cb)

        self.toolbox.set_current_toolbar(TOOLBAR_READ)
        self.unused_download_tubes = set()
        self.want_document = True
        self.download_content_length = 0
        self.download_content_type = None
        # Status of temp file used for write_file:
        self.tempfile = None
        self.close_requested = False
        self.connect("shared", self.shared_cb)

        self.is_received_document = False

        if self._shared_activity and \
            handle.object_id == None:
            # We're joining, and we don't already have
            # the document.
            if self.get_shared():
                # Already joined for some reason, just get the
                # document
                self.joined_cb(self)
            else:
                # Wait for a successful join before trying to get
                # the document
                self.connect("joined", self.joined_cb)

    def keypress_cb(self, widget, event):
        "Respond when the user presses one of the arrow keys"
        keyname = gtk.gdk.keyval_name(event.keyval)
        print keyname
        if keyname == 'plus':
            self.font_increase()
            return True
        if keyname == 'minus':
            self.font_decrease()
            return True
        if keyname == 'Page_Up' :
            self.page_previous()
            return True
        if keyname == 'Page_Down':
            self.page_next()
            return True
        if keyname == 'Up' or keyname == 'KP_Up' \
                or keyname == 'KP_Left':
            self.scroll_up()
            return True
        if keyname == 'Down' or keyname == 'KP_Down' \
                or keyname == 'KP_Right':
            self.scroll_down()
            return True
        return False

    def num_page_entry_activate_cb(self, entry):
        global page
        if entry.props.text:
            new_page = int(entry.props.text) - 1
        else:
            new_page = 0

        if new_page >= self.read_toolbar.total_pages:
            new_page = self.read_toolbar.total_pages - 1
        elif new_page < 0:
            new_page = 0

        self.read_toolbar.current_page = new_page
        self.read_toolbar.set_current_page(new_page)
        self.show_page(new_page)
        entry.props.text = str(new_page + 1)
        self.read_toolbar.update_nav_buttons()
        page = new_page

    def go_back_cb(self, button):
        self.page_previous()

    def go_forward_cb(self, button):
        self.page_next()

    def page_previous(self):
        global page
        page=page-1
        if page < 0: page=0
        self.read_toolbar.set_current_page(page)
        self.show_page(page)
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        v_adjustment.value = v_adjustment.upper - \
            v_adjustment.page_size

    def page_next(self):
        global page
        page=page+1
        if page >= len(self.page_index): page=0
        self.read_toolbar.set_current_page(page)
        self.show_page(page)
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        v_adjustment.value = v_adjustment.lower

    def zoom_in_cb(self,  button):
        self.font_increase()

    def zoom_out_cb(self,  button):
        self.font_decrease()

    def font_decrease(self):
        font_size = self.font_desc.get_size() / 1024
        font_size = font_size - 1
        if font_size < 1:
            font_size = 1
        self.font_desc.set_size(font_size * 1024)
        self.textview.modify_font(self.font_desc)

    def font_increase(self):
        font_size = self.font_desc.get_size() / 1024
        font_size = font_size + 1
        self.font_desc.set_size(font_size * 1024)
        self.textview.modify_font(self.font_desc)

    def mark_set_cb(self, textbuffer, iter, textmark):

        if textbuffer.get_has_selection():
            begin, end = textbuffer.get_selection_bounds()
            self.edit_toolbar.copy.set_sensitive(True)
        else:
            self.edit_toolbar.copy.set_sensitive(False)

    def edit_toolbar_copy_cb(self, button):
        textbuffer = self.textview.get_buffer()
        begin, end = textbuffer.get_selection_bounds()
        copy_text = textbuffer.get_text(begin, end)
        self.clipboard.set_text(copy_text)

    def view_toolbar_go_fullscreen_cb(self, view_toolbar):
        self.fullscreen()

    def scroll_down(self):
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        if v_adjustment.value == v_adjustment.upper - \
            v_adjustment.page_size:
            self.page_next()
            return
        if v_adjustment.value < v_adjustment.upper - \
            v_adjustment.page_size:
            new_value = v_adjustment.value + \
                v_adjustment.step_increment
            if new_value > v_adjustment.upper - \
                v_adjustment.page_size:
                new_value = v_adjustment.upper - \
                    v_adjustment.page_size
            v_adjustment.value = new_value

    def scroll_up(self):
        v_adjustment = \
            self.scrolled_window.get_vadjustment()
        if v_adjustment.value == v_adjustment.lower:
            self.page_previous()
            return
        if v_adjustment.value > v_adjustment.lower:
            new_value = v_adjustment.value - \
                v_adjustment.step_increment
            if new_value < v_adjustment.lower:
                new_value = v_adjustment.lower
            v_adjustment.value = new_value

    def show_page(self, page_number):
        global PAGE_SIZE, current_word
        position = self.page_index[page_number]
        self.etext_file.seek(position)
        linecount = 0
        label_text = '\n\n\n'
        textbuffer = self.textview.get_buffer()
        while linecount < PAGE_SIZE:
            line = self.etext_file.readline()
            label_text = label_text + unicode(line,
                'iso-8859-1')
            linecount = linecount + 1
        label_text = label_text + '\n\n\n'
        textbuffer.set_text(label_text)
        self.textview.set_buffer(textbuffer)

    def save_extracted_file(self, zipfile, filename):
        "Extract the file to a temp directory for viewing"
        filebytes = zipfile.read(filename)
        outfn = self.make_new_filename(filename)
        if (outfn == ''):
            return False
        f = open(os.path.join(self.get_activity_root(),
            'tmp', outfn), 'w')
        try:
            f.write(filebytes)
        finally:
            f.close()

    def get_saved_page_number(self):
        global page
        title = self.metadata.get('title', '')
        if title == '' or not \
            title[len(title)-1].isdigit():
            page = 0
        else:
            i = len(title) - 1
            newPage = ''
            while (title[i].isdigit() and i > 0):
                newPage = title[i] + newPage
                i = i - 1
            if title[i] == 'P':
                page = int(newPage) - 1
            else:
                # not a page number; maybe a
                # volume number.
                page = 0

    def save_page_number(self):
        global page
        title = self.metadata.get('title', '')
        if title == '' or not \
            title[len(title)- 1].isdigit():
            title = title + ' P' +  str(page + 1)
        else:
            i = len(title) - 1
            while (title[i].isdigit() and i > 0):
                i = i - 1
            if title[i] == 'P':
                title = title[0:i] + 'P' + str(page + 1)
            else:
                title = title + ' P' + str(page + 1)
        self.metadata['title'] = title

    def read_file(self, filename):
        "Read the Etext file"
        global PAGE_SIZE,  page

        tempfile = os.path.join(self.get_activity_root(),
            'instance', 'tmp%i' % time.time())
        os.link(filename,  tempfile)
        self.tempfile = tempfile

        if zipfile.is_zipfile(filename):
            self.zf = zipfile.ZipFile(filename, 'r')
            self.book_files = self.zf.namelist()
            self.save_extracted_file(self.zf,
                self.book_files[0])
            currentFileName = os.path.join(
                self.get_activity_root(),
                'tmp', self.book_files[0])
        else:
            currentFileName = filename

        self.etext_file = open(currentFileName,"r")
        self.page_index = [ 0 ]
        pagecount = 0
        linecount = 0
        while self.etext_file:
            line = self.etext_file.readline()
            if not line:
                break
            linecount = linecount + 1
            if linecount >= PAGE_SIZE:
                position = self.etext_file.tell()
                self.page_index.append(position)
                linecount = 0
                pagecount = pagecount + 1
        if filename.endswith(".zip"):
            os.remove(currentFileName)
        self.get_saved_page_number()
        self.show_page(page)
        self.read_toolbar.set_total_pages(
            pagecount + 1)
        self.read_toolbar.set_current_page(page)

        # We've got the document, so if we're a shared
        # activity, offer it
        if self.get_shared():
            self.watfor_tubes()
            self.share_document()

    def make_new_filename(self, filename):
        partition_tuple = filename.rpartition('/')
        return partition_tuple[2]

    def write_file(self, filename):
        "Save meta data for the file."
        if self.is_received_document:
            # This document was given to us by someone, so
            # we have to save it to the Journal.
            self.etext_file.seek(0)
            filebytes = self.etext_file.read()
            f = open(filename, 'wb')
            try:
                f.write(filebytes)
            finally:
                f.close()
        elif self.tempfile:
            if self.close_requested:
                os.link(self.tempfile,  filename)
                logger.debug(
                    "Removing temp file %s because we "
                    "will close",
                    self.tempfile)
                os.unlink(self.tempfile)
                self.tempfile = None
        else:
            # skip saving empty file
            raise NotImplementedError

        self.metadata['activity'] = self.get_bundle_id()
        self.save_page_number()

    def can_close(self):
        self.close_requested = True
        return True

    def joined_cb(self, also_self):
        """Callback for when a shared activity is joined.

        Get the shared document from another participant.
        """
        self.watfor_tubes()
        gobject.idle_add(self.get_document)

    def get_document(self):
        if not self.want_document:
            return False

        # Assign a file path to download if one
        # doesn't exist yet
        if not self._jobject.file_path:
            path = os.path.join(self.get_activity_root(),
                'instance',
                'tmp%i' % time.time())
        else:
            path = self._jobject.file_path

        # Pick an arbitrary tube we can try to
        # download the document from
        try:
            tube_id = self.unused_download_tubes.pop()
        except (ValueError, KeyError), e:
            logger.debug(
                'No tubes to get the document '
                'from right now: %s',
                e)
            return False

        # Avoid trying to download the document multiple
        # timesat once
        self.want_document = False
        gobject.idle_add(self.download_document, tube_id, path)
        return False

    def download_document(self, tube_id, path):
        chan = self._shared_activity.telepathy_tubes_chan
        iface = chan[telepathy.CHANNEL_TYPE_TUBES]
        addr = iface.AcceptStreamTube(tube_id,
           telepathy.SOCKET_ADDRESS_TYPE_IPV4,
           telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0,
           utf8_strings=True)
        logger.debug('Accepted stream tube: '
           'listening address is %r',
           addr)
        assert isinstance(addr, dbus.Struct)
        assert len(addr) == 2
        assert isinstance(addr[0], str)
        assert isinstance(addr[1], (int, long))
        assert addr[1] > 0 and addr[1] < 65536
        port = int(addr[1])

        self.progressbar.show()
        getter = ReadURLDownloader(
            "http://%s:%d/document"
            % (addr[0], port))
        getter.connect("finished",
            self.download_result_cb, tube_id)
        getter.connect("progress",
            self.download_progress_cb, tube_id)
        getter.connect("error",
            self.download_error_cb, tube_id)
        logger.debug("Starting download to %s...", path)
        getter.start(path)
        self.download_content_length = \
            getter.get_content_length()
        self.download_content_type = \
            getter.get_content_type()
        return False

    def download_progress_cb(self, getter,
        bytes_downloaded, tube_id):
        if self.download_content_length > 0:
            logger.debug(
                "Downloaded %u of %u bytes from tube %u...",
                bytes_downloaded,
                self.download_content_length,
                tube_id)
        else:
            logger.debug("Downloaded %u bytes from tube %u...",
                          bytes_downloaded, tube_id)
        total = self.download_content_length
        self.set_downloaded_bytes(bytes_downloaded,  total)
        gtk.gdk.threads_enter()
        while gtk.events_pending():
            gtk.main_iteration()
        gtk.gdk.threads_leave()

    def set_downloaded_bytes(self, bytes,  total):
        fraction = float(bytes) / float(total)
        self.progressbar.set_fraction(fraction)
        logger.debug("Downloaded percent",  fraction)

    def clear_downloaded_bytes(self):
        self.progressbar.set_fraction(0.0)
        logger.debug("Cleared download bytes")

    def download_error_cb(self, getter, err, tube_id):
        self.progressbar.hide()
        logger.debug(
            "Error getting document from tube %u: %s",
            tube_id, err)
        self.alert(_('Failure'),
            _('Error getting document from tube'))
        self.want_document = True
        self.download_content_length = 0
        self.download_content_type = None
        gobject.idle_add(self.get_document)

    def download_result_cb(self, getter, tempfile,
        suggested_name, tube_id):
        if self.download_content_type.startswith(
            'text/html'):
            # got an error page instead
            self.download_error_cb(getter,
                'HTTP Error', tube_id)
            return

        del self.unused_download_tubes

        self.tempfile = tempfile
        file_path = os.path.join(self.get_activity_root(),
            'instance', '%i' % time.time())
        logger.debug(
            "Saving file %s to datastore...", file_path)
        os.link(tempfile, file_path)
        self._jobject.file_path = file_path
        datastore.write(self._jobject,
            transfer_ownership=True)

        logger.debug(
            "Got document %s (%s) from tube %u",
            tempfile, suggested_name, tube_id)
        self.is_received_document = True
        self.read_file(tempfile)
        self.save()
        self.progressbar.hide()

    def shared_cb(self, activityid):
        """Callback when activity shared.

        Set up to share the document.

        """
        # We initiated this activity and have now shared it,
        # so by definition we have the file.
        logger.debug('Activity became shared')
        self.watfor_tubes()
        self.share_document()

    def share_document(self):
        """Share the document."""
        h = hash(self._activity_id)
        port = 1024 + (h % 64511)
        logger.debug(
            'Starting HTTP server on port %d', port)
        self.fileserver = ReadHTTPServer(("", port),
            self.tempfile)

        # Make a tube for it
        chan = self._shared_activity.telepathy_tubes_chan
        iface = chan[telepathy.CHANNEL_TYPE_TUBES]
        self.fileserver_tube_id = iface.OfferStreamTube(
                READ_STREAM_SERVICE,
                {},
                telepathy.SOCKET_ADDRESS_TYPE_IPV4,
                ('127.0.0.1', dbus.UInt16(port)),
                telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST,
                0)

    def watfor_tubes(self):
        """Watch for new tubes."""
        tubes_chan = self._shared_activity.telepathy_tubes_chan

        tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
            connect_to_signal(
            'NewTube', self.new_tube_cb)
        tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
            reply_handler=self.list_tubes_reply_cb,
            error_handler=self.list_tubes_error_cb)

    def new_tube_cb(self, tube_id, initiator, tube_type,
        service, params, state):
        """Callback when a new tube becomes available."""
        logger.debug(
            'New tube: ID=%d initator=%d type=%d service=%s '
            'params=%r state=%d', tube_id, initiator,
            tube_type, service, params, state)
        if service == READ_STREAM_SERVICE:
            logger.debug('I could download from that tube')
            self.unused_download_tubes.add(tube_id)
            # if no download is in progress, let's fetch
            # the document
            if self.want_document:
                gobject.idle_add(self.get_document)

    def list_tubes_reply_cb(self, tubes):
        """Callback when new tubes are available."""
        for tube_info in tubes:
            self.new_tube_cb(*tube_info)

    def list_tubes_error_cb(self, e):
        """Handle ListTubes error by logging."""
        logger.error('ListTubes() failed: %s', e)

    def alert(self, title, text=None):
        alert = NotifyAlert(timeout=20)
        alert.props.title = title
        alert.props.msg = text
        self.add_alert(alert)
        alert.connect('response', self.alert_cancel_cb)
        alert.show()

    def alert_cancel_cb(self, alert, response_id):
        self.remove_alert(alert)
        self.textview.grab_focus()

Estas líneas son el contenido de activity.info:

[Activity]
name = Read Etexts III
service_name = net.flossmanuals.ReadEtextsActivity
icon = read-etexts
exec = sugar-activity ReadEtextsActivity3.ReadEtextsActivity
show_launcher = no
activity_version = 1
mime_types = text/plain;application/zip
license = GPLv2+

Para probar la Actividad, descarga el libro Project Gutenberg al Diario (Journal), ábrelo con la última versión de Read Etexts III y luego comparte la Actividad con un segundo usuario que tenga la Actividad instalada pero no ejecutándose. El segundo usuario debe aceptar la invitación que le aparece en su vista Vecindario. Cuando el segundo usuario acepta, Read Etexts III se iniciará y copiará el libro desde el primer usuario a través de la red y lo cargará. La Actividad primero mostrará una pantalla en blanco, pero luego una barra de progreso aparecerá debajo de la barra de herramientas (toolbar) y mostrará el progreso de la copia. Cuando se termine de hacer la copia, la primer página del libro se mostrará en pantalla.

Entonces, ¿cómo funciona esto? Miremos el código. Los primeros puntos de interés son las definiciones de clases que aparecen al principio: ReadHTTPRequestHandler, ReadHTTPServer, y ReadURLDownloader. Estas tres clases extienden (es una forma de decir que heredan código desde clases padre) clases provistas por el paquete sugar.network (paquete en el cual se encuentran agrupadas todas las clases que trabajan con los mecanismos de red de las XO). Estas clases proveen un Cliente HTTP para recibir el libro y un Servidor HTTP para enviar el libro.

Este es el código usado para enviar un libro:

    def shared_cb(self, activityid):
        """Callback when activity shared.

        Set up to share the document.

        """
        # We initiated this activity and have now shared it,
        # so by definition we have the file.
        logger.debug('Activity became shared')
        self.watfor_tubes()
        self.share_document()

    def share_document(self):
        """Share the document."""
        h = hash(self._activity_id)
        port = 1024 + (h % 64511)
        logger.debug(
            'Starting HTTP server on port %d', port)
        self.fileserver = ReadHTTPServer(("", port),
            self.tempfile)

        # Make a tube for it
        chan = self._shared_activity.telepathy_tubes_chan
        iface = chan[telepathy.CHANNEL_TYPE_TUBES]
        self.fileserver_tube_id = iface.OfferStreamTube(
            READ_STREAM_SERVICE,
            {},
            telepathy.SOCKET_ADDRESS_TYPE_IPV4,
            ('127.0.0.1', dbus.UInt16(port)),
            telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST,
            0)

Notarás que haciendo un hash (hash es una función propia de Python que retorna un código casi único asociado a un objeto) del _activity_id se obtiene un número de puerto. Este puerto es usado por el Servidor HTTP y es pasado a Telepathy, la que lo pone a disposición como una Tubería de Flujo (Stream Tube). Del lado del receptor tenemos el siguiente código:

    def joined_cb(self, also_self):
        """Callback for when a shared activity is joined.

        Get the shared document from another participant.
        """
        self.watfor_tubes()
        gobject.idle_add(self.get_document)

    def get_document(self):
        if not self.want_document:
            return False

        # Assign a file path to download if one doesn't
        # exist yet
        if not self._jobject.file_path:
            path = os.path.join(self.get_activity_root(),
                'instance',
                'tmp%i' % time.time())
        else:
            path = self._jobject.file_path

        # Pick an arbitrary tube we can try to download the
        # document from
        try:
            tube_id = self.unused_download_tubes.pop()
        except (ValueError, KeyError), e:
            logger.debug(
                'No tubes to get the document from '
                'right now: %s',
                e)
            return False

        # Avoid trying to download the document multiple
        # times at once
        self.want_document = False
        gobject.idle_add(self.download_document,
            tube_id, path)
        return False

    def download_document(self, tube_id, path):
        chan = self._shared_activity.telepathy_tubes_chan
        iface = chan[telepathy.CHANNEL_TYPE_TUBES]
        addr = iface.AcceptStreamTube(tube_id,
            telepathy.SOCKET_ADDRESS_TYPE_IPV4,
            telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST,
            0,
            utf8_strings=True)
        logger.debug(
            'Accepted stream tube: listening address is %r',
            addr)
        assert isinstance(addr, dbus.Struct)
        assert len(addr) == 2
        assert isinstance(addr[0], str)
        assert isinstance(addr[1], (int, long))
        assert addr[1] > 0 and addr[1] < 65536
        port = int(addr[1])

        self.progressbar.show()
        getter = ReadURLDownloader(
            "http://%s:%d/document"
            % (addr[0], port))
        getter.connect("finished",
            self.download_result_cb, tube_id)
        getter.connect("progress",
            self.download_progress_cb, tube_id)
        getter.connect("error",
            self.download_error_cb, tube_id)
        logger.debug(
            "Starting download to %s...", path)
        getter.start(path)
        self.download_content_length = \
            getter.get_content_length()
        self.download_content_type = \
            getter.get_content_type()
        return False

    def download_progress_cb(self, getter,
        bytes_downloaded, tube_id):
        if self.download_content_length > 0:
            logger.debug(
                "Downloaded %u of %u bytes from tube %u...",
                bytes_downloaded,
                self.download_content_length,
                tube_id)
        else:
            logger.debug(
                "Downloaded %u bytes from tube %u...",
                bytes_downloaded, tube_id)
        total = self.download_content_length
        self.set_downloaded_bytes(bytes_downloaded,
            total)
        gtk.gdk.threads_enter()
        while gtk.events_pending():
            gtk.main_iteration()
        gtk.gdk.threads_leave()

    def download_error_cb(self, getter, err, tube_id):
        self.progressbar.hide()
        logger.debug(
            "Error getting document from tube %u: %s",
            tube_id, err)
        self.alert(_('Failure'),
            _('Error getting document from tube'))
        self.want_document = True
        self.download_content_length = 0
        self.download_content_type = None
        gobject.idle_add(self.get_document)

    def download_result_cb(self, getter, tempfile,
        suggested_name, tube_id):
        if self.download_content_type.startswith(
            'text/html'):
            # got an error page instead
            self.download_error_cb(getter,
                'HTTP Error', tube_id)
            return

        del self.unused_download_tubes

        self.tempfile = tempfile
        file_path = os.path.join(self.get_activity_root(),
            'instance',
            '%i' % time.time())
        logger.debug(
            "Saving file %s to datastore...", file_path)
        os.link(tempfile, file_path)
        self._jobject.file_path = file_path
        datastore.write(self._jobject,
            transfer_ownership=True)

        logger.debug(
            "Got document %s (%s) from tube %u",
            tempfile, suggested_name, tube_id)
        self.is_received_document = True
        self.read_file(tempfile)
        self.save()
        self.progressbar.hide()

Telepathy nos proporciona la dirección y el número de puerto asociado a una Tubería de Flujos (Stream Tube) y configuramos el Cliente HTTP para que lea desde alli. El cliente lee el archivo por porciones y después de leer cada porción llama al método download_progress_cb() y entonces podemos actualizar una barra de progreso para mostrar el progreso de la descarga. Hay también métodos para el caso en que se produzca un error de descarga y para cuando la descarga haya terminado.

La clase ReadURLDownloader no es sólo útil para transferir archivos por medio de una Tubería de Flujos (Stream Tube), también puede utilizarse para interactuar con sitios web (websites) y servicios web (web services). Mi Actividad Get Internet Archive Books usa esta clase para este propósito.

La única pieza restante es el código que controla la obtención de datos desde la Tubería de Flujo. En este código, adaptado de la Actividad Read (Leer), tan pronto como una instancia de la Actividad recibe un libro cambia de receptora a emisora y ofrece el libro para compartir, de este modo la Actividad podría tener varias Tuberías desde donde obtener el libro:

READ_STREAM_SERVICE = 'read-etexts-activity-http'

    ...

    def watfor_tubes(self):
        """Watch for new tubes."""
        tubes_chan = self._shared_activity.\
            telepathy_tubes_chan

        tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
            connect_to_signal(
            'NewTube',
            self.new_tube_cb)
        tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
            ListTubes(
            reply_handler=self.list_tubes_reply_cb,
            error_handler=self.list_tubes_error_cb)

    def new_tube_cb(self, tube_id, initiator,
        tube_type, service, params, state):
        """Callback when a new tube becomes available."""
        logger.debug(
            'New tube: ID=%d initator=%d type=%d service=%s '
            'params=%r state=%d', tube_id, initiator,
            tube_type,
            service, params, state)
        if service == READ_STREAM_SERVICE:
            logger.debug('I could download from that tube')
            self.unused_download_tubes.add(tube_id)
            # if no download is in progress,
            # let's fetch the document
            if self.want_document:
                gobject.idle_add(self.get_document)

    def list_tubes_reply_cb(self, tubes):
        """Callback when new tubes are available."""
        for tube_info in tubes:
            self.new_tube_cb(*tube_info)

    def list_tubes_error_cb(self, e):
        """Handle ListTubes error by logging."""
        logger.error('ListTubes() failed: %s', e)

La constante READ_STREAM_SERVICE está definida casi al principio del inicio del archivo fuente.

Utilizar tuberías D-Bus (D-Bus Tubes)

D-Bus es una forma de soportar IPC, o Comunicacion Inter-Procesos (Inter-Process Communication), el cual fue desarrollado por el entorno de trabajo GNOME (desktop environment). La idea de IPC es permitir a dos programas que se ejecutan simultáneamente comunicarse entre sí y ejecutar código del otro. GNOME usa D-Bus para proporcionar comunicación entre el entorno de trabajo y los programas que se ejecutan en él, y también entre GNOME y el sistema operativo. Una Tubería D-Bus (D-Bus Tube) es como Telepathy hace posible que una instancia de una Actividad que se ejecuta en una computadora pueda ejecutar métodos en otra instancia de la misma Actividad ejecutándose en otra computadora diferente. En lugar de sólo enviar simples mensajes de texto entre las computadoras o hacer transferencias de archivos, sus Actividades pueden compartirse de forma segura. Esto significa que la Actividad puede permitir a varias personas trabajar juntas en la misma tarea.

Nunca escribí una Actividad que use Tuberías D-Bus pero muchos otros lo han hecho. Vamos a dar un vistazo al código de dos de ellos: Scribble escrito por Sayamindu Dasgupta y Batalla Naval, escrito por Gerard J. Cerchio y Andrés Ambrois, el cual fue escrito para el Ceibal Jam.

Scribble es un programa para dibujar que permite a varias personas trabajar en el mismo dibujo al mismo tiempo. En lugar de permitirte seleccionar el color con el cual vas a dibujar, éste usa los colores de frente y fondo de tu ícono de Buddy (la figura del XO) para dibujar con ellos. De esta forma con varias personas dibujando figuras al mismo tiempo es fácil saber quién dibujó qué. Si te unes a una Actividad Scribble esta actualizará tu pantalla de forma que tu dibujo coincida el de cualquier otro usuario. Scribble en acción se ve de la siguiente manera:


Batalla Naval es una versión del clásico juego Battleship (Batalla Naval). Cada jugador tiene dos grillas: una de ellas para ubicar sus propios barcos (en realidad la computadora posiciona al azar los barcos por ti) y otra grilla en blanco representando el área donde se encuentran los barcos de tu oponente. Tu no puedes ver los barcos de tu oponente y el no puede ver los tuyos. Puedes hacer clic en la grilla de tu oponente (la de la derecha) para indicar donde quieres que tu artillería dispare. Cuando lo haces se ilumina el recuadro correspondiente en tu grilla y el correspondiente en la de tu oponente. Si el cuadrado que seleccionaste corresponde a un cuadrado donde tu oponente tiene un barco, ese cuadro se mostrará en un color diferente. El objetivo del juego es encontrar todos los cuadrados donde están ubicados los barcos de tu oponente antes de que él encuentre los tuyos. El juego en acción se ve así:

Sugiero que descargues la versión más reciente de estas dos Actividades desde Gitorious utilizando estos comandos:

mkdir scribble
cd scribble
git clone git://git.sugarlabs.org/scribble/mainline.git
cd ..
mkdir batallanaval
cd batallanaval
git clone git://git.sugarlabs.org/batalla-naval/mainline.git

Será necesario que realices algunas configuraciones para lograr ejecutar estas aplicaciones en el emulador-sugar. Scribble requiere el componente goocanvas de GTK y los componentes Python que se incluyen con ellos. Estos no fueron instalados por defecto en Fedora 10 pero se pueden instalar por medio de Añadir/Remover Programas (Add/Remove Programs) desde el menú Sistema (System) en GNOME. Batalla Naval no incluye el archivo setup.py, pero esto es fácil de solucionar ya que cada setup.py es idéntico. Copia alguno de los que vienen en los ejemplos del libro dentro del directorio mainline/BatallaNaval.activity y ejecuta ./setup.py dev en ambas Actividades.

Estas Actividades utilizan diferentes estrategias de colaboración. Scribble crea líneas de código Python el cual se pasa a todos los Buddies y cada Buddy usa la función exec para ejecutar los comandos. Este es el código que dibuja un círculo:

    def process_item_finalize(self, x, y):
        if self.tool == 'circle':
            self.cmd = "goocanvas.Ellipse(
                parent=self._root,
                center_x=%d,
                center_y=%d, radius_x = %d,
                radius_y = %d,
                fill_color_rgba = %d,
                stroke_color_rgba = %d,
                title = '%s')" % (self.item.props.center_x,
                self.item.props.center_y,
                self.item.props.radius_x,
                self.item.props.radius_y,
                self._fill_color,
                self._stroke_color, self.item_id)
...

    def process_cmd(self, cmd):
        #print 'Processing cmd :' + cmd
        exec(cmd)
        #FIXME: Ugly hack, but I'm too lazy to
        # do this nicely

        if len(self.cmd_list) > 0:
            self.cmd_list += (';' + cmd)
        else:
            self.cmd_list = cmd

La variable cmd_list se usa para crear una lista de cadenas de texto que contienen todos los comandos que se ejecutaron hasta ese momento. Cuando un nuevo Buddy se une a la Actividad, esta envía la variable para que se ejecuten todos los comandos anteriores, de esta forma el área de dibujo del nuevo usuario tiene el mismo contenido que los otros Buddies.

Este es un enfoque interesante, pero podrías hacer lo mismo utilizando Canales de Texto (TextChannel) por lo que éste no es necesariamente el mejor uso que se puede hacer de las Tuberías D-Bus (D-Bus Tubes). Batalla Naval hace uso de D-Bus de una forma más típica.

Cómo funcionan más o menos las tuberías D-Bus

D-Bus permite que dos programas que se están ejectando se envíen mensajes entre si. Los programas tienen que estar ejecutándose en la misma computadora. Enviar un mensaje es una forma indirecta de tener un programa que ejecute código en otro programa. Un programa define el tipo de mensajes que está dispuesto a recibir y ejecutar. En el caso de Batalla Naval se define un mensaje similar a este: "dime a que cuadrado quieres disparar y te haré conocer si uno de mis barcos o parte de alguno de ellos está en ese cuadrado". El primer programa realmente no ejecuta nada en el segundo, pero el resultado final es similar. Las Tuberías D-Bus son una forma de habilitar que D-Bus envíe mensajes como estos a un programa que se ejecuta en otra computadora.

Piensa por un minuto como podrías hacer que un programa en una computadora ejecute código en otro programa que se está ejecutando en otra computadora. Por supuesto que tendría que utilizar la red. Todo el mundo está familiarizado con el envío de datos por medio de una red. Pero en este caso tendría que enviar código de programa por la red. Deberías ser capaz de decirle al programa que se está ejecutando en la segunda computadora que código quieres que se ejecute. Necesitarás enviar una invocación a un método y todos los parámetros que necesita el método para que se ejecute correctamente, adicionalmente necesitarás una forma de obtener un valor de retorno.

¿No es parecido a lo que hace Scribble en el código que recién miramos? ¿Podríamos hacer que nuestro código haga algo parecido a esto?

Por supuesto si haces esto todo programa en el que quieras ejecutar código remotamente deberá estar escrito para que pueda tratar con esto. Si tienes un abanico de programas que quieres que hagan esto, necesitarás alguna forma de permitir a los programas conocer cuales solicitudes fueron hechas para esto. Sería genial si hubiera un programa ejecutándose en cada computador que se encargue de hacer conexiones de red, convertir las invocaciones a los métodos en datos que puedan ser enviados por medio de la red y luego convertir esos datos nuevamente en invocaciones a métodos y ejecutar dichas invocaciones, además de enviar cualquier valor de retorno hacia el origen de la invocación. Este programa debería ser capaz de conocer en que programa quieres ejecutar el código y ver si la llamada al método se está ejecutando allí. El programa debería estar corriendo todo el tiempo, y sería realmente muy bueno si ejecutar un método en un programa remoto fuera tan simple como ejecutar un método en mi propio programa.

Tal como lo supones, lo que hemos descrito es más o menos lo que son las Tuberías D-Bus. Existen artículos que explican como funcionan en detalle, pero no es necesario conocer como funcionan para usarlas. Necesitas conocer algunas cosas sobre ellas. Primero necesitas saber como usar las Tuberías D-Bus para hacer objetos en tu Actividad que estén disponibles para ser usados por otras instancias de esa Actividad ejecutándose en alguna parte.

Una Actividad que necesita usar Tuberías D-Bus debe definir sobre que tipo de mensajes va a actuar, en efecto que métodos específicos en el programa están disponibles para este uso. Todas las actividades que usan Tuberías D-Bus tienen constantes similares a estas:

SERVICE = "org.randomink.sayamindu.Scribble"
IFACE = SERVICE
PATH = "/org/randomink/sayamindu/Scribble"

Estas son las constantes utilizadas por la Actividad Scribble. La primera constante, llamada SERVICE (Servicio), representa el nombre de bus (bus name) de la Actividad. También se lo llama nombre bien-conocido (well-known name) porque utiliza un nombre de dominio invertido (reversed domain name) como parte del nombre. En este caso Sayamindu Dasgupta tiene un sitio web en http://sayamindu.randomink.org entonces invierte las palabras de esta URL separadas por un punto para formar la primera parte del nombre del bus. No es necesario tener un nombre de dominio propio antes de poder crear un nombre de bus (bus name). Puedes utilizar org.sugarlabs.NombreDeTuActividad si quieres. El punto es que el nombre de bus debe ser único y por convención se hace más sencillo hacerlo utilizando un nombre de dominio inverso.

La constante PATH representa la carpeta del objeto (object path). Se ve como el nombre del bus con barras separando las palabras en lugar de los puntos. Para la mayoría de las Actividades es exactamente como debería ser, pero es posible para una aplicación exponer más de un objeto hacia el D-Bus, en ese caso cada objeto expuesto debería tener su propio nombre único, por convención palabras separadas por barras (slashes “/”).

La tercer constante es IFACE, la cual es nombre de interfaz (interface name). Una interfaz es una colección de señales y métodos relacionados, identificados por un nombre que utiliza la misma convención que el nombre de bus. En el ejemplo de arriba y probablemente en la mayoría de las Actividades que usan Tuberías D-Bus, el nombre de la interfaz y el nombre de bus son idénticos.

Entonces, ¿qué es una señal? Una señal es similar a un método pero en lugar de tener un programa en ejecución que llama a un método en otro programa, una señal es de difusión masiva (broadcast). En otras palabras, en lugar de ejecutar un método en un solo programa este ejecuta el mismo método en varios programas en ejecución, de hecho en cada programa en ejecución que tenga este método conectado a través de D-Bus. Una señal puede pasar datos a la invocación de un método pero no puede recibir ningún valor de retorno. Es como una estación de radio que emite música para todos los que estén sintonizando la emisora. El flujo de información es en un solo sentido.

Por supuesto una estación de radio recibe llamadas de los radioescuchas. Un conductor puede pasar una nueva canción e invitar a los oyentes a llamar a la estación y decir que piensan sobre la misma. La llamada de teléfono es una vía de comunicación bidireccional entre el conductor y el radioescucha, pero es iniciado mediante una llamada de difusión masiva hacia todos los escuchas. De la misma forma tu Actividad podría utilizar una señal para invitar a todos los escuchas (Buddies) a usar un método para llamar nuevamente a la Actividad emisora, entonces este método puede proporcionar y recibir información.

Los métodos D-Bus y las señales tienen firmas (signatures). Una firma es una descripción de los parámetros pasados a un método o señal incluyendo su tipo de dato (data types). Python no es un lenguaje fuertemente tipado (strongly typed)- en informática se usa el término fuertemente tipado cuando se refiere a que el tipo de una variable debe ser definido explícitamente). En un lenguaje fuertemente tipado cada variable tiene un tipo de datos el cual define qué es lo que esta puede hacer y/o contener. Los tipos de datos incluyen cosas como cadenas (strings), enteros (integers), enteros largos (long integers), números de punto flotante (floating point numbers), booleanos (booleans), etc. Cada uno de ellos puede ser usado para un propósito específico. Por cada instancia un booleano sólo puede contener uno de dos valoresVerdadero (True) o Falso (False). Una cadena puede ser usada para almacenar cadenas de caracteres, pero incluso si estos caracteres representan un número no es posible utilizar las cadenas para realizar cálculos. Lo que necesitas hacer es convertir la cadena en uno de los tipos numéricos. Un entero puede contener enteros hasta cierto tamaño, y un entero largo puede contener enteros de un tamaño mucho mayor. Un número de punto flotante es un número con un punto decimal en notación científica. Esto suele ser usado para realizar cálculos aritméticos, los cuales requieren resultados redondeados.

 

En Python puedes poner cualquier cosa en cualquier variable y el lenguaje por si mismo decidirá como gestionar los mismos. Para hacer que Python trabaje con D-Bus, el cual requiere variables fuertemente tipadas, que Python no tiene, es necesario tener un forma de decirle al D-Bus qué tipo de variables debería tener para pasárselas a un método. Puedes hacer esto por medio del uso de una cadena de firma (signature string) como un argumento al método o señal. Los métodos tienen dos cadenas: una in_signature (podría decirse que es una firma de entrada) y una out_signature (es la firma de salida). Las señales sólo tienen un parámetro firma (signature). Algunos ejemplos de cadenas de firma:

ii dos parámetros, ambos enteros (integer)
sss Tres parámetros, todos cadenas (string)
ixd

Tres parámetros, un entero (integer), un entero largo (long) y un número de punto flotante de doble precisión (double).

a(ssiii)

Un conjunto (array) donde cada elemento del conjunto es una tupla que contiene dos cadenas y tres enteros.

Puedes encontrar más información sobre cadenas de firma en el tutorial sobre dbus-python en: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html.

Hello Mesh (Hola malla) y Friends (Amigos)

Si estudias el código fuente de algunas Actividades compartidas puedes darte cuenta que muchas de ellas contienen métodos casi idénticos, como si todas ellas fueran una copia de la misma fuente. De hecho es probable que la mayoría sí lo haya sido. La Actividad Hello Mesh fue desarrollada para ser un ejemplo de como usar Tuberías D-Bus en una actividad compartida. Es clásico ver en los libros de programación que el primer ejemplo de programa es uno que imprime las palabras “Hola Mundo” (Hello World) por consola o muestra la frase en una ventana. Siguiendo esta tradición Hello Mesh es un programa que no hace mucho más. Podrás encontrar el código fuente en Gitorious en http://git.sugarlabs.org/projects/hello-mesh.

Hello Mesh es ampliamente copiado porque demuestra como hacer cosas que todas las Actividades compartidas necesitan hacer. Cuando tienes una Actividad compartida debes ser capaz de hacer dos cosas:

  • Enviar informacion o comandos a otras instancias de tu Actividad.

  • Proporcionar a los Buddies que se unan a tu Actividad una copia del estado actual de la Actividad.

Es posible hacer esto utilizando dos señales y un método:

  • Una señal llamada Hello() que alguien que se une a la Actividad envía a todos los participantes. El método Hello() no tiene parámetros.

  • Un método llamado World() que las instancias que reciben Hello() envían como respuesta al emisor que acaba de unirse. Este método toma una cadena de texto como argumento, el cual es utilizado para representar el estado actual de la Actividad.

  • Otra señal llamada SendText() que envía mensajes de texto a todos los participantes. Estos representan una actualización de estado de la Actividad compartida. En el caso de Scribble este debería informar a las otras instancias que la propia instancia acaba de dibujar una nueva figura.

En lugar de estudiar la Actividad Hello Mesh por sí misma, prefiero que demos un vistazo al código derivado de ésta, usado en la Actividad Batalla Naval.

Esta Actividad tiene la particularidad de poder ejecutarse tanto como Actividad o como un programa autónomo de Python. El programa autónomo no soporta compartir y corre en una ventana. La clase Activity es una subclase de Window, entonces cuando el código se está ejecutando en modo autónomo la función init() en BatallaNaval.py obtiene una ventana, y cuando el mismo código se ejecuta como Actividad la instancia de la clase BatallaNavalActivity es pasado a init():

from sugar.activity.activity import Activity, ActivityToolbox
import BatallaNaval
from Collaboration import CollaborationWrapper

class BatallaNavalActivity(Activity):
    ''' The Sugar class called when you run this
        program as an Activity. The name of this
        class file is marked in the
        activity/activity.info file.'''

    def __init__(self, handle):
        Activity.__init__(self, handle)

        self.gamename = 'BatallaNaval'

        # Create the basic Sugar toolbar
        toolbox = ActivityToolbox(self)
        self.set_toolbox(toolbox)
        toolbox.show()

        # Create an instance of the CollaborationWrapper
        # so you can share the activity.
        self.colaboracion = CollaborationWrapper(self)

        # The activity is a subclass of Window, so it
        # passses itself to the init function
        BatallaNaval.init(False, self)

Otra habilidad que tiene BatallaNaval es que todo el código que posibilita la colaboración se encuentra en su propia clase CollaborationWrapper (El nombre de esta clase es aproximadamente Contenedor de Colaboración) que toma la instancia de la clase BatallNavalActivity en su constructor. Esto separa el código de colaboración del resto del programa. Este es el código de la clase CollaborationWrapper.py:

import logging

from sugar.presence import presenceservice
import telepathy
from dbus.service import method, signal
# In build 656 Sugar lacks sugartubeconn
try:
  from sugar.presence.sugartubeconn import \
      SugarTubeConnection
except:
  from sugar.presence.tubeconn import TubeConnection as \
      SugarTubeConnection
from dbus.gobject_service import ExportedGObject

''' In all collaborative Activities in Sugar we are
    made aware when a player enters or leaves. So that
    everyone knows the state of the Activity we use
    the methods Hello and World. When a participant
    enters Hello sends a signal that reaches
    all participants and the participants
    respond directly using the method "World",
    which retrieves the current state of the Activity.
    After the updates are given then the signal
    Play is used by each participant to make his move.
    In short this module encapsulates the logic of
    "collaboration" with the following effect:
        - When someone enters the collaboration
          the Hello signal is sent.
        - Whoever receives the Hello signal responds
          with World
        - Every time someone makes a move he uses
          the method Play giving a signal which
          communicates to each participant
          what his move was.
'''

SERVICE = "org.ceibaljam.BatallaNaval"
IFACE = SERVICE
PATH = "/org/ceibaljam/BatallaNaval"

logger = logging.getLogger('BatallaNaval')
logger.setLevel(logging.DEBUG)

class CollaborationWrapper(ExportedGObject):
    ''' A wrapper for the collaboration methods.
        Get the activity and the necessary callbacks.
 '''

    def __init__(self, activity):
        self.activity = activity
        self.presence_service = \
            presenceservice.get_instance()
        self.owner = \
            self.presence_service.get_owner()

    def set_up(self, buddy_joined_cb, buddy_left_cb,
        World_cb, Play_cb, my_boats):
        self.activity.connect('shared',
            self._shared_cb)
        if self.activity._shared_activity:
            # We are joining the activity
            self.activity.connect('joined',
                self._joined_cb)
            if self.activity.get_shared():
                # We've already joined
                self._joined_cb()

        self.buddy_joined = buddy_joined_cb
        self.buddy_left = buddy_left_cb
        self.World_cb = World_cb
        # Called when someone passes the board state.
        self.Play_cb = Play_cb
        # Called when someone makes a move.

        # Submitted by making World on a new partner
        self.my_boats = [(b.nombre, b.orientacion,
            b.largo, b.pos[0],
            b.pos[1]) for b in my_boats]
        self.world = False
        self.entered = False

    def _shared_cb(self, activity):
        self._sharing_setup()
        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
            OfferDBusTube(
            SERVICE, {})
        self.is_initiator = True

    def _joined_cb(self, activity):
        self._sharing_setup()
        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
            ListTubes(
            reply_handler=self._list_tubes_reply_cb,
            error_handler=self._list_tubes_error_cb)
        self.is_initiator = False

    def _sharing_setup(self):
        if self.activity._shared_activity is None:
            logger.error(
                'Failed to share or join activity')
            return

        self.conn = \
            self.activity._shared_activity.telepathy_conn
        self.tubes_chan = \
            self.activity._shared_activity.telepathy_tubes_chan
        self.text_chan = \
            self.activity._shared_activity.telepathy_text_chan

        self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].\
            connect_to_signal(
            'NewTube', self._new_tube_cb)

        self.activity._shared_activity.connect(
            'buddy-joined',
            self._buddy_joined_cb)
        self.activity._shared_activity.connect(
            'buddy-left',
            self._buddy_left_cb)

        # Optional - included for example:
        # Find out who's already in the shared activity:
        for buddy in \
            self.activity._shared_activity.\
                get_joined_buddies():
            logger.debug(
                'Buddy %s is already in the activity',
                buddy.props.nick)

    def participant_change_cb(self, added, removed):
        logger.debug(
            'Tube: Added participants: %r', added)
        logger.debug(
            'Tube: Removed participants: %r', removed)
        for handle, bus_name in added:
            buddy = self._get_buddy(handle)
            if buddy is not None:
                logger.debug(
                    'Tube: Handle %u (Buddy %s) was added',
                    handle, buddy.props.nick)
        for handle in removed:
            buddy = self._get_buddy(handle)
            if buddy is not None:
                logger.debug('Buddy %s was removed' %
                    buddy.props.nick)
        if not self.entered:
            if self.is_initiator:
                logger.debug(
                    "I'm initiating the tube, "
                    "will watch for hellos.")
                self.add_hello_handler()
            else:
                logger.debug(
                    'Hello, everyone! What did I miss?')
                self.Hello()
        self.entered = True


    # This is sent to all participants whenever we
    # join an activity
    @signal(dbus_interface=IFACE, signature='')
    def Hello(self):
        """Say Hello to whoever else is in the tube."""
        logger.debug('I said Hello.')

    # This is called by whoever receives our Hello signal
    # This method receives the current game state and
    # puts us in sync with the rest of the participants.
    # The current game state is represented by the
    # game object
    @method(dbus_interface=IFACE, in_signature='a(ssiii)',
        out_signature='a(ssiii)')
    def World(self, boats):
        """To be called on the incoming XO after
        they Hello."""
        if not self.world:
            logger.debug('Somebody called World on me')
            self.world = True   # Instead of loading
                                # the world, I am
                                # receiving play by
                                # play.
            self.World_cb(boats)
            # now I can World others
            self.add_hello_handler()
        else:
            self.world = True
            logger.debug(
                "I've already been welcomed, doing nothing")
        return self.my_boats

    @signal(dbus_interface=IFACE, signature='ii')
    def Play(self, x, y):
        """Say Hello to whoever else is in the tube."""
        logger.debug('Running remote play:%s x %s.', x, y)

    def add_hello_handler(self):
        logger.debug('Adding hello handler.')
        self.tube.add_signal_receiver(self.hello_signal_cb,
            'Hello', IFACE,
            path=PATH, sender_keyword='sender')
        self.tube.add_signal_receiver(self.play_signal_cb,
            'Play', IFACE,
            path=PATH, sender_keyword='sender')

    def hello_signal_cb(self, sender=None):
        """Somebody Helloed me. World them."""
        if sender == self.tube.get_unique_name():
            # sender is my bus name, so ignore my own signal
            return
        logger.debug('Newcomer %s has joined', sender)
        logger.debug(
            'Welcoming newcomer and sending them '
            'the game state')

        self.other = sender

        # I send my ships and I get theirs in return
        enemy_boats = self.tube.get_object(self.other,
            PATH).World(
            self.my_boats, dbus_interface=IFACE)

        # I call the callback World, to load the enemy ships
        self.World_cb(enemy_boats)

    def play_signal_cb(self, x, y, sender=None):
        """Somebody placed a stone. """
        if sender == self.tube.get_unique_name():
            return  # sender is my bus name,
                    # so ignore my own signal
        logger.debug('Buddy %s placed a stone at %s x %s',
            sender, x, y)
        # Call our Play callback
        self.Play_cb(x, y)
        # In theory, no matter who sent him

    def _list_tubes_error_cb(self, e):
        logger.error('ListTubes() failed: %s', e)

    def _list_tubes_reply_cb(self, tubes):
        for tube_info in tubes:
            self._new_tube_cb(*tube_info)

    def _new_tube_cb(self, id, initiator, type,
        service, params, state):
        logger.debug('New tube: ID=%d initator=%d '
            'type=%d service=%s '
            'params=%r state=%d', id, initiator, '
            'type, service, params, state)
        if (type == telepathy.TUBE_TYPE_DBUS and
            service == SERVICE):
            if state == telepathy.TUBE_STATE_LOCAL_PENDING:
                self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES]
                    .AcceptDBusTube(id)
            self.tube = SugarTubeConnection(self.conn,
                self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES],
                id, group_iface=
                    self.text_chan[telepathy.\
                        CHANNEL_INTERFACE_GROUP])
            super(CollaborationWrapper,
                self).__init__(self.tube, PATH)
            self.tube.watparticipants(
                self.participant_change_cb)

    def _buddy_joined_cb (self, activity, buddy):
        """Called when a buddy joins the shared
        activity. """
        logger.debug(
            'Buddy %s joined', buddy.props.nick)
        if self.buddy_joined:
            self.buddy_joined(buddy)

    def _buddy_left_cb (self, activity, buddy):
        """Called when a buddy leaves the shared
        activity. """
        if self.buddy_left:
            self.buddy_left(buddy)

    def _get_buddy(self, cs_handle):
        """Get a Buddy from a channel specific handle."""
        logger.debug('Trying to find owner of handle %u...',
            cs_handle)
        group = self.text_chan[telepathy.\
            CHANNEL_INTERFACE_GROUP]
        my_csh = group.GetSelfHandle()
        logger.debug(
            'My handle in that group is %u', my_csh)
        if my_csh == cs_handle:
            handle = self.conn.GetSelfHandle()
            logger.debug('CS handle %u belongs to me, %u',
                cs_handle, handle)
        elif group.GetGroupFlags() & \
            telepathy.\
            CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES:
            handle = group.GetHandleOwners([cs_handle])[0]
            logger.debug('CS handle %u belongs to %u',
                cs_handle, handle)
        else:
            handle = cs_handle
            logger.debug('non-CS handle %u belongs to itself',
                handle)
            # XXX: deal with failure to get the handle owner
            assert handle != 0
        return self.presence_service.\
            get_buddy_by_telepathy_handle(
            self.conn.service_name,
            self.conn.object_path, handle)

La mayor parte del código de arriba es similar a lo que se ha visto en otros ejemplos, y la mayor parte de este puede ser usado tal cual en cualquier Actividad que necesite hacer llamadas D-Bus. Por esta razón vamos a enfocarnos en el código que específicamente utiliza D-Bus. El punto lógico de inicio es el método Hello(). Por supuesto no existe nada mágico en el nombre "Hello". Hello Mesh intenta ser un “Hola Mundo” (Hello World) usando Tuberías D-Bus, entonces por convención las palabras “Hola” (Hello) y “Mundo”(World) tendrán que ser usadas. El método Hello() es enviado a todas las instancias de la Actividad para informarles que una nueva instancia de la misma esta lista para recibir información del estado de la Actividad compartida. Tu Actividad probablemente necesitará algo similar, pero sientete libre de nombrarlo de otra forma, y si estás escribiendo código para alguna tarea definitivamente deberías darles otro nombre:

    # This is sent to all participants whenever we
    # join an activity
    @signal(dbus_interface=IFACE, signature='')
    def Hello(self):
        """Say Hello to whoever else is in the tube."""
        logger.debug('I said Hello.')

    def add_hello_handler(self):
        logger.debug('Adding hello handler.')
        self.tube.add_signal_receiver(
            self.hello_signal_cb,
            'Hello', IFACE,
            path=PATH, sender_keyword='sender')
...

    def hello_signal_cb(self, sender=None):
        """Somebody Helloed me. World them."""
        if sender == self.tube.get_unique_name():
            # sender is my bus name,
            # so ignore my own signal
            return
        logger.debug('Newcomer %s has joined', sender)
        logger.debug(
            'Welcoming newcomer and sending them '
            'the game state')

        self.other = sender

        # I send my ships and I returned theirs
        enemy_boats = self.tube.get_object(
            self.other, PATH).World(
            self.my_boats, dbus_interface=IFACE)

        # I call the callback World, to load the enemy ships
        self.World_cb(enemy_boats)

Lo más interesante en este código es la siguiente línea en la cual Python invoca a un Decorator (Decorator es una forma avanzada de realizar herencia en Python):

    @signal(dbus_interface=IFACE, signature='')

Cuando insertas @signal delante del nombre de un método causa el efecto de agregar los dos parámetros mostrados a la llamada al método siempre que es invocado, es decir cambia una llamada regular de un método por una llamada a una señal D-Bus. El parámetro signature es una cadena vacía que indica que esta invocación al método no tiene ningún parámetro. El método Hello() no hace nada cuando se ejecuta localmente, pero cuando es recibido por otras instancias de la Actividad hace que se ejecute el método World(), quien envía la posición de sus barcos y recibe la posicion de los barcos de los nuevos participantes como respuesta.

Batalla Naval aparentemente es un programa demostrativo. Es un juego de dos jugadores, pero en el código no existe nada que impida que más jugadores se unan al juego y no hay forma de controlarlos si lo hacen. Idealmente debería ser posible que sólo el primer jugador que se une a la Actividad y quien inició la Actividad participen del juego y hacer que el resto de los participantes que se unen a la Actividad sean solamente espectadores.

Ahora veremos como está hecho el método World():

    # This is called by whoever receives our Hello signal
    # This method receives the current game state and
    # puts us in sync with the rest of the participants.
    # The current game state is represented by the game
    # object
    @method(dbus_interface=IFACE, in_signature='a(ssiii)',
        out_signature='a(ssiii)')
    def World(self, boats):
        """To be called on the incoming XO after
        they Hello."""
        if not self.world:
            logger.debug('Somebody called World on me')
            self.world = True   # Instead of loading the world,
                                # I am receiving play by play.
            self.World_cb(boats)
            # now I can World others
            self.add_hello_handler()
        else:
            self.world = True
            logger.debug("I've already been welcomed, "
                "doing nothing")
        return self.my_boats

Aquí se muestra otro decorador (decorator), este convierte el método World() en una invocación a un método D-Bus. La firma (signature) es más interesante que la del método Hello(). Ésta define una llamada a un conjunto (array) de tuplas donde cada tupla está compuesta por dos cadenas y tres enteros. Cada elemento en el array representa una barco y sus atributos. World_cb apunta a un método en BatallaNaval.py, (y también lo hace Play_cb). Si estudias el código de init() en BatallaNaval.py podrás ver cómo sucede esto. World() es invocado desde el método hello_signal_cb() que acabamos de ver. Este es enviado a quien se une a la Actividad que nos envió anteriormente Hello() a nosotros.

Finalmente veremos la señal Play():

    @signal(dbus_interface=IFACE, signature='ii')
    def Play(self, x, y):
        """Say Hello to whoever else is in the tube."""
        logger.debug('Running remote play:%s x %s.', x, y)

    def add_hello_handler(self):
...
        self.tube.add_signal_receiver(self.play_signal_cb,
            'Play', IFACE,
            path=PATH, sender_keyword='sender')
...
    def play_signal_cb(self, x, y, sender=None):
        """Somebody placed a stone. """
        if sender == self.tube.get_unique_name():
            return  # sender is my bus name, so
                    # ignore my own signal
        logger.debug('Buddy %s placed a stone at %s x %s',
            sender, x, y)
        # Call our Play callback
        self.Play_cb(x, y)

Esta es una señal por lo que solamente se tiene una cadena firma, ésta indica que los parámetros de entrada son dos enteros.

Existen muchas formas de mejorar esta Actividad. Cuando se juega contra la computadora en modo no-compartido el juego sólo hace jugadas al azar (es decir cuando se juega contra la computadora no se hace uso de intelingencia artificial, característica fundamental de cualquier juego). El juego no limita el número de jugadores a dos, ni hace del resto espectadores del juego. No controla que los participantes ejecuten sus jugadas por turnos. Cuando un jugador termina de hundir todas las embarcaciones de su oponente no sucede nada que nos avise que ganamos la partida. Finalmente no se hace uso de gettext() para las cadenas de texto que se muestran en la Actividad lo que significa que esta no podría ser traducido a otros idiomas que no sea español.

Como es tradición en cualquier libro, dejaré que la realización de estas mejoras sea un ejercicio para el estudiante.

1

  1. Traducido Vladimir Castro, Bolivia.^


your comment:
name :
comment :

If you can't read the word, click here
word :