Bullet 第8回

Java Communications API (1)
  1. はじめに
  2. Communications API のインストール
  3. Communications API の構成
  4. ポートを調べる
  5. ポートのオープン
  6. 通信
  7. おまけ
  8. ソースコードのダウンロード

 
  はじめに  
 

PLC や温調計といった FA の機器とコンピュータを接続するために使用される方法の 1 つに RS-232C があります。読者の皆さんも使われたことがあると思います。これまで RS-232C を扱うには、C や C++ を使用してきたのではないでしょうか。しかし、せっかく Java を FA の用途に使用するのであれば、このような機器との通信も Java で扱いたくなると思います。

Java でハードウェアを直接アクセスするには、JNI (Java Native Interface) を使用して C などで作成したネイティブコードをコールするようにします。JNI はさまざまな用途で使用することができ、便利なのですが、プログラミングは少し複雑になってしまいます。できれば JNI を使わないで、Java だけで RS-232C を使用したいと思いませんか。こんなときに役に立つのが、Java Communications API です。

Communications API は Java で RS-232C を用いたシリアル通信と IEEE 1284 を用いたパラレル通信を行うための拡張 API です。現在のバージョンは Windows 版と x86 用の Solaris 版が 2.0、SPARC 用 Solaris 版が 2.0.1 です。Solaris/SPARC 版だけ 2.0.1 ですが、これはバグフィックスなので、機能的には両者とも全く同一です。

今回と次回でこの Communications API の解説を行っていきます。Communications API ではシリアル通信とパラレル通信を扱うことができますが、この講座では RS-232C を用いたシリアル通信に絞って解説をしていきたいと思います。また、Communications API はネイティブコードを呼び出しているため、使用した例題はすべてアプリケーションになっています。このため、アプレットと異なり、読者の方が御自分でコードをコンパイル、実行していただく必要があります。

今回は Communications API の基本的な使い方、次回は Communications API を使用した例題として実際にRS-232C を用いて、機器と PC を 接続した監視システムの構築を行っていきたいと思います。


 
  ▲このページのトップへ戻る  
  Communications API のインストール  
 

Communications API は拡張 API なので、Java2 SDK もしくは JDK をインストールしただけでは使うことはできません。まず、Sun のウェブサイトからダウンロードしましょう。

Java Communications API のダウンロードサイト
http://java.sun.com/products/javacomm/index.html

Communications API には前述したように Windows 版と Solaris 版があるのでお使いの OS に応じてダウンロードを行ってください。Solaris 版はお使いのコンピュータの CPU に応じて Intel 版と SPARC 版があります。ダウンロードしたファイルは Windows 版が javacomm20-win32.zip、Solaris/Intel 版が javacomm20-x86.tar.Z、Solaris/SPARC 版が javax_comm-2_0_1-solsparc.tar.Z です。Solaris/SPARC 版だけはバージョンが 2.0.1 になっていますが、これは 2.0 のバグフィックスで、機能的には両者とも全く同一です。

まず、ダウンロードしたファイルは圧縮されていますのでこれを解凍します。この先は、使用している Java のバージョンによってインストールの方法が変わります。

JDK 1.1.x の場合

Java をインストールしたディレクトリを C:\jdk1.1.8 とします。ディレクトリの区切り記号は \ で記述してあるので、Solaris の場合は / で置き換えてください。

  1. Windows 版の場合、win32com.dll を C:\jdk1.1.8\bin にコピーします。Solaris 版では libSolarisSerialParallel.so を環境変数 LD_LIBRARY_PATH が示すディレクトリにコピーします。
  2. comm.jar を CLASSPATH に設定します。comm.jar を C:\jdk1.1.8\lib にコピーした場合、CLASSPATH は次のように設定します。
        set CLASSPATH=C:\jdk1.1.8\lib\comm.jar
    すでにCLASSPATH を設定されている場合は
        set CLASSPATH=C:\jdk1.1.8\lib\comm.jar;%CLASSPATH%
    と設定します。 Solaris 版では set の代わりに setenv を使用して設定を行います。
  3. javax.com.properties を comm.jar と同じディレクトリにコピーします。javax.com.property はポリシーファイルと呼ばれるものです。

Java 2 の場合

