Modelos de negocio FLOSS, la universidad, el sector privado y el Estado

Distintos laboratorios de investigación y vinculación de mi facultad del área química y del área computación, junto a algunas empresas de software locales (VATES y otra) y del sector petroquímico (Petroquímica Rio Tercero) están conformando un "Consorcio Asociativo Público Privado (CAPP)" en el marco del programa FSTICS del Ministerio de Ciencia y Tecnología de la Nación, con el fin de desarrollar una plataforma de desarrollo I+D de software para la industria. Para que contextualicemos de que hablamos: el programa estipula un aporte estatal de hasta el 60% para un proyecto de hasta 38 millones de pesos.

Mi participación es minúscula y aledaña: el impulsor de este proyecto es el Dr. Martín Cismondi, director de mi proyecto integrador que se trata de un prototipo del (tipo de) software que se desarrollaría desde esta plataforma.

Cismondi es Phd en ingenieria quimica y sus investigaciones en el campo del equilibrio de fases tienen mucha aplicación en la industria. En particular en el petróleo, donde el impacto que puede tener mejorar el rendimiento de un proceso (por ejemplo de destilación) se mide en millones de morlacos.

Ayer tuve oportunidad de participar de una reunión, sirviendo de una especie de "traductor" entre los quimicos y los informáticos.

Si bien, repito, no tengo voz (mucho menos voto) en nada de lo que allí se resuelva, tengo interés en investigar y transmitir de la manera más clara posible otros modelos de negocio que, por lo que presencié ayer, están lejos de ser tenidos en cuenta. Me refiero, claro, a que, sobre todo (no muy increíblemente) desde la facultad, no se ve más allá de un modelo de negocio de software cerrado de venta por licencia.

Apunto algunas cuestiones sin orden:

- El nicho de mercado es bastante acotado: industrias de gran escala mayormente transnacionales. Separadamente tiene mucha utilidad y aceptación en centros de investigación académicos.

image1 Desde el grupo conformado, no existe conocimiento cabal de las soluciones de software específicas que ese mercado, el industrial, demanda. Sí se sabe que el -*expertise-* y el -*know how-* que el grupo de investigación de Cismondi tiene son el valor diferencial.

image2 No existe hoy peso específico suficiente, desde el punto de vista del producto existente, para competir con las empresas de software proveedoras de este nicho, sobre todo en los mercados de USA y Europa. Por ejemplo Aspen Tech.

image3 Es difícil, a priori inimaginable, desarrollar un producto genérico que satisfaga necesidades de diversos clientes potenciales. La especifidad de los problemas de cada industria se supone grande.

image4 Preguntas para hacerse ¿cuantos clientes reales existen en el mercado? ¿qué "llegada" se tiene con ellos? ¿que posibilidades reales de venderle un software cerrado existen, teniendo en cuenta que no se sabe cabalmente cuales son sus demandas? Supongiendo que se conoce un problema específico ¿cuanto sale desarrollar una solución ? ¿se puede correr el riesgo de desarrollarla sin tener asegurada su comercialización?

Soy un novato en el área negocios con FLOSS (y en negocios, a secas, también) pero lo que se me ocurre viene más o menos por este lado:

1. Generar una estrategia para una fuerte inserción en el ámbito académico internacional

  • Para esto es indispensable la libre disponibilidad del código fuente de manera que los métodos numérico-científicos sean transparentes, reproducibles y verificables. Ver este post en OpenScience Project.
  • Para evitar practicas predatorias sobre este trabajo, que alguien cierre y comercialice, hay que orientar a una licencia FLOSS vírica, donde los trabajos derivados mantengan la condicion libre.
  • De esta manera se apunta a constituir una comunidad tecnologico-científica donde el nucleo de desarrollo se mantendrá en los autores originales pero permitiendo y aprovechando código y feedback de los usuarios.
  • En esta etapa el beneficio no es a priori monetario y en cambio sí en cuanto a calidad del software, inserción y difusión del/los producto/s, fidelización de usuarios y prestigio del equipo de desarrollo.

