Cómo usar el MSCOMM32.OCX

Por Pedro Pascua
© Copyrights 1998 by FoxPress, All rights reserved
FoxPress, Marzo 1998

En este artículo vamos a ver un ejemplo de formulario de comunicaciones vía puerto serie utilizando el control de comunicaciones de Microsoft que viene en la edición profesional del VFP. Dicho control se incluye como una OCX (MSCOMM32.OCX) y sólo funciona en Windows 95 y NT.

El control MSComm, nos permite acceder a cualquier puerto COMn: para transmitir o recibir datos, a través de unas pocas propiedades y métodos con un requerimiento mínimo de código. Cada control gestiona un puerto serie por lo que si necesitamos controlar más de un puerto a la vez deberemos usar tantos controles MSComm como puertos queramos acceder simultáneamente. Una advertencia importante: el ejemplo está realizado utilizando la versión 5.0 del control, correspondiente al Service Pack2. Si se utiliza otra versión del control, al intentar modificar o ejecutar el formulario se puede producir un error de control OLE. Este problema que se produce si pegamos el control en el formulario, se puede solventar instanciando directamente el control mediante la sentencia:

oComm =CreateObject("MsCommLib.MsComm")

pero entonces no podemos codificar directamente el evento OnComm del control que es desde donde se gestionan el flujo de datos de la transmisión y la programación resulta algo más compleja.

En nuestro caso, para no complicar el ejemplo, vamos a utilizar el control pegándolo en el formulario y codificando el evento OnComm.

Antes de entrar de lleno en el ejemplo, podemos hacer alguna prueba para comprobar la funcionalidad del control. Vamos a crear un programa my sencillo con las siguientes líneas:

oCom=CreateObject("MSCOMMLIB.MSCOMM")
oCom.CommPort = 2 
oCom.Settings = "14400,N,8,1"
oCom.PortOpen = .T.
oCom.Output = "ATDT123456789"+CHR(13)

Con estas cinco líneas deberíamos conseguir que el módem que tenemos en el puerto COM2, llamara al número 123456789 y conectara a 14400 baudios, sin paridad, con 8 bits de datos y uno de parada. La conexión se cortaría sin más que añadir

oCom.PortOpen = .T.

Sencillo, ¿no?...

Las propiedades más usadas de este control son:

- CommPort: especifica el número de puerto COM asignado al control. Puede ser 1,2,3 ó 4 según los puertos que tengamos instalados.

- Settings: como hemos visto en el ejemplo anterior, especifica la configuración de velocidad, paridad bits de datos y bit de parada para el puerto serie. Es una cadena con los correspondientes valores separados por comas. Por defecto su valor es "9600,N,8,1"

- PortOpen: valor lógico que indica si el puerto está abierto y activo. Para abrir el puerto basta poner este valor a True.

- Output: a esta propiedad se le asigna la cadena de caracteres que queremos enviar a través del puerto y automáticamente, envía dicha cadena al bufer de salida.

- CommEvent: contiene el valor correspondiente al último evento o error de comunicación que se haya producido.

- Sthreshold: especifica el número mínimo de caracteres en el bufer de salida que son enviados.

- OutBufferCount: guarda el número de caracteres que están esperando en el bufer de transmisión. Este será cero si hemos puesto a cero la propiedad Sthreshold. Dándole a esta propiedad el valor 0 limpiaremos el bufer de salida.

- OutBufferSize: especifica el tamaño del bufer de transmisión. Por defecto es de 512 bytes.

Todas estas propiedades se pueden encontrar en la ayuda que acompaña al control, accesible desde el menú que aparece al pulsar el botón derecho sobre el icono del control, una vez pegado en el formulario.

Aparte de estas propiedades, el otro punto clave para utilizar este control es el evento OnComm. El es el encargado de notificarnos todos los sucesos que ocurren durante la conexión, desde la llegada de un carácter, cambios en las líneas Carrier Detect (CD) y Request To Send (RTS) o errores producidos en la comunicación. Es aquí pues, donde deberemos programar la gestión de estos sucesos. Todas las constantes correspondientes a los valores de los eventos de error y de comunicación están especificadas en la ayuda.

El ejemplo que hemos creado para ilustrar la utilidad del control está realizado con el VFP5.0 . Consiste en un formulario desde el que podemos llamar o recibir una llamada a través de un módem estándar, mantener una sesión de Chat entre las dos máquinas y transferir ficheros en ambas direcciones. El formulario es el mismo para el que llama que para el que recibe la llamada ya que funciona en ambos sentidos.

Por si alguien no tiene posibilidad de probarlo a través de módem, comentar que también funciona conectando directamente los puertos series de dos ordenadores a través de un cable tipo "Modem Null". Con la única diferencia de que no hay que marcar número, sino que basta con abrir el puerto. Además, resulta más económico.

Utilizamos los Combobox de la parte superior para seleccionar el Puerto de conexión y la velocidad. Obviamente, la velocidad está supeditada a la capacidad del módem que utilicemos. Además, el que seleccionemos 14400 y el módem lo soporte, no asegura que la conexión se realice a esa velocidad, ya que a veces, el ruido de fondo de la línea telefónica puede hacer que la velocidad de conexión sea inferior a la teórica.

Una vez establecidos los parámetros del puerto, lo activamos al pulsar el botón Abrir puerto con el siguiente código:

If Thisform.CommControl.PortOpen = .F.
	Thisform.CommControl.PortOpen = .T.
	Thisform.CommControl.Output="ATZ"+Chr(13)
	Thisform.CommControl.Output="ATE1"+Chr(13)
	Thisform.LuzDTR.DisabledBackColor =RGB(0,255,0)
	Thisform.Start.Value = 1
	This.Caption = "Cerrar Puerto"
else
	Thisform.CommControl.PortOpen = .F.
	Thisform.LuzDTR.DisabledBackColor = RGB(192,192,192)
	Thisform.Start.Value = 0
	This.Caption = "Abrir Puerto"
Endif 

Hemos puesto unos textbox que cambiamos de color para simular las luces que nos indican el estado del módem. Dicho estado se chequea periódicamente según los intervalos que marca un control Timer que hay pegado en en el formulario.

**	Evento Timer()	**
** Luz CTS (Clear To Send)
if ThisForm.CommControl.CTSHolding = .T.
	Thisform.LuzCTS.DisabledBackColor=RGB(0,255,0)
else
	Thisform.LuzCTS.DisabledBackColor=RGB(192,192,192)
endif
** Luz CD (Carrier Detect)		
if ThisForm.CommControl.CDHolding = .T.
	Thisform.LuzCD.DisabledBackColor = RGB(0,255,0)
else
	Thisform.LuzCD.DisabledBackColor=RGB(192,192,192)
endif
** Luz DSR (Data Set Ready)
if ThisForm.CommControl.DSRHolding = .T.
	Thisform.LuzDSR.DisabledBackColor=RGB(0,255,0)
else
	Thisform.LuzDSR.DisabledBackColor=RGB(192,192,192)
endif
** Luz DTR (Data Terminal Ready)
if ThisForm.CommControl.DTREnable = .T.
	Thisform.LuzDSR.DisabledBackColor=RGB(0,255,0)
else 
	Thisform.LuzDSR.DisabledBackColor=RGB(192,192,192)
Endif

Estas "luces" nos avisan en el caso de que se corte alguna de estas líneas de conexión entre los puertos.

Una vez abierto el puerto podemos mandar al módem que marque el número indicado en el textbox situado a tal efecto sin más que mandar al puerto la secuencia de control de marcado:

If Thisform.CommControl.PortOpen = .T.
   Secuencia = "ATD" 
   Secuencia = Secuencia + ;
       AllTrim(Thisform.Telefono.Value)+Chr(13)
   Thisform.CommControl.Output = Secuencia
Else
   Wait Window Nowait "Abra primero el puerto..."
Endif


 

En el caso de que el formulario esté funcionando como receptor de la llamada, detectaremos que alguien está llamando capturando el mensaje comEventRing (= 6) en el evento OnComm del control como veremos después, mandando al módem receptor que conteste a la llamada. Si todo ha ido bien hasta ahora, deberíamos haber conseguido establecer la comunicación entre los dos modems, hecho lo cual sólo queda enviar los datos por la línea que hemos abierto.

Para distinguir si los datos que vamos a mandar son mensajes de Chat o parte de un fichero que estamos transfiriendo, creamos una propiedad del form llamada lChat y que almacena un valor True cuando la cadena es de un mensaje para así poder interpretar los datos recibidos consecuentemente. Así pues, cuando mandamos una línea de chat, (lo que se produce en el valid del texbox OutData), el receptor va almacenando los caracteres en otra propiedad custom llamada InBuffer hasta que nos llega un retorno de carro (ASCII 13) que interpretamos como fin de línea.

** OutData.Valid()
If Empty(This.Value)
   Return 1
EndIf	
If Thisform.CommControl.PortOpen = .T.
   * Generamos la secuencia de envío
   Secuencia = AllTrim(Thisform.OutData.Value);
        +CHR(13)
   * La enviamos al puerto
   Thisform.CommControl.Output = Secuencia	
   * guardamos la línea en el cursor cLog
   Insert Into cLog (User_id,Mensaje,Hora);
         Values;
         ('Local:',ThisForm.OutData.Value,DATETIME())
   ThisForm.Grid1.Refresh()
   Thisform.OutData.Value = ""		
   Return 0					
else
   Wait Window Nowait [Abra primero el puerto]
endif

Los mensajes los vamos guardando en un cursor que se crea en el Init() del formulario con un campo que identifica si el mensaje es del usuario local o remoto y otro campo en el guardamos la fecha-hora de envío. De este modo si nos interesara, podríamos volcar toda la conversación en un fichero tipo log. Este cursor lo vinculamos a un grid con el que conseguimos mediante la propiedad DynamicForeColor de las columnas, diferenciar con colores los mensajes de los dos comunicantes.

** Método Init() del form
Create cursor cLog (User_id C(8),;
       MensajeC(200),;
       Hora T) 
ThisForm.Grid1.RecordSource = 'cLog'
ThisForm.Grid1.Column1.Dynamicforecolor=;
"IIF(User_id='Local:',RGB(0,192,0),;
       RGB(0,0,192))"
ThisForm.Grid1.Column2.Dynamicforecolor=;
	"IIF(User_id='Local:',;
	RGB(0,192,0),RGB(0,0,192))"

Para mandar un fichero, lo primero que hacemos es enviar un código que informe al receptor de que va a recibir un fichero. Además le enviamos dentro de ese código el nombre del fichero y su tamaño. A partir de ahí, abrimos el fichero y vamos leyendo bloques de 2 Kb y enviándolos directamente al puerto. El tamaño de los bloques de fichero que se van enviando depende de como hayamos configurado los bufers de transmisión y recepción, ya que si éstos no son pequeños con respecto al tamaño de los bloques que enviamos, pueden llenarse saturando la comunicación. Otra cosa importante a la hora de transmitir ficheros es asegurarse de que la propiedad NullDiscard está a false para que nos se descarte ningún carácter del fichero.

************************************
* Click() del botón enviar fichero *
************************************
If Not Thisform.CommControl.PortOpen 
   MessageBox("Abra primero la conexión",0+64,"Aviso")
   Return