Java 2 では CLASSPATH の設定を行わなくてもよいため、JDK 1.1.x より簡単にインストールできます。Java をインストールしたディレクトリを <JDK> とします。Solaris 版の場合は JDK 1.1.x の時と同様に区切り記号を / で置き換えてください。

  1. Windows 版の場合、win32com.dll を <JDK>\jre\bin にコピーします。Solaris 版では libSolarisSerialParallel.so を環境変数 LD_LIBRARY_PATH が示す場所にコピーします。
  2. comm.jar を <JDK>\jre\lib\ext にコピーします。Java 2 ではこのディレクトリにおかれた jar ファイルは CLASSPATH で指定しなくても読み込むようになります。
  3. javax.com.properties を <JDK>\jre\lib にコピーします。

図 1 SerialDemo

これで Communications API を使用することができるようになりました。ためしに Communications API に付属しているサンプルをコンパイル、実行してみましょう。Communications API を解凍するとできる samples というディレクトリにサンプルプログラムがあります。この中で SerialDemo を実行してみましょう。

SerialDemo があるディレクトリで javac を使用して、SerilaDemo.java をコンパイルしてみてください。このとき、Java 2 を使われていた場合、「SerialDemo.java は推奨されない API を使用またはオーバーライドしています。」と警告がでますが、特に問題はなく動作します。無事コンパイルができれば正しくインストールができたことになります。

コンパイルが終わったら実行してみましょう。そうすると図 1 のようなウィンドウが表示されるはずです。

このウィンドウの下部には RS-232C の設定項目があり、設定を行うことができます。一番上部のテキストエリアは書き込みができるようになっており、ここに書きこんだ内容が送信されます。受信したデータはウィンドウ中央のグレーの部分に表示されます。

このサンプルを使用すれば、2 台の PC を RS-232C のクロスケーブルでつなげることで、チャットのようなものができます。

この他にも、BalckBox というサンプルもあります。こちらは簡単な RS-232C のアナライザとして使うことができるアプリケーションです。こちらもぜひ試してみてください。

 

 

 
  ▲このページのトップへ戻る  
  Communication API の構成  
 

Communications API は小さいライブラリなので、全体を見通すのはそれほど大変ではありません。そこで、クラス構成を簡単に見ていきましょう。Communications API のパッケージは javax.comm だけです。このパッケージで定義されているものは、インタフェースが 4 種類、クラスが 6 種類、例外が 3 種類です。インタフェースとクラスの概要を次に示しておきます。

Interface  
CommDriver パラレルポートとシリアルポートを扱うドライバのためのインタフェース。アプリケーションレベルでは使用しない
CommPortOwnershipListener ポートの専有状態を知らせるイベントを扱うための Listener
ParallelPortEventListener パラレルポートで発生する ParallelPortEvent を扱うための Listener
SerialPortEventListener シリアルポートで発生する SerialPortEvent を扱うための Listener

 

Class  
CommPort パラレルポートやシリアルポートなどのコミュニケーションポートを扱うためのクラス。 通信での高レベルなインタフェースを提供する。abstract クラスなので、このクラス単体で使用することはなく、実際には派生クラスである ParallelPort と SerialPort を使用する
CommPortIdentifier コミュニケーションポートを制御するための中心となるクラス。ポートのオープンやポートの専有に関する処理を行う。
ParallelPort パラレルポート。パラレル通信を行うための低レベルのインタフェースを提供する
SerialPort シリアルポート。ParallelPort と同様にシリアル通信を行うための低レベルインタフェースを提供する
ParallelPortEvent パラレルポートの状態が変化したときに発生するイベント。バッファとエラーに関するイベントがある
SerialPortEvent シリアルポートの状態が変化したときに発生するイベント。キャリアデテクトやエラーに関するイベントがある
 
  ▲このページのトップへ戻る  
 

ポートを調べる

 
 


ここまでは Communications API のパッケージについて説明してきましたが、それでは実際に Cmmunications API を使ってみましょう。

まずは通信に使用するポートを調べることからはじめてみましょう。ポートを調べるためには CommPortIdentifier クラスを使用します。Communications API で扱うことのできるポートにはシリアルポートとパラレルポートの 2 種類があります。これを調べるためのサンプルとして PortList.java を作ってみました。このサンプルはポートの一覧を表示して、使用状況とそのポートがシリアルかパラレルかを表示します。

ソースはこちらからも見ることができます PortList.java

 1: import javax.comm.*;
 2: import java.util.*;
 3: 
 4: public class PortList {
 5:    
 6:     Enumeration portList;
 7:
 8:     public PortList(){
 9:         // ポートのリストを取得
10:         portList = CommPortIdentifier.getPortIdentifiers();
11:     }
12:
13:     public void showList(){
14:         CommPortIdentifier portID;
15:
16:         while(portList.hasMoreElements()){
17:             // リストからポートを取り出す
18:             portID = (CommPortIdentifier)portList.nextElement();
19:
20:             // ポートの名前
21:             System.out.print("Port Name : " + portID.getName() + ",");
22:
23:             // ポートの使用状況
24:             if(portID.isCurrentlyOwned()){
25:                 System.out.print(" Owned,");
26:             }else{
27:                 System.out.print(" Not Owned,");
28:             }
29:
30:             // ポートのタイプ (シリアル or パラレル)
31:             switch(portID.getPortType()){
32:               case CommPortIdentifier.PORT_SERIAL:
33:                 System.out.println(" Kind : Serial");
34:                 break;
35:               case CommPortIdentifier.PORT_PARALLEL:
36:                 System.out.println(" Kind : Parallel");
37:                 break;
38:             }
39:         }
40:     }
41:
42:     public static void main(String args[]){
43:         (new PortList()).showList();
44:     }
45: }

筆者の環境で実行した結果は次のようになりました。

C:\temp\CommAPI>java PortList
Port Name : COM1, Owned, Kind : Serial
Port Name : COM2, Not Owned, Kind : Serial
Port Name : COM3, Not Owned, Kind : Serial
Port Name : COM4, Not Owned, Kind : Serial
Port Name : LPT1, Not Owned, Kind : Parallel
Port Name : LPT2, Not Owned, Kind : Parallel

シリアルポートが 4 ポート、パラレルポートが 2 ポートあることが分かると思います。この中で COM1 だけが使用中になっていますが、この時ちょうど PDA と通信を行っていたときでした。

それではプログラムの説明をして行きましょう。10 行目では、CommPortIdentifier クラスの getPortIdentifiers メソッドを使用して、すべてのポートの CommPortIdentifier オブジェクトの Enumeration を取得しています。getPortIdentifiers メソッドは static メソッドなので、CommPortIdentifier クラスのインスタンスがなくても使用することができます。

16 から 39 行目までの while ループの中では Enumeration オブジェクトから CommPortIdentifier を取り出してポートについて調べています。まず、21 行で getName メソッドを使用してポート名を出力しています。その後、24 行目でポートが使われているかを isCurrentlyOwned メソッドを使って調べ、さらに 31 行目でポートの種類を getPortType メソッドを使って調べています。

ポート名を指定して、CommPortIdentifier を得るには getPortIdentifier(String portName) メソッドを使用します。このメソッドも static なのでいつでも使用することが可能です。Windows の場合はポート名に COM1 などが使用されますが、Solaris では /dev/ttya などが使用されます。両者のプラットフォームで動作するようなアプリケーションを作るときには、プラットフォームによる差異をなるべくハードコーディングしないことが賢明です。

 

 
  ▲このページのトップへ戻る  
  ポートのオープン  
 

ポートのオープンにも CommPortIdentifier を使用します。たとえば COM1 をオープンさせるのであれば次のように書くことができます。

 1:    CommPortIdentifier id = CommPortIdentifer.getPortIdentifier("COM1");
 2:    SerialPort port = (SerialPort)id.open("ApplicationName", 10000);

ポートのオープンには CommPortIdentifier クラスの open メソッドを使用します。この例では、CommPortIdentifier オブジェクトを直接指定する方法で取得していますが、前の例のように Enumeration を取得してから、ポート名を調べる方法も使用することができます。

open メソッドの戻り値はオープンした CommPort オブジェクトです。実際に通信に使うには、通信の種類に応じて ParallelPort か SerialPort にキャストする必要があります。

open メソッドの第 1 引数はポートをオープンするアプリケーションの名前を入れます。厳密にアプリケーションの名前でなくてもかまいません。第 2 引数はタイムアウトまでの時間で、ミリ秒で指定します。

