Comunicaciones Serie en Windows (y III)

Se explica el proceso que se sigue en una conexión y el establecimiento de parámetros

Por Mario Rodríguez

Continuamos con la explicación del proceso de conexión a través de los componentes suministrados en la anterior entrega. Estos componentes también se incluyen con ésta y deben ser instalados en el IDE de Delphi mediante el archivo MundoDel.dpk que también se incluye con los ejemplos. La instalación de los componentes se realiza como siempre: se abre el archivo MundoDel.dpk desde el IDE y se pulsa en el botón Install. Una vez finalizado el proceso, se tendrá una nueva pestaña entre los componentes, denominada “Mundo Delphi” con dos componentes, tal y como se ve en la Figura 1.

El componente TPuertoSerie permite el acceso directo al puerto y debería ser utilizado cuando no se requieran características del módem. Por ejemplo, cuando se tenga un módem nulo (del que se habló en la primera entrega de esta serie). El componente TControlModem añade al anterior las características de un módem enchufado al puerto serie.

En el ejemplo que se incluye con esta entrega se utiliza el componente ‘TControlModem’ y, en lo que sigue, se tratarán las características que se aplican a este componente. Dado que ‘TControlModem’ se deriva de ‘TPuertoSerie’ y que añade a este último muy pocas características, la discusión que sigue se aplica a ambos componentes y, cuando no sea así, se dirá.

El ejemplo de esta entrega no es operativo en el sentido que se requiere algún tipo de protocolo de transferencia de ficheros para poder trasladar, de una máquina a otra, uno o varios archivos. Así que debe tomarse como una plantilla sobre la que situar el protocolo.

Una de las características distintivas del módem es, precisamente, la que se explica a continuación: en la conexión directa mediante un módem nulo no tiene sentido ya que se podrán enviar y recibir las tramas (esto es, los grupos de bytes) en el mismo momento en que la conexión física sea establecida. En la Figura 2 se muestra la primera pestaña de la pantalla principal de la aplicación.

Se han creado una serie de parámetros para la conexión en modo cliente: un identificador de cliente, el número de teléfono a marcar y el directorio donde se guardarán los archivos que suministre el servidor. El identificador del cliente es necesario para que el servidor sepa que el que está llamando corresponde a un cliente reconocido. De no hacerlo así, cualquiera que llame por teléfono recibiría, a cambio, el archivo o archivos que envíe el servidor. El teléfono, lógicamente, es aquel en que está el servidor a la escucha. Y el directorio de descarga es donde se situarían los archivos que envíe el servidor. Esto último puede variar de una aplicación a otra: quizá el cliente pueda llamar al servidor y, una vez establecida la conexión, sea el propio cliente el que envíe los archivos al servidor. En ese momento el cliente se convierte en servidor pero de alguna forma había que denominarlos.

La conexión en modo servidor tiene sólo dos parámetros: el identificador del cliente, que debe ser el mismo que posea el cliente que llama, y el archivo que se enviará al cliente reconocido. Como antes, esto puede variar de una aplicación a otra y hasta es posible enviar varios archivos seguidos o simplemente recibir e incluso que la comunicación sea en ambas direcciones, esto es, que ambos se intercambien archivos.

El paso de modo cliente a servidor y viceversa se realiza mediante una opción de menú. En el Listado 1 puede verse cómo se efectúa el cambio en la aplicación de ejemplo. En ella, dos opciones de menú llaman al evento “MnuArchModoClick()”.

Cuando se pasa a “modo servidor” se comprueba si el número de llamadas (sonidos del teléfono) es mayor que cero. Si no lo es, se pone a 1. Esto es necesario para que el módem avise de cuándo llega un “ring” a la aplicación. Ponerlo a cero evita que la aplicación sea informada del evento.

procedure TFrmModemER.MnuArchModoClick(Sender: TObject);
begin
  if (MnuArchModoCliente.Checked) then
  begin
    // Marcar el menú correspondiente
    MnuArchModoCliente.Checked := FALSE;
    MnuArchModoServidor.Checked := TRUE;
    // Indicar que nos ponemos en modo servidor
    ControlModem.ConfigModem.Servidor := TRUE;
    // O se pone un número de 'ring' mayor que cero o
    // no funcionará el descuelgue automático del teléfono
    if (0 = ControlModem.ConfigModem.NumRings) then
      ControlModem.ConfigModem.NumRings := 1;
    // En modo servidor, se abre el puerto y se pone en modo "escucha".
    if (not (ControlModem.Conectado)) then ControlModem.Abrir;
  end
  else
  begin
    // Marcar el menú correspondiente
    MnuArchModoCliente.Checked := TRUE;
    MnuArchModoServidor.Checked := FALSE;
    // Indicar que quedamos en "modo cliente"
    ControlModem.ConfigModem.Servidor := FALSE;
    // Aquí quizá habría que cerrar y reabrir el puerto
  end;
end;

Listado 1. Cambio de modo: cliente o servidor

Cuando la propiedad “NumRings” tiene un valor superior a cero, durante la apertura del puerto se escribe en el mismo la cadena “S0=NumRings” (ver el procedimiento ‘TControlModem.Abrir()’, en el módulo ‘ConModem.pas’). Esto hace que el registro “S0” del módem adquiera el valor del número de llamadas que deben ser monitorizadas antes de que el módem envíe un evento de tipo ‘EV_RING’ a la aplicación (ver la captura de este evento en el procedimiento ‘THiloLectura.ComprobarFlags()’ del módulo ‘HiloLeer.pas’).

Este es el método por el que podemos situar un módem en modo auto-respuesta en cualquier aplicación: abrir el puerto y escribir en el mismo “S0=X” donde, claro está, ‘X’ es un número mayor que cero y menor de 255. Luego deberíamos leer los eventos del puerto hasta recibir un evento de tipo ‘EV_RING’ desde el módem.

Por suerte, los componentes entregados hacen este trabajo. El Listado 2 muestra cómo se atiende a un evento ‘EV_RING’ desde la aplicación de ejemplo. Si se está en modo cliente no se hace nada, excepto dibujar el indicador del “ring”. Si se está en modo servidor se contesta a la llamada mediante la rutina ‘TControlModem.ContestarLlamada()’. El Listado 3 muestra esta rutina.

La primera operación que se realiza es escribir en el puerto la cadena “ATA”. “AT” le indica al módem que lo que sigue es un comando (en este caso, el comando “A”). Esto le indica al módem que descuelgue y genere la señal de portadora (CD por Carrier Detect) que son esos ruidos característicos que se escuchan al inicio de una conexión por módem. Es habitual que en el otro lado, en el del cliente, se escriba el comando “ATD” que indicaría al módem que se va a establecer una comunicación.

procedure TFrmModemER.ControlModemCambiarRING(Sender: TObject);
  EstadoRINGPaint(EstadoRING);  // Dibujar el indicador de 'RING'
  // ¿Ha habido un ‘ring’ y estamos en modo servidor?
  if ((ControlModem.EstadoRING) and (ControlModem.ConfigModem.Servidor)) then
  begin
    // ¿Se ha logrado contactar con un módem remoto?
    if (ControlModem.ContestarLlamada(Trim(IdServidor.Text))) then
    begin
      // ¿Se ha conectado al equipo remoto?
      if (emConectado = ControlModem.EstadoModem) then
      begin
        // ¿Hay algo que enviar?
        if ('' <> Trim(ArchivoAEnviar.Text)) then
        begin
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
          // Aquí entraría en marcha el protocolo de transferencia de
          // archivos: se leerían tramas del archivo o archivos a enviar
          // y se escribirían mediante 'ControlModem.Escribir()'.
          //
          // Finalmente, se cerraría el puerto y se volvería a abrir
          // colocándose, como ahora, a la escucha de llamadas entrantes.
          // ...
          // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        end;
      end;
    end;
  end;

Listado 2. Ejemplo de detección de un ring

A continuación se espera un tiempo hasta vaciar la cola de escritura (‘EsperarVaciarBuffer()’) y se obtiene la respuesta del módem (‘ObtenerRespuestaModem()’): si esta respuesta ha sido correcta, se tendrá uno de los códigos de conexión (“CONNECT” o “CARRIER”) que habrán actualizado el campo “FEstadoModem” de la clase al valor correspondiente.

Se ha de hacer notar que el procedimiento ‘ObtenerRespuestaModem()’ presupone siempre que se devuelven desde el módem los códigos de resultado en forma de texto, esto es, se escribe al abrir el puerto el comando “X4” cosa que los componentes hacen automáticamente (ver la primera entrega de esta serie).

Otro aspecto a tener en cuenta es que puede que determinados módem devuelvan códigos de resultado distintos. Si falla por algún motivo, situar un punto de ruptura en ‘ObtenerRespuestaModem()’ y comprobar qué se lee desde el puerto.