2. La repercusión de la masa crítica generada abre la puerta al mercado por las siguientes razones:

  • Muchos de los académicos que utilicen y formen parte de un "comunidad" alrededor del sofware FLOSS desde las universidades o llevaran su conocimiento a la industria.
  • Tambien los investigadores dentro de la industria tienen vínculos fluidos con la comunidad científica, sobre todo en estas áreas de investigación de vanguardia de directa aplicabilidad.
  • El mercado a priori imposible de acceder se puede hacer más permeable: los responsables de investigación demandan la utilización del software en los procesos de la industria o sus labores particulares.
  • Si el costo para la industria es nulo o bajo y de calidad, hay muchas probabilidades de que sea aceptado.
  • La ventaja diferencial del cliente es evitar el vendor lock-in. Sin embargo, la ventaja competitiva del equipo de desarrollo original, por su -*know how-* adquirido es insuperable para cualquier potencial competidor.

3. Insertado en el mercado se generan posibilidades de lucro en dos áreas

  • Customización y extensión del software bajo requerimientos específicos. Entra en el marco de SaaS
  • Capacitación y entrenamiento a equipos técnicos de la industria y la academia, tanto en el área de aplicación (quimicos) como de desarrollodo de software.

Muchísimo material para leer sobre el tópico hay en este sitio. Yo empezaré en cuanto tenga tiempo. Invito también, de manera enfática, a leer la justificación de que el software desarrollado en el marco de mi proyecto integrador lo libero como Software Libre

Más allá del negocio

Además de lo expuesto se me cruzan cuestionamientos sobre el rol de la universidad y el estado en este escenario.

Como ciudadano y futuro profesional festejo la inversión que en los últimos años el Estado ha realizado para ciencia y tecnología. Apostar a generar valor en áreas tan estrategicas como el software es necesario, y plantearlo desde el punto de vista de fortalecer la industria nacional (pymes) es entonces doblemente valioso.

Pero...

¿son los laboratorios de investigación de las universidades actores que deben generar recursos por sí mismos? ¿A quien pertenece el conocimiento que se produce en la universidad? ¿A quién debe pertenecer? ¿La universidad actual responde a un proyecto de país ? ¿Existe tal cosa? ¿No debiera el Estado, formar parte societaria en los consorcios para los cuales está aportando grandes recursos, además de ser agente regulador? ¿quién y cómo regula la relación e intereses existentes entre los grupos de investigación y el sector privado? ¿no se puede prestar a malversaciones y abuso del sello de la universidad?

Bonito, feito pero efectivo

Hace 5 años que laburo en una organización de trabajo barrial. Como no recibimos aportes privados y rara vez del Estado (a veces presentamos proyectos a programas) siempre andamos buscando formas de sostener económicamente nuestras actividades.

Un recurso, no por viejo en desuso, es el del "bono contribución", que muchas veces incluye un sorteo con algún premio.

Además de la algo ingrata tarea de venderlos, hay que hacerlos. Alguno tiene uno de esos sellos numeradores (un mecanimismo que va incrementando el contador automáticamente ) pero igual cualquiera se vuelve un burócrata (más cuando hay que sellar dos veces - el bono y talón -).

Así que ayer, en 20 minutos hice Bonito, un programa feo pero efectivo.

Primero hice una plantilla en Inkscape donde entran 6 de estos bonitos en un A4.

Como el SVG es XML que es texto, la marca XXXX se puede reemplazar fácilmente por el número que corresponda. Yo quería que me quedaran así:

De esta manera, simplemente tengo que meter 6 broches a la izquierda y recortar, ya quedan ordenados para repartir entre los compañeros y compañeras.

Para algunas cosas, Inkscape se puede usar por línea de comandos, por ejemplo para convertir entre los formatos que soporta. Así paso el SVG con los números reemplazados a un PDF. Después concateno todos los PDF de una tanda (por ejemplo agrupados de a 10, como en el dibujo) con Ghostscript.

Acá el código:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#       bonito.py
#
#       Copyright 2010 Martin Gaitán <gaitan(at)gmail.com>
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

import sys, os
import subprocess
import glob

def markToNum(content, num, mark='XXXX'):
    num_as_string = '0' * (4 - len(str(num))) + str(num)
    return content.replace(mark, num_as_string, 2)

def main():

    if len(sys.argv) <= 1:
        print "Modo de uso:"
        print sys.argv[0] + " archivo.svg"
        sys.exit(1)
    else:
        with open(sys.argv[1]) as input:
            svg_content = input.read()

        index_from = int(raw_input('Desde [0]') or 0)
        number_by_page = int(raw_input('Num por pagina [6]') or 6)
        grouped_by =  int(raw_input('agrupar de a [20]') or 20)
        planchas = int(raw_input('Planchas [1]') or 1)


        for plancha in range(planchas):
            counter_from = index_from + plancha*number_by_page*grouped_by

            for pagina in range(grouped_by):
                page_content = svg_content
                for bono in range(number_by_page):
                    num_remplazo = counter_from + bono*grouped_by + pagina
                    page_content = markToNum(page_content, num_remplazo)

                with open('temp.svg', 'w') as output_svg:

                    output_svg.write(page_content)

                subprocess.call(["inkscape", "-f", 'temp.svg', '--export-dpi=150', '-A', 'temp%s.pdf' % ("0"*(4 - len(str(pagina))) + str(pagina)) ])

            generator = ['gs',
                         '-q',
                         '-sPAPERSIZE=a4',
                         '-dNOPAUSE',
                         '-dBATCH',
                         '-sDEVICE=pdfwrite',
                         '-sOutputFile=%s-%i-%i.pdf' % (sys.argv[1][:-4], counter_from, num_remplazo),] + \
                        ['temp%s.pdf' % ("0"*(4 - len(str(pagina))) + \
                            str(pagina)) for pagina in range(grouped_by)]

            subprocess.call(generator)

            for temp in glob.glob('temp*'):
                os.remove(temp)

if __name__ == '__main__':
    main()

Nada que no se pueda hacer con Bash, cierto, pero mucho más fácil de escribir (y de leer).

De paso, acá está la plantilla, por si a alguno le sirve.

Django Dash: hacé una aplicación web en 2 dias

Leemos en la web

The Django Dash is a chance for Django enthusiasts to flex their coding skills a little and put a fine point on “perfectionists with deadlines” by giving you a REAL deadline. 48 hours from start to stop to produce the best app you can and have a little fun in the process.

Suena a PyWeek [1], pero en 2 dias. ¿Será indicio de lo rápido que se programa con Django?

Atenti que la inscripción cierra el 8 de agosto (mañana).

Usando PubSub para un panel de mensajes

La aplicación que estoy desarrollando, GPEC, genera muchos mensajes que pueden ser útiles para el usuario.

En softwares con GUI’s sencillas suele utilizarse la barra de estado para mostrar mensajes contextuales e información sobre el resultado de una acción. Pero si estos mensajes son muchos y de diversa índole, este espacio puede no bastar, sobre todo por la volatilidad de la información que la barra de estado muestra.

Una solución posible es usar un panel con un ListCtrl de manera de poder agregar los mensajes quedando un registro completo y cronológico; un log propiamente dicho.

image0 Surge acá un detalle: si los mensajes se generan desde "cualquier parte" del programa, todas esas "partes" deberían tener referencia de la instancia del panel/widget de log.

Un ejemplo: todos las demostraciones de la aplicación de demo de wxPython tienen la siguiente estructura:

class TestPanel(wx.Panel):

    def __init__(self, parent, log):
        self.log = log
        wx.Panel.__init__(self, parent, -1)
        ...

        self.log.WriteText(' message ... ')

Esto vuelve la aplicación muy acoplada: la instancia log (que es un caja de texto en el demo) se pasea por distintos namespaces para estar disponible en todos lados.

Denotemos la falta de flexibilidad: ¿que pasa si queremos ’loggear’ mensajes desde un objeto donde no estaba previsto? ¿qué pasa si queremos cambiar el widget que muestra los mensajes y el método para anexar mensajes tiene otro nombre? ¿y si además de mostrarlos, con algunos los mensajes queremos hacer alguna otra cosa (ejecutar un simple sonido de alarma, por ejemplo) ?

PubSub

Una manera más elegante y eficiente es utilizar PubSub una implentación en Python del paradigma de publicación-suscripción.

System Message: WARNING/2 (<string>, line 56)

Explicit markup ends without a blank line; unexpected unindent.

Su implementación es trivial. Incluso viene incorporado dentro de wxPython.

El Publisher (generalmente importado como pub) envía mensajes (cualquier objeto python) asociados a un tópico (una cadena)

from wx.lib.pubsub import Publisher as pub

pub.sendMessage('log', ('ok', 'Ready! You can send any message from anywhere.') )

En este ejemplo, el tópico, que elegí arbritrariamente, es ’log’, y el mensaje es la tupla ('ok', 'Ready! You can send any message from anywhere.')

Del otro lado del mostrador, cualquiera puede suscribirse a los mensajes con determinado tópico y asociarlos a un método/función.

class LogMessagesPanel(wx.Panel):
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)

        pub.subscribe(self.OnAppendLog, 'log')

    def OnAppendLog(self, msg):
        data = msg.data<img105|center>
        #do your things with the data!

pub.subscribe bindea los mensajes con tópico ’log’ al método OnAppendLog pasando un objeto msg. Nuestro mensaje real, la tupla que enviamos, está en msg.data

Nada impide que sean muchos los objetos que envien mensajes con tópico ’log’ y muchos otros estén suscriptos a él. Y esto funciona sin importar dónde ocurra cada cosa! [1].

Como ejemplo completo dejo el panel log. Podés probarlo creando otro frame independiente que envie mensajes de log.

#       Log Panel: example of PubSub implementation
#
#       Copyright 2010 Martin Gaitán <gaitan(at)gmail.com>
#
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.


import wx
from wx.lib.pubsub import Publisher as pub
import time
import sys

from wx.lib.embeddedimage import PyEmbeddedImage

icons = {}
icons['ok'] = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAAK5QTFRF"
    "AAAA////2vO/+P3z+f31VJkEVJcEU5UEUpQEcc0GcMsGcMkGb8gGbcYGbcQGbMEGZLYFXKYF"
    "UJAET48ETo0ETYwEasAGddALd9ANeNERetETe9IVfdIXfdMZftMbf9Mci9cwlNpAlttElttF"
    "l9xHmtxLm91NnN1Pnd1Rnt5ToN9Xod9ZpuFhp+FjqOFlquJnr+RyseR0veiKwuqSy+2j4PTI"
    "9vzv9/zxidYs3fPCGN7g1AAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsT"
    "AAALEwEAmpwYAAAAB3RJTUUH2gcMBC0Irn+MQAAAAINJREFUGNNjYCAJ6Bui8vXkpAxQ+LJa"
    "ylx6CL6utKYGJyszkGVpAuZLaahzsYH4phYKxgwMOpJqatzsID6ThbIGt5G+hKoKDweIz8Ai"
    "YaGtIiOnrMIrxAwxjJlXXl1VSYlPmBlmPLOAuLIivwgzwkJzQV4xUWZkJ5mzi7CgOtqMBbcH"
    "AVouCiZO5Tf/AAAAAElFTkSuQmCC")

#----------------------------------------------------------------------
icons['error'] = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAA"
    "CXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1QsKCTIOk1O0sAAAAhpJREFUOMudk71Pk1EU"
    "xn/3bQvlo76gxBpNoGibArYLxJREh466MmnSgRH+IjsydGYjspBAWEQHMJIWmmhj+LAkpC1N"
    "Wyj2PcfhfUsQiRpPcnOTm/P8nuQ59xhuVA5SwDyQBsa95xKwDixlYOt6v7khfjscCi1MJpME"
    "w2F8AwOoKp2zM87LZYqFAtVmM5uBxd8AOXg3nUy+HJmZ4XJ7G+f0FBwHVUUMWEN38U9NUt3d"
    "5VOxuJqBVwC+rvN0Mvn6XixGe3MTbTRABFVBRUEEabboHHyjPxbDtqzoi2r1wTKsmBykhkOh"
    "98/m5rhYW0OdDqqKKqh6YgXFhakxBFMpPm9sUGu1Zi1gfjKR4HJnB3UcEFABFXEP6oEUVMFx"
    "uMjneTwWAZj3A+lg+D4/PnxERRkrf+dPtW/fQao1Ag8fAaT9wLjVP4iKG9jfSkRR7WB6AgDj"
    "FnCV9r8A8PpUBQA/UOrUzyYUg6rDl5ERt0HEvVE3PDdVRN0gtdkCKFnA+vnJCcYeuiL/Iu46"
    "iidWwRocpF2rAaxbwNL+3h6BeNwly3WRNzrtTkNQY/BHIpQODwGWrAxs1VqtbKWQp2fqqQu5"
    "5op3qygYQ080RuPggHq7nc3Alg9gGVaeVyqzts8XHUgkcBoN5KJ99RMVMHaI3vgEjXKZwvHx"
    "agbe3LpMdl/fwpPRUQK2jentRUVwmk069Tpfj466zou3buP/rPNPwkdmHrlYdncAAAAASUVO"
    "RK5CYII=")

#----------------------------------------------------------------------
icons['info'] = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAA"
    "CXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1QMCBiAyOlCc9wAAADV0RVh0Q29tbWVudAAo"
    "YykgMjAwNCBKYWt1YiBTdGVpbmVyCgpDcmVhdGVkIHdpdGggVGhlIEdJTVCQ2YtvAAACq0lE"
    "QVQ4y5WSS0hUcRTGv//cO+O9OtdHJllqZlI+CEUiE8ShBiXUpKKQbGLKcGG0CBOyRcsQMapF"
    "j02ii4igAgMDJcgQSXQmSkrNijSVTJ1xdJy5d6739W/loDURfqtz4Pt+nHM4BBFUXNnUTCkc"
    "BiUpIISYiDHLEPq0/2Vr459edn1TVN6UwTCk72JtZVq5vQBxAg9VN+DxBVN7ej9cISbmLKV6"
    "SX9X69eIAJYlb++11G/fk7ljLBBSRmd9khJaVVlF06NtJflZ6enJe1tuPXYBiF/LMGtFSdXV"
    "2/Xnj5YVH8z5rKi6m7eYFQBEpyCqTg3vSmiWi+YQzXM7dSYlc3L8bScAmMIkhj1Xbi9AIKSO"
    "sSxDLWbGIIRQUBiabiiUwuganOwuKswBw3Kn1nJhwKpC4wUrD1nRV1XNSDQzpguJsZxzV7Lg"
    "yN+91QElkDrlEf08zyEgqVxtXYNpww0IIUTRdEiyajIMKiZYo+D1Sy5RCjErQUkbnpaGFUU3"
    "qaqxFuEBiGFADM9KXl8gRpRVy4qk+jOSYzH6w/deVHQ6tyQp7m+LHsFqSfD6g4gTeLWjrVnc"
    "sIKZ0QZ63gyDj2JzJ34tiwDw0yfqHye8/t7hmYUlUTZX7E+rGHKNwcph4q8bbBNCxzu7+uWZ"
    "6fnstCTroWvtAzfPHM665P4yH2JYkui0Z1X7F5dy3IMjqsD6SsOrr/+DY6cbq2QqPC8rLbIU"
    "HshGNM9B1XX4lkUMucbxzjWixXHS5SftNx5EBABAbV1DYohucQVkpDtOJDGEACwLdHXPBRnN"
    "v6+j7c4U/qeaGrvV6bRRz2IfFWUX/T75jDqdNhrJy/6DoQLA/bsPkZ2bjempaWxaTqeNzsy+"
    "MgLSEP00+sLY7AQAUFV98nqNIMQfWVhYeJ2XF/sokuk3Vkw2XnyKHQQAAAAASUVORK5CYII=")

#----------------------------------------------------------------------
icons['warning'] = PyEmbeddedImage(
    "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAA"
    "CXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1gIQDictt+6SdwAAAehJREFUOMuVk8FKG2EU"
    "hb87mc40iTODEJlK7UZDxCCGQUqaMASCSGODdBe6ECJddOcwG/EJXBt0ZcBCwEVJVgWtr1AI"
    "PoBQXAQKhRZc1EBJMX831hLItPbAvYvL4Ttnc4UIhfDIgLcAA3jdgC/jfHoUwISjeXgOcAFH"
    "QHWcT4tI91KwWlhY0J6kUloKVkPw7g0wYS8NemJ3l0yzSRp0E/buBQihOAXFbLksej6Pns+T"
    "LZdlCoohFPmXdqB7AsNep6M8z1Oe56lep6NOYLgD3b82CGHFhaWM74vh+ziOg+M4GL5PxvfF"
    "haUQViIBJuxnwUgEAaJpWJaFZVmIppEIArJgmLA/FhDC+jSk5woFjFIJEcG2bWzbRkQwSiXm"
    "CgWmIR3C+gggBDGgsXibruk6IvKngQiarpMIAhbBMKARgsDtCuHVLLReLC8bk2dnEIuhlKLf"
    "7wOQTCYREbi54WptjQ/n54NLqDfgnYQQewxXT8GaPz7mYbWKUgqlFLVaDYB2u42IICL8OD3l"
    "YmODLnz/DJOawGYczJlcjnilcmcUEVzXxXXdkVu8UmEmlyMOpsCmbMPXl5CaPThgol6/S/89"
    "wAhARLhutbjc2uI9fJNDGD4DecD/6SfwEZT+CQ4H8GYi4i+idA3DHjR/AZfefQgctOETAAAA"
    "AElFTkSuQmCC")

class LogMessagesPanel(wx.Panel):
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)

        self.list = wx.ListCtrl(self, -1,  style=  wx.LC_REPORT|wx.SUNKEN_BORDER)

        self.setupList()

        sizer = wx.BoxSizer()
        sizer.Add(self.list, 1, wx.EXPAND)
        self.SetSizerAndFit(sizer)

        pub.subscribe(self.OnAppendLog, 'log')

    def setupList(self):
        """sets columns and append a imagelist """

         #setup first column (which accept icons)
        info = wx.ListItem()
        info.m_mask = wx.LIST_MASK_TEXT | wx.LIST_MASK_IMAGE | wx.LIST_MASK_FORMAT
        info.m_image = -1
        info.m_format = 0
        info.m_text = "Message"
        self.list.InsertColumnInfo(0, info)
        self.list.SetColumnWidth(0, 550)

        #insert second column
        self.list.InsertColumn(1, 'Time')
        self.list.SetColumnWidth(1, 70)

        #setup imagelist and an associated dict to map status->image_index
        imgList = wx.ImageList(16, 16)


        self.icon_map = {}
        for key, bitmap in icons.iteritems():
            indx = imgList.Add( bitmap.GetBitmap() )
            self.icon_map[key] = indx
        self.list.AssignImageList(imgList, wx.IMAGE_LIST_SMALL)

    def OnAppendLog(self, msg):
        ico = self.icon_map[msg.data[0]]
        message = msg.data[1]
        index = self.list.InsertImageStringItem(sys.maxint, message, ico)
        self.list.SetStringItem(index, 1, time.strftime('%H:%M:%S'))

        self.list.EnsureVisible(index) #keep scroll at bottom

class TestFrame(wx.Frame):
    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, id, "Log Panel demo")
        self.log = LogMessagesPanel(self, -1)
        self.SetSize((620,150))
        self.SetMinSize((620,150))

if __name__ == "__main__":

    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    main_frame = TestFrame(None, -1)
    app.SetTopWindow(main_frame)
    main_frame.Show()

    pub.sendMessage('log', ('ok', 'Ready! You can send any message from anywhere.') )
    pub.sendMessage('log', ('info', "Just import pubsub.Publisher and send a 'log' message") )
    pub.sendMessage('log', ('warning', "The message data is a tuple ('icon', 'message') ") )