ポートは一度に 1 つのアプリケーションしか使うことができません。タイムアウトを設定するのは、もしポートが他のアプリケーションがポートを使用していたら、設定した時間だけオープンできるのを待つためです。この時間中にポートが解放されれば使用することができます。タイムアウトの時間がすぎてもポートを専有することができなかった場合は PortInUseException が発生します。

ところで、ポートが開放されたとか、どのアプリが待ち状態にあるかという情報はどのようにして取得するのでしょうか。

ポートの専有に関する情報が変更したときにはイベントが発生します。なぜか、専用のイベントクラスは使用されませんが、イベントを受け取るリスナは CommPortOwnershipListener インタフェースです。イベントが発生すると、CommPortOwnershipListener オブジェクトの ownershipChange メソッドがコールされます。ownershipChange メソッドの引数は int で、これがイベントの種類を表しています。イベントの種類は次の 3 種類です。

CommPortOwnershipListener.PORT_OWNED ポートがオープンされたとき
CommPortOwnershipListener.PORT_OWNEWSHIP_REQUESTED ポートのオープンを要求したとき
CommPortOwnershipListener.PORT_UNOWNED ポートを開放したとき

このイベントを受け取るには CommPortIdentifier の addPortOwnershipListener メソッドで登録します。

この動作を理解するために PortOpener.java というサンプルを作ってみました。

ソースコードはこちらから参照できます PortOpener.java

PortOpener を実行するとボタンが 1 つだけ配置されたウィンドウが表示されます。このボタンは状態に応じて表示ラベルが変化します。初期状態では "Open" と表示されており、この時ボタンをクリックすると open メソッドをコールします。open メソッドの処理中は "Requesting...." と表示され、ボタンをクリックすることができなくなります。ポートをオープンすることができると、"Close" と表示されます。この時にクリックするとポートを開放します。

PortOpener を実行すると標準出力にポートの状態変化を出力するようにしてみました。PortOpner を複数起動させて、ボタンをクリックするとポートがどのように状態変化するか分かると思います。図 2 は 2 つの PortOpner を起動させた様子です。

PortOpener の引き数は 2 つあり、第 1 引き数が open メソッドで使用するアプリケーションの名前、第 2 引き数にオープンするポート名としました。同じアプリケーションを起動させたときに区別がつきやすいように、アプリケーション名を引き数で与えるようにしました。図 2 では左側が Opener1、右側が Opener2 となっています。

a) 左側の Opener1 が COM1 をオープンしたところ

b) 右側の Opener2 が COM1 のオープンを要求

c) Opener1 が COM1 を解放。すぐにポートの要求をしていた Opener2 が専有を行う

d) Opener2 が COM1 を解放
図 2 PortOpener

まず a) では Opener1 が COM1 ポートのオープンを行ったところです。するとコンソールに Opener1 が専有したというメッセージが出力されています。イベントは open メソッドを一度コールしないと配送されないようです。そのため Opner2 のコンソールにはイベントのメッセージが表示されません。

次に、b) は Opener1 が COM1 をオープンした状態のままで、Opener2 に COM1 のオープンをさせてみたところです。このとき、Opener2 は COM1 をすぐオープンすることはできませんが、オープン要求はされています。要求されるとイベントが発生するために両方のコンソールに要求があったと出力されています。

c) は Opener1 が COM1 を解放したときです。まず、解放のイベントが発生します。解放されるとすぐに要求を行っていた Opener2 がポートをオープンすることができます。そこで、おなじようにイベントが発生します。

最後の d) は Opener2 が COM1 を解放したところで、イベントが発生しています。

