Porte Seriali e VB.NET

Sul forum ho letto (e anche risposto) a parecchie domande che riguardano l’uso e/o la gestione di porte seriali in ambiente .NET.

Lungi dall’essere esaustivo come post, e quasi sicuramente pieno di bestialità, mi permetto di illustrare alcuni metodi che ho implementato nei miei programmi, grazie ai quali ho ottenuto comunque buoni risultati.

A partire dalla versione 2 del .NET framework, è disponibile una classe SerialPort che permette di gestire in maniera abbastanza completa le porte seriali.

Questa classe contiene le funzioni di lettura e scrittura, oltre che di gestione della porta stessa (che ometto di descrivere) e di tre eventi dei quali invece vorrei dire qualcosa:

  • DataReceived

Viene lanciato quando un carattere è presente nel buffer di ricezione.  Nel suo handler espone un oggetto della classe System.IO.Ports.SerialDataReceivedEventArgs, che ci indica se è stato ricevuto un carattere valido oppure il carattere di EOF (end of file)

  • ErrorReceived

Viene lanciato in caso di errore di trasmissione. Nel suo handler espone un oggetto della classe System.IO.Ports.SerialErrorReceivedEventArgs, che ci indica quale errore si è verificato (buffer overflow in trasmissione o ricezione, errore di parità etc.)

  • PinChanged

Viene lanciato in caso di cambiamento sui pin “fisici” della seriale, quali DSR, CD, RI o CTS oppure break. L’oggetto della classe System.IO.Ports.SerialPinChangedEventArgs esposto nel suo handler permette di capire quale evento si è verificato.

Raccomando di perdere qualche minuto e di leggere l’help in linea, si potrebbero trovare informazioni interessanti.

Alcune pessime pratiche comuni

Arrivati a questo punto vorrei parlare di alcuni errori, o meglio, alcune pratiche non ottimali spiegando il perché queste pratiche non mi piacciono e le considero errori.

Il modo più comune con il quale un programma comunica con un qualsiasi periferico via seriale è il cosiddetto “Get Param/Set Param” in cui la comunicazione è “pilotata” dal PC ed il periferico risponde a tono.

Il codice che normalmente viene utilizzato è qualcosa che assomiglia a questo:

SerialPort1.WriteLine("Data Request")
Dim Risultato As String = SerialPort1.ReadLine()

Questo banale codice in realtà è, a mio modo di vedere, un vero orrore.

Per quale motivo?

I motivi sono più di uno.

Il primo è che essendo la periferica attaccata ad un filo, può essere che la periferica non risponda in tempo utile (o non risponda affatto) ed il sistema vada in eccezione di timeout.

Una pezza peggiore del male sarebbe di gestire il timeout con un bel Try/Catch, visto che le eccezioni dovrebbero evitare di verificarsi tranne in casi eccezionali, e qui è ben lungi dall’esserlo.

Quando può succedere una cosa del genere?

Se la comunicazione è via “seriale vera”, può succedere, ad esempio, che la comunicazione sia disturbata dai macchinari elettrici nei dintorni (una seriale a 9600 baud può trasmettere qualche centinaio di metri), o peggio, topi o altri animali possono rosicchiare il cavo (ebbene si, mi è già successo!)

Se la comunicazione è via “Seriale virtuale da USB”, invece la situazione può essere ben peggiore a seconda che lo stack di comunicazione sia o meno fatto decentemente. Ad esempio, lo stack USB che viene rilasciato da NXP e CodeRed per l’uso sui micro ARM delle serie LPC1X (Cortex M3) ed LPC2X (ARM7TDMI), derivato dal (peraltro ottimo) lavoro di Bertrid Sikken, LPCUSB, ha due pesanti BUG sulla gestione dei NACK, che fanno si che la comunicazione si pianti spesso e volentieri e, comunque, sia esageratamente lenta, soprattutto quando si spostano notevoli moli di dati su e giù per una virtual com.

A parte la (doverosa) correzione dei BUG (9 righe), l’avere un sistema di comunicazione Bullet-proof è comunque indispensabile per evitare le bruttissime schermate di eccezione (ancora peggio quando questa è UnHandled).

Il secondo non è un vero e proprio problema, nel senso che il programma non si pianta, quanto un funzionamento “brutto” da vedere, e che comunque non gestisce il caso in cui si voglia interrompere un’operazione la cui esecuzione può durare anche qualche minuto ed è il fatto che la gestione della seriale in una form viene generalmente effettuata sul thread principale.