Tras haber conectado al módem remoto se ponen a nivel alto las señales DTR (Data Terminal Ready), que informa al módem que el ordenador está activo y listo para una comunicación, y RTS (Request To Send), que indica al módem que el ordenador quiere transmitir datos.

Si a la función ‘ContestarLlamada()’ se le pasa una cadena vacía en el argumento, el proceso de conexión acaba aquí y se vuelve a la rutina llamadora (en nuestro ejemplo, a ‘ControlModemCambiarRING()’) que podría comenzar el envío o recepción de datos en ese mismo momento.

En cambio, si ‘ContestarLlamada()’ recibe un argumento válido, explora la lectura del módem buscando el código de conexión con el cliente que debería coincidir con el que el servidor tiene anotado, esto es, con la cadena que se le ha pasado en el argumento a esta función. Tómese esto como un ejemplo de conexión que debería ser modificado para las características que requiera su aplicación. Lo importante aquí es mostrar cómo se utiliza la clase ‘TLecturaLinea’ para explorar lo leído del módem.

function TControlModem.ContestarLlamada(const CodigoConexion: String): Boolean;
var
  LecturaLinea: TLecturaLinea;
  CodigoRecibido: String;
begin
  Result := FALSE;
  if ((emContestando <> EstadoModem) and (emConectado <> EstadoModem)) then
  begin
    // Iniciamos el 'Estado del módem'.
    // Si hay suerte, vale 'emConectado' a la salida
    FEstadoModem := emContestando;
    try
      // Esto no debería pasar nunca: la línea debería estar
      // abierta si se ha recibido un 'Ring'. En caso contrario,
      // se está haciendo una llamada que va a fallar de seguro.
      if (not (LineaAbierta)) then Abrir;
      // Por la misma razón, esto debería ser siempre TRUE
      if (LineaAbierta) then
      begin
        // Descolgar y generar señal de portadora (CD)
        EscribirString('ATA'+ #13#10);
        // Esperar 10 segundos a que se vacíe la cola
        // de escritura y vaciar el buffer de salida
        EsperarVaciarBuffer(ESPERA_10_SEGUNDOS);
        // Obtener la respuesta textual del módem
        ObtenerRespuestaModem;
        // Si se ha detectado un (otro) 'Ring', volver a intentarlo
        if (emRing = EstadoModem) then ObtenerRespuestaModem;
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        // Cualquiera de estos códigos indican que se ha efectuado la conexión
        //
        // Si tenemos 'emCarrier' o 'emConnect' se indica al módem que el
        // terminal está preparado para enviar (DTR = 'Data terminal ready')
        // y que se desea enviar (RTS = 'Request To Send')
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        if (((emCarrier = EstadoModem) or (emConnect = EstadoModem)) and
            (PonerDTR(TRUE)) and (PonerRTS(TRUE)))
        then
        begin
          if ('' = CodigoConexion) then
          begin
            // Si no hay código de conexión, la rutina acaba aquí
            FEstadoModem := emConectado;
            Result := TRUE;
          end
          else
          begin
            // Si hay código de conexión, comprobar el código del cliente
            LecturaLinea := TLecturaLinea.Create(Self);
            try
              CodigoRecibido := '';  // Iniciar variable
              repeat
                Application.ProcessMessages;
                // Si el carácter leído no pertenece a la cadena...
                if (0 >= Pos(LecturaLinea.LeerChar, CodigoConexion)) then
                begin
                  // ... y realmente se ha leído algo...
                  if (0 <> LecturaLinea.ByteActual) then
                    CodigoRecibido := '';  // ... reiniciar la cadena
                end
                else
                begin
                  CodigoRecibido := CodigoRecibido + Char(LecturaLinea.ByteActual);
                  if (0 = CompareStr(CodigoRecibido, CodigoConexion)) then
                    FEstadoModem := emConectado;  // Código recibido
                end;
                // Se está en el bucle mientras se conectado a un equipo
                // remoto y no se haya recibido el código de identificación
              until ((not (Conectado)) or
                   (not (EstadoRLSD)) or
                   (emConectado = FEstadoModem));
              Result := (emConectado = FEstadoModem);  // Resultado
            finally
              LecturaLinea.Free;
            end;
          end;
        end;
      end;
    except
      FEstadoModem := emError;
    end;
  end;
end;