WindowsFormで、シリアルCOMポートから受信したデータを処理する

Z-1170というデータコレクターがあり、なかなか便利に使っています。
ただ、PCへのデータアップロードが専用のアプリorハイパーターミナル経由となり、そのまま使うのはいろいろと手間でした。


データコレクター自体はUSB接続なのですが、よく見ると、「インターフェース:USBシリアル(COMポートとして認識)」と記載されていました。
そこで、シリアルCOMポートのハンドリングをすればいいのかなと考え、C# + WindowsFormで作ってみました。

■環境

■実装の流れ

  1. WindowsFormにSerialPortコントロールを置く
  2. SerialPortコントロールのPortNameプロパティに、デバイスマネージャーで認識されているZ-1170のポート番号(COM3 等)に設定
  3. SerialPortコントロールのDtrEnableプロパティ・RtsEnableプロパティを、Trueにする(任意)
  4. SerialPortコントロールのDataReceivedイベントに、処理を記載

■DataReceivedイベントの記載内容

シリアルポートからのデータ受信は、WindowsFormのスレットとは異なるスレッドで動作するため、
Control.Invokeメソッドを使用して、WindowsFormのスレッドに結果を返します。


Control.Invokeメソッドの使い方は、C#のバージョンが上がるにつれ、色々と使いやすくなりました。
5パターンほどありますが、いずれも同じ結果になります。

//  C#1.1 & C#2.0+匿名メソッドで必要な、デリゲート宣言
delegate void SerialPortDeligate1(string recieve);
delegate void SerialPortDeligate2();


//  C# 1.1 用のデリゲートで呼ばれるメソッド
private void AddText(string text)
{
    textBox1.Text += text + Environment.NewLine;
}


//  シリアルポートから受信した時のイベント
private void srlCOM_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
    //  シリアルポートより受信した際の、末尾文字列
    srlCOM.NewLine = Environment.NewLine;
    
    //  受信データの追加
    var code = srlCOM.ReadLine();

    //  C# 1.1 (デリゲート宣言と実際のメソッド追加が必要)
    this.Invoke(new SerialPortDeligate1(AddText), new object[] { code });

    //  C# 2.0 + 匿名メソッド (デリゲート宣言が必要)
    this.Invoke(new SerialPortDeligate2(delegate { textBox2.Text += code + Environment.NewLine; }));

    //  C# 2.0 + MethodInvoker (デリゲートの宣言不要)
    this.Invoke(new MethodInvoker(delegate { textBox3.Text += code + Environment.NewLine; }));

    //  C# 3.0 + ラムダ式 + MethodInvokerへキャスト
    this.Invoke((MethodInvoker)(() => { textBox4.Text += code + Environment.NewLine; }));

    //  C# 3.0 + ラムダ式 + MethodInvokerを生成
    this.Invoke(new MethodInvoker(() => textBox5.Text += code + Environment.NewLine));
}


なお、今回C# 3.0のパターンでは MethodInvokerを使ってますが、汎用デリゲートのActionでも同様の結果が得られます。
MethodInvokerを使った理由は、MethodInvoker デリゲート (System.Windows.Forms) にて、

MethodInvoker は、void パラメータ リストを指定してメソッドを呼び出すときに使用する簡単なデリゲートを提供します。このデリゲートは、コントロールの Invoke メソッドを呼び出す場合や、簡単なデリゲートが必要だが定義するのが面倒な場合に使用できます。

と記載されているため、今回の用途に一致しているかなと感じたためです。


また、Control.InvokeメソッドとControl.BeginInvokeメソッドの2種類ありますが、今回はSerialPortデータは同期的に漏れなく受け取った方が良いかと考え、前者を使いました。
ちなみに、Control.BeginInvokeメソッドについては、Delegate.BeginInvokeメソッドと異なり、EndInvokeメソッドは使わなくて良いようです。

■ソース

使い回すことを考えて、呼ぶ側と呼ばれる側(受信データをプロパティに詰めて返す)に分けています。
エラーハンドリングとかは必要ですが、以下のコードでは省略しています。
Gist:WindowsFormでのシリアルポート通信のサンプル

呼ぶ側
using System;
using System.Windows.Forms;

namespace SerialTest
{
    public partial class CallForm : Form
    {
        public CallForm()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //  シリアルポートからの読込開始
            var dialog = new ReceiveForm();
            dialog.ShowDialog();

            //  モーダルフォーム終了後、読込データを反映
            textBox1.Text = dialog.ReadCode;
        }
    }
}
呼ばれる側
using System;
using System.Windows.Forms;

namespace SerialTest
{
    public partial class ReceiveForm : Form
    {
        //  シリアルポートから読み込んだデータ
        public string ReadCode { get; private set; }

        public ReceiveForm()
        {
            InitializeComponent();
        }

        private void ReceiveForm_Load(object sender, EventArgs e)
        {
            serialPort1.Open();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            serialPort1.Close();
            this.Close();
        }

        private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
        {
            //  シリアルポートより受信した際の、末尾文字列
            serialPort1.NewLine = Environment.NewLine;

            //  受信データの追加
            var code = serialPort1.ReadLine();

            //  Control.Invokeメソッドで、UIスレッドへ処理を反映(タイプ量が少ない生成方式を使った)
            this.Invoke(new MethodInvoker(() => ReadCode += code + Environment.NewLine));
        }
    }
}