組み込み系ソフトウェア開発では、機器間の通信にシリアル通信(RS-232C)を使用することがよくあります。そのため、試験ツールとしてRS-232Cを使うプログラムを作りたいことがあります。
シリアル通信がテキスト(ASCII文字)であれば、各種ターミナルソフト(例:TeraTerm)を使えばプログラムを作成しなくても送受信する試験ができます。しかし、組み込み系では通信データにテキストではなくバイナリを使用することが多く、標準的なターミナルソフトでは対応できません(一部バイナリデータを扱うターミナルソフトもあるようです)。
試験ツールをC言語で書くと、GUIを使うのが大変で、また文字列処理も不得手なので、Javaを使って作りたいなと思うことがあります。
Javaからシリアルポート(俗にRS-232C)とパラレルポートを使用するためのオプションAPIとして、Java Communication API仕様が制定されています。現時点でAPIのバージョンは3.0です。このJava Communication API仕様(Ver.3.0)の実装がSunから無償で公開されています。現時点ではSPARC Solaris/x86 Solaris/x86 Linux版のみとなっています。
SunのJava Communicatins APIサイト(英語)
Windows版はAPI仕様Ver.2.0の頃の実装が昔提供されていたものの最近は提供されていません。昔のバージョンはメンテナンス期間が過ぎておりバグ修正もされていません。したがって、Sunの昔のWindows版実装を入手して使用するよりも、Java Communication API仕様に準拠したオープンソースのRXTXを使用した方がよいでしょう。
RXTX : serial and parallel I/O libraries supporting Sun's CommAPI
izumiさんよりRTXTのURLについて移動先の情報を頂き修正しました(2011/01/26)。
RXTXのライブラリは、Java Communications APIとクラス名・メソッド名については同じですが、パッケージ名がjavax.commではなくgnu.ioとなっているため、import文を変更する必要があります。
RXTXのサイトから以下ファイルをダウンロードします(バージョンは2008.1.6時点で最新のもの)。
これを適切なディレクトリに展開します。(ここでは、C:\java以下に展開します)
コンパイル時は、クラスパスにC:\java\rxtx-2.1-7-bins-r2\RXTXcomm.jarを追加します。実行時は、コンパイル時のクラスパスに加え、システム・プロパティjava.library.pathにC:\java\rxtx-2.1-7-bins-r2\Windows\i386-mingw32を記述します。(環境変数PATHに追加してもよい)
インストール文書には、RXTXcomm.jarをJDKのextdirにコピーし、rxtxSerial.dllとrxtxParallel.dllをJDKのbinにコピーするようあります。確かにここに置けば設定不要となりますが、JDKのバージョンを変えるごとに入れ直しになるので、ここでは他のプログラム開発と同様コマンドラインで指定することにします。
C:\Users\torutk\work> javac -cp \ C:\java\rxtx-2.1-7-bins-r2\RXTXcomm.jar;. MyApp.java C:\Users\torutk\work> java -cp \ C:\java\rxtx-2.1-7-bins-r2\RXTXcomm.jar;. \ -Djava.library.path=C:\java\rxtx-2.1-7-bins-r2\Windows\i386-mingw32 \ MyApp
ポート名を指定してシリアルポートを取得する方法と、使用可能なポート一覧(Enumeration)を取得し、その中から選択する方法があります。
import gnu.io.CommPortIdentifier; import gnu.io.SerialPort; import gnu.io.NoSuchPortException; import gnu.io.PortInUseException; class SomeClass { boolean openSerialPort() { try { portId = CommPortIdentifier.getPortIdentifier("COM1"); port = (SerialPort)portId.open("SomeClass", 2000); } catch (NoSuchPortException e) { e.printStackTrace(e); return false; } catch (PortInUseException e) { e.printStackTrace(e); return false; } return true; } CommPortIdentifier portId; SerialPort port; }
CommPortIdentifierのstaticメソッドgetPortIdentifierで、指定した名前のポート識別子を取得します。指定した名前に対応する識別子がないときはNoSuchPortExceptionがスローされます。
CommPortIdentifierクラスにはこのメソッドの他に、getPortIdentifiersメソッドですべてのポート識別子のEnumerationを取得するメソッドがあります。
ポート識別子が取得できたら、次はポートのopenを行います。openメソッドで指定している引数は、ポートを使用するアプリケーション名(String)と、ポートが他のアプリケーションに使用されていた場合にポートが解放されるのを待つ待ち時間(int型:ミリ秒)です。
openメソッドの戻り値はCommPort型ですが、シリアルポートの場合はCommPortの派生クラスSerialPort型のインスタンスが返却されます。ここではシリアルポートの識別子"COM1"を指定しているので、instanceofでの検査を省略してSerialPort型にダウンキャストしています。
シリアル通信を行うには、少なくても通信速度、データビット数、ストップビット数、パリティ、フロー制御を設定する必要があります。
: import gnu.io.UnsupportedCommOperationException; class SomeClass { : boolean setSerialPort() { try { port.setSerialPortParams( 19200, // 通信速度[bps] SerialPort.DATABITS_8, // データビット数 SerialPort.STOPBITS_1, // ストップビット SerialPort.PARITY_NONE // パリティ ); port.setFlowControlMode(SerialPort.FLOWCONTROL_NONE); } catch (UnsupportedCommOperationException e) { e.printStackTrace(); return false; } port.setDTR(true); port.setRTS(false); return true; } : }
通信速度、データビット数、ストップビット数、パリティの設定は、SerialPortクラスのsetSerialPortParamsメソッドで行います。通信速度はint型の値を直接指定しますが、残りはSerialPortクラスの定数を使用します。
フロー制御は、SerialPortクラスのsetFlowControlModeメソッドで行います。引数で指定するパラメータはビットマスクとなっているため、必要に応じて複数の定数を論理演算(OR)で指定します。
RS-232C制御信号のうち出力となるのがDTRとRTSです。Java Communications APIでは、SerialPortクラスのsetDTRメソッド、setRTSメソッドで信号をアクティブ・インアクティブに切り替えることが出来ます。初期化の手続きとしてはまずDTRをアクティブにするので、setDTR(true)を呼び出しています。
PC/AT互換機のRS-232Cは、デフォルトでDTR、RTS信号がアクティブのようです。上述サンプルのようにsetDTR/setRTSメソッドでfalseにするとDTR、RTS信号をインアクティブにできますが、プログラム終了後はDTR、RTS信号がまたアクティブになります。
一般的なモデムと端末間シリアル通信における制御信号の制御手順を以下に示します。
PC同士でシリアル通信を行う場合、クロスケーブルを使用します。このとき、制御信号の結線の仕方にいくつかの種類があるようです。そのなかの1つに、RTSとCTSを直結し、相手側のCDと結線するものがあります。
データを受信するには、シリアルポートから入力ストリームを取得し、そのストリームをreadします。readは通常ブロッキングするため、Java Communications APIではブロッキングを好まない場合のためにイベントで受信する仕組みも提供されています。
class SomeClass { : public void read() { InputStream in = port.getInputStream(); byte[] buffer = new byte[1024]; while (true) { int numRead = in.read(buffer); if (numRead == -1) { break; } else if (numRead == 0) { try { Thread.sleep(200); } catch (InterruptedException e) { // 割り込まれても何もしない } } // bufferから読み出し処理 } } }
SerialPortインスタンスからgetInputStreamでストリームを取得できます。あとはそこから読み出すだけです。
ここで、readメソッドは受信データがなければブロックすると思っていましたが、RXTXでは戻り値0で即リターンしています。
SunのJava Communications API実装(Windows版2.0)で試してみると、readメソッドがブロックしています。実装系によって振る舞いに多少の違いがあるようです。
先の入力ストリームのreadの場合、スレッドを1つ専用に割り付けることになります。もう1つのデータ受信方法として、イベントによる受信があります。SerialPortEventListenerを定義してSerialPortに登録しておくと、データ受信が発生したときにSerialPortEventListenerのserialEventメソッドが呼ばれます。データ受信以外の制御信号の変化やパリティ等のエラーもこのイベントで受信することができます。
import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; import java.util.TooManyListenersException; : class SomeClass { : class SerialPortListener implements SerialPortEventListener { public void serialEvent(SerialPortEvent event) { if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE) { read(); } } } void enalbeListener() { try { port.addEventListener(new SerialPortListener()); port.notifyOnDataAvailable(true); } catch (TooManyListenersException e) { // エラー処理 } } : }
SerialPortEventListenerインタフェースの唯一のメソッドserialEventを実装します。引数SerialPortEventインスタンスのgetEventTypeメソッドでイベント種類を判別します。データ受信の場合は、DATA_AVAILABLE定数で識別します。このイベント発生後にシリアルポートの入力ストリームを読み出せば、受信バッファにデータが入っているので入力ストリームの読み出しがブロックされたり0バイトでリターンすることはありません(タイムアウトを設定する場合を除く)。
イベントの登録はSerialPortインスタンスにaddEventListenerメソッドで行います。このメソッドは検査例外TooManyListenersExceptionを投げるので、try-catchで囲んでいます。SerialPortクラスの実装は、イベントリスナーを1つだけしか保持しないとAPIドキュメントに記載されています。
addEventListenerメソッドの登録以外に、SerialPortインスタンスのnotifyOnDataAvailable(true)を呼んでおく必要があります。忘れるとデータ受信イベントがリスナーに渡されません。
データ受信イベントは数バイト〜十数バイト着信した時点で発生するようです。そのため、これを超えるサイズのデータを送信している場合、一回の受信イベントでは一部分しか受信することができません。数回の受信イベントをまとめる必要があります。
SerialPortのスーパークラスであるCommPortのenableReceiveThresholdメソッドを使うと、受信イベントの発生を引数で指定したサイズのバイト数を受け取るまで遅延させることができます。例えば64を指定した場合、データが64バイト受信するまでは受信イベントが発生しません。これは逆に64バイト未満のデータを受信しても、受信イベントが発生しないということにもなります。
また、CommPortクラスのenableReceiveTimeoutメソッドを使うと、受信イベント待ちを指定した時間(ミリ秒)ごとにタイムアウトさせて受信イベントを発生させることができます。タイムアウトによる受信イベントのタイミングで入力データを読み出した際は、サイズ0でリターンします。
シリアル通信で、データブロックを周期的に送信している場合、受信側がそのデータブロックの先頭・終了を認識して切り分けるには工夫が必要となります。SerialPortのinputStreamからreadするデータサイズはタイミングによって変化します。データブロックの途中までであったり、あるいはデータブロックの途中から読み出すこともあります。
シリアル通信の場合は、データブロックの先頭を切り出す特殊な方法を通信フォーマットとして埋め込んでおくのがよさそうです。(データ中には表われないコードをデータブロックの先頭を示すなど)
それがない場合は、データ非送信期間が一定時間経過することで切り分けるのが一案です。(例:enableReceiveTimeoutで1000ミリ秒をセットしておき、データ受信イベントで読み出しサイズ0であったとき=1000ミリ秒データ受信がないとき、受信バッファをクリアして次の読み出しをデータ先頭とする)