C# SerialConsoleApp3: Gestión línea serie

Esta vez vamos evitar usar el manejador de evento o método delegado usando SerialDataReceivedEventHandler. En vez de eso vamos a tomar el control del intervalo de lectura del buffer del puerto serie. Primero añadimos el espacio de nombres System.Timers para usar la clase Timer en nuestra nueva clase COMTimer.

Clase COMTimer

La clase COMTimer solo tiene dos atributos, una clase Timer y un booleano que nos informa de si el intervalo de tiempo de espera ha expirado, el objetivo es que podamos consultar la propiedad bTimeOut en cualquier momento y comprobar si el periodo definido en mili-segundos ha transcurrido (no bloquea la ejecución de nuestro programa principal).

Constructor

Inicializamos la bTimeOut a false (se pondrá true cuando expire el intervalo). También definimos algunos atributos de la clase Timer.

  • AutoReset: indica si Timer debe generar el evento Elapsed solo una vez (false) o repetidamente (true).
  • Enabled: indica si Timer debe generar el evento Elapsed.
  • Interval: Obtiene o establece el intervalo, expresado en milisegundos, en el que se generará el evento Elapsed.
  • Elapsed: Se produce cuando transcurre el intervalo. Definimos el método OnTimeoutCOMEvt que será invocado cuanto el intervalo transcurra.

Evento OnTimeoutCOMEvt

Cuando el intervalo de tiempo expire el método OnTimeoutCOMEvt es invocado, establece bTimeOut a true y para la instancia _objCOMTimer de la clase Timer  con el método Stop.

Start

Clase COMPort

Hemos definido un nuevo método de la clase COMPort que viene a sustituir el método delegado EvtCOMDataRx. Recibe como parámetro el periodo de espera para recibir un nuevo byte (en ms).  Inicia el timer y a continuación entra en un bucle infinito hasta que expira el tiempo o hay algo para leer por el buffer serie.

Uso de la clase COMPort

En vez de usar Console.Read para para ejecución del programa al final usamos Console.KeyAvailable para saber si hay pulsación de teclado para salir de la aplicación, mientras tanto trata de leer (le pasamos 1 ms de intervalo de Tout)

C# SerialConsoleApp2: Gestión básica de la línea serie

A partir del ejercicio C# SerialConsoleApp1: Gestión básica de la línea serie vamos a seguir refinando nuestra clase básica para comunicaciones RS232.

Constructor

Hemos sobrecargado el método OpenCOMPort para simplificarlo, el nuevo método sólo recibe el parámetro con la cadena que define el puerto que queremos abrir, en la mayoría de los casos el resto de parámetros se mantienen y suelen ser los mismos.

Definimos una serie de constantes como atributos de la clase con los valores por defecto.

Destructor de la clase

También comprobamos en el destructor de la clase si el puerto aún sigue abierto y si es así lo cerramos.

GetPortList: Nuevo método

Definimos un nuevo método para obtener la lista de puertos y así poder hacer lo que queramos (no solo mostrarlos por consola), por ejemplo alimentar un selector desplegable con esta información en una aplicación de ventanas.

El nuevo método retorna una lista de cadenas. Una clase List<T> representa una colección de objetos del mismo tipo indexados en una lista por posición. Al igual que cualquier clase tiene un constructor, propiedades y métodos (Add, BinarySearch, …)

Su uso es igualmente sencillo:

Leer de la línea serie byte a byte

Vamos a mejorar ligeramente el método de lectura para leer el buffer serie un valor cada vez, esto nos permite mayor control de la información leída realizando un tratamiento de cada byte a medida que llega.

Resultado

Volvemos a conectar el Arduino del primer ejercicio para poder leer la cadena que envía.

 

C# SerialConsoleApp1: Gestión básica de la línea serie

Conector DB9 (9 pines). Para conexiones en serie, ya que permite una transmisión asíncrona de datos según lo establecido en la norma RS-232

Hoy en día la mayoría de los temarios y contenidos de cursos de lenguajes de programación se dirigen a aplicaciones y servicios en la nube o interfaces gráficos de escritorio, pocas veces se tratan temas como protocolos de comunicación entre máquinas a bajo nivel.

El protocolo serie (RS232) aún hoy es un sistema de comunicaciones muy común en maquinas industriales, es conocido, confiable y además ya existe un montón de código desarrollado en cualquier lenguaje de programación.

Por desgracia hoy día la mayoría de máquinas no traen por defecto este puerto (en los PCs industriales sin embargo es común que vengan con varios puertos COM), en su lugar usan un puerto COM emulado mediante controladores del sistema operativo sobre un cable USB (por ejemplo un dispositivo Arduino).

Inclusión del espacio de nombres

Para trabajar con los puertos COM debemos añadir las siguientes líneas al comienzo del programa (más sobre System.IO en MSDN).

Primer paso: Obtención de puertos COM del sistema

Lo primero es crear una clase básica que albergara todas nuestras funciones para trabajar con el puerto serie. La clase inicial sólo contendrá un método público para imprimir por consola una enumeración de los puertos disponibles en el sistema operativo.

Para obtener los puertos hemos empleado el método SerialPort.GetPortNames. Este método estático retorna un array de cadenas con los nombres de los puertos serie.

Como no tengo otra cosa he conectado mi placa Arduino al PC y este es el resultado.

Abriendo el puerto COM

Lo siguiente es diseñar un método para configurar y abrir el puerto serie, la función recibe los parámetros de configuración de la línea.

Lo más interesante de esta función es la definición del evento DataReceived. Hacemos que DataReceived apunte a un método nuestro (EvtCOMDataRx) que será invocado cada vez que la línea serie indique que ha recibido nuevos datos (handle o manejador), de esta forma no tenemos que estar constantemente preocupados de si han llegado nuevos datos o no.

Enviando datos de prueba de Arduino a nuestro programa

Para no perder el tiempo he usado un proyecto super básico que ilustra como enviar una cadena desde nuestra placa Arduino al PC.

 

Cuando ejecuto mi aplicación C# puedo ver los datos enviados en mi PC!

Posts relacionados

Enlaces externos

C# ThreadingBasics (II): Operaciones básicas con Threads

Código en GitHub

GetThreadState: Determinar el estado de un thread

El siguiente ejercicio nos permite obtener información valiosa sobre el estado de un Thread. Añadimos los siguientes métodos:t1

Y añadimos el siguiente código a la función principal Main:

t1

El Thread pasa por varios estados, podemos obtener el estado del hilo accediendo a la propiedad ThreadState que es un campo enumerado.

Inicialmente el estado del Thread t1 es Unstarted antes de ser arrancado. Cuando invocamos el método Start() inmediatamente pasa al estado Running. La llamada a Sleep provoca que pase al estado WaitSleepJoin. Finalmente la llamada Abort hace que pase a estado Aborted. El thread t2 finaliza con normalidad y su estado estado final es Stopped.

TheadPriority: Establecer prioridad en la ejecución de un thread


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Threading;

namespace TheadPriority
{
class ThreadSample
{
private bool _isStopped = false;

public void Stop()
{
this._isStopped = true;
}

public void CountNumbers()
{
long counter = 0;
while (!this._isStopped)
{
counter++;
}
Console.WriteLine("{0} with {1,11} priority " +
"has a count = {2,13}",
Thread.CurrentThread.Name,
Thread.CurrentThread.Priority,
counter.ToString("N0"));
}
}

class Program
{
static void RunThreads()
{
var sample = new ThreadSample();
var t1 = new Thread(sample.CountNumbers);
t1.Name = "t1";
var t2 = new Thread(sample.CountNumbers);
t2.Name = "t2";

t1.Priority = ThreadPriority.Highest;
t2.Priority = ThreadPriority.Lowest;

t1.Start();
t2.Start();

Thread.Sleep(TimeSpan.FromSeconds(10));
sample.Stop();
}

static void Main(string[] args)
{
Console.WriteLine("Main: current thread priority: {0}",
Thread.CurrentThread.Priority);
Console.WriteLine("Main: Calling RunThreads()");
RunThreads();
Console.WriteLine("Press any key to exit....");
Console.ReadLine();
}
}
}

C# ThreadingBasics: Operaciones básicas con threads

Creando un thread en C#

Vamos a crear una aplicación de consola. Añadimos el espacio de nombres using System.Threading;. Creamos una función static PrintNumbers (los hilos no retornan nada ni reciben parámetros) que escribe por consola del 1 al 10 usando un bucle for.

t1

Una instancia de un programa en ejecución se puede definir como un proceso. Un proceso consiste en uno o más threads. Esto significa que cuando ejecutamos un programa, siempre tenemos un thread principal que ejecuta el código del programa.

El método PrintNumbers lo invocaremos dos veces, desde el Main y como thread o hilo concurrente.

t2

Pausando un thread

En el siguiente ejemplo dentro de la función PrintNumbersWithDelay hacemos una llamada al método Thread.Sleep que  suspende la ejecución durante 2 segundos (usamos la estructura TimeSpan para con el método  FromSeconds para convertir de segundos en milisegundos, la magnitud reconocida por el método Sleep).

t1

Haciendo que un thread espere

En este caso vamos a hacer que la función principal Main detenga su ejecución  y espere a que el el thread acabe. Usamos el método Thread.Join()  que bloquea el subproceso de llamada hasta que finaliza el subproceso representado por esta instancia, si le pasásemos un valor al método esperaría el tiempo especificado a que el thread finalice.

t1Abortando la ejecución de un thread

Ahora el hilo principal Main espera 6 segundos y aborta la ejecución del thread con el método Abort() que lanza la excepción ThreadAbortException en el subproceso en el que se invoca.

t1 No es muy recomendable usar el método Abort ya que el resultado puede ser incierto.

 

C# GuiUpdateThread: Refrescar el contenido de la ventana desde un subproceso

En ocasiones cuando desarrollamos aplicaciones de ventanas es necesario desarrollar un proceso largo, si lo hacemos de la forma clásica puede suceder que la interfaz se bloquee, lo ideal en estos casos lo ideal es usar subprocesos.

Vamos a emplear una clase Thread para actualizar el contenido de un campo de edición en un formulario. El thread arranca cuando pulsamos un botón y activa un contador de 0 a 100 con un Sleep en cada ejecución del bucle, cada iteración refresca el contenido del campo de edición con el valor del contador.

t1

Controles:

  • btnStartThread: Botón con leyenda “&StartThread” y diseño plano, cuando hacemos clic sobre el desencadena el evento btnStartThread_Click que ejecuta el subproceso txtNumUpdateThread.
  • txtNum: Campo de edición deshabilitado (Enabled=False).

Cuando pulsamos el botón programamos el arranque del evento:

t2

Un hilo es una instancia de la clase Thread, al crear la instancia debemos pasar como parámetro al constructor la instancia de un delegado (ThreadStart), el delegado apunta al método que corre en segunda plano actualizando el contenido del campo de edición txtNum (txtNumUpdateThread).

Ahora creamos el método txtNumUpdateThread, este será el encargado de refrescar el contenido del campo de edición:

t3

Cuando tenemos actualizar un componente del interfaz gráfico desde un método en un hilo separado en vez de escribir simplemente:

txtNum.Text = x.ToString();

Debemos usar el método Invoke del control (la propiedad InvokeRequired del control contiene true si debemos usar un método de invocación).

“El acceso a los controles de Windows Forms no es intrínsecamente seguro para subprocesos. Si tiene dos o más subprocesos que manipulan el estado de un control, es posible que este acabe teniendo un estado incoherente”

Podemos comprobarlo añadiendo algo de código a la función anterior:

t4

 

GuiUpdateThread2: Segundo ejemplo


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Threading;

namespace GuiUpdateThread2
{
public partial class Form1 : Form
{
// delegado con la misma firma del método
public delegate void UpdateTextCallback(string msg);

public Form1()
{
InitializeComponent();
}

private void UpdateTxtMsg(string StrMsg)
{
TxtMsg.Text = StrMsg;
}

private void UpdateTxtThread()
{
string StrTxt = "Texto generado desde otro hilo.";
if (TxtMsg.InvokeRequired)
{
TxtMsg.Invoke(new UpdateTextCallback(this.UpdateTxtMsg),
new object[] { StrTxt });
}
else
{
TxtMsg.Text = StrTxt;
}
}

private void button1_Click(object sender, EventArgs e)
{
ThreadStart delegado = new ThreadStart(UpdateTxtThread);
Thread hilo = new Thread(delegado);
hilo.Start();
}
}
}

 

Posts en este blog:

Enlaces externos: