Lectura y Escritura en Puertos Serie con .Net Framework 2.0

Por Vinicio Montero

downlanim.gif Descargar ejemplos de este artículo (113 KB).

Contenido

 Introducción
 Acceso a la Lista de Puertos del Sistema
 Configuración, Apertura y Cierre del Puerto
 Transmisión de Datos
 Recepción de Datos
 Código del Usuario
 Conclusión
 Referencias

Introducción

En este artículo se muestra la simplicidad con la cual se pueden manipular los Puertos Seriales utilizando el Framework 2.0. Aquí veremos, además, la utilización de algunas de las nuevas funciones y namespaces que proveen acceso a los recursos del sistema sin necesidad de invocar las API de Windows.

El Framework 2.0 ha mejorado sustancialmente el acceso a los recursos del computador integrando una serie de funciones capaces de hacer casi todo el trabajo por nosotros. En este documento veremos la utilización de algunos elementos, como son:

  • El namespace My para acceder a la lista de puertos en el computador:
For Each strPort As String In My.Computer.Ports.SerialPortNames
  tscbPorts.Items.Add(strPort)
Next
  • Se utiliza además System.IO.Ports para crear un objeto que interactúe directamente con los puertos serie, y se abre el puerto directamente desde los recursos del sistema:
Private WithEvents RS232 As New System.IO.Ports.SerialPort
RS232 = My.Computer.Ports.OpenSerialPort(SelectedPort) 

Acceso a la Lista de Puertos del Sistema

Algo que debemos tener en cuenta es que nuestra aplicación no siempre será utilizada por personas con conocimientos avanzados, por lo tanto es necesario dotar al usuario final de la menor cantidad de informaciones técnicas con el objetivo de reducir los márgenes de errores que puedan ser cometidos; el Framework 2.0 permite hacer esto sin necesidad de escribir grandes cantidades de líneas de código.

Una forma muy eficaz de hacer esto es que tu aplicación presente solamente la lista de puertos disponibles en el sistema. En vez de permitirle al usuario escribir el nombre del puerto, se ofrece todos los puertos disponibles en una lista desplegable; de esta manera el usuario final nunca tendría que intentar acceder a los recursos del sistema para obtener esta información; de esta manera contribuimos además a minimizar la posibilidad de errores o excepciones que puedan surgir por una configuración incorrecta.

El siguiente código utiliza la instrucción For Each para obtener la lista de puertos disponibles desde la colección My.Computer.Ports.SerialPortNames; estos datos son obtenidos en formato string y luego son colocados en una lista desplegable:

For Each strPort As String In My.Computer.Ports.SerialPortNames
   tscbPorts.Items.Add(strPort)
Next
If tscbPorts.Items.Count > 0 Then
   tscbPorts.SelectedIndex = 0
Else
   'No hay puertos en el sistema
End If

El código anterior puebla la lista desplegable tscbPorts con los nombres de los puertos, luego determina si se encontró algún puerto y, si esto es así, selecciona por defecto el primero de ellos.

 

Configuración, Apertura y Cierre del Puerto

En la programación tradicional, utilizando las API de Windows o librerías de terceros que finalmente invocan esas API, siempre hemos tenido que escribir algunas líneas de código tales como BaudRate, Stopbit, Databit, Parity, etc., para configurar el puerto antes de abrirlo. Por supuesto, eso también es posible en el Framework 2.0; todas estas propiedades pueden ser establecidas. Sin embargo, en este artículo pretendo mostrar las nuevas funcionalidades que permiten iniciar todas estas propiedades en una sola línea de código.

Debido a que ya tenemos una lista desplegable -tscbPorts, la cual contiene los nombres de los puertos- y suponiendo que el usuario haya hecho su elección, accedemos a la propiedad SelectedItem para obtener el nombre del puerto seleccionado; luego de abrir el puerto, deshabilitamos la lista para evitar que el usuario seleccione otro puerto e intente abrir un segundo puerto (por supuesto que tú puedes establecer los controles que desees para este fin sin embargo, como método de simplificación, utilizo el camino más corto).

