MokaByte Numero  37  - Gennaio 2000
Le Java Communications API
di 
Massimo
Carli
In questo articolo esaminiamo le API che permettono di far interagire un applet o applicazione Java con le porte seriali e parallele


Le Java Communications API (JCA) permettono di creare delle applicazioni o applet portabili, che necessitino di comunicazioni attraverso porta seriale e/o parallela. Dopo la realizzazione delle JNI (Java Native Interface), che permettono di creare implementazioni native (in C o C++) di metodi Java, le JCA rappresentano un ulteriore passo verso la possibilità di utilizzare Java come linguaggio di programmazione completo verso la scrittura di codice che prima competeva principalmente a C/C++. In questo articolo esamineremo le JCA e vedremo come sia possibile accedere a risorse come le porte seriali e parallele, mantenendo un elevato grado di portabilità.


Utilizzo dei driver
Interagire con le porte seriali o parallele di un sistema è una operazione solitamente molto legata al sistema operativo stesso. Accedere alla seriale, ad esempio, in Windows è diverso dal farlo in un ambiente Solaris. Sicuramente, però, le operazioni che si possono e vogliono eseguire sono le stesse per cui, nelle JCA, è stata definita l'interfaccia javax.comm.CommDriver che dovrà essere implementata da ciascun Driver JavaComm il cui compito è proprio quello di tradurre le varie operazioni sulle porte, in codice nativo legato alla particolare implementazione della JVM.
I compiti del driver sono quelli dei metodi che descrive:
  • void initialize() : Questo metodo deve assicurarsi della presenza dell'hardware e caricare le librerie native che ne permettono l'utilizzo. E' compito di questo metodo anche la determinazione delle porte disponibili e la memorizzazione dell'elenco in un oggetto di tipo CommPortIdentifier.
  • CommPort getCommPort(String portName,int portType) : Questo metodo permette di ottenere un riferimento, come vedremo meglio in seguito, ad un oggetto CommPort, rappresentativo di una porta. I parametri sono relativi al nome della porta (es:COM1 in Windows) ed al tipo della stessa (ad esempio: Serial).
Ogni applicazione che utilizza le JCA potrà essere portata in ogni sistema operativo per il quale esiste un CommDriver, ovvero una implementazione della precedente interfaccia. La cosa importante è che il codice Java non deve essere minimamente modificato ma sarà sufficiente fornire una diversa libreria nativa utilizzata dal Driver. Nel caso di Windows si utilizza la libreria Win32Com.dll.
Per capire come viene associata una libreria ad una particolare implementazione della JVM, vediamo come si installano le JCA. Ottenuto, dal sito JDC (Java Developer Connection), il file javacomm20-win32.zip e scompattato, si hanno, oltre ad alcuni esempi, i seguenti file:
  • comm.jar : Rappresenta il file .jar delle classi relative alle JCA. Nella versione relativa a Windows, si nota, al suo interno, una classe di nome com.sun.comm.Win32Driver che rappresenta proprio l'implementazione dell'interfaccia javax.comm.CommDriver relativa al sistema operativo utilizzato. In fase di installazione, questo file deve essere posizionato nella subdirectory in cui sono contenute le classi del JDK o comunque in una directory contenuta nel CLASSPATH.
  • Win32Com.dll : Nella implementazione del metodo initialize() della classe Win32Driver, ci dovrà essere il caricamento di tutte le librerie native utilizzate. Nel caso di Windows, tali librerie sono rappresentate dal singolo file Win32Com.dll. Esso va posizionato nella directory <jdk1.1>\bin dove <jdk1.1> è la directory base di installazione del JDK (ad esempio: <jdk1.1>=C:\JDK1.1.5).
  • javax.comm.properties : Questo file di proprietà contiene proprio il nome del driver da utilizzare e va posizionato nella directory <jdk1.1.>\lib. Esso contiene, oltre ad un commento esplicativo, la riga Driver=com.sun.comm.Win32Driver.
E' molto importante eseguire l'installazione in modo corretto in quanto la mancanza del file javax.comm.properties provoca un errore di mancato reperimento del Driver, mentre l'errata posizione della .dll fa sì che non vengano trovate le varie porte.
 
 