Un altra cosa che personalmente non mi piace è la gestione della ricezione fatta sfruttando l’evento DataReceived.

Molti puristi storceranno il naso, ma questa gestione corre il rischio di provocare degli errori imprevisti.

Contrariamente a quanto pensa la maggioranza dei programmatori, questo evento scatta solo “QUASI” ad ogni byte ricevuto, come ci ricorda l’help del .NET Framework, che riporto qui ad imperitura memoria

“The DataReceived event is not guaranteed to be raised for every byte received.“

Insomma, a seconda della velocità della seriale e del numero di byte da leggere può cambiare il comportamento della funzione ed il numero di volte che l’evento scatta.

 

Miglioriamo il nostro codice

Non è che pretenda di scrivere il miglior codice del mondo, ma codice funzionante si.

Dato che normalmente le operazioni da fare sono inviare dati al dispositivo seriale e leggerne il risultato, perché non costruire una funzione ad hoc per farlo?

Ecco che quindi ho scritto una funzione idonea, che ho chiamato ScriviELeggi (che fantasia!), che è fatta all’incirca così

Private Function ScriviELeggi(PortaSeriale As IO.Ports.SerialPort, Messaggio As String, TimeOutMilliseconds As Integer, ByRef Risultato As String) As Boolean
        Dim TimeOutCounter As Integer = 0
        Dim SB As New StringBuilder 
        Dim c As String = ""
        PortaSeriale.DiscardInBuffer()
        PortaSeriale.DiscardOutBuffer()
        Thread.Sleep(10)
        Try
            PortaSeriale.Write(Messaggio)
        Catch ex As Exception
            Return False
        End Try

        Thread.Sleep(10)
        While True
            While PortaSeriale.BytesToRead > 0
                c = Chr(PortaSeriale.ReadChar)
                If CondizioneFineLettura() Then
                    Risultato = SB.ToString
                    Return True
                Else
                    SB.Append(c)
                End If
            End While
            Thread.Sleep(10)
            TimeOutCounter = TimeOutCounter + 10
            If TimeOutCounter > TimeOutMilliseconds Then
                Return False
            End If
        End While
        Return True
    End Function

Dove CondizioneFineLettura è, come dice il nome stesso, la condizione  che indica che la lettura è stata terminata correttamente.

Può essere un carattere ben specifico, un frame fatto in un determinato modo, un numero di caratteri… dipende dall’applicazione specifica.

Qualora invece di una stringa si debba leggere un array di caratteri si può banalmente correggere la funzione in maniera opportuna.

Detto questo si noti che la funzione gestisce anche correttamente il timeout ed è non bloccante a parte il tempo in cui essa va in condizione di timeout ed è pertanto pienamente idonea alle applicazioni “Get Param/Set Param”, anche quando esse muovono mega di dati.

I più attenti mi possono far notare che la funzione di fatto congela il thread per il tempo della lettura o al più per il tempo necessario ad andare in timeout.

Questo è vero, ma questa funzione può tranquillamente lavorare in multithread.

Ad esempio, usando un BackgroundWorker, la funzione può lavorare correttamente, senza bloccare l’interfaccia principale.

Piccolo esempio (fatto maluccio ma funziona)

Private Sub bwComunicazione_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles bwLetturaOvo.DoWork
    Dim Seriale As New System.IO.Ports.SerialPort
    Dim RisultatoLettura As String = ""
    Dim ErroriLettura As Integer = 0
    Dim Finito As Boolean = False
    Dim RisultatoOk As Boolean = True
    Dim Progresso As Integer
    Dim bw As ComponentModel.BackgroundWorker = CType(sender, ComponentModel.BackgroundWorker)
    Seriale.PortName = "COM5"
    Seriale.BaudRate = 38400
    Seriale.ReadBufferSize = 2000
    Dim IniziateTimbrature As Boolean = False
    Try
        Seriale.Open()
        While (Finito <> True)
            If bw.CancellationPending Then
                Seriale.Close()
                Exit Sub
            End If
            If ScriviELeggi(Seriale, ComandoIEsimo, 1000, RisultatoLettura) = True Then
                ParseRisposta(RisultatoLettura)  
                bw.ReportProgress(Progresso)
            Else
                ErroriLettura = ErroriLettura + 1
            End If
            If ErroriLettura > NumeroMassimoErrori Then
                Finito = True
            End If
        End While
    Catch ex As Exception
        MsgBox(ex.Message)
        If Not Seriale Is Nothing Then
            If Seriale.IsOpen Then
                Try
                    Seriale.Close()
                Catch ex1 As Exception
                    Return
                End Try
            End If
            Return
        End If
    End Try
    If Seriale.IsOpen Then
        Seriale.Close()
    End If
End Sub

Questa “semplice” funzione gestisce semplicemente l’evento DoWord di un BackgroundWorker, il quale può essere attivato alla pressione di un pulsante mediante il metodo RunWorkerAsync.

Si può vedere come la stessa funzione gestisca anche l’interruzione del lavoro andando a testare se c’è una richiesta di interruzione, ed anche inviando al thread principale il feedback dell’ avanzamento dell’esecuzione mediante il metodo ReportProgress

Piccola nota sulla parte di gestione delle eccezioni, che è fatta un po’ con i piedi.

Questo intortamento è nato quando ho cercato di gestire la rimozione di una porta seriale virtuale mentre c’era una lettura in corso. Al momento non ho trovato niente di meglio, ma, tempo permettendo, prometto di metterci mano e rendere la funzione degna.

Rimangono ancora da gestire le comunicazioni completamente asincrone, ovvero, nelle quali i dati arrivano in maniera completamente asincrona, che, data la casistica veramente molto ampia, mi rimane difficile da implementare in una qualche maniera completa, ed inoltre, in tanti anni di programmazione, ho dovuto implementare una volta sola.

Per un discorso veramente completo (e per codice molto diverso dal mio) rimando alla lettura del libro di Jan Axelson “Serial Port Complete”, seconda edizione.

Sperando che il mio sproloquio sia utile a qualcuno, vi rimando alla prossima.

posted @ mercoledì 18 gennaio 2012 18:00

Print

Comments on this entry:

# re: Porte Seriali e VB.NET

Left by massimo at 28/02/2013 20:14
Gravatar
Complimenti per l'articolo posso chiederti secondo te come posso gestire la trasmissione di 8+1 bit + bit di stop. stavo pensando di usare il bit di parità ma quando cambio stato mi manda bit sbagliati. hai dei suggerimenti.

Massimo

# re: Porte Seriali e VB.NET

Left by Angelo56 at 05/04/2013 21:52
Gravatar
Ottimo articolo, denota una competenza profonda.
Anche io ho un passato da programmatore, ma ho parecchi limiti.
Ti scrivo perchè ho realizzato un programmino in VB2010 per leggere dati da Arduino il quale li legge da un sensore (un giroscopio). Arduino trasmette in continuazione dei dati sulla porta usb vista dal PC come seriale.
Da pc, con vb2010, con la classica classe IO.Ports.SerialPort e successiva readLine() leggo i dati (57600,8n1). Il problema è che un buon 10% dei caratteri è corrotto. Mi chiedo come sia possibile visto che se leggo la stessa virtual port con un programma terminale non c'è un errore che è uno.
Può essere che leggendo come suggerisci tu con un altro thread si risolva il problema?
Ciao e grazie
Angelo

# re: Porte Seriali e VB.NET

Left by Antonio at 22/11/2013 10:50
Gravatar
Articolo che hai scritto è stato molto esaustivo, ti volevo chiedere se potevi darmi delle delucidazione sulla trasmissione seriale con protocollo XOn/XOff per un registratore di cassa EDIT.
Dovrei implementare un programmino lato desktop che si interfacci con il registratore per scaricare il registro giornaliero dei movimenti di cassa.
Grazie in anticipo

# re: Porte Seriali e VB.NET

Left by Luigi at 01/02/2014 22:25
Gravatar
Da 6 mesi sono passato al VB.Net (versione 2010) e trovo difficolta' a migrare il codice realizzato in VB6 per la stampa sulla Epson termica T88IV usb.
Sto utilizzando l'oggetto OposPOSPrinter. Avete suggerimenti per come fare? Qualcuno ha un esempio da mostrare per l'apertura di una porta USB termica in VB.Net? Grazie

Your comment:



 (will not be displayed)


 
 
 
Please add 8 and 3 and type the answer here:
 

Live Comment Preview: