.NET2.0時代のRS232Cシリアルポートプログラミング(2)

.NET2.0のSerialPortクラスを実際に使って、モデム等のRS232Cインタフェース機器を制御する場合、SerialPortクラスのDtrEnableプロパティのデフォルトはtrueではなくfalseであることに注意する。

public bool DtrEnable { get; set; }

true to enable Data Terminal Ready (DTR); otherwise, false. The default is false.

この場合のData Terminal Ready (DTR)は日本語では"データ端末レディ"と言われているシリアルポートの信号線の一つであり、通信状態であることを知らせる為に使用するが、機器によっては常時Onである。従って、そのような機器の場合このプロパティをEnabledにしないとSerialPortクラスの各種イベントが正しく発生しない。

...なんてことはない、これで一日はまってたんだけどね。機器の仕様はちゃんと見ようぜ -> 俺

.NET2.0時代のRS232Cシリアルポートプログラミング(1)

私は業務系のプログラマなので、普段は装置とピア-ピアで通信するようなプログラムはまず書かないけれど、それでも偶に書く必要に迫られる場合がある。
「下手の横好き」とはよく言ったものだけど、経験が無い癖に、大したこともできないくせに装置制御のプログラムを書くのは、とても楽しい。普段、泥臭くて論理的では無いビジネスロジックを書いているせいか、仕様通りに、きっちりと応答を返してくれる装置を制御するプログラミングは良い気分転換になるのかもしれない。(本業にしている方、ごめんなさい)

今回のネタはシリアルポート通信。一般的なPCのシリアルインタフェースはとうにUSBに移行してるんだけど、装置制御の世界では未だにRS232C規格のシリアルポートが多く使われていたりする。
.NETにおけるシリアルポート制御のプログラミングは、.NET2.0以前までは鬼門だった。というのも.NET Framework 1.xでは何故かシリアルポート制御のサポートは、ばっさりと捨てられており一切のAPIが公開されていないからだ。(Windows CE.NETでは需要があるせいか、シリアル通信はサポートされている)従って、.NET1.x時代にシリアルポート制御(RS232C制御)を行うためには以下のどちらかを選択するしかなかった。

.NETからはCOM相互運用機能を介して使用可能。

例:

//こんな感じでインスタンス化して使用できる
MSCommLib.MSComm comm = new MSCommLib.MSCommClass();
comm.CommPort = 1;
comm.Settings = "9600,N,8,1";
comm.InputMode = MSCommLib.InputModeConstants.comInputModeText;

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/comm98/html/vbobjcomm.asp

  • Win32APIを直接使用する

.NETからはP/Invoke(プラットホーム呼び出し)を使用してラッパークラス等を作って使用する。
P/InvokeのコードはDCB構造体やOVERRAPPED構造体を使用しており、単純には書けないので、MSDNの記事を参照して欲しい -> Use P/Invoke to Develop a .NET Base Class Library for Serial Device Communications

Microsoftも当時は、ここまでシリアルポート通信が生き残ると思わなかったのだろうか、結局.NET1.x系ではサポートもされずじまいだったが、どうしてなかなか装置系のインタフェースは長生きするのが物事の理だ。その反省からか、先日正式に公開された.NET2.0では、シリアルポート制御は正式にサポートされており、そのためのSystem.IO.Portsネームスペースが追加されている。

  • SerialPortクラス (System.IO.Ports.SerialPort)
public class SerialPort : Component

見てのとおり、Componentから派生しているので扱い易いし、ポートからのデータの受取はイベントドリブンで書かれているため、実際の使い方も簡単だ。

SerialPortクラスの使用例

〜
SerialPort commPort = new SerialPort();
commPort.DataReceived += new SerialDataReceivedEventHandler(OnDataReceived);

commPort.PortName= "COM1"; //ポート名(P/Invokeのように末尾に':'は付けない)
commPort.BaudRate = 9600; //ボーレート
commPort.DataBits = 8; //データビット
commPort.StopBits = 1; //ストップビット
commPort.Parity = Parity.Even; //パリティ
commPort.Handshake = Handshake.None; //ハンドシェイク
commPort.ReadTimeout = 30000;
commPort.WriteTimeout = 30000;
commPort.Open();
〜
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
    Trace.WriteLine(commPort.ReadExisting());
}

コードを見ても非常にシンプルで簡単であることが解るだろう。

重要: SerialPortクラスのDataReceivedイベントは内部では非同期コールバックを使用しており、イベントハンドラはセカンダリスレッドからコールバックされるので、SerialPortクラスをコントロールとして、Formや他のコントロール上で使用する場合は、適切なInvokメソッドやMethodInvokerクラスを使用して、UIスレッドと同期する必要があることに注意する必要があるだろう。

SerialPortクラスをForm上で使用する場合のDataReceivedイベントハンドラの同期例

delegate void MessageDataDelegate(string data);
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
    MessageDataDelegate dlgte = new MessageDataDelegate(MessageData);
    string recvData = commPort.ReadExisting();
    //UIスレッドと同期が必要
    this.Invoke(dlgte, new object[]{ recvData });
}
private void MessageData(string data)
{
    textBox1.Text = data;
}