Comunicaciones Serie en Windows II (2ª parte)

Establecer un diagrama de clases no es fácil. Además está sujeto al gusto y experiencia del programador. Así que, lo que sigue debe tomarse como una de las formas (la mía) de crear la estructura de clases para el acceso al puerto serie: no es la única y, probablemente, no sea la perfecta. Las decisiones sobre el diseño de las clases se explicarán a continuación.

En principio, se tenderá a denominar a las clases y a sus propiedades y métodos en castellano. Quizá esto no sea lo más apropiado, sobretodo si se va a distribuir internacionalmente. He preferido utilizar este método dado que, por ejemplo, me resulta más comprensible (y, por qué no decirlo, agradable) una llamada como Puerto.Abrir que otra de tipo Port.Open.

Se mantiene el prefijo T para todas las clases por compatibilidad con el código de la VCL. Por la misma razón, se mantiene el prefijo F para los campos de una clase, el prefijo Get en los procedimientos de obtención de propiedades, el prefijo Set para las funciones de establecimiento de los valores de las propiedades y el prefijo On para los eventos de las clases. Seguido de estos prefijos irá el nombre que corresponda en castellano. El resto de procedimientos y funciones estarán en castellano.

Los nombres de las variables seguirán la denominada notación polaca (prefijos distintos para distintos tipos de datos), parte de la cual se muestra en la tabla 1, con el nombre que siga al prefijo en castellano y comenzando cada palabra por mayúsculas. Los nombres de las constantes, si hacen referencia a alguna contenida en el API de Windows (por ejemplo, en el archivo Windows.pas) se mantendrán con su nombre original o uno que lo sugiera. El resto de las constantes irán en castellano y completamente en mayúsculas, para diferenciarlas de las variables.

Prefijo Dato Descripción
b Boolean Variables True o False
by Byte Variables de un solo byte
c Char Variables de un sólo carácter
dw LongWord Doble palabra (32 bits) sin signo
h LongWord Variable de tipo THandle
i Integer Variables enteras
sz PChar Cadena terminada en NULL (#0)
ui LongWord Enteros (32 bits) sin signo
w Word Palabra (16 bits) sin signo
Tabla 1. Algunos prefijos de la notación polaca

Se ha aislado cada clase en un archivo de módulo distinto con el objeto de que sea fácil localizar, estudiar y, si se requiere, modificar o reemplazar completamente una clase. El nombre del archivo de módulo se ha elegido de tal forma que sea un acrónimo de la clase que contiene, manteniendo el formato 8.3 para evitar los problemas de cambio o pérdida de nombre que puede suceder con algunos compresores de archivos y programas de copia de seguridad. Cada uno de los procedimientos y funciones dentro del módulo tiene comentarios en el encabezado que explican el fundamento del método. Cuando se requiere, el cuerpo de la rutina contiene comentarios aclaratorios.

Se ha intentado que cada clase sea lo más independiente posible de las demás proporcionando sólo aquella información que es imprescindible para su funcionamiento. Esto permite que se recree una clase parcial o completamente y que pueda ser integrada, sin muchas modificaciones, en la estructura de clases.

Se ha minimizado, en lo posible, el que cualquiera de las clases lance excepciones. De hecho, por sí mismas, no lanzan ninguna. Esto es así porque, en general, los programas de comunicaciones se realizan de forma desatendida (sin un usuario enfrente). Lanzar una excepción cuando nadie está mirando, cuando nadie puede tomar medidas para solucionar el problema (aunque sólo sea pulsar el botón Aceptar del diálogo de la excepción), parece un poco estúpido. Además, esto implicaría que si, por ejemplo, se está en una comunicación a través del teléfono, no se podrían tomar medidas (en este caso, el cierre del puerto y, con ello, la liberación de la línea, con el consiguiente ahorro para el cliente) hasta que algún usuario pasase por allí y cerrase el diálogo.

Sé que este enfoque no es el adecuado en la mayoría de las aplicaciones, donde es preferible el denominado diseño por contrato: una clase proporciona una funcionalidad que debe cumplir; si falla en su cometido debe impedir que el desarrollo normal continúe o, al menos, hacer notar de alguna manera la contingencia. En el capítulo 11 del libro «Construcción de Software Orientado a Objetos» de Bertrand Meyer, bajo el epígrafe Un módulo tolerante, se oye al autor proclamar: Los módulos básicos simples pero desprotegidos pudieran no ser lo suficientemente robustos para ser utilizados por los clientes… se ha visto que generalmente éste no es el enfoque correcto…. Sin embargo, el mismo autor parece absolvernos cuando, más adelante (y en letra más pequeña que la habitual) dice: El enfoque tolerante sigue siendo útil para los elementos software que tienen que ver no con otros elementos de software sino con datos que provienen del mundo exterior, tales como entradas de usuario o datos procedentes de un sensor. (¡Uf!)

Para entendernos, programar en la forma ortodoxa tradicional requeriría del control de errores:

try
   Hacer_Algo;
except
   // Tratar el error. No se ha podido Hacer_Algo
end;

O bien que cada clase proporcionase una forma de comprobar el estado antes de actuar:

if (Posible_Estado) then Hacer_algo;

Lo que no garantiza que se pueda hacer algo. Por ejemplo, el puerto está preparado para ser abierto (Posible_Estado = TRUE). Ahora se intenta abrir pero resulta que otra aplicación está usando en esos momentos el puerto: la clase no ha cumplido su contrato. En la forma tolerante, que es la que se utiliza en las clases que se desarrollarán a continuación, la programación es más laxa:

if (Posible_Estado) then 
begin
   Hacer_algo;
   if (Se_Ha_Hecho_Algo) then ...
end;

Que también puede usarse (más bien, es recomendable) sin comprobar el estado (esto es, sin el if (Posible_Estado)…). Esto hace más complicadas las clases internamente ya que continuamente se requiere que se compruebe el estado (if…) antes de una operación con lo que, por un lado, los clientes de las clases tendrán menos comprobaciones que realizar y, por otro, los programas que utilicen las clases pueden ser desatendidos ya que el control de errores está incluido en las clases y, en general, no saldrán al exterior.

Los errores que pueden producirse en la comunicación son de dos tipos: los asociados a las funciones del API de Windows y los propios de las clases (por ejemplo, el intento de apertura de un puerto que está siendo utilizado en esos momentos o el choque entre un proceso de lectura y uno de escritura). Cada clase principal tiene un campo interno, de tipo DWORD (LongWord, en Delphi: un entero sin signo de 32 bits), que indica el último error detectado para que la aplicación pueda estudiarlo y, si lo desea, mostrar el mensaje correspondiente al usuario. Asimismo, un campo del mismo tipo indicará el error (o su ausencia) cuando una llamada al API falle (o acierte). Al producirse un error, en vez de lanzar una excepción, se ha preferido generar un evento que avise a la aplicación de la contingencia. Cada evento de este tipo comienza por el prefijo OnError. Los errores y su significado se explican en los comentarios del código fuente.

Las excepciones a cualquiera de las reglas anteriores, si las hay, se explicarán en su lugar correspondiente. El diagrama de clases (al menos las que aquí se van a ver) se muestra, esquemáticamente, en la figura 3 y se comenta seguidamente. Puede, desde ya, detectarse una excepción: la clase TTimeoutsPuerto (de la que se hablará en su momento), está, al menos en parte, en inglés. Los Timeouts se refieren al tiempo de espera entre un byte y el siguiente: demasiado largo como para usarlo en el nombre de una clase y creo que, de esta manera, queda más claro su papel en la estructura.

Los pasos en las huellas

De alguna manera volvemos hacia atrás al estudiar el diagrama de clases. Sólo se tratará de aquello que varíe sobre lo dicho anteriormente. Si se desea más información, se deberá mirar en el código fuente que, como se ha dicho anteriormente, está profusamente comentado. En este punto estudiaremos la estructura de clases. Se indicará la funcionalidad de cada clase y el archivo de módulo donde se encuentra definida e implementada de tal forma que se pueda estudiar in situ mientras se lee (ver también la figura 3).

TPuertoBase es la clase base de la que derivan el resto de las clases principales. Su declaración e implementación se encuentra en el archivo PrtoBase.pas.

Cualquier puerto de un ordenador, sea serie o paralelo, posee dos operaciones que pueden realizarse sobre el mismo: la apertura y el cierre. La clase TPuertoBase proporciona la funcionalidad requerida para ello. La función Abrir(), como su nombre indica, abre el puerto y la función Cerrar() lo cierra. El puerto también puede abrirse mediante el establecimiento de la propiedad Conectado a TRUE y puede cerrarse poniendo dicha propiedad a FALSE. La apertura efectiva del puerto producirá el evento OnAbrir y, el cierre del mismo, el evento OnCerrar.

Los posibles errores producidos en el funcionamiento de la clase producen, por un lado, un evento (ya se ha comentado antes que no se lanzan excepciones) y, por otro, la posibilidad de recuperar tanto el código como el texto descriptivo del error mediante propiedades y métodos de la clase.

El evento OnErrorWin32 se produce cuando una de las llamadas al API de Windows falla. Al detectarse un error, el campo ErrorWin32 tomará el valor del código de error detectado y el texto descriptivo del error puede recuperarse mediante la propiedad ErrorWin32String que, si no se ha producido ningún error, devolverá una cadena vacía. La función pública MensajeErrorWin32() puede ser utilizada como alternativa a la propiedad ErrorWin32String si se le pasa el valor del campo ErrorWin32, esto es, algo así como MensajeErrorWin32(ErrorWin32).

El evento OnErrorSerie se produce cuando se detecta un error en el puerto serie. Los errores que pueden producirse en el puerto son de varios tipos (error de trama, de paridad, saturación del buffer,…). Se recuperan los errores, internamente, mediante una llamada a la función del API de Windows ClearCommError() (la descripción de los errores, en inglés, puede encontrarse en la ayuda en pantalla de esta función). Una vez detectado el error, la propiedad ErrorSerie tomará el valor del código de error producido (una de las constantes de Windows.pas que comienzan por CE_ más un desplazamiento para evitar confusiones con otros errores), y puede recuperarse la descripción del mismo mediante la propiedad ErrorSerieString que, si no se ha producido ningún error devolverá una cadena vacía. La función pública MensajeErrorSerie() puede ser utilizada como alternativa a la propiedad ErrorSerieString si se le pasa el valor del campo ErrorSerie, esto es, algo así como MensajeErrorSerie(ErrorSerie).

El evento OnErrorPuerto se produce cuando falla una de las operaciones de la clase o sus derivadas. En realidad, la clase TPuertoBase se limita a almacenar el código de error en la propiedad ErrorPuerto y a devolver la cadena Error desconocido desde la propiedad ErrorPuertoString: son las clases derivadas las que deben proporcionar la descripción del error y su código. La función pública MensajeErrorPuerto() puede ser utilizada como alternativa a la propiedad ErrorPuertoString si se le pasa el valor del campo ErrorPuerto, esto es, algo así como MensajeErrorPuerto(ErrorPuerto).

Es curioso que no se utilicen en esta clase ninguna de las funciones del API de Windows específicas de comunicaciones (a excepción de ClearCommError(), tratada antes): la apertura del puerto se realiza, internamente, mediante CreateFile() (que se vio en la anterior entrega de esta serie), y el cierre del mismo mediante la función CloseHandle() que recibe como argumento el handle del puerto a cerrar (en general, cualquier handle es cerrado con esta función).

La clase TPuertoBase es abstracta: proporciona una especie de plantilla para que los métodos y propiedades de las clases derivadas sean consistentes. A este nivel no se proporciona la funcionalidad de escribir o leer del puerto. Tampoco ofrece la posibilidad de configurar el puerto. Se ha decidido así porque se ha pensado que debe ser lo más general posible (de hecho, con muy pocos cambios podría trabajar con archivos de disco). Además, si en el futuro se desea, puede derivarse de aquí una clase que, por ejemplo, acceda al puerto paralelo.

Se ha derivado de TComponent con el objeto de facilitar, en tiempo de diseño, la modificación de las propiedades de configuración del puerto. Gran parte de las propiedades del puerto ya están implementadas en los componentes entregados junto a este artículo y otras tantas pueden implementarse fácilmente.

La clase TPuertoSerie deriva de TPuertoBase y su declaración e implementación puede encontrarse en el archivo PtoSerie.pas.

TPuertoSerie añade la posibilidad de leer y escribir en el puerto, esto es, hace efectivas las funciones abstractas de la clase TPuertoBase y se convierte, por sí misma, en una clase efectiva (no abstracta) que puede ser usada cuando el acceso al puerto se realice, por ejemplo, a través de un módem nulo (del que se habló en el artículo anterior).

Mantiene, como cliente (ver figura 3), las propiedades del puerto a través de varias clases que sirven de utilidad. Estas clases que, generalmente, se limitan a mantener los valores de las propiedades del puerto se han derivado indirectamente de TPersistent para que tengan la capacidad de guardar y cargar las propiedades (ver figura 4). El campo ConfigPuerto de TPuertoSerie es el que expone la configuración actual del puerto.

Tanto el proceso de lectura como el de escritura están implementados a través de hilos (TThread). El hilo de lectura, THiloLectura, se encuentra implementado en el módulo HiloLeer.pas y el de escritura, THiloEscritura, en el módulo HiloEscr.pas. La comunicación entre los hilos y las clases de control del puerto (TPuertoSerie, por ejemplo) se realiza a través del envío y recepción de mensajes a y desde los hilos (ver más adelante).

La clase TControlModem deriva de TPuertoSerie y su declaración e implementación puede encontrarse en el archivo ConModem.pas.

TControlModem añade a las propiedades de cualquier puerto serie aquellas características que se aplican a un módem enchufado al puerto: la capacidad de recibir llamadas, de contestar a las mismas y, en general, el manejo de las señales que en un módem externo pueden contemplarse a través de sus indicadores luminosos.