PortOpener は PortOwnershipEvent を受けるために、PortOwnershipListener をインプリメントさせました。

 1: public class PortOpener implements CommPortOwnershipListener { 

イベントを登録するのはコンストラクタの中です。コンストラクタの該当部分を示しておきます。getPortIdentifier メソッドを使用して CommPortIdentifier オブジェクトを取得します。イベント登録は CommPortIdentifier オブジェクトに対して行います。

 1:        try{
 2:            // CommPortIdentifier を取得
 3:            portID = CommPortIdentifier.getPortIdentifier(portName);
 4:
 5:            // ポートの専有状態の監視を行うためのリスナを登録
 6:            portID.addPortOwnershipListener(this);
 7:        }catch(NoSuchPortException ex){
 8:            System.out.println(portName + "can't be found.");
 9:            ex.printStackTrace();
10:            System.exit(1);
11:        }

イベントを処理するのは ownershipChange メソッドです。

 1:    public void ownershipChange(int type){
 2:        switch(type){
 3:          case PORT_OWNED:
 4:            // ポートが専有されている場合
 5:            System.out.println(portID.getName() + " is Owned by " 
 6:                               + portID.getCurrentOwner());
 7:            break;
 8:          case PORT_UNOWNED:
 9:            // ポートが専有されていない場合
10:            System.out.println(portID.getName() + " is Unowned");
11:            break;
12:          case PORT_OWNERSHIP_REQUESTED:
13:            // ポートの専有を要求している場合
14:            System.out.println(portID.getName() + " is Requested");
15:            break;
16:        }
17:    }

ポートがオープンされている場合は、portID.getCurrentOwner メソッドを使用して、ポートをオープンしているアプリケーションの名前を得ることができます。CommPortIdentifier#open の引き数のアプリケーション名がここで取得されます。

ポートがオープンされているときに、open がコールされると PORT_OWNERSHIP_REQUESTED のイベントが発生します。ポートのオープン要求はキューにスタックされ、ポートが開放されたときにキューの先頭がオープンすることができます。

ポートのオープンとクローズの部分をさらっと見ておきましょう。ポートをオープンするときはボタンのラベルを変更し (3 行目)、クリックできないようにしておきます (4 行目)。そして、8 行目で open メソッドをコールします。ここではタイムアウトの時間を 1 分間としました。

実際にポートがオープンできるまで open メソッドはブロックされます。open メソッドが終了したら、ボタンをクリックできるようにし (10 行目)、ラベルを "Close" に変更します (13 行目)。14 行目の openFlag はポートがオープンしているかどうかを示すフラグで、ボタンがクリックされたときにポートの状態によって呼び出す関数を決めるために用いています。

 1:   public void open(){
 2:       try{
 3:           openButton.setLabel("Requesting....");
 4:           openButton.setEnabled(false);
 5:
 6:           // ポートのオープン
 7:           // タイムアウトは 1分間=60,000ms
 8:           port = portID.open(appName, 60000);
 9:
10:           openButton.setEnabled(true);
11:
12:           // ボタンの文字列を変更
13:           openButton.setLabel("Close");
14:           openFlag = true;
15:       }catch(PortInUseException ex){
16:           // タイムアウトを過ぎた場合
17:           System.out.println(portID.getName() + " is owned by someone.");
18:       }
19:   }

クローズはポートが null でなければ close コマンドを呼び出して、ボタンのラベルを変更するだけです。

 1:    public void close(){
 2:        if(port != null){
 3:            // ポートをクローズ
 4:            port.close();
 5:            port = null;
 6:
 7:            // ボタンの文字列を変更
 8:            openButton.setLabel("Open");
 9:            openFlag = false;
10:        }
11:    }
 
  ▲このページのトップへ戻る  
  通信  
 

無事にポートのオープン/クローズができるようになりましたから、次に行うのは通信です。とはいっても、それほど難しく考えることはありません。シリアル通信といえども、ストリームを使用するため、ファイルの読み書きや、ソケット通信と同じように行うことができます。ただし、まったく同じというわけではありません。重要なのは、通信条件を設定しなくてはいけないということです。これは送受信両方とも必要です。

ここでは簡単な通信の例としてある定型の文字列を送信するアプリケーションと、それを受信するアプリケーションを作ってみましょう。

ただし、送受信ともポートのオープン/クローズや通信の設定などは共通に行うことができます。そこで、この部分を行うだけの SerialPortHandler というクラスをつくりました。送受信用のクラスは SerialPortHandler を派生させ、送受信の部分だけ加えるだけにしました。送信用が SerialPortWriter、受信用が SerialPortReader です。

ソースはこちらから

シリアル通信には通信速度やデータ長、ストップビット、フローコントロールなどが送受信の両端であっている必要があることはご存知だと思います。そこで、この設定から行っていきましょう。SerialPortHandler のコンストラクタで設定を行っています。

 1:        try{
 2:            // ポートのオープン
 3:            port = (SerialPort)portID.open("SerialPortHandler", 5000);
 4:        }catch(PortInUseException ex){
 5:            // タイムアウトを過ぎた場合
 6:            ex.printStackTrace();
 7:            System.exit(1);
 8:        }
 9:
10:        try {
11:            // 通信条件の設定
12:            port.setSerialPortParams(9600, // 通信速度 9600 baud
13:                                     SerialPort.DATABITS_8, // データビット 8bit
14:                                     SerialPort.STOPBITS_1, // ストップビット 1bit
15:                                     SerialPort.PARITY_NONE); // パリティ なし
16:
17:            // フローコントロールの設定
18:            // ここではハードフローコントロール(RTS/CTS) を使用
19:            port.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_IN
20:                                    | SerialPort.FLOWCONTROL_RTSCTS_OUT);
21:        } catch (UnsupportedCommOperationException ex){
22:            ex.printStackTrace();
23:            System.exit(1);
24:        }

まずはポートのオープンを行います (3行目)。その後で、通信設定を行うのですが、ここで使用するのは setSerialPortParams メソッドです。このメソッドは通信速度、データビット長、ストップビット長、パリティの設定を行うことができます。通信速度以外の設定値は SerialPort クラスに定数として設定してあります。下の表にその一覧を示しました。たとえばデータ長が 7 bit の時は SerialPort.DATABITS_7 を引き数に指定します。SerialPortHandler ではそれぞれ 9600 baud, 8bit, 1bit, パリティなしに設定しました (12 〜 15 行目)。

データビット SerialPort.DATABITS_5 5 bit
SerialPort.DATABITS_6 6 bit
SerialPort.DATABITS_7 7 bit
SerialPort.DATABITS_8 8 bit
ストップビット SerialPort.STOPBITS_1 1 bit
SerialPort.STOPBITS_1_5 1-1/2 bit (通常は使われない)
SerialPort.STOPBITS_2 2 bit
パリティ SerialPort.PARITY_NONE パリティなし
SerialPort.PARITY_EVEN 奇数パリティ
SerialPort.PARITY_ODD 偶数パリティ
SerialPort.PARITY_MARK マークパリティ
SerailPort.PARITY_SPACE スペースパリティ
フローコントロール SerialPort.FLOWCONTROL_NONE フローコントロールなし
SerialPort.FLOWCONTROL_XONXOFF_IN ソフトウェアフローコントロール (入力)
SerialPort.FLOWCONTROL_XONXOFF_OUT ソフトウェアフローコントロール (出力)
SerialPort.FLOWCONTROL_RTSCTS_IN ハードウェアフローコントロール (入力)
SerialPort.FLOWCONTROL_RTSCTS_OUT ハードウェアフローコントロール (出力)

フローコントロールは専用のメソッド setFlowControlMode で行います。このメソッドの引数も SerialPort クラスに設定してある定数を用いて行うことができます。たとえば XON/XOFF を使用した場合は、SerialPort.FLOWCONTROL_XONXOFF_IN と SerialPort.FLOWCONTROL_XONXOFF_OUT を使用します。前者が受信用、後者が送信用です。それでは、送受信を両方とも行う場合はどうするのでしょう。そのときはこの 2 つの定数の OR をとるようにします。

 1:    port.setFlowControlMode(
 2:               SerialPort.FLOWCONTROL_XONXOFF_IN | SerialPort.FLOWCONTROL_XONXOFF_OUT);

SerialPortHandler ではフローコントロールに RTS/CTS を使用したハードフローコントロールを行っています (19, 20 行目)。このクラスも送受信を行えるように、SerialPort.FLOWCONTROL_RTSCTS_IN と SerialPort.FLOWCONTROL_RTSCTS_OUT の OR をとっています。

通信条件の設定はポートをオープンしている間は、いつでも行うことができます。ただし、送信側と受信側の両方で同じ設定を行わないと通信できないのは自明ですね。

SerialPortHandler を用いて通信条件の設定をおこないましたから、次に送信側についてみていきましょう。送信は SerialPortWriter で行っています。前述したように SerilaPortWriter は SerialPortHandler の派生クラスになるので、SerailPortWriter で定義するのは送信の処理だけです。

送信を行うにはポートからストリームを取り出して行います。これには SerialPort クラスの getOutputStream メソッドを使用します。SerialPortWriter では、この処理をコンストラクタで行っています。

 1:    public SerialPortWriter(String portName){
 2:        super(portName);
 3:
 4:        try {
 5:            // 出力用の Writer を生成
 6:            writer = new PrintWriter(
 7:                     new BufferedWriter(
 8:                     new OutputStreamWriter(port.getOutputStream())));
 9:        } catch (IOException ex){
10:            ex.printStackTrace();
11:            System.exit(1);
12:        }
13:    }

コンストラクタの始めにスーパークラスのコンストラクタを呼び出して、ポートのオープン、通信条件の設定を行います (2 行目)。ストリームを取り出すのはその後の 6 〜 8 行目です。8 行目で、getOutputStream メソッドを使用してストリームを取得します。取得したストリームは Writer に変換され、それに BufferedWriter さらに PrintWriter をかぶせます。Buffered がついているストリームや Reader/Writer はバッファ処理を行うため、入出力処理速度を向上することができます。PrintWriter を使用すると、println メソッドが使用することができるので扱いが楽になります。ようするに、標準出力 System.out と同じ使い勝手となるのです。

データの送信はとても単純です。単にストリームに書き出すだけです。SerialPortWriter では PrintWriter を使用しているので println メソッドが使用できますが、その他のストリームや Writer クラスでは write メソッドなどを使用して書き出します。

 1:    public void write(){
 2:        writer.println("SerialPortWriter write TEST.");
 3:        writer.flush();
 4:        close();
 5:    }

2 行目で書き出しを行っています。単純な例なので、単に文字列を出力しているだけです。普通のストリームと何ら変わりなく使えるので、数値やバイト列なども送信することもできますし、ObjectOutputStream クラスを使えばオブジェクトを送信することも可能です。

次に受信側です。受信はデータ待ちの処理が入ります。単に read メソッドを使用して待ち処理を行ってもいいのですが、read メソッドで処理がブロックされてしまいます。Communications API には、もう少し巧妙な仕組みが含まれています。それはイベントを使用した方法です。シリアル通信に関するイベントは SerialPortEvent、パラレル通信に関するイベントは ParallelPortEvent です。それぞれのイベントを受けるためのリスナは SerailPortEventListener, ParallelPortEventListener になります。

SerialPortEvent は通信エラーが発生したとき、キャリアデテクト (CD) したときなどがありますが、データを受信したときにも配信されます。このイベントを使うことで、イベント処理ルーチンの中でデータの受信処理を行うことができます。

受信用に作成したクラスは SerialPortReader です。SerialPortReader では、SerailPortEvent のリスナ登録をコンストラクタで行っています。

 1:    public SerialPortReader(String portName){
 2:        super(portName);
 3:
 4:        try {
 5:            // SerialPortEvent を受け取るためのリスナの登録
 6:            port.addEventListener(this);
 7:        } catch(TooManyListenersException ex){
 8:            ex.printStackTrace();
 9:            System.exit(1);
10:        }
11:
12:        // Data Available イベントを受け取るようにする
13:        port.notifyOnDataAvailable(true);
14:
15:        try {
16:            // 入力用の Reader を生成
17:            reader = new BufferedReader(
18:                     new InputStreamReader(port.getInputStream()));
19:        } catch (IOException ex){
20:            ex.printStackTrace();
21:            System.exit(1);
22:        }
23:    }

2 行目で、スーパークラスのコンストラクタを呼び出しているのは、SerailPortWriter と同様です。その後、6 行目で addEventListener メソッドを使用して SerilaPortEvent のリスナ登録を行います。ところが、通常のイベントと異なり、これだけではイベントを受けることはできません。イベントを受け取るためのメソッドを利用して受け取るイベントを指定する必要があります。SerialPort クラスには notify で始まるメソッドが 10 個定義してあります。こられの関数がイベントを受け取るための指定を行うメソッド群です。SerialPortReader では 13 行目で nofityOnDataAvailable メソッドを使用して、データの受信イベントを受け取るようにしています。

その後、Reader を SerialPort から取得しています。これは SerialPortWriter で行ったものを、Reader に変えただけです。

データの受信には SerialPortEventListener インタフェースで定義された serialEvent メソッドの中で行います。

 1:    public void serialEvent(SerialPortEvent event) {
 2:        switch(event.getEventType()) {
 3:          case SerialPortEvent.BI:
 4:          case SerialPortEvent.OE:
 5:          case SerialPortEvent.FE:
 6:          case SerialPortEvent.PE:
 7:          case SerialPortEvent.CD:
 8:          case SerialPortEvent.CTS:
 9:          case SerialPortEvent.DSR:
10:          case SerialPortEvent.RI:
11:          case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
12:            // Data Available 以外のイベントは処理しない
13:            break;
14:          case SerialPortEvent.DATA_AVAILABLE:
15:            // Data Available の処理
16:            String buffer = null;
17:
18:            try {
19:                while (reader.ready()) {
20:                    // データの読み込み
21:                    buffer = reader.readLine();
22:		    
23:                    // 標準出力への出力
24:                    System.out.println(buffer);
25:                }
26:            } catch (IOException e){}
27:            break;
28:        }
29:    }

2 行目で getEventType メソッドを用いて、イベントの種類に応じた処理を switch で行わせます。SerailPortReader はデータ受信イベント SerialPortEvent.DATA_AVAILABLE 以外は使用しないので、そのまま break で抜けてしまいます (3 〜 13 行目)。データの読み取りは 21 行目で行っています。BufferedReader を使用しているので 1 行単位の読み込みを行うことができますが、その他のストリームや Reader では read メソッドを使用して行います。読み取った文字列を 24 行目で標準出力に書き出しています。

2 台の PC もしくは WS をクロスのシリアルケーブルでつなげれば、SerialPortWriter と SerialPortReader を実行することができます。また、複数のポートが使用できるコンピュータであれば、クロスケーブルでポートを結ぶようにすれば (たとえば COM1 と COM2 をクロスケーブルで接続する)、1 台のコンピュータでも実行することが可能です。図 3 に示すのは筆者の PC で COM1 と COM2 をつないで、SerialPortReader と SerialPortWriter を動作させた実行結果です。SerialPortReader の方に SerialPortWriter write TEST. と出力されているのが確認できると思います。

Communications API の紹介は以上で終わりです。ここまで説明した機能を使えば、RS-232C を利用した通信が自由にできることと思います。次回は、RS-232C を用いた通信の例ということで実際に機器と PC を RS-232C でつないで、データ監視を行うアプリケーションを作っていきたいと思います。

図 3 SerialPortReader/Writer の実行例

 

 
  ▲このページのトップへ戻る  
  おまけ  
 

本編では扱った例はとても簡単なものでしたので、もうすこし複雑なアプリケーションを作ってみましょう。といっても、それほど難しいものではありません。Communications API のサンプルにある SimpleDemo をもっとシンプルにしたコンソール版 SimpleDemo です。

ソースコードはこちらから見ることができます。 DumbTerminal.java

DumbTerminal は SerialPortHandler のサブクラスで、送受信の両方の機能を備えたターミナルソフトです。

ただし、やっていることは SerialPortWriter, SerialPortReader とそれほど違いはありません。SerialPortWriter は送信する文字列が定数になっていましたが、DumbTerminal では標準入力から入力できるようにしました。受信の機能はまったく同じです。

標準入力からの読み込みを行うために、専用のスレッドをつくりそこでシリアルポートへの送信も行っています。スレッドで実行される run メソッドを下に示しておきます。下記のソースでは sysReader が標準入力に対応する Reader、comWriter がシリアルポートへの Writer です。

 1:    public void run(){
 2:        String buffer = null;
 3:        System.out.flush();
 4:
 5:        while(true){
 6:            try {
 7:                // データの読み込み
 8:                buffer = sysReader.readLine();
 9:                comWriter.println(buffer);
10:                comWriter.flush();
11:            } catch (IOException e){}
12:        }
13:    }

5 行目から 12 行目までのループでは、まず標準入力から 1 行単位で入力文字列を取得します。sysReader は BufferedReader なので readLine メソッドが使用できます。その後、comWriter に入力された文字列を送信し、フラッシュを行います。

図 4 に同一 PC 上で、COM1 と COM2 をクロスケーブルでつないで場合の実行例を示しておきます。受信した文字列ははじめに "> " をつけて表示するようにしてあります。図 4 で示されるように、日本語での送受信も問題なく行うことができました。

図 4 DumbTerminal の実行例
 
  ▲このページのトップへ戻る  
  ソースコードのダウンロード  
 

今回用いた全てのソースファイルとクラスファイルはここからダウンロードできます samples.zip


Bullet JavaおよびJavaに関する商標は、米国Sun Microsystems社の登録商標または商標です。

 
  ▲このページのトップへ戻る