La Comunicazione Seriale
Ci sto lavorando
Questo è un argomento molto importante in quanto permette la comunicazione del PC con il mondo esterno ed in particolare con Arduino un microcontrollore che fa miracoli. Esso è in grado di interfacciasi con un'infinità di sensori consentendo l'accesso ad un'infinità di applicazioni. La comunicazione seriale avviene attraverso la vecchia porta seriale RS- 232 ormai sostituita dalla più performante USB. Comunicare con Arduino è l'obiettivo principale di queste pagine. Vogliamo farlo mediante l'uso del linguaggio java, per questo bisogna scaricare delle apposite librerie. Se ne trovano alcune in rete una è la RxTx abbastanza complessa da gestire e, sembra non più supportata, quindi dopo alcuni tentativi ho preferito cercare qualcosa di più accessibile e credo di averlo trovato in una libreria il cui nome è esplicativo JSSC acronimo di "Java Simple Serial Connector" scritta dal russo Alexey Sokolov alias scream3r.
Istallazione:
Il materiale scaricato consta di:
JSSC-2.8.0.jar - è la vera e propria libreria, ossia la collezione di classi compilate (bycode) tipo file.class.
JSSC-2.80-javadoc.jar - è il reference. Qui il jar ha funzione di file zip. Se vogliamo averlo a disposizione in locale possiamo scompattare il file jar con winzip ed inserire il tutto in una direttory sul pc bastera eseguire l'index.html. Se si vuole risparmiare spazio c'è chi lo ha messo online per noi lo troviamo nei link della documentazione (qui sopra) alla voce JSSC Reference.
JSSC-2.8.0-sources.jar - ci sono i sorgenti i file.java delle 7 classi che compongono la libreria.
A questo punto dobbiamo aggiungere la libreria (JSSC-2.8.0.jar) nel posto giusto e questo è:
C:\Program Files\Java\jre1.8.0_151\lib\ext.
Copiamola in questa posizione.
Per testarla ho pensato di creare un progetto di nome "ArduinoRGB" con il quale andremo a colloquiare via porta seriale con Arduino sul quale avremo allestito un semplice circuito costituito da un led ed un potenziometro lineare. La comunicazione sarà bidirezionale ossia dal PC potremo inviare comandi per accendere o spengere il led o farlo lampeggiare con una frequenza che potremo impostare. Dal lato Arduino potremo agendo sul potenziometro far variare in modo continuo la luminosità del led da zero alla massima luminosità inviando le relative informazioni al PC che le userà per visualizzare i valori da o a 5 Volt della tensione ai capi del potensiometro mediante una sorta di voltmetro digitale. Si potrà viceversa pilotare il led simulando l'azione del potenziometro tramite uno slider che spostato con il mouse farà variare le luminosità del led.
Questo è il pannello di controllo lato PC:
Creiamo il progetto ed importiamo la libreria JSSC-2.8.0.jar così:
Clicchiamo su Add JAR/Folder... e andiamo a pescarla dove l'abbiamo messa ossia in:
C:\Program Files\Java\jre1.8.0_151\lib\ext.
Ora possiamo passare al codice:
1 package arduinorgb;
2
3 import java.awt.*;
4 import java.awt.Color;
5 import java.awt.event.*;
6 import java.util.Locale;
7 import javax.swing.*;
8 import jssc.SerialPort;
9 import jssc.SerialPortEvent;
10 import jssc.SerialPortEventListener;
11 import jssc.SerialPortException;
12
13 public class cFrame extends JFrame {
14 JDialog dialog;
15 JLabel labelDialog1;
16 SerialPort serialPort;
17 public cFrame() {
18 serialInitialize();
19 initComponents();
20 myInitialize();
21 }
22 @SuppressWarnings("unchecked")
23 // <editor-fold defaultstate="collapsed" desc="Generated Code">
24 private void initComponents() {
...
127
128 private void jToggleButton1ItemStateChanged(java.awt.event.ItemEvent evt) {
129
130 byte[] bb = new byte[1];
131 int status = evt.getStateChange();
132 if(status == ItemEvent.SELECTED){
133 jToggleButton1.setText("Spegni");
134 jToggleButton3.setSelected(false);
135 jToggleButton3.setText("Attiva il Potenziometro");
136 bb[0]=(byte)'1';
137 }else{
138 bb[0]=(byte)'0';
139 jToggleButton1.setText("Accendi");
140 }
141 try{
142 serialPort.writeBytes(bb);
143 }catch (SerialPortException ex) {
144 System.out.println(ex);
145 }
146 }
147
148 private void jToggleButton2ItemStateChanged(java.awt.event.ItemEvent evt) {
149
150 byte[] bb = new byte[1];
151 int status = evt.getStateChange();
152 if(status == ItemEvent.SELECTED){
153 jToggleButton2.setText("No Blink");
154 jToggleButton3.setSelected(false);
155 jToggleButton3.setText("Attiva il Potenziometro");
156 jToggleButton1.setSelected(false);
157 jToggleButton1.setText("Accendi");
158 bb[0]=(byte)'2';
159 }else{
160 bb[0]=(byte)'0';
161 jToggleButton2.setText("Blink");
162 }
163 try{
164 serialPort.writeBytes(bb);
165 }catch (SerialPortException ex) {
166 System.out.println(ex);
167 }
168 }
169
170 private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
171
172 String txt=jTextField1.getText();
173 if(isNumeric(txt)){
174 txt = "w"+txt;
175 try{
176 serialPort.writeString(txt);
177 }catch (SerialPortException ex) {
178 System.out.println(ex);
179 }
180 }else{
181 dialog.setVisible(true);
182 }
183
184 }
185
186 private void jToggleButton3ItemStateChanged(java.awt.event.ItemEvent evt) {
187
188 byte[] bb = new byte[1];
189 int status = evt.getStateChange();
190 if(status == ItemEvent.SELECTED){
191 jToggleButton3.setText("Disattiva il Potenziometro");
192 jToggleButton2.setSelected(false);
193 jLabel3.setVisible(true);
194 jToggleButton2.setText("Blink");
195 bb[0]=(byte)'3';
196 }else{
197 jToggleButton3.setText("Attiva il Potenziometro");
198 bb[0]=(byte)'4';
199 jLabel3.setVisible(false);
200 }
201 try{
202 serialPort.writeBytes(bb);
203 }catch (SerialPortException ex) {
204 System.out.println(ex);
205 }
206 }
207
208 private void jSlider1StateChanged(javax.swing.event.ChangeEvent evt) {
209
210 jToggleButton3.setText("Attiva il potenziometro");
211 jToggleButton3.setSelected(false);
212 int gv=jSlider1.getValue();
213 String st=""+gv;
214 jLabel4.setText(st);
215 st = "s"+st+"\r\n";
216 try{
217 serialPort.writeString(st);
218 }catch (SerialPortException ex) {
219 System.out.println(ex);
220 }
221 }
222
223 public boolean isNumeric(String str){
224 return str.matches("-?\\d+(\\.\\d+)?");
225 }
226 public void myInitialize(){
227 String desc = "<html><p align=\"center\">Attenzione!<br/>"
228 +"Non è un numero</p></html>";
229 labelDialog1 = new JLabel(desc,JLabel.CENTER);
230 labelDialog1.setFont(new java.awt.Font("Tahoma", 1, 24));
231 jLabel3.setVisible(false);
232
233 dialog = new JDialog(this, "JDialog", true);
234 dialog.setBounds(850, 575, 400, 150);
235 dialog.getContentPane().setLayout(new BorderLayout());
236 dialog.getContentPane().add(BorderLayout.CENTER,labelDialog1);
237 dialog.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
238 }
239 public void serialInitialize() {
240 try {
241 serialPort = new SerialPort("COM3");
242
243 System.out.println("Port opened: " + serialPort.openPort());
244
245 System.out.println("Params setted: " + serialPort.setParams(115200, 8, 1, 0));
246 Thread.sleep(2000);
247
248 int mask = SerialPort.MASK_RXCHAR;
249
250 serialPort.setEventsMask(mask);
251
252 serialPort.addEventListener(new SerialPortReader());
253 System.out.println("La porta è: " + serialPort.getPortName());
254 System.out.println("PRONTI******");
255 } catch (SerialPortException | InterruptedException ex){
256 System.out.println(ex);
257 }
258 }
259 public class SerialPortReader implements SerialPortEventListener {
260 @Override
261 public void serialEvent(SerialPortEvent event) {
262 String str="",stv;
263 byte[] by= new byte[1];
264 while(true){
265 try {
266 by = serialPort.readBytes(1);
267 if(by[0]!=10 && by[0]!=13){
268 if(by[0]>47 && by[0]<58){
269 str +=(char)by[0];
270 }
271 }else{
272 if(by[0]==13) {
273 try{
274 stv=toVolt(str);
275 try{
276 jLabel3.setText(stv);
277 }catch(NullPointerException ex){
278
279 }
280 break;
281 }catch(NumberFormatException ex){
282
283 }
284 }
285 }
286 }catch (SerialPortException ex) {
287 System.out.println(ex);
288 }
289 }
290 }
291 }
292 private String toVolt(String str){
293 double dbt=0.0;
294 double dbr;
295 String ris;
296 try{
297 dbt = Double.parseDouble(str);
298 }catch(NullPointerException np){
299 System.out.println("toVolt(): "+np);
300 }
301 dbr=(dbt*5)/1023;
302 ris=String.format(new Locale("en"),"%1$.2f",dbr);
303 return(ris);
304 }
305 public static void main(String args[]) {
306
...
328
329
330 java.awt.EventQueue.invokeLater(new Runnable() {
331 public void run() {
332 cFrame cf = new cFrame();
333 cf.getContentPane().setBackground(new Color(240,240,240));
334 cf.setVisible(true);
335 }
336 });
337 }
338 // Variables declaration - do not modify
339 private javax.swing.JButton jButton1;
340 private javax.swing.JLabel jLabel1;
341 private javax.swing.JLabel jLabel2;
342 private javax.swing.JLabel jLabel3;
343 private javax.swing.JLabel jLabel4;
344 private javax.swing.JSlider jSlider1;
345 private javax.swing.JTextField jTextField1;
346 private javax.swing.JToggleButton jToggleButton1;
347 private javax.swing.JToggleButton jToggleButton2;
348 private javax.swing.JToggleButton jToggleButton3;
349 // End of variables declaration
350 } |
Discussione:
L'unica classe realizzata estende un JFrame ed è anche eseguibile (righe da 340 a 348) con l'uso della classe anonima che implementa l'interfaccia Runnable con l'override del suo unico metodo run e questo, ricordo, per far si che i componenti swing lavorino nel thread EDT.
Il frame di base è stato creato in modalità visual, le aree a sfonfo grigio sono autogenerate da Netbeans e non sono editabili e sono contenute nel metoto initComponents() (righe 31-135). Per completare il form ho creato il metodo myInizialize() (righe 236-248). Le parti del codice che interessano la comunicazione seriale sono nel metodo serialInitialize() (righe 249-269) e serialPortReader() (righe 270-302).
Iniziamo con l'analizzare serialInitialize()
.
239 public void serialInitialize() {
240 try {
241 serialPort = new SerialPort("COM3");
242
243 System.out.println("Port opened: " + serialPort.openPort());
244
245 System.out.println("Params setted: " + serialPort.setParams(115200, 8, 1, 0));
246 Thread.sleep(2000);
247
248 int mask = SerialPort.MASK_RXCHAR;
249
250 serialPort.setEventsMask(mask);
251
252 serialPort.addEventListener(new SerialPortReader());
253 System.out.println("La porta è: " + serialPort.getPortName());
254 System.out.println("PRONTI******");
255 } catch (SerialPortException | InterruptedException ex){
256 System.out.println(ex);
257 }
258 } |
Vediamo cosa accade. Si crea un'istanza della classe serialPort fornendo al costruttore la porta, la COM3 nel nostro caso. C'e anche il modo di fargli rilevare la lista delle com disponibili ma puichè nel mio PC Arduino si collega mediante la COM3 sono andato diretto. Una volta creato l'oggetto porta seriale bisogna aprirla. Questo provoca una singolare reazione da parte di Arduino il quale in seguito alla apertura della porta si resetta. Una volta aperta bisogna impostarla il metodo setParams riceve 4 argomenti che sono
- baudRate - data transfer rate (115200)
- dataBits - number of data bits (8)
- stopBits - number of stop bits (1)
- parity - parity (0)
La pausa di 2 secondi Thread.sleep(2000); servirà a dare il tempo ad Arduino di resettarsi.
Ora parliamo un attimo di mask. Faccio una premessa molto sintetica (per un approfondimento si veda la documentazione 1° link) nella trasmissione seriale RS 232 ci sono 9 pin (9 cavi) due servono per la trasmissione dei dat uno TX da PC a Arduini e RX da arduino a PC poi ce ne sono altri che servono ai dispositivi per passarsi informazioni di altro tipo per esempio sul RTS il PC dice ad Ar. sei pronto che ti devo inviare dei dati e Ar. se prondo gli risponde sul CTS si ok sono pronto invia pure. Invece il PC avverte Ar di essere pronto a ricevere dati sul DTR ed Ar gli risponde che è pronto adf inviargleli sul DSR. Quindi se parliamo di ricezione avremo i dati su RX ed i messaggi su CTS e DSR. Ora sinceramente di quello che si dicono fra loro il PC con Ar a noi non ci interessa la cosa che invece ci interessa sono i dati in arrivo parliamo di serialPortReader() lettura. La maschera serve a filtrare fa passare solo i dati che ci interessano.
Nella maschera, dobbiamo specificare i tipi di eventi che vogliamo monitorare. Se per esempio, ci interessa sapere solo se ci sono dati in arrivo la maschera deve avere il valore seguente: MASK_RXCHAR. Se, per esempio, abbiamo bisogno di sapere anche dei cambiamenti negli stati delle linee CTS e DSR, la maschera deve apparire così: SerialPort.MASK_RXCHAR + SerialPort.MASK_CTS + SerialPort.MASK_DSR.
Ma poiche a noi interessano solo i dati in arrivo useremo
int mask = SerialPort.MASK_RXCHAR;
Nella righa 252 andiamo ad associare un ascoltatore (l'oggetto SerialPortReader) alla serialPort . L'ascoltatore è un oggetto che implementa un'interfaccia di tipo serialPoreEventListener e quindi sarà costretto a definire il metodo serialEvent all'interno del quale adremo a fargli fare quello che vogliamo quando si scatena l'evento, ossia quando arriva qualche dato. Ad esempio leggerlo.
Naturalmete il tuttio deve stare all'interno di un blocco try ... catch in quanto si potrebbe verificare una eccezione del tipo SerialPortException.
Ora passiamo a discutere la classe SerialPortReader
259 public class SerialPortReader implements SerialPortEventListener {
260 @Override
261 public void serialEvent(SerialPortEvent event) {
262 String str="",stv;
263 byte[] by= new byte[1];
264 while(true){
265 try {
266 by = serialPort.readBytes(1);
267 if(by[0]!=10 && by[0]!=13){
268 if(by[0]>47 && by[0]<58){
269 str +=(char)by[0];
270 }
271 }else{
272 if(by[0]==13) {
273 try{
274 stv=toVolt(str);
275 try{
276 jLabel3.setText(stv);
277 }catch(NullPointerException ex){
278
279 }
280 break;
281 }catch(NumberFormatException ex){
282
283 }
284 }
285 }
286 }catch (SerialPortException ex) {
287 System.out.println(ex);
288 }
289 }
290 }
291 } |
Questa è la parte che si fa carico di rimanere in acolto da eventuale invio di dati da parte di Arduino.
SerialPortReader è l'ascoltatore ossia la classe che starà in ascolto e che verrà asociata al source cFrame (vedi linea 252 nel metodo serialInitialize()). Nel momento che verrà lanciato un evento, ogni volta che avremo dati in arrivo dalla seriale, verrà eseguito il metodo serialEvent(). Vediamo cosa fa questo metodo:
- Crea un array di byte di un solo elemento
- Entra in un ciclo while
- Legge il primo byte a disposizione by = serialPort.readBytes(1);
- righe 267-270 se questo carattere letto è diverso da un line feed 10 o carriage return 13 che di solito stanno ad indicare la fine della stringha inviata
- verifico se è un numero. Per essere un numero deve avere un codice ASCII compreso tra 48 e 57 (0 e 9). Se lo è lo vado ad aggiungere ala stringa str
- Se invece è un ritorno carrello (13) significa che la stringa inviata è completa e possiamo quindi utilizzarla. Il metodo toVolt trasforma il numero inviato da Arduino con valori da 0 a 1023 in valori espressi in volt da 0 a 5 ed in formato decimale con 2 decimali dopo il punto
- Questo valore (in forma di stringa) viene visualizzato nella label
I vari cicli try catch sono necessari per intercettare le varie eccezioni che si possono verificare e che farebbero terminare il programma
|