Bullet 第8回

Java Communications API (2)
  1. はじめに
  2. 温度監視システムの設計
  3. DARWIN を使用したデータ収集
  4. 温度監視システムの実装
  5. リモート監視システムへの発展
  6. おまけ
  7. ソースコードのダウンロード

 
  はじめに  
 

前回は基礎編ということで、 Java Communications API を使用して RS-232C 通信を行う方法の解説を行いました。今回は応用編ということで、実際に Communications API を使用したアプリケーションを作成して行きたいと思います。

FA で使われる機器の中で RS-232C を使用できるものはいろいろありますが、そのような機器の 1 つとして筆者の所属する横河電機に DARWIN という製品を取りあげました。今回はこの DARWIN を使用して温度の監視システムを作っていきたいと思います。

DARWIN は温度・電圧・電流・流量・圧力などの測定データの収集を行う装置群の総称です。このシリーズにはPC と接続してデータ収集を行う DA100 や、チャートレコーダとして使用するハイブリッドレコーダ DR130 などがあります。DARWIN は RS-232C や GP-IB、Ethernet などを使用して PC などに接続することができ、またさまざまな種類のモジュールをつけ替えて使用することができます。

今回は DARWIN シリーズの中の DA100 を使用します。まず、DA100 と RS-232C で接続した PC でローカルに監視を行うシステムを作成します。ただし、DA100 をお持ちでない方にもサンプルを動作させることができるように DA100 のシミュレータも作ってみました。

次に、これを基本にして他のコンピュータからリモートで温度監視を行うシステムに発展させて行こうと思います。本講座の 1 回目から 3 回目でソケットを使ったリモート監視システムの解説を行いましたが、同じ方法ではつまらないので、ソケット以外の方法で通信を使用していきましょう。Java のコアライブラリには RMI (Remote Method Invocation) という ORB (Object Remote Broker: リモートにあるオブジェクトにアクセスするための機構) がありますので、これを使用していきます。

DARWIN Web Page
http://www.yokogawa.co.jp/Measurement/Bu/DARWIN/DARWIN.html

DARWIN DA100 紹介ページ
http://www.yokogawa.co.jp/Measurement/Bu/DA100/DA100.html

図 1 DARWIN DA100

 

 
  ▲このページのトップへ戻る  
  温度監視システムの設計  
 

温度監視システムを作成する前に、システムの設計をして行きましょう。設計は UML (Unified Modeling Language) を使用して行いました。ここでは UML に関する解説は省略させていただきますが、オブジェクト指向設計を行うには欠かせないものになっています。OMG でも UML を標準として採用しており、今後ますます普及して行くと思います。詳しくは次にあげた Web サイトや参考書籍をご覧ください。

オージス総研 UML チュートリアル
http://www.ogis-uml-university.com/tutorial/index.htm

オージス総研 UML レファレンス
http://www.ogis-uml-university.com/reference/index.htm

かんたん UML
オージス総研 著 千藤雅弘 監修、翔泳社、ISBN4-88135-759-X

UML ユーザガイド
Grady Booch 著、ピアソン・エデュケーション、ISBN4-89471-155-9

温度監視システムはオペレータが装置が収集したデータを何らかの方法で監視するわけですが、これをユースケース図で記述すれば次のようになります。図の中で人型で表されたものがアクタ、楕円で表されているのがユースケースです。アクタには監視システムを使用するオペレータと、データの収集を行う機器があります。オペレータはデータの監視 (参照) を行います。データ監視ユースケースは DA100 からデータを収集し、それをオペレータに参照させるという動作になります。

図 2 温度監視システムのユースケース図

 

このユースケースを実現するためのクラス構成を考えて行きましょう。オペレータが監視をするためには何らかのデータ表示を行わなくてはなりません。今回はこの講座の第 6 回で作成したバーメータを使用していきましょう。また、機器と通信してデータを収集するためのクラスも必要です。そして、この 2 つのクラスをまとめるためのクラスがあります。データ収集を行うクラスを DataCollector、システムをまとめるためのクラスを TemperatureMonitor クラスとしました。

ここで DataCollector について考えてみましょう。今回は機器は DA100 としましたが、実際にはデータの収集ができ、そのデータを何らかの手段でコンピュータに送信する機器であればなんでもいいわけです。使う側から見れば、なるべく同じ使い方でどんな機器でも使えるようにしたいだけでしょう。しかし、クラスのメソッドが独自に作られていれば、使い方は変わってしまいます。

そこで、DataCollector はデータ収集を行うためのメソッドを定義したインタフェースとして、実際の機器と通信するクラスは DataCollector インタフェースをインプリメントするようにして行きます。今回は Darwin だけしか使わないので、DarwinDataCollector クラスだけを実装していきます。しかし、機器を変更する場合はこれに対応するクラスを作る必要があります。

全体のクラス図は図 3 のようになります。TemperatureMonitor クラスが BarMeter と DataCollector の関連を持っています。

図 3 クラス構成

 

データ収集から表示の一連の処理方法はイベントを使った方法などいろいろ考えられますが、ここでは図 4 に示した単純な方法をとりました。TemperatureMonitor オブジェクトが周期的に DataCollector オブジェクトにデータ収集させるのですが、図 4 では一回分のデータ収集の流れを示しています。

図 4 データ収集シーケンス

 

まず、TemperatureMonitor オブジェクトが DataCollection オブジェクトにデータ収集の依頼します。その結果、返ってきたデータを BarMeter オブジェクトにセットします。BarMeter オブジェクトは値がセットされると、表示の更新を行います。その後、インターバルをとって、再びデータ収集を繰り返します。

システムの大まかな構成や処理が分かってきたので、設計はここまでにしておき、次の章から実際にコードを書いていきましょう。

 

 
  ▲このページのトップへ戻る  
  DARWIN を使用したデータ収集  
 

 

まず、RS-232C を使用してデータ収集を行う部分から説明していきます。前述したように、この部分は DataCollection インタフェースをインプリメントしたクラスを使用します。DA100 を使用して温度のデータを収集するので、このクラスを DarwinDataCollector としましょう。

ソースはこちらから参照できます

まず、データを収集する前に、RS-232C を利用するための処理が必要です。処理は前回説明したように

CommPortIdentifier を取得 -> SerialPort のオープン -> 通信条件の設定 -> Writer/Readerのとりだし

という流れで行います。DarwinDataCollector クラスでは CommPortIdentifier の取得をコンストラクタ、残りの部分を open メソッドの中で行っています。

コンストラクタを次に示します。処理自体は前回示したコードと変わりありません。CommPortIdentifier#getPortIdentifier メソッドを使用して CommPortIdentifier オブジェクトを取得します。ポート名は簡単にするためメンバ変数で設定してしまいましたが、Pure Java のアプリケーションにするならば設定ファイルから読み込ませるようにするなどの工夫が必要です。また、Solaris で動作させる場合や、COM1 以外のポートで動作させる場合はこの値を変更する必要があります。

 1:    protected CommPortIdentifier portID;
 2:    protected String portName = "COM1";
 3:
 4:    public DarwinDataCollector(){
 5:        // PortIdentifier の生成
 6:        portID = getCommPortIdentifier(portName);
 7:    }
 8:
 9:    protected CommPortIdentifier getCommPortIdentifier(String portname){
10:        CommPortIdentifier portID = null;
11:        try{
12:            // CommPortIdentifier を取得
13:            portID = CommPortIdentifier.getPortIdentifier(portname);
14:        }catch(NoSuchPortException ex){
15:            System.out.println(portname + " can't be found.");
16:            System.exit(1);
17:        }
18:
19:        return portID;
20:    }

次に open メソッドでポートのオープンからストリームの取出しまで行います。

DA100 と RS-232C で通信するための通信条件はDA100 の前面にあるディップスイッチを使用して設定します。詳しくは DA100 の「DA100 データアクイジションユニット 通信インタフェースユーザーズマニュアル」を参照していただくことにしますが、今回は工場出荷時の初期設定の値をそのまま使用していきます。パラメータは次のとおりです。

表 1 DA100 と通信条件
パラメータ
ボーレート 9600 bps
データ長 8 bit
パリティ Even
ストップビット 1 bit
ハンドシェーク なし

 

DarwinDataCollector クラスではこれらの値をメンバ変数に直接記述していますが、これもポート名と同様に設定ファイルなどから読み込むようにさせたほうが汎用的につかえるようになります。

すこし長くなりますが、ストリームの取出しまでの部分を次に示してみます。

 1:    protected SerialPort port;
 2: 
 3:    protected int baudrate = 9600;
 4:    protected int databits = SerialPort.DATABITS_8;
 5:    protected int stopbits = SerialPort.STOPBITS_1;
 6:    protected int parity = SerialPort.PARITY_EVEN;
 7:    protected int flowcontrol = SerialPort.FLOWCONTROL_NONE;
 8:
 9:    public void open(){
10:        try{
11:            // ポートのオープン
12:            port = (SerialPort)portID.open("DarwinDataCollector", 5000);
13:        }catch(PortInUseException ex){
14:            // タイムアウトを過ぎた場合
15:            System.out.println("Other application is using " + portName + ".");
16:            System.exit(1);
17:        }
18:
19:        // 通信条件の設定
20:        setSerialPortParameters(port);
21:
22:        // Reader/Writer の抽出
23:        writer = getWriter(port);
24:        reader = getReader(port);
25:
26:        // DARWIN の初期化
27:        darwinInitialize();
28:
29:        // DARWIN のレンジ設定
30:        for(int i = 0 ; i < channelSize ; i++){
31:            setRange(i+1, min, max);
32:        }
33:    }
34:
35:    protected void setSerialPortParameters(SerialPort port){
36:        try {
37:            // 通信条件の設定
38:            port.setSerialPortParams(baudrate, databits, stopbits, parity);
39:            port.setFlowControlMode(flowcontrol);
40:        } catch (UnsupportedCommOperationException ex){
41:            ex.printStackTrace();
42:            System.exit(1);
43:        }
44:    }
45:
46:    protected PrintWriter getWriter(SerialPort port){
47:        PrintWriter writer = null;
48:        try {
49:            // 出力用の Writer を生成
50:            writer = new PrintWriter(new BufferedWriter(
51:                     new OutputStreamWriter(port.getOutputStream())));
52:        } catch (IOException ex){
53:            ex.printStackTrace();
54:            System.exit(1);
55:        }
56:
57:        return writer;
58:    }
59:
60:    protected BufferedReader getReader(SerialPort port){
61:        BufferedReader reader = null;
62:        try {
63:            // 入力用の Reader を生成
64:            reader = new BufferedReader(new InputStreamReader(port.getInputStream()));
65:        } catch (IOException ex){
66:            ex.printStackTrace();
67:            System.exit(1);
68:        }
69:        
70:        return reader;
71:    }

3 から 7 行目で前述した通信条件を記述しています。ポートのオープンは 12 行目です。オープンができたら、通信条件の設定を行います。これは 35行目からの setSerialPortParameters メソッドで行っています。まず、38 行目でボーレート、データ長、ストップビット長、パリティを設定し、39行目でフローコントロールを設定します。

Writer は 46 行目からの getWriter メソッドで取得します。前回説明をしたのと同様にここでも BufferedWriter をかぶせることでパフォーマンスを向上させます。Reader は 60 行目からの getReader メソッドです。こちらも BufferedReader をかぶせます。

これで RS-232C 通信の準備は整いました。それでは DA100 と通信する部分を記述して行きましょう。

DA100 と通信を行うには DA100 のコマンドを使用しておこないます。基本的には何らかのコマンドをコンピュータから DA100 に送信し、その返答を受け取るというスタイルになります。

コマンドには文字列を使用します。コマンドは行単位で DA100 に送信するため、コマンド文字列の最後に 16進数で 0A 0D を送信します。DA100 は 0A 0D まで受信するとコマンド解析を行い、戻り値を送信します。この場合も行単位で送信され、行末には 0A 0D が送信されます。戻り値がない場合でも ACK が返るようになっています。したがって、何らかのコマンドをコンピュータ側から送信したら、必ず受信を行わなくてはなりません。

DA100 の詳しいコマンドの解説は省略させていただきますが、サンプルで使用したものだけ下表に示しておきます。

表 2 サンプルで使用した DA 100 のコマンド
コマンド パラメータ 動作
RS0   DA100 のリセット
RC0   DA100 の初期化
SRp1,p2,p3,p4,p5 p1: チャネル番号
p2: 入力の種類
p3: 入力のタイプ
p4: 最小値
p5: 最大値
入力の種類とレンジの設定
入力の種類には温度や電圧などが入る
入力のタイプには熱電対を使うなら型を指定する
TSp1 p1: データの種類 収集するデータの種別を設定する
p1 が 0 のときは測定データ、1 のときは設定データ
ESC T   トリガの実行
TS コマンド送信後に使用し、TS コマンドで設定されたデータの準備を行う
(ESC は 16 進数で 1B)
FMp1,p2,p3 p1: データフォーマット
p2: 先頭チャネル
p3: 終了チャネル

複数のチャネルからのデータ収集
返信されるデータのフォーマットは p1 が 0 なら文字列、1 ならバイナリ

 

RS0 コマンド、RC0 コマンド、SR コマンドは初期化時に一度だけ使用されます。SR コマンドで指定する入力の種類には次のようなものがあります。

表 3 DA 100 で測定可能な物理量の例
入力の種類 p2 の表記 p3 の表記 測定範囲
電圧 VOLT

20mV
60mV
200mV
2V
6V
20V
50V

-20 〜 20mV
-60 〜 60mV
-200 〜 200mV
-2 〜 2V
-6 〜 6V
-20 〜 20V
-50 〜 50V

温度 (熱電対) TC R
S
K
0 〜 1760 ℃
0 〜 1760 ℃
-200 〜 1370 ℃
直流電流 mA 20mA -20 〜 20mA

 

今回は温度の測定を行うので SR コマンドの p2 は TC、p3 は K タイプの熱電対を使いましたので、K としました。

TS コマンドと ESC T、FM コマンドはデータ収集時にペアで使われるのですが、TS コマンドを送信したらかならず ESC T コマンドを送信し、その後 FM コマンドを送信するようにします。FM コマンドの戻り値がデータになり、DA100 からは複数行送信されます。

このようなコマンドを DA100 に送信するわけですが、それぞれのコマンド専用に送信メソッドをつくるより、汎用のコマンド送信メソッドを作ってみましょう。

 1:    // コマンドの送信
 2:    protected String sendCommand(String command) {
 3:        writer.println(command);
 4:        writer.flush();
 5:
 6:        // コマンドには必ず戻り値がある
 7:        String result = null;
 8:        try{
 9:            result = reader.readLine();
10:        }catch(IOException ex){
11:            ex.printStackTrace();
12:        }
13:
14:        return result;
15:    }

sendCommand メソッドは引き数で与えられたコマンドを DA100 に送信して、DA100 からの返信を受信し、戻り値とするメソッドです。2 行目でコマンドの送信を行います。writer オブジェクトは PrintWriter クラスのオブジェクトなので、println メソッドが使用できます。このため、行末の 0A 0D を明示的に送信しなくても println メソッドで自動的に付加してくれます。

コマンドを送信したらフラッシュを行い (4 行目) 、DA100 からのデータ待ちになります。DA100 からは必ず行単位で送信されるので readLine を使用することができます。DA100 から送信されたデータを受信して、戻り値にします (14 行目)。

sendCommand メソッドを作ることで、コマンドを送信するのは簡単になります。たとえば、上述した open メソッドの 27 行目でコールされる darwinInitialize メソッドは次のようになります。

 1:    // DARWIN の初期化
 2:    protected void darwinInitialize(){
 3:        // Darwin Reset
 4:        sendCommand("RS0");
 5:        
 6:        // Darwin Initialize
 7:        sendCommand("RC0");
 8:    }

単純に RS0 と RC0 を文字列で sendCommand メソッドに送信しているだけです。

同じように入力の指定の部分は次のようになります。

 1:    protected String type = "TC,K,"; // 入力のタイプ ここではK型熱電対を使用
 2:
 3:    // 数値整形用フォーマッタ
 4:    protected NumberFormat formatter;
 5:
 6:    protected void initFormatter(){
 7:        // 数値出力の整形用クラス
 8:        formatter = NumberFormat.getInstance();
 9:        // 出力桁数を 3桁にする
10:        formatter.setMinimumIntegerDigits(3);
11:    }
12:
13:    // DARWIN のレンジ設定
14:    protected void setRange(int ch, int minimum, int maximum){
15:
16:        initFormatter();
17:
18:        // レンジの設定には SR コマンドを使用
19:        // 書式 SRp1,p2,p3,p4,p5
20:        //        p1: チャネル番号 p2: 入力の種類 p3: 測定のタイプ
21:        //        p4: 最小値 p5: 最大値
22:        StringBuffer command = new StringBuffer();
23:        command.append("SR");
24:        command.append(formatter.format(ch));
25:        command.append(",");
26:        command.append(type);
27:        command.append(String.valueOf(minimum));
28:        command.append(",");
29:        command.append(String.valueOf(maximum));
30:
31:        sendCommand(command.toString());
32:    }

入力の指定には SR コマンドを使用します。今回は温度測定なので、p2 には TC、K 型熱電対を使用するので p3 には K を指定します。ソースでは 1 行目に定数でこれを指定しています。

4 行目の formatter オブジェクトは java.text.NumberFormat クラスのオブジェクトで数値を整形して出力するときに使用します。formatter は 4 から 9 行目の initFormatter メソッドで初期化しています。8 行目で最低でも 3 桁出力するように設定してあるので、たとえば 3 を出力するときでも 003 となります。NumberFormat クラスの format メソッドで整形した文字列を得ることができます。

最小値、最大値はこの NumberFormat を使用して 3 桁の出力を行うようにしてあります。

DA100 からの送信されたデータが 1 行の時は sendCommand メソッドを使用できるのですが、複数行になる FM コマンドの時だけ扱いが異なります。そこで、FM コマンドに特化したメソッドを用意しました。それが次に示す requestData メソッドです。

 1:    protected Vector requestData(){
 2:        // データの取得
 3:        // 書式 FMp1,p2,p3
 4:        //         p1: 測定データフォーマット指定 0=ASCII, 1=バイナリ
 5:        //         p2: 出力先頭チャネル  p3: 出力終了チャネル
 6:        writer.println("FM0,001," + formatter.format(channelSize));
 7:        writer.flush();
 8:
 9:        String buffer;
10:        StringTokenizer tokenizer;
11:        String value;
12:        Vector results = new Vector();
13:
14:        // データの受信
15:        // 書式   1行目 DATEYYMMDD     YY: 年 MM: 月 DD: 日
16:        //        2行目 TIMEhhmmss     hh: 時間 mm: 分 ss: 秒
17:        //        3行目 S1S2A1A1A2A2A3A3A4A4UUUUUCCC,±DDDDDE-e
18:        //                S1: データステータス  N: ノーマル E: 異常
19:        //                S2: データ順序  スペース: 中間データ E: 最終データ
20:        //                A1,A2,A3,A4: アラームステータス
21:        //                U: 単位 
22:        //                C: チャネル番号
23:        //                ±: データの正負
24:        //                DDDDD: データの仮数部
25:        //                E-e: データの指数部 (eに数値が入る)
26:        //  以下、チャネル数分、3行目と同じフォーマットでデータを受信できる
27:        try{
28:            buffer = reader.readLine(); // 時間情報は使用しない 
29:            buffer = reader.readLine(); 
30:        }catch(IOException ex){
31:            ex.printStackTrace();
32:            System.exit(1);
33:        }
34:
35:        for(int i = 0 ; i < channelSize ; i++){
36:            try{
37:                buffer = reader.readLine();
38:                tokenizer = new StringTokenizer(buffer, ",");
39:                tokenizer.nextToken();
40:                value = tokenizer.nextToken(); // データ部分の切りだし
41:                results.add(new Double(value));
42:            }catch(IOException ex){
43:                ex.printStackTrace();
44:                System.exit(1);
45:            }
46:        }
47:        return results;
48:    }

まず、16 行目でコマンドを送信しています。ここでも、NumberFormat を使用して数値出力の整形を行います。

24 行目から 36 行目のコメントに DA100 が送信するデータのフォーマットが記してあります。受信したデータの 1, 2 行目は日付と時間ですが、今回のサンプルではこの情報は使用しないので、読み込むだけにしてあります (38, 39 行目)。3 行目から実際のデータです。DA100 から複数のチャンネルのデータを収集するときは、チャネル数だけこのフォーマットで DA100 から送信されます。

このフォーマットはカンマの後に数値データが入っているので、java.util.StringTokenizer クラスを使用してカンマの前後で文字列を分割します (48 行目)。java.util.StringTokenizer クラスは特定の文字で文字列を分割するときにとても役立ちます。分割する文字列も選べるので、今回の例のようにカンマ以外の場合でも使用することができます。

切り出した文字列を使用して Double クラスのオブジェクトを生成し、それを Vector クラスのオブジェクトである results に格納します (51 行目)。すべてのチャネルのデータを受信したら results を戻り値として返します。

さて、ここで DataCollector インタフェースが定義しているメソッドを DarwinDataCollector クラスで実装してみましょう。DataCollector インタフェースは次に示す 6 個のメソッドを定義しています。

 1: public interface DataCollector {
 2:     int getChannelSize();
 3:     int getMinimumValue();
 4:     int getMaximumValue();
 5:     Vector getData();
 6:     void open();
 7:     void close();
 8: }

getChannelSize メソッドは、収集するデータのチャネル数を返します。getMinimumValue メソッドと getMaximumValue メソッドはそれぞれ最小値、最大値を返すメソッドです。この 3 つのメソッドは単に定数を返すだけの単純なものになります。open メソッドと close メソッドは機器のオープンとクローズです。open メソッドはすでに前で示したので、close メソッドだけ示しておきます。

 1:  public void close(){
 2:        writer.close();
 3:        try{
 4:            reader.close();
 5:        }catch(IOException ex){
 6:            ex.printStackTrace();
 7:        }
 8:        port.close();
 9:    }

close メソッドでは writer と reader をクローズしてから、ポートをクローズするだけです。

DataCollector インタフェースで最も重要なメソッドがデータを取得するための getData メソッドです。DarwinDataCollector クラスでは次のようにデータを収集しています。

 1:    public Vector getData(){
 2:        // TS コマンドを使用して、取得するデータの選択を行う
 3:        // 書式 TSp1
 4:        //          p1: 0 測定データ
 5:        //              1 設定データ
 6:        sendCommand("TS0");
 7:
 8:        // データ出力前にはトリガを送る必要がある
 9:        // トリガ書式  ESC T
10:        sendCommand("\u001bT");
11:
12:        // データの取得
13:        return requestData();
14:    }

データ収集には、前述したように TS コマンド、ESC T コマンド、FM コマンドを続けて使用します。まず、TS コマンドを送信し (6 行目)、トリガである ESC T を送信します (10 行目)。13 行目の requestData は前に示したように FM コマンドを使用して、データを DA100 から取得する関数です。requestData メソッドの戻り値は Vector ですから、そのまま getData の戻り値として使用できます。

 

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

監視システムの実装

 
 


DA100 と通信してデータを収集する DarwinDataCollector クラスができたので、BarMeter とあわせて監視システムを作っていきます。この 2 つのクラスの間を取り持つのが前述した TemperatureMonitor クラスです。

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

TemperatureMonitor クラスは DarwinDataCollector オブジェクトと BarMeter オブジェクトを生成ます。また、周期的に DarwinDataCollector オブジェクトにデータ収集を要求し、得られたデータを BarMeter で表示させます。

TemeratureMonitor は DataCollector オブジェクトと BarMeter オブジェクトをメンバ変数にもちます。実際にオブジェクトとして持つのは DarwinDataCollector オブジェクトなのですが、ここではインターフェースの DataCollector を通して操作を行うようにします。こうすることで、機器が変化したとしても必要最小限のソース変更ですむようになります。また、メータは複数あるので Vector に保持することにしましょう。また、周期的な処理を行うための Thread もメンバに変数に持つようにしました。

    protected DataCollector collector;
    protected Vector meters;
    protected Thread dataCollectionThread;

TemperatureMonitor のコンストラクタでは DarwinDataCollecto オブジェクトと BarMeter オブジェクトを生成します。また、BarMeter オブジェクトを表示するためのウィンドウを生成しています。

 1:    public TemperatureMonitor(){
 2:
 3:        // データコレクターの生成
 4:        collector = new DarwinDataCollector();
 5:        collector.open();
 6:        
 7:        // フレームの生成
 8:        Frame frame = new Frame("TemeratureMonitor");
 9:        frame.setBounds(100, 100, 300, 150);
10:        frame.setLayout(new GridLayout(0,4));
11:        frame.setBackground(Color.lightGray);
12:        frame.addWindowListener(new WindowAdapter(){
13:            public void windowClosing(WindowEvent event){
14:                stop();
15:                collector.close();
16:                System.exit(0);
17:            }
18:        });
19:
20:        // バーメータの生成
21:        meters = new Vector();
22:        for(int i = 0 ; i < collector.getChannelSize() ; i++){
23:            BarMeter barmeter = new BarMeter();
24:            barmeter.setForeground(Color.yellow);
25:            barmeter.setBackground(Color.black);
26:            barmeter.setMinimum(0);
27:            barmeter.setMaximum(100);
28:            Panel panel = new Panel();
29:            panel.add(barmeter);
30:            frame.add(panel);
31:            meters.add(barmeter);
32:        }
33:
34:        frame.setVisible(true);
35:    }

4 行目で DarwinDataCollector オブジェクトを生成し、次の行で DA100 のオープンを行います。これで DA100 の準備は終わりましたから、次は GUI です。

バーメータを表示するにはウィンドウがなければならないので、Frame を使用します。12 から 18 行目は Windows でいえばウィンドウの右上の×ボタンをクリックされたときの動作を記述したものです。×をクリックされたら、周期スレッドをストップし (14行目)、DA100 をクローズしてから (15 行目)、システムを終了させます (16 行目)。

最後に BarMeter オブジェクトを生成して Frame オブジェクトに貼りつけます。そして、DA100 に接続されたセンサのチャネルの分だけ BarMeter オブジェクトを生成します (23 行目)。24, 25 行目で色の設定を行い、26, 27 行目で BarMeter の表示最小値と最大値を設定します。さらに BarMeter を Panel に貼りつけ、Panel を Frame に貼りつけます。そして、Vector のオブジェクトである meters に BarMeter オブジェクトを保持させます (31 行目)。

最後に Frame オブジェクトを表示させます。

周期スレッドは start メソッドの中で生成します。スレッドの中心となるのは run メソッドです。

 1:    public void start(){
 2:        dataCollectionThread = new Thread(this);
 3:        dataCollectionThread.start();
 4:    }
 5:
 6:    public void run(){
 7:        Vector data;
 8:        BarMeter barmeter;
 9:
10:        Thread currentThread = Thread.currentThread();
11:        while(currentThread == dataCollectionThread){
12:            // データの収集
13:            data = collector.getData();
14:
15:            // データの表示
16:            for(int i = 0 ; i < collector.getChannelSize() ; i++){
17:                barmeter = (BarMeter)meters.elementAt(i);
18:                barmeter.setValue(((Double)data.elementAt(i)).doubleValue());
19:            }
20:
21:            try{
22:                currentThread.sleep(INTERVAL);
23:            }catch(InterruptedException ex){}
24:        }
25:    }

13 行目で DataCollector オブジェクトである collector からデータを取得します。getData メソッドの戻り値は Vector で、複数チャネルのデータが保持されています。その Vector オブジェクトに保持されているオブジェクトを 1 つづつ取り出して (17 行目)、BarMeter オブジェクトにセットします (18 行目)。これをチャネル数くりかえします。BarMeter オブジェクトは setValue がコールされると、表示を更新します。

最後に 22 行目で、次の周期まで sleep します。INTERVAL は定数で 1 秒にしました。

これで監視システムの完成です。コンパイルして実行してみましょう。このアプリケーションはパッケージを第 6 回の BarMeter の時と同様に工業応用部会の URL を使用して、jp.gr.javacons.industry.seminar.tempmonitor としました。したがって、コンパイルと実行は次のようになります。

C:\temp\CommAPI>javac jp\gr\javacons\industry\seminar\tempmonitor\TemperatureMonitor.java

C:\temp\CommAPI>java jp.gr.javacons.industry.seminar.tempmonitor.TemperatureMonitor

実行すると、4 つのバーメータが並んで温度を示すようになります。

図 5 TemperatureMonitor の実行例

 

ここまでは DA100 を使用されていることを前提に説明してきましたが、DA100 をお持ちでない方のために DA100 のシミュレータを作ってみました。

ソースはこちらです DarwinSimulator.java

DarwinSimulator クラスはシリアルポートを COM2 にハードコーディングしていますので、この他のポートを使用するときにはここを書きかえてください。

同じ PC で、COM1 と COM2 をクロスのシリアルケーブルを使って接続している環境で、DarwinSimulator クラスを使用して TemperatureMonitor を動作させてみましょう。このときは、TemperatureMonitor クラスを実行する前に DarwinSimulator クラスを実行する必要があります。

C:\temp\CommAPI>start java jp.gr.javacons.industry.seminar.tempmonitor.DarwinSimulator

C:\temp\CommAPI>java jp.gr.javacons.industry.seminar.tempmonitor.TemperatureMonitor

 

 
  ▲このページのトップへ戻る  
  リモート監視システムへの発展  
 

せっかく監視システムを作ったのですから、リモートでも監視できるようにしてみましょう。RMI を使えばほとんどソースを変更することなくリモート監視システムに発展させることができます。RMI は Java の標準の ORB で、リモートにあるオブジェクトにメッセージセンディングをすることができます。しかも、使い方はとても簡単です。

RMI の解説は Java の公式サイトにある RMI 入門が簡潔で分かりやすいので、参考になさってください。

RMI 入門
http://java.sun.com/products/jdk/1.2/ja/docs/ja/guide/rmi/getstart.doc.html

リモート監視システムに発展させるために作成するクラスとインタフェースは次の 4 つです。

作成するクラス

RemoteDataCollector はリモートにあるクライアントに対するサーバ側のインタフェースとなります。サーバ側ではこの RemoteDataCollector をインプリメントした実装クラスが必要になりますが、これが DataCollectorDelegator です。Delegate という単語は移譲などと約されますが、ようするにリモートからの要求を DataCollector に仲介する役目をもつクラスです。

TemperatureMonitorServer クラスと TemperatureMonitorClient クラスは、文字通りサーバとクライアントです。

リモートでの監視のシーケンスは図 6 のようになります。ローカルで監視していた場合は TemperatureMonitor が直接 DataCollector にデータの要求を行っていたのですが、リモート監視では TemperatureMonitorClient と DataCollector の間に RMI で通信を行う RemoteDataCollector と DataCollectorDelegator が入った形になっています。

図 6 リモート監視システムのデータ収集シーケンス

 

まず、RemoteDataCollector を作りましょう。このインタフェースは java.rmi.Remote インタフェースを派生させて作ります。また、各メソッドは java.rmi.RemoteException を投げるように設定します。

RemoteDataCollector インタフェースはほとんど DataCollector インタフェースと同じですが、open メソッドと close メソッドだけありません。これは、オープンとクローズはサーバが行い、クライアントは単にデータを表示するだけだからです。

 1: public interface RemoteDataCollector extends Remote {
 2:     int getChannelSize() throws RemoteException;
 3:     int getMinimumValue() throws RemoteException;
 4:     int getMaximumValue() throws RemoteException;
 5:     Vector getData() throws RemoteException;
 6: }

この RemoteDataCollector をインプリメントする実装クラスが DataCollectorDelegator です。

 1: public class DataCollectorDelegator extends UnicastRemoteObject
 2:	                implements RemoteDataCollector, Serializable {
 3:
 4:     protected transient DataCollector collector;
 5: 
 6:     public DataCollectorDelegator(DataCollector collector)
 7:                                         throws RemoteException {
 8:         super();
 9:         this.collector = collector;
10:     }
11:
12:     public int getChannelSize(){
13:         return collector.getChannelSize();
14:     }
15:
16:     public int getMinimumValue(){
17:         return collector.getMinimumValue();
18:     }
19:
20:     public int getMaximumValue(){
21:         return collector.getMaximumValue();
22:     }
23:
24:     public Vector getData(){
25:         return collector.getData();
26:     }
27: }

DataCollectorDelegator は java.rmi.server.UnicastRemoteObject を派生させて作ります。コンストラクタで DataCollector が引き数として渡されるので、それをメンバ変数に代入します (9 行目)。コンストラクタは RemoteException を投げるようにすることも RMI の約束のひとつです。

RemoteDataCollector で定義された関数はすべて DataCollector オブジェクトの関数を呼び出しているだけです。

次にサーバのクラスを見ていきましょう。

 1: public class TemperatureMonitorServer  {
 2:     protected DataCollector collector;
 3:     protected DataCollectorDelegator delegator;
 4: 
 5:     public TemperatureMonitorServer(){
 6:         try{
 8:             // データコレクターの生成
 9:             collector = new DarwinDataCollector();
10:             collector.open();
11: 
12:             // Delegator の生成
13:             delegator = new DataCollectorDelegator(collector);
14: 
15:             // RMI の登録
16:             Naming.rebind("//localhost/MonitoringServer", delegator);
17:             System.out.println("TemperatureMonitoringServer start.");
18:         }catch(Exception ex){
19:             ex.printStackTrace();
20:             System.exit(1);
21:         }
22:     }
23:
24:     public static void main(String[] args){
25:         new TemperatureMonitorServer();
26:     }
27: }

TemperatureMonitorServer クラスはメンバ変数に DataCollector クラスのオブジェクトと DataCollectorDelegator クラスのオブジェクトを持ちます。コンストラクタではこの 2 つのオブジェクトを生成しています (9, 13 行目)。機器のオープンもここで行います (10 行目)。delegator はリモートからアクセスできるように RMI のネームサービスに登録をします (16 行目)。登録の時にはオブジェクトの名前が必要になります。rebind メソッドの第 1 引き数がその名前になります。名前は //hostname/objectname という形になります。ここではホスト名を localhost にしてありますが、必要に応じて書きかえてください。第 2 引き数が登録を行うオブジェクトになります。

クライアントの TemperatureMonitorClient クラスは TemperatureMonitor クラスとほとんど変わりありません。変更があるのはコンストラクタの部分だけです。

TemperatureMonitor クラスでは DataCollector オブジェクトを直接生成していました。

 1:    public TemperatureMonitor(){
 2:
 3:        // データコレクターの生成
 4:        collector = new DarwinDataCollector();
 5:        collector.open();

これが TemperatureMonitorClient クラスでは RMI のネームサーバで検索を行って、RemoteDataCollector オブジェクトを得るようになっています。 検索には名前が必要です。サーバで登録した名前と同じ名前をここで指定します。

 1:    public TemperatureMonitorClient(){
 2:
 3:        // ルックアップを行ってデータコレクターを取得する
 4:        try{
 5:            collector = (RemoteDataCollector)Naming.lookup("//localhost/MonitoringServer");
 6:        }catch(Exception ex){
 7:            ex.printStackTrace();
 8:            System.exit(1);
 9:        }
           以下省略

RemoteDataCollector オブジェクトを取得できたら、後は TemperatureMonitor クラスとまったく変わりなく、RemoteDataCollector クラスのメソッドをコールすることができます。

コンパイルはサーバとクライアントで別々に行います。また、RMI を使用するために rmic を使用して DataCollectorDelegator の Stub/Skelton を作成する必要があります。

C:\temp\CommAPI>javac jp\gr\javacons\industry\seminar\remotetempmonitor\TemperatureMonitorServer.java

C:\temp\CommAPI>javac jp\gr\javacons\industry\seminar\remotetempmonitor\TemperatureMonitorClient.java

C:\temp\CommAPI>rmic jp.gr.javacons.industry.seminar.remotetempmonitor.DataCollectorDelegator

 

rmic を実行すると DataCollctorDelegator_Stub.class と DataCollectorDelegator_Skel.class の 2 つのファイルが生成されます。この 2 つのクラスがクライアントからのメソッドコールを中継して、サーバに引き渡す役目を担っています。

実行する前に注意しておきたいのですが、このアプリケーションはサンプルということでセキュリティに関してまったく考慮していません。実際に RMI のアプリケーションを作成するときは

  • セキュリティマネージャを使用する
  • Policy ファイルを使用してアクセス権の制御を行う

の 2 つのことを行わなければなりません。RMI で用意されたセキュリティマネージャは java.rmi.RMISecurityManager クラスです。また、policy ファイルについては次のドキュメントを参照してください。

http://java.sun.com/products/jdk/1.2/ja/docs/ja/guide/security/PolicyFiles.html
http://java.sun.com/products/jdk/1.2/ja/docs/ja/guide/security/permissions.html

実行するときには RMI のネームサーバである rmiregistry をまず実行します。次に、TemperatureMonitorServer を実行しましょう。

C:\temp\CommAPI>java jp.gr.javacons.industry.seminar.remotetempmonitor.TemperatureMonitorServer
TemperatureMonitoringServer start.

 

実行されると、"TemperatureMonitoringServer start." と表示されるので、これを確認してから TemperatureMonitorClient を実行します。複数の TemperatureMonitorClient を起動することも可能です。

これで Communications API の解説は終わりです。Communications API を使えば、簡単に RS-232C を使えることが分かっていただけたでしょうか。現在は Communications API で使用できるのは RS-232C と IEEE 1284 だけですが、USB や IEEE 1394 (iLink もしくは FireWire) なども同じように Java で使えればいいと望むのは筆者だけでしょうか。今後、RS-232C よりも USB などで接続される機器も増えてくると思われるので、ぜひ実現していただきたいものです。

 

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

本編ではリモート監視システムを RMI で作成しましたが、まったく同じように Socket を使用しても作ることは可能です。その場合、RMI が行っていた ORB の処理を自作する必要があります。簡易的な ORB を実現させるため、クライアント側で動作する DataCollectorProxy クラスと、 サーバ側で動作する DataCollectorStub クラスを作成しました。また、RMI の例で示した RemoteDataCollector インタフェースに相当する SocketDataCollector を導入しました。この 2 つのインタフェースの違いは Remote クラスの派生クラスかどうかという部分と、RemoteException を投げるかどうかです。ですから、定義してある関数はまったく同一です。

作成したクラス

Socket 通信は DataCollectorProxy オブジェクトと DataCollectorStub オブジェクトの間で行われます。クライアントが SocketDataCollector に定義してあるメソッドをコールすると、DataCollectorProxy オブジェクトは DataCollectorStub オブジェクトにコールされたメソッド名を送信します。引き続き、メソッドの引き数も送信します。DataCollectorStub はまずメソッド名を受け取り、メソッドに応じた引き数を読み込むようにします。引き数をすべて受信したら、DarwinDataCollector の当該メソッドをコールします。メソッドの戻り値を受け取ったら、DataCollectorProxy に送信します。DataCollectorProxy はこれを受信して、クライアントにメソッドの戻り値として引き渡します。

今回はメソッドの引き数や戻り値に Vector 以外のオブジェクトがなく、Vector に保持してあるデータも double のデータなので、ObjectInput/OutputStream を使用せずに、DataInput/OutputStream を使用しました。メソッドの引き数が何らかのオブジェクトである場合は ObjectInput/OutputStream を使用し、オブジェクトをシリアライズして送信する必要があります。

簡易的な ORB を作ったので、クライアントのコードは RMI やローカル監視の場合とほとんど変わりません。異なるのは、コンストラクタの部分だけです。collector が DataCollectorProxy になっています。DataCollectorProxy は通信するホストをコンストラクタの引き数とします。

 1:    protected SocketDataCollector collector;
 2:
 3:    public SocketTemperatureMonitorClient(){
 4:
 5:        collector = new DataCollectorProxy("localhost");
           以下省略     

これで、RMI の場合とほとんど同じ方法でリモート監視システムが実現できます。ただし、DataCollectorProxy/Stub クラスは DataCollector クラス専用なので、他のクラスを使用するには Proxy/Stub を一から書きなおさなければなりません。RMI は汎用にするために、この部分を rmic を用いて Proxy/Stub の自動生成を行っているわけです。

 

 
  ▲このページのトップへ戻る  
  ソースコードのダウンロード  
 

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

samples.zip の中には第 6 回で作成した BarMeter も含まれています。


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

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