Astrazione delle porte di comunicazione
Nel precedente paragrafo abbiamo visto come si possa interagire con parti native di un sistema, mantenendo un elevato grado di portabilità di una applicazione. A questo punto, le altre classi delle JCA, vedono un oggetto di tipo CommDriver e non si interessano minimamente della particolare implementazione. Se si utilizzano le JCA, non dobbiamo, comunque, mai interagire direttamente con il Driver ma utilizziamo la classe javax.comm.CommPortIdentifier che fornisce i metodi per:

  • Determinare le porte di comunicazione disponibili:
  • Determinare la proprietà (OwnerShip) di una particolare porta
  • Aprire una determinata porta di comunicazione
  • Gestire la contesa di una porta tra applicazioni diverse
Vediamo nel dettaglio come si raggiungono questi obiettivi.
 
 

Determinare le porte di comunicazione disponibili
L'oggetto CommPortIdentifier è di fondamentale importanza nelle JCA in quanto è quello che interagisce con il Driver. Quando tale classe viene caricata in memoria, accede al file javax.comm.properties che indica il nome del Driver da caricare. Essa, in un inizializzatore statico (vedi Riquadro), carica il Driver e chiede la lista delle porte disponibili. Il Driver utilizzerà la libreria nativa per ottenere tale lista e registrerà ciascuna porta nell'oggetto CommPortIdentifier attraverso il suo metodo addPortName() specificando, per ognuna, un nome ed un valore intero caratteristico del tipo di porta (SERIAL o PARALLEL). Dopo che tale classe è stata caricata, e l'inizializzatore statico eseguito, essa conterrà la lista delle porte disponibili. Tale lista si ottiene come Enumeration di oggetti CommPortIdentifier, attraverso il metodo staticgetPortIdentifiers().
 
 

Derminare la proprietà (OwnerShip) di una porta
Una volta ottenuta la lista delle porte disponibili in un sistema, è possibile sapere se questa porta è attualmente in uso da una particolare applicazione oppure è libera. Il metodo isCurrentlyOwned() applicato ad un oggetto CommPortIdentifier descrittivo di una porta, permette di ottenere un valore booleano che indica, se true, che la porta è occupata. Il metodo getCurrentlyOwer() permette, inoltre, di conoscere il nome della applicazione che, in quel momento, occupa la porta.
 
 

Aprire una determinata porta di comunicazione

Per aprire ed impossessarsi del controllo di una determinata porta, si possono seguire due metodi:

  • Testare, con il metodo isCurrentlyOwed(), se la porta è libera e successivamente aprirla con il metodo open().
  • Tentare l'apertura della porta con il metodo open(). Nel caso in cui tale porta fosse occupata, verrà sollevata una eccezione di tipo javax.comm.PortInUseException.
Il primo metodo non è ottimale in quanto potrebbe succedere che al momento di esecuzione del metodo isCurrentlyOwed(), la porta sia libera ma diventi occupata al momento dell'esecuzione della open(). Tanto vale provare ad eseguire immediatamente la open(), e gestire, eventualmente, l'eccezione che ne consegue nel caso in cui la porta sia occupata. L'oggetto relativo all'eccezione, contiene un campo (currentOwner) che indica anche il nome dell'applicazione che attualmente è in possesso della porta.
Il metodo open() permette di impossessarsi della porta relativa all'oggetto CommPortIdentifier su cui tale metodo è invocato. Esso permette di specificare il nome dell'applicazione che richiede la porta, ed un timeout passato il quale, se la porta nel frattempo non si è resa disponibile, il metodo esce e solleva l'eccezione. Il metodo open() ritorna un oggetto javax.comm.CommPort rappresentativo di una porta di cui, attraverso il metodo getPortType(), è possibile di conoscerne il tipo (SERIAL o PARALLEL). Dell'oggetto CommPort ottenuto dal metodo open() si farà, in base al tipo, un cast con la classe javax.comm.SerialPort o javax.comm.ParallelPort.
 
 

Gestire la contesa di una porta tra applicazioni diverse
Le JCA permettono di gestire le eventuali contese relative all'acquisizione del controllo sulle porte. Questo è ottenuto utilizzando l'ormai collaudata gestione degli eventi per delega. L'oggetto CommPortIdentifier dispone dei metodi che l'identificano come sorgente di eventi di tipo PortOwnership che contengono informazioni relativamente alle richieste di ownership o chiusure delle porte. Se un oggetto vuole essere notificato delle richieste o dei rilasci relativi alle porte, implementerà l'interfaccia javax.comm.CommPortOwnershipListener e si registrerà come ascoltatore all'oggetto CommPortIdentifier attraverso il metodo addCommPortOwnershipListener(). L'interfaccia prevede la definizione del metodo ownershipChange(int tipo) il cui parametro è un valore indicativo del tipo di evento. I tipi di eventi possibili sono:

  • PORT_OWNERSHIP_REQUESTED : Un evento di questo tipo indica che è stata inoltrata una richiesta di possesso della porta. In questa fase è importante il timeout. Supponiamo che l'applicazione A sia in possesso della porta COM3. Di seguito l'applicazione B vuole la stessa porta ed eseguirà una open() sull'oggetto CommPortIdentifier descrittore della stessa. L'applicazione B starà in attesa della porta COM3 un tempo specificato nel secondo parametro (il timeout appunto) del metodo open(). A questo punto, un oggetto in ascolto delle varie richieste può gestire l'assegnazione della porta COM3. Non appena l'applicazione B inoltra la richiesta della porta, tale oggetto ne è notificato attraverso un evento di tipo PORT_OWNERSHIP_REQUESTED. L'oggetto CommPortIdentifier che notifica l'evento sarà rappresentativo della porta. Se si vuole che l'applicazione B assuma il controllo della porta COM3, basterà togliere il controllo all'applicazione A con una close(). Scaduto il timeout, l'applicazione B verrà in possesso della porta. Nel caso in cui non si volesse concedere la porta all'applicazione B, basterà ignorare l'evento.
  • PORT_OWNED : Questo evento viene notificato dall'oggetto CommPortIdentifier descrittore di una porta, quando la stessa è stata ottenuta da un'applicazione.
  • PORT_UNOWNED : Questo evento viene generato nel caso in cui la porta sia stata liberata.
Attraverso una gestione ad eventi per delega, è quindi possibile gestire l'assegnazione delle varie porte alle varie applicazioni.

Gestione delle porte: oggetto CommPort
Come visto, attraverso il metodo open() dell'oggetto CommPortIdentifier descrittore di una porta, è possibile ottenerne la ownership. Il metodo open() ritorna, infatti, un oggetto di tipo javax.comm.CommPort. Questa classe astratta raggruppa l'insieme delle operazioni relative sia ad una porta seriale che parallela. I metodi più significativi sono quelli che permettono di:

  • Chiudere e liberare una porta. Questa operazione, ottenuta con il metodo close(), genera un evento PortOwnership di tipo PORT_UNOWNED.
  • Impostare le dimensioni dei buffer di lettura e di scrittura.
  • Ottenere un riferimento all'InputStream ed all'OutputStream di una porta per poter comunicare con essa.
Dall'ultimo punto si intuisce il meccanismo attraverso il quale si interagisce con ciascuna porta. Per inviare dei dati su una porta, basterà ottenerne l'OutputStream su cui scrivere. Analogamente per la lettura, basterà ottenere un riferimento all'oggetto InputStream, e leggere da esso.
Dall'oggetto CommPort, dopo aver testato il tipo di porta ottenuta attraverso il metodo getPortType(), basterà fare una operazione di cast con la classe SerialPort o ParallelPort per la gestione di una porta Seriale o Parallela rispettivamente.

Le porte seriali
Nel caso in cui il tipo della porta ottenuta sia SERIAL, basterà fare una operazione di cast dell'oggetto CommPort ottenuto dalla open() con la classe SerialPort. Vediamo l'utilizzo di tale classe attraverso la descrizione di una operazione di lettura e di scrittura sulla porta.
 

  • Scrittura su porta seriale

  • Effettuare una operazione di scrittura sulla porta seriale è molto semplice. Le operazioni da effettuare sono quelle del Listato1.
Listato 1 - Scrittura su Seriale
package         dev.javacomm;

