Java Communications API (1) |
はじめに | ||||||||||||||||||||||||||||||||||||||||||||||||||
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 のダウンロードサイト 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 のバージョンによってインストールの方法が変わります。 Java をインストールしたディレクトリを C:\jdk1.1.8 とします。ディレクトリの区切り記号は \ で記述してあるので、Solaris の場合は / で置き換えてください。
Java 2 では CLASSPATH の設定を行わなくてもよいため、JDK 1.1.x より簡単にインストールできます。Java をインストールしたディレクトリを <JDK> とします。Solaris 版の場合は JDK 1.1.x の時と同様に区切り記号を / で置き換えてください。
これで 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 種類です。インタフェースとクラスの概要を次に示しておきます。
|
||||||||||||||||||||||||||||||||||||||||||||||||||
▲このページのトップへ戻る | ||||||||||||||||||||||||||||||||||||||||||||||||||
まずは通信に使用するポートを調べることからはじめてみましょう。ポートを調べるためには CommPortIdentifier クラスを使用します。Communications API で扱うことのできるポートにはシリアルポートとパラレルポートの 2 種類があります。これを調べるためのサンプルとして PortList.java を作ってみました。このサンプルはポートの一覧を表示して、使用状況とそのポートがシリアルかパラレルかを表示します。 ● ソースはこちらからも見ることができます PortList.java
筆者の環境で実行した結果は次のようになりました。
シリアルポートが 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 をオープンさせるのであれば次のように書くことができます。
ポートのオープンには CommPortIdentifier クラスの open メソッドを使用します。この例では、CommPortIdentifier オブジェクトを直接指定する方法で取得していますが、前の例のように Enumeration を取得してから、ポート名を調べる方法も使用することができます。 open メソッドの戻り値はオープンした CommPort オブジェクトです。実際に通信に使うには、通信の種類に応じて ParallelPort か SerialPort にキャストする必要があります。 open メソッドの第 1 引数はポートをオープンするアプリケーションの名前を入れます。厳密にアプリケーションの名前でなくてもかまいません。第 2 引数はタイムアウトまでの時間で、ミリ秒で指定します。 ポートは一度に 1 つのアプリケーションしか使うことができません。タイムアウトを設定するのは、もしポートが他のアプリケーションがポートを使用していたら、設定した時間だけオープンできるのを待つためです。この時間中にポートが解放されれば使用することができます。タイムアウトの時間がすぎてもポートを専有することができなかった場合は PortInUseException が発生します。 ところで、ポートが開放されたとか、どのアプリが待ち状態にあるかという情報はどのようにして取得するのでしょうか。 ポートの専有に関する情報が変更したときにはイベントが発生します。なぜか、専用のイベントクラスは使用されませんが、イベントを受け取るリスナは CommPortOwnershipListener インタフェースです。イベントが発生すると、CommPortOwnershipListener オブジェクトの ownershipChange メソッドがコールされます。ownershipChange メソッドの引数は int で、これがイベントの種類を表しています。イベントの種類は次の 3 種類です。
このイベントを受け取るには 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 ポートのオープンを行ったところです。するとコンソールに Opener1 が専有したというメッセージが出力されています。イベントは open メソッドを一度コールしないと配送されないようです。そのため Opner2 のコンソールにはイベントのメッセージが表示されません。 次に、b) は Opener1 が COM1 をオープンした状態のままで、Opener2 に COM1 のオープンをさせてみたところです。このとき、Opener2 は COM1 をすぐオープンすることはできませんが、オープン要求はされています。要求されるとイベントが発生するために両方のコンソールに要求があったと出力されています。 c) は Opener1 が COM1 を解放したときです。まず、解放のイベントが発生します。解放されるとすぐに要求を行っていた Opener2 がポートをオープンすることができます。そこで、おなじようにイベントが発生します。 最後の d) は Opener2 が COM1 を解放したところで、イベントが発生しています。 PortOpener は PortOwnershipEvent を受けるために、PortOwnershipListener をインプリメントさせました。
イベントを登録するのはコンストラクタの中です。コンストラクタの該当部分を示しておきます。getPortIdentifier メソッドを使用して CommPortIdentifier オブジェクトを取得します。イベント登録は CommPortIdentifier オブジェクトに対して行います。
イベントを処理するのは ownershipChange メソッドです。
ポートがオープンされている場合は、portID.getCurrentOwner メソッドを使用して、ポートをオープンしているアプリケーションの名前を得ることができます。CommPortIdentifier#open の引き数のアプリケーション名がここで取得されます。 ポートがオープンされているときに、open がコールされると PORT_OWNERSHIP_REQUESTED のイベントが発生します。ポートのオープン要求はキューにスタックされ、ポートが開放されたときにキューの先頭がオープンすることができます。 ポートのオープンとクローズの部分をさらっと見ておきましょう。ポートをオープンするときはボタンのラベルを変更し (3 行目)、クリックできないようにしておきます (4 行目)。そして、8 行目で open メソッドをコールします。ここではタイムアウトの時間を 1 分間としました。 実際にポートがオープンできるまで open メソッドはブロックされます。open メソッドが終了したら、ボタンをクリックできるようにし (10 行目)、ラベルを "Close" に変更します (13 行目)。14 行目の openFlag はポートがオープンしているかどうかを示すフラグで、ボタンがクリックされたときにポートの状態によって呼び出す関数を決めるために用いています。
クローズはポートが null でなければ close コマンドを呼び出して、ボタンのラベルを変更するだけです。
|
||||||||||||||||||||||||||||||||||||||||||||||||||
▲このページのトップへ戻る | ||||||||||||||||||||||||||||||||||||||||||||||||||
通信 | ||||||||||||||||||||||||||||||||||||||||||||||||||
無事にポートのオープン/クローズができるようになりましたから、次に行うのは通信です。とはいっても、それほど難しく考えることはありません。シリアル通信といえども、ストリームを使用するため、ファイルの読み書きや、ソケット通信と同じように行うことができます。ただし、まったく同じというわけではありません。重要なのは、通信条件を設定しなくてはいけないということです。これは送受信両方とも必要です。 ここでは簡単な通信の例としてある定型の文字列を送信するアプリケーションと、それを受信するアプリケーションを作ってみましょう。 ただし、送受信ともポートのオープン/クローズや通信の設定などは共通に行うことができます。そこで、この部分を行うだけの SerialPortHandler というクラスをつくりました。送受信用のクラスは SerialPortHandler を派生させ、送受信の部分だけ加えるだけにしました。送信用が SerialPortWriter、受信用が SerialPortReader です。
シリアル通信には通信速度やデータ長、ストップビット、フローコントロールなどが送受信の両端であっている必要があることはご存知だと思います。そこで、この設定から行っていきましょう。SerialPortHandler のコンストラクタで設定を行っています。
まずはポートのオープンを行います (3行目)。その後で、通信設定を行うのですが、ここで使用するのは setSerialPortParams メソッドです。このメソッドは通信速度、データビット長、ストップビット長、パリティの設定を行うことができます。通信速度以外の設定値は SerialPort クラスに定数として設定してあります。下の表にその一覧を示しました。たとえばデータ長が 7 bit の時は SerialPort.DATABITS_7 を引き数に指定します。SerialPortHandler ではそれぞれ 9600 baud, 8bit, 1bit, パリティなしに設定しました (12 〜 15 行目)。
フローコントロールは専用のメソッド setFlowControlMode で行います。このメソッドの引数も SerialPort クラスに設定してある定数を用いて行うことができます。たとえば XON/XOFF を使用した場合は、SerialPort.FLOWCONTROL_XONXOFF_IN と SerialPort.FLOWCONTROL_XONXOFF_OUT を使用します。前者が受信用、後者が送信用です。それでは、送受信を両方とも行う場合はどうするのでしょう。そのときはこの 2 つの定数の OR をとるようにします。
SerialPortHandler ではフローコントロールに RTS/CTS を使用したハードフローコントロールを行っています (19, 20 行目)。このクラスも送受信を行えるように、SerialPort.FLOWCONTROL_RTSCTS_IN と SerialPort.FLOWCONTROL_RTSCTS_OUT の OR をとっています。 通信条件の設定はポートをオープンしている間は、いつでも行うことができます。ただし、送信側と受信側の両方で同じ設定を行わないと通信できないのは自明ですね。 SerialPortHandler を用いて通信条件の設定をおこないましたから、次に送信側についてみていきましょう。送信は SerialPortWriter で行っています。前述したように SerilaPortWriter は SerialPortHandler の派生クラスになるので、SerailPortWriter で定義するのは送信の処理だけです。 送信を行うにはポートからストリームを取り出して行います。これには SerialPort クラスの getOutputStream メソッドを使用します。SerialPortWriter では、この処理をコンストラクタで行っています。
コンストラクタの始めにスーパークラスのコンストラクタを呼び出して、ポートのオープン、通信条件の設定を行います (2 行目)。ストリームを取り出すのはその後の 6 〜 8 行目です。8 行目で、getOutputStream メソッドを使用してストリームを取得します。取得したストリームは Writer に変換され、それに BufferedWriter さらに PrintWriter をかぶせます。Buffered がついているストリームや Reader/Writer はバッファ処理を行うため、入出力処理速度を向上することができます。PrintWriter を使用すると、println メソッドが使用することができるので扱いが楽になります。ようするに、標準出力 System.out と同じ使い勝手となるのです。 データの送信はとても単純です。単にストリームに書き出すだけです。SerialPortWriter では PrintWriter を使用しているので println メソッドが使用できますが、その他のストリームや Writer クラスでは write メソッドなどを使用して書き出します。
2 行目で書き出しを行っています。単純な例なので、単に文字列を出力しているだけです。普通のストリームと何ら変わりなく使えるので、数値やバイト列なども送信することもできますし、ObjectOutputStream クラスを使えばオブジェクトを送信することも可能です。 次に受信側です。受信はデータ待ちの処理が入ります。単に read メソッドを使用して待ち処理を行ってもいいのですが、read メソッドで処理がブロックされてしまいます。Communications API には、もう少し巧妙な仕組みが含まれています。それはイベントを使用した方法です。シリアル通信に関するイベントは SerialPortEvent、パラレル通信に関するイベントは ParallelPortEvent です。それぞれのイベントを受けるためのリスナは SerailPortEventListener, ParallelPortEventListener になります。 SerialPortEvent は通信エラーが発生したとき、キャリアデテクト (CD) したときなどがありますが、データを受信したときにも配信されます。このイベントを使うことで、イベント処理ルーチンの中でデータの受信処理を行うことができます。 受信用に作成したクラスは SerialPortReader です。SerialPortReader では、SerailPortEvent のリスナ登録をコンストラクタで行っています。
2 行目で、スーパークラスのコンストラクタを呼び出しているのは、SerailPortWriter と同様です。その後、6 行目で addEventListener メソッドを使用して SerilaPortEvent のリスナ登録を行います。ところが、通常のイベントと異なり、これだけではイベントを受けることはできません。イベントを受け取るためのメソッドを利用して受け取るイベントを指定する必要があります。SerialPort クラスには notify で始まるメソッドが 10 個定義してあります。こられの関数がイベントを受け取るための指定を行うメソッド群です。SerialPortReader では 13 行目で nofityOnDataAvailable メソッドを使用して、データの受信イベントを受け取るようにしています。 その後、Reader を SerialPort から取得しています。これは SerialPortWriter で行ったものを、Reader に変えただけです。 データの受信には SerialPortEventListener インタフェースで定義された serialEvent メソッドの中で行います。
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 でつないで、データ監視を行うアプリケーションを作っていきたいと思います。
|
||||||||||||||||||||||||||||||||||||||||||||||||||
▲このページのトップへ戻る | ||||||||||||||||||||||||||||||||||||||||||||||||||
おまけ | ||||||||||||||||||||||||||||||||||||||||||||||||||
本編では扱った例はとても簡単なものでしたので、もうすこし複雑なアプリケーションを作ってみましょう。といっても、それほど難しいものではありません。Communications API のサンプルにある SimpleDemo をもっとシンプルにしたコンソール版 SimpleDemo です。 ● ソースコードはこちらから見ることができます。 DumbTerminal.java DumbTerminal は SerialPortHandler のサブクラスで、送受信の両方の機能を備えたターミナルソフトです。 ただし、やっていることは SerialPortWriter, SerialPortReader とそれほど違いはありません。SerialPortWriter は送信する文字列が定数になっていましたが、DumbTerminal では標準入力から入力できるようにしました。受信の機能はまったく同じです。 標準入力からの読み込みを行うために、専用のスレッドをつくりそこでシリアルポートへの送信も行っています。スレッドで実行される run メソッドを下に示しておきます。下記のソースでは sysReader が標準入力に対応する Reader、comWriter がシリアルポートへの Writer です。
5 行目から 12 行目までのループでは、まず標準入力から 1 行単位で入力文字列を取得します。sysReader は BufferedReader なので readLine メソッドが使用できます。その後、comWriter に入力された文字列を送信し、フラッシュを行います。 図 4 に同一 PC 上で、COM1 と COM2 をクロスケーブルでつないで場合の実行例を示しておきます。受信した文字列ははじめに "> " をつけて表示するようにしてあります。図 4 で示されるように、日本語での送受信も問題なく行うことができました。
|
||||||||||||||||||||||||||||||||||||||||||||||||||
▲このページのトップへ戻る | ||||||||||||||||||||||||||||||||||||||||||||||||||
ソースコードのダウンロード | ||||||||||||||||||||||||||||||||||||||||||||||||||
今回用いた全てのソースファイルとクラスファイルはここからダウンロードできます samples.zip JavaおよびJavaに関する商標は、米国Sun Microsystems社の登録商標または商標です。 |
||||||||||||||||||||||||||||||||||||||||||||||||||
▲このページのトップへ戻る |