EndIF
* Seleccionamos el fichero
lcFile = GetFile()
lcName = lcFile
* extraemos el nombre sin path
Do While AT("\",lcName)>0
   lcName = substr(lcName,at("\",lcName)+1)
Enddo
lnSize = FSIZE(lcFile)
lnCanal = FOPEN(lcFile,0) 
* abrimos el fichero a bajo nivel en modo 
* lectura
If lnCanal = -1
   MessageBox("Error al abrir el fichero "+ 
   lcFile,0+16,"Aviso")
   Return
EndIf
* Mandamos aviso de Inicio transmisión 
Thisform.CommControl.Output = "**ITF/"+ALLT(lcName)+;
      "/"+ALLT(STR(lnSize))+"**"+CHR(13)	
Wait Wind "Iniciando transmisión ;
      " TimeOut 3
lnParcial = 0
Do While Not FEOF(lnCanal)
   lcBufferOut = FREAD(lnCanal,2048)
   Thisform.CommControl.Output = lcBufferOut	
   lnParcial = lnParcial +LEN(lcBufferOut)
   ThisForm.Status.Value=100*lnParcial/ lnSize
   ThisForm.txtStatus.Value = "Enviando fichero: "+lcName+". Completados "+ALLT(STR(lnParcial))+" de 
         "+ALLT(STR(lnSize))+" bytes."
EndDo
?CHR(7)
MessageBox("Fichero enviado",0+64,"Aviso")
Thisform.Status.Value = 0
ThisForm.txtStatus.Value = ""
ThisForm.Refresh()
FCLOSE(lnCanal)
ThisForm.lChat = .T.

Veamos la codificación del evento OnComm :

*	Capturamos el último evento producido
Evento = ThisForm.CommControl.CommEvent
Do case
   Case Evento = 2	&& Recibo datos
        ThisForm.ProcesaEntrada
   Case Evento = 7
        Thisform.txtStatus.Value = "Se ha recibido un caracter EOF"
   Case Evento = 6
        Thisform.txtStatus.Value = "Alguien está llamando ..."
        * descolgamos...
	Secuencia = "ATA" 
	Secuencia = Secuencia + Chr(13)
	Thisform.CommControl.Output = Secuencia
   Case Evento = 1001
        Thisform.txtStatus.Value = "Se ha recibido un BREAK "
   Case Evento = 1007
        Thisform.txtStatus.Value = "Carrier Detect Timeout"
   Case Evento = 1002
        Thisform.txtStatus.Value = "Clear To Send Timeout"
   Case Evento = 1003
        Thisform.txtStatus.Value = "Data Set Ready Timeout"
   Case Evento = 1004
        Thisform.txtStatus.Value = "Framing Error"
   Case Evento = 1006
        Thisform.txtStatus.Value = "Port Overrun"
   Case Evento = 1008
        Thisform.txtStatus.Value = "Receive Buffer Overflow"
   Case Evento = 1009
        Thisform.txtStatus.Value = "Parity Error"
   Case Evento = 1010
        Thisform.txtStatus.Value = "Transmit Buffer Full"
Endcase

Faltaría en este ejemplo implementar todas las actuaciones necesarias en función de los diversos tipos de error producido.

Finalmente, veamos el método que hemos creado para procesar los datos de entrada según sean mensajes de chat o parte del fichero en transmisión:

*******************************
* Método ProcesaEntrada() *
*******************************
lcEntrada = Thisform.CommConTrol.Input
If ThisForm.lChat && modo chat
    lnLen = LEN(lcEntrada)
    lnCRpos = AT(CHR(13),lcEntrada)
    ThisForm.InBuffer = ThisForm.InBuffer +
    SUBSTR(lcEntrada,1,lnLen-IIF(lnCRpos>0,1,0))
    If lnCRpos > 0
       IF LEFT(ThisForm.InBuffer,5) = "**ITF"
          ThisForm.lChat = .F.
          * Leemos el nombre y tamaño del fichero que 
          * nos envían
          ThisForm.InBuffer = ;
	        STRTRAN(ThisForm.InBuffer,"*","")
          lnNamePos = AT("/",ThisForm.InBuffer,1) + 1
          lnSizePos = AT("/",ThisForm.InBuffer,2) + 1 
          lcName = SUBSTR(ThisForm.InBuffer,;
                lnNamePos,lnSizePos-lnNamepos-1)
          lnSize = VAL(SUBSTR(ThisForm.InBuffer,lnSizePos))
          ThisForm.txtStatus.Value = "Recibiendo Fichero : "+lcName+" - "+"Tamaño : "+ALLT(STR(lnSize))+" bytes"
          lnParcial = 0
          ThisForm.NuevoFichero = FCREATE(lcName)
       Else	
          Insert Into	cLog (User_id,Mensaje,Hora) Values ("Remoto:",
                 ThisForm.InBuffer,DateTime())
          ThisForm.Grid1.Refresh()		
          ThisForm.InBuffer = ""
       EndIf			
    EndIf		
   Else	&& modo transmisión fichero
    ThisForm.Status.Value=100*lnParcial/lnSize
    lnNumChars=FWRITE(ThisForm.NuevoFichero,
    lcEntrada)	
    lcError = FERROR()
    IF lcERROR != 0
      wait wind nowait "Se ha producido un error en la escritura del fichero"
    ENDIF
    lnParcial = lnParcial + lnNumChars
    Thisform.txtStatus.Value = "Recibiendo Fichero : "+lcName+" - Completados "+ALLT(STR(lnParcial))+" bytes de "+ALLT(STR(lnSize))
EndIf		
If lnParcial = lnSize && fin transmisión
   FCLOSE(ThisForm.NuevoFichero)
   MessageBox("Fichero recibido",0+64,"Aviso")
   ?CHR(7)
   ThisForm.Status.Value = 0
   Thisform.txtStatus.Value = ""
   EndIf
EndIF

Puede que el código parezca algo enrevesado, debido a que hemos incluido la funcionalidad de enviar ficheros mezclada con la de mandar cadenas de texto. Desde luego, resultaría más sencillo hacerlo por separado, pero bueno, esto no es sino una pequeña muestra de lo que puede dar de sí el control. Con unos pocos cambios, también podríamos adaptar este ejemplo para copiar ficheros a nuestro portatil si no tenemos tarjeta de red, ya que como hemos dicho al principio, no hace falta que haya un módem entre los puertos COM conectados. Otra posible utilidad de este control sería la de capturar datos de los lectores de códigos de barras que tienen la conexión por puerto serie en vez de por teclado, para introducir lecturas en aplicaciones de TPV. En general, podemos obtener lecturas de cualquier dispositivo que conectemos a un puerto serie.