Debido a que la manipulación de puertos es muy volátil, es necesario tomar control sobre las excepciones con el objeto de evitar el mal funcionamiento de nuestra aplicación y a la vez informar al usuario de las causas y fallas en la apertura del puerto. Para esto, encierro el código en un bloque Try , Catch , End Try y utilizo las excepciones más comunes -System.IO.IOException y System.UnauthorizedAccessException- para obtener un reporte detallado del error, ya sea porque el puerto esté abierto o no exista, y finalmente utilizo System.Exception para atrapar cualquier otra excepción que pueda surgir:

Dim SelectedPort As String = tscbPorts.SelectedItem
Try
   RS232 = My.Computer.Ports.OpenSerialPort(SelectedPort, 9600, IO.Ports.Parity.None, 8, IO.Ports.StopBits.One)
   txtConsole.AppendText(RS232.PortName & " abierto a las " &_
   Now.ToString & vbCrLf)
   tscbPorts.Enabled = False
Catch ex As System.IO.IOException
   txtConsole.AppendText("Error abriendo el puerto: " & _
   vbCrLf & ex.Message & vbCrLf)
Catch ex As System.UnauthorizedAccessException
   txtConsole.AppendText("El pueto ya esta abierto: " & vbCrLf & _
   ex.Message & vbCrLf)
Catch ex As System.Exception
   txtConsole.AppendText("Error general accediendo al puerto:" & _
   vbCrLf & ex.Message & vbCrLf)
End Try

Como se puede observar, en la tercera línea el puerto es abierto e inicializado con los parámetros de configuración ya preestablecidos dentro del código. Para llevar a cabo una codificación más robusta y flexible al usuario, te recomiendo utilizar un archivo .XML para guardar esta configuración.

 

Transmisión de Datos

Con un objeto System.IO.Ports.SerialPort podemos escribir casi todo tipo de datos hacia un puerto serie; entre éstos, podemos enviar desde un simple caracter hasta un buffer de carácteres y/o bytes; sin embargo, para este ejemplo utilizamos la función WriteLine(), la cual envía una cadena de carácteres y a la vez un terminador de línea encontrado en la propiedad NewLine.

Para este ejemplo utilizaremos una caja de texto llamada tstxtSend. Antes de enviar datos a un puerto serie te recomiendo verificar que el tipo de dato a enviar concuerde con la función que vamos a utilizar; en este ejemplo sólo se verifica si se está intentando enviar una cadena vacía o no, evitando así resultados no deseados.

If tstxtSend.Text.Length > 0 AndAlso RS232.IsOpen Then
   RS232.NewLine = vbCrLf
   RS232.WriteLine(tstxtSend.Text.Trim)
   txtConsole.AppendText("Cadena enviada: " & _tstxtSend.Text.Trim & vbCrLf)
Else
   txtConsole.AppendText("Nada ha sido enviado" & vbCrLf)
End If

 

Recepción de Datos

La recepción de datos es bastante sencilla o bien puede ser un poco más compleja cuando se requiere interactividad o cuando la recepción de datos puede darse de manera espontánea y no programada. En este artículo veremos ambas formas; dividiremos este tema en dos partes para mostrar lo simple o compleja que puede ser.

Método simple: hacemos referencia a la lectura de datos programada, o sea que se ejecuta el método de lectura del objeto justo en el momento en que deseamos recibir los datos. Por ejemplo:

StrRead = objeto.Read
IntRead = objeto.ReadByte
IntRead = objeto.ReadChar
StrRead = objeto.ReadExisting
StrLineRead = objeto.ReadLine

Puedes utilizar cualquiera de estos métodos de acuerdo a tus necesidades o al tipo de dato que deseas recibir; como puedes notar, en las 3 primeras letras se indica el tipo de datos a leer. Generalmente este método aplica a ocasiones en que enviamos datos a un dispositivo y luego esperamos una respuesta. Por ejemplo:

RS232.WriteLine(tstxtSend) 'Se Envia el dato.
RS232.ReadExisting() 'Lee la respuesta.

Método Complejo: A menudo estamos interactuando con dispositivos que pueden no requerir una trasmisión de datos en ambos sentidos o que pueden genera información de una manera espontánea. Es entonces donde entra en juego el evento DataReceived, el cual se dispara en el mismo momento en que se detecta la llegada de datos al puerto previamente abierto. Este evento se dispara en un hilo o Thread separado; puedes manipular los datos en ese hilo de modo separado, como por ejemplo enviarlos a una base de datos, pero muchas veces se necesita pasar esta información al hilo principal para desplegar estos datos en algún objeto del formulario; necesitaremos utilizar un delegado para invocar un método dentro del hilo principal que contiene la Interfaz de Usuario.

Delegate Sub WriteDataDelegate(ByVal str As String)

En el siguiente código se muestra el procedimiento utilizado por el evento:

Private Sub RS232_DataReceived(ByVal sender As Object, ByVal e As 
System.IO.Ports.SerialDataReceivedEventArgs) _
Handles RS232.DataReceived
   Dim strData As String = RS232.ReadExisting
   Dim WriteInvoke As New _
WriteDataDelegate(AddressOf Me.WriteData)
   Me.Invoke(WriteInvoke, strData)
End Sub

Si observamos el código, encontraremos que al dispararse el evento DataReceived lo primero que hacemos es obtener los datos provenientes del disposivo conectado al puerto ya abierto, en este ejemplo se utiliza ReadExisting. Una vez obtenidos los datos provenientes del puerto, procedemos a instanciar el procedimiento WriteData a través del delegado WriteDataDelegate. Finalmente invocamos el procedimiento previamente instanciado pasando los datos capturados desde el puerto serie. Dentro del procedimiento WriteData colocamos el siguiente código:

Private Sub WriteData(ByVal str As String)
   txtConsole.AppendText(str)
End Sub 

La razon de que el evento se dispare en hilo separado ofrece sus ventajas y desventajas de acuerdo a la cantidad de información a recibir y del procesamiento de esta información. Debido a que el Framework utiliza ciertos recursos del sistema para manejar los hilos, sería conveniente no utilizar el evento DataReceived a menos que la situación lo amerite.

Sin embargo, este mismo evento nos da una ventaja enorme cuando, por ejemplo, se recibe una cadena de texto que debe ser analizada, procesada y finalmente almacenada en una base de datos, porque todo este proceso es realizado en hilos separados de la interfaz de usuario. Recomiendo investigar sobre System.Threading, Delegate e Invoke().

 

Código del Usuario

A continuación te dejo el código necesario para esta aplicación; nota que las definiciones de los controles del formulario no están en este código; sin embargo, al final te dejo la lista de controles utilizados.

