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:
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)
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.)
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.