El puerto serie y Qt


13 de febrero de 2012


Table of Contents

1. Introducción
2. QextSerialPort
2.1. Obteniendo la biblioteca
2.2. Compilando e instalando el componente
2.3. Ayuda para usar la biblioteca
2.4. Usando la biblioteca
3. Ejemplos
3.1. Transmisión sencilla
3.2. Recepción sencilla
3.3. "ECO": Recepción-transmisión simultánea
4. Proyecto plantilla para las prácticas de la asignatura
5. Algo más serio: Un visor GPS NMEA-0183
6. Relación de componentes serie para Qt
7. Descargas

1. Introducción

Qt no incluye ningún componente para manejar puertos serie, pero se puede recurrir a opciones de código abierto de terceras partes para añadir esta característica.

En el curso 2009/2010 se trabajó en localizarlas, elegir la más adecuada y mostrar cómo hacer uso de ella. La selección de candidatos se hizo según los siguientes criterios por orden de prioridad: código abierto, multi-plataforma, modelo de licencia, aceptación.

En su momento, el ganador fué QextSerialPort, que es la biblioteca considerada aquí. La principal "pega" de esta biblioteca es la falta de una licencia, problema que aún no se ha resuelto y dificulta un aprovechamiento legal.

En Qt5 se está planteando la incorporación de otra bibloteca, pero la licencia tampoco es interesante. Cuando esté claro, actualizaré la información.

Esta página es un tutorial básico para usar la biblioteca de comunicaciones serie seleccionada y hacer un recopilatorio de otras posibles opciones.

Esto NO es un tutorial para entender qué es un puerto serie ni cómo se programa, para eso es mejor buscar en otra parte o utilizar los apuntes de clase sobre comunicaciones RS-232 y RS-485.

2. QextSerialPort

2.1. Obteniendo la biblioteca

La biblioteca está disponible como código fuente C/C++ en http://code.google.com/p/qextserialport/.

Para descargarla es necesaria una utilidad de gestión de versiones de código llamada Mercurial. En Linux, para conseguir descargar los fuentes, se deberá abrir un terminal y escribir:

hg clone https://qextserialport.googlecode.com/hg/ qextserialport

y se creará el directorio qextserialport con el código fuente del componente, los manuales y los ejemplos.

[Note]Note

Si la utilidad hg no está instalada, Ubuntu Linux nos dirá cómo instalarla.

En Windows, deberemos instalar antes alguna de las utilidades para Windows de Mercurial.

2.2. Compilando e instalando el componente

La manera "tradicional" de compilar algo para Qt es la que se vió en "Al grano con Qt" -> Compilando a pelo".

Como estamos más acostumbrados a los IDE, vamos a ilustrarlo aquí con Qt Creator.

  • Primero abrimos Qt Creator.

  • Después elegimos Archivo-> Abrir archivo o proyecto. Y nos vamos al directorio qextserialport y seleccionamos el archivo de proyecto qextserialport.pro.

    Por curiosidad, echad un vistazo al .pro. Destacar que lo que se generará es una biblioteca y que la configuración de proyecto cambia según se trate de Windows, Linux o MacOS.

    Figure 1. Abriendo el proyecto de QextSerialPort

    Abriendo el proyecto de QextSerialPort

  • Ahora hay que construir el proyecto, para ello haremos Construir -> Reconstruir todo. Como resultado se creará el directorio build con bibliotecas de enlazado dinámico con el nombre lib*.so. si es para Linux o *.dll si es para Windows.

    Figure 2. Vistazo a las bibliotecas creadas tras compilar

    Vistazo a las bibliotecas creadas tras compilar

    [Note]Note

    Una biblioteca de enlazado dinámico es una biblioteca normal de C/C++ u otro lenguaje que se enlaza al ejecutable en el momento de la ejecución. En contraposición a esto están las bibliotecas estáticas, que se enlazan al ejecutable cuando lo construimos. Para que los ejecutables encuentren las bibliotecas dinámicas, estas deben estar en el lugar apropiado.

  • Para instalar las bibliotecas en el caso de Linux vamos a copiarlas al directorio /usr/lib del sistema. Necesitamos privilegios de superusuario para hacerlo. Por ejemplo, podemos abrir un terminal y hacer:

    $ sudo bash
    # cd tmp 
    # cd qextserialport 
    # cd build 
    # cp lib* /usr/lib

    [Important]Important

    Recordad que estamos como superusuarios. Si metemos la pata, podemos destrozar el sistema.

    Para instalar las librerías en el caso de Windows, deberemos conseguir que estén en la ruta de búsqueda de ejecutables. Bastará con dejar la .dll en el mismo directorio que la aplicación o copiarla al directorio c:\Windows\system32 o similar. Como en Linux, como tenéis privilegios avanzados, es peligroso cargarse algo del directorio de sistema de windows.

    Estas maneras de instalar la biblioteca no son las más adecuadas, pero para el caso nos sirven.

2.3. Ayuda para usar la biblioteca

Al descargar el componente se crea también el directorio html con el manual. Abriendo el archivo index.html se accede a la ayuda.

El manual es muy escueto y está orientado a conocedores del tema y que se entiendan bien con Qt.

Para centrar las cosas, indicar que QextSerialPort es una clase derivada de QIODevice de Qt, que crea una abstracción de los sistemas de ficheros y similares (en propiedad, se dice flujos de datos). Esto significa que el puerto serie se nos mostrará como si de un flujo de datos se tratase.

Para tener un acceso más inmediato a la ayuda, se ha copiado aquí la ayuda del componente QextSerialPort 1.2 a diciembre de 2009.

Algunos enlaces que hacen referencia a clases externas fallarán. Se deberán buscar esas clases en el manual de Qt.

2.4. Usando la biblioteca

2.4.1. Preparando el proyecto

Partamos de que tenemos un proyecto Qt operativo. El primer paso sera modificar el archivo de proyecto (.pro) para dar cabida a la biblioteca. El siguiente listado muestra un ejemplo típico:

# Proyecto sencillo para mostrar la funcionalidad de qextserialport
# Ejemplo que transmite algo
#
# Author: Angel Perles for the DSII subject
# Date: 2010-01-14

QT       -= gui

TARGET = qextserialport_sencill_transmisio
CONFIG   += console
CONFIG   -= app_bundle  # MacOS no es el objetivo

TEMPLATE = app

SOURCES += main.cpp

# Añadidos mios al proyecto creado por defecto.
# Como son dependientes de la plataforma destino, utilizamos
# opciones distintas según la plataforma objetivo

win32 {
    # anyadir las librerias de qext...
    LIBS += -LS:/dsii/dsii_qt/qextserialport/build
    LIBS += -lqextserialportd1

    # anaydir donde localizar los archivos de cabecera de qext...
    INCLUDEPATH += S:/dsii/dsii_qt/qextserialport

    # etiquetas para C que permite saber que tipo de implementación serie es
    DEFINES  = _TTY_WIN_
}

unix {
    # anyadir las librerias de qext...
    LIBS += -L/home/aperles/tmp/qextserialport/build
    LIBS += -lqextserialportd

    # anaydir donde localizar los archivos de cabecera de qext...
    INCLUDEPATH += /home/aperles/tmp/qextserialport

    # etiquetas para saber en que tipo de sistema estamos
    DEFINES   = _TTY_POSIX_

}

Como deseamos trabajar en, al menos, dos plataformas, se han creado las secciones win32 (Microsoft Windows) y unix (*nix, i.e. Linux, MacOSX, Android, ...) con la configuración particular para dicha plataforma.

Con LIBS se le indica al proyecto dónde ha de buscar las bibliotecas (-L) y qué biblioteca usar (-l).

Con INCLUDEPATH se indica dónde buscar los archivos de cabecera adicionales (cuando se hace #include <algunacosa.h>).

Con DEFINES se crean etiquetas para todos los fuentes de C que formen parte del proyecto (equivalente a hacer #define ALGO, pero no hace falta tocar el código).

2.4.2. Creando una instancia del componente

QextSerialPort es una clase Qt que se utilizará de la misma manera que cualquier otra.

En el manual se indican distintos tipos de constructores. Por claridad, se empleará el que menos configuraciones iniciales hace. Su prototipo es:

QextSerialPort::QextSerialPort ();

Por ejemplo, se puede crear una instancia de la clase de la siguiente manera:

QextSerialPort *puerto; // puntero al objeto que maneja el puerto

void funcion(void)
{
    QextSerialPort *puerto = new QextSerialPort();

}

2.4.3. Configurando la conexión

El primer paso es configurar el comportamiento del objeto y el puerto a utilizar. Para ello, se pueden utilizara los siguientes métodos:

void setQueryMode (QueryMode mode);

void setPortName (const QString &name);

Con setQueryMode() se establece la manera en que se atenderá la información enviada/recibida por nuestro programa. Se tienen dos opciones: Polling (por encuesta) y EventDriven (por eventos). (De momento solo he probado Polling).

Con setPortName() establecenos el nombre del dispositivo serie a utilizar.

Por ejemplo, se podría hacer:

    puerto->setQueryMode(QextSerialPort::Polling);
    puerto->setPortName("com1");

A continuación se deberán configurar los parámetros típicos de una conexión serie. Para ello se pueden utilizar los siguientes métodos:

void setBaudRate (BaudRateType);

void setDataBits (DataBitsType);

void setParity (ParityType);

void setStopBits (StopBitsType);

void setFlowControl (FlowType);

En el manual del componente se pueden consultar los posible parámetros. Como ejemplo, para una configuración 9600-8N1 sin control de flujo se podría hacer:

    puerto->setBaudRate(BAUD9600);
    puerto->setDataBits(DATA_8);
    puerto->setStopBits(STOP_1);
    puerto->setParity(PAR_NONE);
    puerto->setFlowControl(FLOW_OFF);

2.4.4. Abriendo y cerrando la conexión

Para que una aplicación puede utilizar un determinado puerto serie, primero debe solicitarlo al sistema operativo para que se lo asigne. A esto se le suele denominar "abrir" el puerto serie.

De la misma manera, cuando terminamos de usar una conexión serie, debemos devolverla al SO. A esto se le llama "cerrar" el puerto.

Para gestionar estas actividades se pueden utilizar los siguientes métodos:

bool open (OpenMode mode);

void close ();

La función open() permite "abrir" el puerto y saber si se ha logrado abrirlo. Como parámetro admite los valores ReadOnly (solo lectura/recibir), WriteOnly (solo escritura/enviar), ReadWrite (lectura/recibir y escritura/enviar).

Por ejemplo, se podría hacer:

    printf("Abriendo el puerto ... ");
    if (puerto->open(QextSerialPort::WriteOnly)) {
        printf("abierto!!!\n");
    } else {
        printf("vaya, esto CASCA\n");
    }

    // ahora se usa el puerto
    ...

    printf("Cerrando el puerto\n");
    puerto->close();

2.4.5. Enviando y recibiendo datos

Una vez abierta la conexión, se pueden enviar datos empleando distintos métodos. Por ejemplo :

putChar (char c);

write (const QByteArray &byteArray);

write (const char *data, qint64 maxSize);

El método putChar() permite enviar un dato y el método write() permite enviar al buffer de salida una cadena o maxSize datos apuntados por el puntero data.

Por ejemplo:

char *mensaje = "Bon dia pel mati";

char datos[] = {125,42,37,9};

//...
puerto->putChar(34);
puerto->putChar('A');
puerto->putChar(0xF3);
puerto->write(mensaje);
puerto->write(datos,4);

Para recibir datos disponemos también de varios métodos. Por ejemplo:

qint64 bytesAvailable ();

getChar (char *c);

read (qint64 maxSize);

read (char *data, qint64 maxSize);

Debemos tener en cuenta que las funciones de recepción son bloqueantes, es decir, si no se ha recibido nada, la función se queda a la espera de que llegue algo.

Esto no es adecuado en el caso de que queramos que nuestra aplicación atienda a otras cosas simultáneamente, así que, para evitar el bloqueo, se puede usar el método bytesAvailable(), que devuelve el número de datos disponibles en el buffer de entrada o un valor <= 0 si no hay nada o hay problemas.

Con los métodos getChar() y read() podremos recoger uno o varios datos respectivamente del buffer de entrada.

Por ejemplo:

    while (1) {
        int num_datos;
        char dato;

        num_datos = puerto->bytesAvailable();
        if (num_datos > 0) {    // hay algo
            puerto->getChar(&dato);
            printf("Recibido el ascii %d (%c)\n",(int)dato, dato);
        }
    }

3. Ejemplos

3.1. Transmisión sencilla

/* Proyecto sencillo para mostrar la funcionalidad de qextserialport
   Ejemplo que transmite algo

  Author: Angel Perles for the DSII subject
  Date: 2010-01-14
*/

#include <QtCore/QCoreApplication>
#include <stdio.h>

#ifdef Q_OS_UNIX
#include <unistd.h> /* anyadir esta cabecera en los *nix para la función sleep() */
#endif

// anyadimos la cabecera con la informacion sobre la clase
#include <qextserialport.h>

// nombre del puerto que deseamos abrir
#define PUERTO_NOMBRE "/dev/ttyS0"
//#define PUERTO_NOMBRE "/dev/ttyUSB0"      // adaptador USB-serie
//#define PUERTO_NOMBRE "COM1"
//#define PUERTO_NOMBRE "COM3"

QextSerialPort *puerto; // puntero al objeto que maneja el puerto

int main(int argc, char *argv[])
{
    //QCoreApplication a(argc, argv);       // no eliminar esto en aplicaciones serias

    printf ("Creando instancia de QextSerialPort para abrir %s\n", PUERTO_NOMBRE);
    QextSerialPort *puerto = new QextSerialPort();
    puerto->setPortName(PUERTO_NOMBRE);
    puerto->setQueryMode(QextSerialPort::Polling);  // modo mas sencillo

    printf("Configurando puerto a 9600-8N1 sin control de flujo\n");
    puerto->setBaudRate (BAUD9600);
    puerto->setDataBits(DATA_8);
    puerto->setStopBits(STOP_1);
    puerto->setParity(PAR_NONE);
    puerto->setFlowControl(FLOW_OFF);

    printf("Abriendo el puerto ... ");
    if (puerto->open(QextSerialPort::WriteOnly)) {
        printf("abierto!!!\n");
    } else {
        printf("vaya, esto CASCA\n");
    }

    printf("Enviar \"Hola, puerto serie\" al puerto serie\n");
    puerto->write("Hola, puerto serie");

    //esperar un poco a que salga todo por el puerto
#ifdef Q_OS_WIN
    Sleep(1);
#else
    sleep(1);
#endif

    printf("Cerrando el puerto\n");
    puerto->close();

    printf("Saliendo\n");

    //return a.exec();

    return(0);
}

3.2. Recepción sencilla

/* Proyecto sencillo para mostrar la funcionalidad de qextserialport
   Ejemplo que recibe cosas

  Author: Angel Perles for the DSII subject
  Date: 2010-01-14
*/

#include <QtCore/QCoreApplication>
#include <stdio.h>

// anyadimos la cabecera con la informacion sobre la clase
#include <qextserialport.h>

// nombre del puerto que deseamos abrir
#define PUERTO_NOMBRE "/dev/ttyS0"
//#define PUERTO_NOMBRE "/dev/ttyUSB0"      // adaptador USB-serie
//#define PUERTO_NOMBRE "/dev/rfcomm3"        // Bluettoth SPP
//#define PUERTO_NOMBRE "COM1"

QextSerialPort *puerto; // puntero al objeto que maneja el puerto

int main(int argc, char *argv[])
{
    //QCoreApplication a(argc, argv);       // no eliminar esto en aplicacione serias

    printf ("Creando instancia de QextSerialPort para abrir %s\n", PUERTO_NOMBRE);
    QextSerialPort *puerto = new QextSerialPort();
    puerto->setPortName(PUERTO_NOMBRE);
    puerto->setQueryMode(QextSerialPort::Polling);  // modo mas sencillo

    printf("Configurando puerto a 9600-8N1 sin control de flujo\n");
    puerto->setBaudRate (BAUD9600);
    puerto->setDataBits(DATA_8);
    puerto->setStopBits(STOP_1);
    puerto->setParity(PAR_NONE);
    puerto->setFlowControl(FLOW_OFF);

    printf("Abriendo el puerto ... ");
    if (puerto->open(QextSerialPort::ReadOnly)) {
        printf("abierto!!!\n");
    } else {
        printf("vaya, esto CASCA\n");
        exit(1);
    }

    printf("Entrando al bucle que mira si se van recibiendo cosas:\n");
    while (1) {
        int num_datos;
        char dato;

        num_datos = puerto->bytesAvailable();
        if (num_datos > 0) {    // hay algo
            puerto->getChar(&dato);
            printf("Recibido el ascii %d (%c)\n",(int)dato, dato);
        }
    }

    // aqu´i no llegamos nunca, otro dia sera
    printf("Cerrando el puerto\n");
    puerto->close();

    printf("Saliendo\n");

    //return a.exec();

    return(0);
}

3.3. "ECO": Recepción-transmisión simultánea

/* Proyecto sencillo para mostrar la funcionalidad de qextserialport
   Ejemplo de recepcion y transmision

  Author: Angel Perles for the DSII subject
  Date: 2010-01-14
*/

#include <QtCore/QCoreApplication>
#include <stdio.h>
#ifdef Q_OS_UNIX
#include <unistd.h> /* anyadir esta cabecera en los *nix para la función sleep() */
#endif
#include <ctype.h>

// anyadimos la cabecera con la informacion sobre la clase
#include <qextserialport.h>

// nombre del puerto que deseamos abrir
#define PUERTO_NOMBRE "/dev/ttyS0"
//#define PUERTO_NOMBRE "/dev/ttyUSB0"      // adaptador USB-serie
//#define PUERTO_NOMBRE "COM1"

QextSerialPort *puerto; // puntero al objeto que maneja el puerto

int main(int argc, char *argv[])
{

    //QCoreApplication a(argc, argv);       // no eliminar esto en aplicacione serias

    printf ("Creando instancia de QextSerialPort para abrir %s\n", PUERTO_NOMBRE);
    QextSerialPort *puerto = new QextSerialPort();
    puerto->setPortName(PUERTO_NOMBRE);
    puerto->setQueryMode(QextSerialPort::Polling);  // modo mas sencillo

    printf("Configurando puerto a 9600-8N1 sin control de flujo\n");
    puerto->setBaudRate (BAUD9600);
    puerto->setDataBits(DATA_8);
    puerto->setStopBits(STOP_1);
    puerto->setParity(PAR_NONE);
    puerto->setFlowControl(FLOW_OFF);

    printf("Abriendo el puerto ... ");
    if (puerto->open(QextSerialPort::ReadWrite)) {
        printf("abierto!!!\n");
    } else {
        printf("vaya, esto CASCA\n");
    }

    printf("Entrando al bucle\n");
    printf("Pulsa \"f\" en el otro lado para salir\n");

    bool fin;
    fin = false;

    do {
        int num_datos;
        char dato;

        // comprobar si se ha recibido algo y sacarlo por pantalla
        num_datos = puerto->bytesAvailable();
        if (num_datos > 0) {    // hay algo, saco un solo byte
            puerto->getChar(&dato);
            printf("Recibido el ascii %d (%c)\n",(int)dato, dato);
            // envio el ascii de vuelta por el canal serie
            //sleep(1);
            puerto->putChar(toupper(dato));
            if (dato == '-') {  // tecla de fin
                fin = true;
            }
        }
    } while (fin == false);

    printf("Se ha pulsado \'f\'' en el otro extremo. Me salgo\n");
    puerto->write("\nAdios. Hasta la proxima\n");

    //esperar un poco a que salga todo por el puerto
#ifdef Q_OS_WIN
    Sleep(1);
#else
    sleep(1);
#endif

    printf("Cerrando el puerto\n");
    puerto->close();

    printf("Saliendo\n");

    //return a.exec();

    return(0);
}

4. Proyecto plantilla para las prácticas de la asignatura

Para que en las práctica de la asignatura se entre en materia más rápidamente, se ha preparado un proyecto plantilla para Qt que incluye QextSerialPort precompilado para Linux y Windows.

El proyecto está formado por los módulos main.cpp (programa principal), mainwindows.cpp (código asociado a un formulario visual) y serie.cpp (código asociado al manejo del puerto serie).

Destacar el módulo serie.cpp, que contiene funciones de soporte para el manejo del puerto serie e implementan las funciones genéricas que se proponen en los apuntes de teoría. Es importante aprender a desarrollar estas funciones y a usarlas, así que se ha creado la ayuda para usar las funciones del módulo serie.cpp.

El proyecto se deberá adaptar al problema concerto que se pida en prácticas. Una vez construido y ejecutado, aparecerá una ventana similar a la mostrada aquí.

Figure 3. Aspecto de la plantilla ejecutándose en Linux Ubuntu 10.04

Aspecto de la plantilla ejecutándose en Linux Ubuntu 10.04

En el área de descargas se puede obtener la plantilla.

5. Algo más serio: Un visor GPS NMEA-0183

Como un ejemplo más serio de uso, se propone una aplicación capaz de interpretar el flujo de datos de un GPS compatible NMEA-0183 y obtener la posición actual.

Para que sea lo más real posible (sin pasarse), la funcionalidad del tratamiento se ha encapsulado en una clase llamada TGeoposition, que será la encargada de ofrecer una interfaz sencilla a la aplicación. Si se mira el resto del código de la aplicación, se verá que es muy sencillo (creo).

Se ha procurado que le código de TGeoposition sea parecido a los proyectos que se puedan obtener en la red. A diferencia del lo mostrada hasta ahora, este código está documentado en inglés y se ha empleado doxygen para generar automáticamente la documentación.

Podemos acceder a la documentación de TGeoposition generada automáticamente con doxygen.

La siguiente imagen muestra el aspecto del visor.

Figure 4. Aspecto del visor GPS

Aspecto del visor GPS

En el área de descargas se puede obtener este proyecto.

6. Relación de componentes serie para Qt

Lo que se ha encontrado por ahí es:

Esto está obsoleto a fecha de hoy. En gitorious hay más proyectos, pero no está claro aún su futuro.

QextSerialPort

Licencia: Se propone new BSD license (perfecto para nosotros). Aún no está claro a 5 de febrero de 2011.
Plataformas: Windows y POSIX (la mayoría de los *nix, incluyendo Mac).
Comentarios: Esta biblioteca parece que está bien aceptada y asentada, así que es un candidato perfecto para empezar a trabajar. Hay bastante actividad en los foros para ampliarla y corregir defectos.
URL: http://code.google.com/p/qextserialport/
Más URLS: http://www.qt-apps.org/content/show.php/QT_comport?content=95246, http://www.chuktech.net/serial/index.html

QPort

Licencia: GPL (el código que hagamos debe ser abierto).
Plataformas: Windows y POSIX (la mayoría de los *nix, incluyendo Mac).
Comentarios: Esta biblioteca proporciona un "widget" para incorporar en Designer e incorporar fácilmente el componente en un formulario. Así es como lo hemos hecho normalmente en Borland C++ Builder con comport. A la larga, esa manera de trabajar tiene pegas, así que prefiero dejarlo en la recámar. Por dentro utiliza QextSerialPort. David ha contactado con el autor para que le eche una mano probándola.
URL: http://www.sebest.com.ar/?q=node/44
Más URLS: http://sourceforge.net/projects/qport/

QxtSerialDevice

Licencia: LGPL (perfecta para nosotros).
Plataformas: la parte serie es POSIX.
Comentarios: Por ahí dicen que esta biblioteca puede manejar el serie, pero no veo nada al respecto en la documentación. Eso sí, esta librería es una extesión de Qt con un montón de cosas. Vaya, vaya. (Puede que sea QAbstractSocket).
URL: http://www.libqxt.org/
Más URLS: http://sourceforge.net/projects/qport/


QSerialDevice

Licencia: GPL (el código que hagamos debe ser abierto).
Plataformas: multiplataforma, no sé cuales.
Comentarios: Parece que es una reimplementación de QextSerialPort que corriege algunos de los planteamientos de la anterior. Solo hay una versión que salió en septiembre de 2009. Mejor no nos arriesgamos, no creo que sea bueno empezar con una biblioteca de una única persona y con solo una versión.
URL: http://qt-apps.org/content/show.php/QSerialDevice?content=112039
Más URLS: http://fireforge.net/projects/qserialdevice/

QSerialPort

Licencia: ??
Plataformas: es para Qtopia (embedded Linux en móviles, navegadores GPS, ...).
Comentarios: Da la sensación de que es una biblioteca hecha adrede por Trolltech (fundadora de Qt) para meterse en el mundo de los móviles (los Samsung Omnia, LG Viewty, etc. llevan eso) y poder comunicarse con los chips internos del móvil. La dejamos ahí.
URL: http://doc.trolltech.com/qtopia4.2/qserialport.html
Más URLS: http://fireforge.net/projects/qserialdevice/

7. Descargas