Public Class Console
   Private WithEvents RS232 As New System.IO.Ports.SerialPort
   Private _strData As String = ""
 
   Private ReadOnly Property DataRecivida()
      Get
         DataRecivida = _strData
         _strData = ""
      End Get
   End Property
 
   Private Sub Console_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)       Handles
 MyBase.Load
      For Each strPort As String In My.Computer.Ports.SerialPortNames
      tscbPorts.Items.Add(strPort)
      Next
      If tscbPorts.Items.Count > 0 Then
         tscbPorts.SelectedIndex = 0
      Else
         'No hay puertos en el sistema
      End If
   End Sub
 
   Private Sub ToolStripButton1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles
 ToolStripButton1.Click
      Dim SelectedPort As String = tscbPorts.SelectedItem
      Try
         RS232 = My.Computer.Ports.OpenSerialPort(SelectedPort, 9600, IO.Ports.Parity.None, 8,
 IO.Ports.StopBits.One)
         txtConsole.AppendText(RS232.PortName & " abierto a las " & Now.ToString & vbCrLf)
         tscbPorts.Enabled = False
         Timer1.Start()
      Catch ex As System.IO.IOException
         txtConsole.AppendText("Error abriendo el puerto: " & vbCrLf & ex.Message & vbCrLf)
      Catch ex As System.UnauthorizedAccessException
         txtConsole.AppendText("El pueto ya esta abierto: " & vbCrLf & ex.Message & vbCrLf)
      Catch ex As System.Exception
         txtConsole.AppendText("Error general accediendo al puerto:" & vbCrLf & ex.Message & vbCrLf)
      End Try
   End Sub
 
   Private Sub ToolStripButton2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles
 ToolStripButton2.Click
      RS232.Close()
      Timer1.Stop()
      txtConsole.AppendText(RS232.PortName & " cerrado a las " & Now.ToString & vbCrLf)
      tscbPorts.Enabled = True
   End Sub
 
   Private Sub ToolStripButton4_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles
 ToolStripButton4.Click
      If RS232.IsOpen Then
         RS232.Close()
      End If
      Me.Dispose()
   End Sub
 
   Private Sub RS232_DataReceived(ByVal sender As Object, ByVal e As
 System.IO.Ports.SerialDataReceivedEventArgs) Handles RS232.DataReceived
      While _strData.Length > 0
 
      End While
      _strData = RS232.ReadLine
   End Sub
 
   Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
      If _strData.Length > 0 Then
         txtConsole.AppendText(DataRecivida & vbCrLf)
      End If
   End Sub
 
   Private Sub ToolStripButton3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles
 ToolStripButton3.Click
      If tstxtSend.Text.Length > 0 AndAlso RS232.IsOpen Then
         RS232.NewLine = vbCrLf
         RS232.WriteLine(tstxtSend.Text.Trim)
         txtConsole.AppendText("Cadena enviada: " & tstxtSend.Text.Trim & vbCrLf)
      Else
         txtConsole.AppendText("Nada ha sido enviado" & vbCrLf)
      End If
   End Sub
End Class
 
Controles utilizados en el formulario del formulario
 
ToolStrip1 = New System.Windows.Forms.ToolStrip
ToolStripButton1 = New System.Windows.Forms.ToolStripButton
ToolStripButton2 = New System.Windows.Forms.ToolStripButton
ToolStripButton3 = New System.Windows.Forms.ToolStripButton
ToolStripButton4 = New System.Windows.Forms.ToolStripButton
tscbPorts = New System.Windows.Forms.ToolStripComboBox
tstxtSend = New System.Windows.Forms.ToolStripTextBox
txtConsole = New System.Windows.Forms.TextBox
Timer1 = New System.Windows.Forms.Timer(Me.components)

 

Conclusión

El .Net Framework 2.0 está dotado de nuevas clases y un namespace que hacen realmente posible la interacción con los puertos serie del computador de una manera eficiente, fácil y sobre todo dentro del ambiente de código manejado sin necesidad de recurrir a librerías y objetos de terceros.

 

Referencias

La información que se muestra en este artículo fue encontrada en la librería en línea de MSDN y en la ayuda de Visual Studio 2005 Edición Express. El código fue desarrollado en Visual Basic Express Beta 2.

Bb972257.Vinicio_Montero(es-es,MSDN.10).gif Vinicio Montero trabaja en Santo Domingo, Dominicana, como responsable del departamento de servicios de una importante empresa de Ventas y Servicios Informáticos en Antillas Menores y el Caribe; y ha trabajado en diferentes corporaciones tanto publicas como privadas, siendo responsable de la administracion de sistemas de negocios integrados. . Posee experiencia programando con Cobol RM87, Foxpro para DOS y Windows, Visual Foxpro y Visual Studio .net; además de extensos conocimientos de infraestructura de redes y plataformas de sistemas operativos. Cuenta con tres certificaciones Microsoft Bussines Solutions, así como otras aplicaciones de negocios. Ha obtenido 3 Estrellas del programa Desarrollador 5 Estrellas de Microsoft MSDN.