import          javax.comm.*;
import          java.io.*;
 

// Questa classe permette di scrivere sulla porta
// seriale il cui nome è specificato nel costruttore
// della stessa.
//
 

public class ScriviSuSeriale  {
 

// Crea un oggetto che scrive il valore della stringa
// message sulla porta seriale port.
//
public ScriviSuSeriale(String port, String message){
  try{

  // Prendiamo il descrittore della porta scelta. 
  // Nel caso in cui la porta non esista verrà
  // sollevata una eccezione di tipo
  // NoSuchPortException.

  CommPortIdentifier id_porta
  d_porta= CommPortIdentifier.getPortIdentifier(port);

  // Proviamo a prendere l'ownership della porta data. 
  // Nel caso in cui la porta sia già occupata verrà 
  // sollevata una eccezione
  // di tipo PortInUseException
  CommPort porta=id_porta.open("ScriviSuSeriale",200);

  // Otteniamo un riferimento all'OutputStream 
  // della porta

  OutputStream out=porta.getOutputStream(); 
  // Impostiamo le proprietà di comunicazione nel
  // seguente modo:
  // baudrate 9600
  // dataBits DATABITS_8
  // stopBits DATABITS_1
  // parity PARITY_NONE
  // Nel caso in cui i valori impostati non siano
  // supportati verrà sollevata una eccezione di 
  // tipo UnsupportedCommOperationException

SerialPort      porta_seriale=(SerialPort)porta;
porta_seriale.setSerialPortParams(9600,
             SerialPort.DATABITS_8,
             SerialPort.STOPBITS_1,
             SerialPort.PARITY_NONE);
// Procediamo, quindi, alla scrittura del 
// messaggio sulla porta

PrintStream ps= new PrintStream(out);
ps.print(message);
Chiudiamo la porta
ps.close();
porta.close();
}
catch(NoSuchPortException ne){
  System.out.println("La porta "+port+" non e' presente");
}catch(PortInUseException pe){
  System.out.println("La porta "+port+" e' occupata da
             "+pe.currentOwner);
}catch(UnsupportedCommOperationException ue){
  System.out.println("La porta non supporta le proprietà
              impostate");
}catch(IOException ioe){
  System.out.println("Errore di IO");
}// fine try/catch

        }// fine
 

public static void main(String[] args){
 if(args.length<2)
  System.out.println("Usage java ScriviSuSeriale <port name> <message>");

 else{
   ScriviSuSeriale scrivi= new ScriviSuSeriale(args[0],args[1]);
}// fine else
        }// fine
 
 

}// fine

     
     
     
     

    Il primo passo consiste nell'ottenere un riferimento all'identificatore della porta. Questo si ottiene attraverso il metodo getPortIdentifier(). Se la porta esiste, e quindi non sono state sollevate eccezioni, si esegue una open() per diventarne l'owner. Se essa è disponibile si ottiene l'oggetto JavaComm dal quale si ottiene il riferimento allo stream di scrittura. Non resta che impostare i tradizionali parametri della porta seriale (baudrate, bit dati, bit stop e parità) attraverso il metodo setSerialPortParameter() e scrivere sullo stream attraverso i comuni metodi. Per l'impostazione delle proprietà della porta seriale è bene utilizzare le costanti static definite nella stessa classe SerialPort.
     
     
     

  • Lettura da seriale

  • La lettura da porta seriale avviene attraverso la gestione di eventi di tipo javax.comm.SerialPortEvent. Se osserviamo il Listato2, 
Listato 2 - Lettura dalla Seriale

package         dev.javacomm;
import          javax.comm.*;
import          java.io.*;

/* Questa classe permette di leggere dalla porta seriale
 il cui nome è specificato nel costruttore della stessa.
*/

public class LeggiDaSeriale  extends Thread implements SerialPortEventListener {

// Riferimento all'InputStream da cui leggere
 private         InputStream     in;
 
 
 

// Crea un oggetto che legge dalla porta seriale 
// specificata nel costruttore

public LeggiDaSeriale(String port){
 try{
 // Prendiamo il descrittore della porta scelta. 
 // Nel caso in cui la porta non esista verrà sollevata una 
 // eccezione di tipo NoSuchPortException.

CommPortIdentifier      id_porta;
id_porta=CommPortIdentifier.getPortIdentifier(port);

// Proviamo a prendere l'ownership della porta data. 
// Nel caso in  cui la porta sia già occupata verrà sollevata
// una eccezione di tipo PortInUseException
 

CommPort porta=id_porta.open("LeggiDaSeriale",200);

// Otteniamo un riferimento all'InputStream della porta
in=porta.getInputStream(); 

// Impostiamo le proprietà di comunicazione nel 
// seguente modo:
// baudrate 9600
// dataBits DATABITS_8
// stopBits DATABITS_1
// parity   PARITY_NONE

// Nel caso in cui i valori impostati non 
// siano supportati verrà sollevata una eccezione di 
// tipo UnsupportedCommOperationException 

SerialPort porta_seriale=(SerialPort)porta;
porta_seriale.setSerialPortParams(9600,
                 SerialPort.DATABITS_8,
                 SerialPort.STOPBITS_1,
                 SerialPort.PARITY_NONE);

// Ci poniamo in ascolto di eventi sulla porta. 
// Solamente un oggetto si può registrare come 
// ascoltatore per cui bisogna gestire l'eccezione 
// TooManyListenersException

porta_seriale.addEventListener(this);

// Avviamo il processo di lettura
start();
}

catch(NoSuchPortException ne){
   System.out.println("La porta "+port+" non e' presente");
}

catch(PortInUseException pe){
   System.out.println("La porta "+port+" e' occupata da
               "+pe.currentOwner);
}

catch(UnsupportedCommOperationException ue){
   System.out.println("La porta non supporta le proprietà
                impostate");
}

catch(IOException ioe){
   System.out.println("Errore di IO");
}

catch(java.util.TooManyListenersException tme){
   System.out.println("Si può registrare solamente un
             ascoltatore");
}
// fine try/catch

// Rappresenta il corpo del Thread di lettura

public void run() {
  try {
    Thread.sleep(10000);
  } catch (InterruptedException e) {}
}// fine run
 

public void serialEvent(SerialPortEvent event) { switch(event.getEventType()) {
 case SerialPortEvent.BI:
 case SerialPortEvent.OE:
 case SerialPortEvent.FE:
 case SerialPortEvent.PE:
 case SerialPortEvent.CD:
 case SerialPortEvent.CTS:
 case SerialPortEvent.DSR:
 case SerialPortEvent.RI:
 case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
 break;
 case SerialPortEvent.DATA_AVAILABLE:
  byte[] readBuffer = new byte[20];
  try {
    while (in.available() > 0){
      int numBytes = in.read(readBuffer);
   } 
   System.out.print(new String(readBuffer));
  }
  catch (IOException e) {}
  break;
 }// fine switch
}// fine serialEvent 
 

public static void main(String[] args){
  if(args.length<2)
    System.out.println("Usage java LeggiDaSeriale 
               <port name>");
  else{
     LeggiDaSeriale leggi= new LeggiDaSeriale(args[0]);
  }// fine else

}// fine main
 

}// fine

     
    notiamo che la classe LeggiDaSeriale implementa l'interfaccia SerialPortEventListener e si registra come ascoltatore della porta. Non appena sulla porta seriale in esame si rendono disponibili delle informazioni, di dati o di controllo, viene generato un evento di tipo SerialPortEvent. L'insieme della costati static della classe SerialPortEvent ci permettono di stabilire il tipo di informazione e di gestirla di conseguenza. Ad esempio, se l'evento è di tipo DATA_AVAILABLE significa che ci sono nuovi dati da leggere dallo stream. Se il tipo di evento è PE significa che si è verificato un errore di parità, ecc.


Gestione porte parallele
Per inviare dei dati attraverso una porta parallela si segue un procedimento analogo (Listato 3) a quanto fatto per le porte seriali ovvero:

  1. Si ottiene un riferimento all'oggetto CommPortIdentifier descrittivo della porta.
  2. Se il tipo della porta è PARALLEL si fa una operazione di cast dell'oggetto CommPort ottenuto attraverso il metodo open(), con la classe ParallelPort.
  3. Si ottiene un riferimento agli stream per la lettura o scrittura attraverso porta
  4. Si impostano le proprietà della porta attraverso il metodo setMode()
  5. Si inviano o leggono dati dalla porte come si fa comunemente con gli stream.
Listato 3 - Scrittura su Parallela

package         dev.javacomm;

import          javax.comm.*;
import          java.io.*;

// Questa classe permette di scrivere sulla porta parallela
// il cui nome è specificato nel costruttore della stessa.
 

public class ScriviSuParallela  {
 
 

// Crea un oggetto che scrive il valore della stringa
// message sulla porta parallela port

public ScriviSuParallela(String port, String message){
  try{
  //Prendiamo il descrittore della porta scelta. 
  // Nel caso in cui la porta non esista verrà sollevata 
  // una eccezione
  // di tipo NoSuchPortException

CommPortIdentifier id_porta; 
id_porta= CommPortIdentifier.getPortIdentifier(port);

// Proviamo a prendere l'ownership della porta data. 
// Nel caso in cui la porta sia già occupata verrà 
// sollevata una eccezione di tipo PortInUseException
 

CommPort porta=id_porta.open("ScriviSuParallela",200);
// Otteniamo un riferimento all'OutputStream della porta

OutputStream    out=porta.getOutputStream(); 
// Impostiamo il modo di comunicazione in LPT_MODE_ECP 
// ovvero
// Enhanced capabilities port 
// Nel caso in cui i valori impostati non siano supportati
// verrà sollevata una eccezione di tipo 
// UnsupportedCommOperationException
 

  ParallelPort    porta_parallela=(ParallelPort)porta;
  porta_parallela.setMode(ParallelPort.LPT_MODE_SPP);

  // Procediamo, quindi, alla scrittura del messaggio 
  // sulla porta
  PrintStream ps= new PrintStream(out);
  ps.print(message);

  // Chiudiamo la porta
  ps.close();
  porta.close();
  System.out.println("Porta chiusa");
}
catch(NoSuchPortException ne){
  System.out.println("La porta "+port+" non e' presente");
}

catch(PortInUseException pe){
  System.out.println("La porta "+port+" e' occupata da "+
     pe.currentOwner);
}

catch(UnsupportedCommOperationException ue){
   System.out.println("La porta non supporta le proprietà 
     impostate");
}

catch(IOException ioe){
   System.out.println("Errore di IO");
}// fine try/catch

}// fine
 
 

public static void main(String[] args){
  if(args.length<2)
    System.out.println("Usage java ScriviSuParallela <port name>
      <message>");
  else{
    ScriviSuParallela scrivi= new ScriviSuParallela(args[0],args[1]);
  }// fine else
}// fine
 

}// fine classe 

Se esaminiamo nel dettaglio i metodi della classe ParallelPort, notiamo come essa sia predisposta all'utilizzo della parallela per la comunicazione con la stampante. Esistono, infatti, metodi come isPrinterBusy() o isPrinterSelected() che permettono di sapere, rispettivamente, se la stampante collegata alla porta specificata è occupata o se è selezionata.
Anche la lettura da parallela avviene in modo simile al caso della porta seriale solamente che, in questo caso, gli eventi da gestire saranno di tipo ParallelPortEvent che conterranno informazioni sulle variazioni dei dati.
 
 

Conclusioni
In questo articolo abbiamo esaminato nel dettaglio il funzionamento delle JCA che si rivelano un ottimo strumento per la scrittura di applicazioni Java portabili che utilizzino le porte seriali e parallele. Per ora queste classi supportano le porte seriali RS232 e le porte parallele IEEE1284 ma la JavaSoft ha promesso, entro breve tempo, la disponibilità di classi per la gestione di protocolli di comunicazione di livello più alto quali i classici X/Y/ZModem.
 

Bibliografia
[1] Documentazione JavaComm presso http://java.sun.com

 

Massimo Carli laureato in Ingegneria Elettronica presso l’Università di Padova, si occupa di Java dal suo inizio e sviluppa applicazioni per Internet ed Intranet completamente in Java. 
Attualmente lavora presso una ditta di Mestre e collabora con l'Università di Padova. E’ redattore della rivista MokaByte (http://www.programmers.net/Riviste).




MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it