シリアル通信モニタを作ってみる(on VS for Mac) (2)

続いて実際にポートを介してデータの受信を行ってみます。今回作るソフトは

  1. データ受信
  2. 即座に画面に出力

という極めてシンプルな動作をするターミナルソフトです。

使用する SerialPort クラス

先の記事にも載せましたが、
MSDN にある説明ページをこちらに掲載しますので、適宜ご参考にどうぞ。

実際のプログラミング

SerialPort クラスの初期化

まずはポートの指定等が必要です。上のリファレンスでコンストラクタとして簡単にポート初期化が出来てしまう物がありますが、今回はとりあえず理解しながらということで逐一メンバ変数に値を代入していく方法を取ります。

目標としてはコンソールの実行時引数を利用して各種設定を反映させるということですが、初めての製作ということで決め打ちでやってみましょう。今回はポート名を /dev/tty.usbmodem1412、Baud Rate を 115200bps としたいと思います。

初期化段階でのコードはこちら。

private static SerialPort port; // SerialPortのインスタンス
public static void Main(string[] args) {
    port = new SerialPort(); // インスタンス化

    port.PortName = "/dev/tty.usbmodem1412"; // 通信するポート名
    port.Parity = Parity.None; // パリティ
    port.DataBits = 8; // 送信するデータのビット数 通常は8
    port.BaudRate = 115200; // baud rate
    port.StopBits = StopBits.One; // ストップビットの数
    port.DtrEnable = false; // DTRを用いるか
    port.RtsEnable = false; // RTSを用いるか
}

こんな感じに直接値を代入していきます。

ポートのオープン

さあ実際にポートをオープンしていきましょう。但しここで別のターミナルソフトウェアが既に目的のポートを開いていた場合等で実行時にエラーが生じることがあります。このため、例外を捕捉してやらなければなりません。こんな感じに。

try {
    port.Open(); // ポートをオープン
}
catch( Exception ) { // 例外が起きたらとりあえず握り潰してプログラム終了
// ポートオープンに失敗した場合
    Console.WriteLine($"Fail: Port open of {port.PortName}. Application stops.");
    Environment.Exit(1); // 終了コード1を発行して終了
}

簡易的に作るため、例外はキャッチするがその詳細については捨てるようにしています。また、例外が発生した時点でその後の動作は諦め、Environment.Exit() でプログラムを終了させています。

実際の読み取り動作(ポーリング)

event やら delegate やらは(僕がよく分かっていないので)使いません。今回は逐一値を 1byte ずつ読んでいって逐一出力させます。

port.DiscardInBuffer(); // 現段階で受信バッファに溜まっているデータをすべて破棄
while (true) { // 無限ループで順次読んでいく
    char dat = (char)port.ReadByte(); // 1バイト分データを読む char型(ASCIIコード)にキャストして格納
    Console.Write(dat); // 読んだデータをそのままコンソールに書き込む
    // Console.Write((char)port.ReadByte()); // 上2行を縮めて書くとこんな感じ
}

最初の port.DiscardInBuffer() は僕の趣味です。必須ではありませんが、直前まで溜め込んでいた文字列がプログラム実行時にドワっと出力されるのが個人的に余り好きではないです。

読み出される値の型は byte 型です。しかしながらこれをそのまま Console.WriteLine() に突っ込むとナマの数値として出力されてしまうため、一旦 char 型にキャストしています。

一応できた

あとはシリアル通信用のインターフェイスを介してターゲットに接続して確認するだけ。TXD と RXD のクロス配線には気をつけてくださいね〜。僕はこれで動作を確認しました。こんなに簡単に作れるなんてすごいですね。コード全景はこちら。

using System;
using System.IO.Ports;

namespace ser {
    class MainClass {
        private static SerialPort port; // SerialPortのインスタンス
        public static void Main(string[] args) {
            port = new SerialPort(); // インスタンス化

            port.PortName = "/dev/tty.usbmodem1412"; // 通信するポート名
            port.Parity = Parity.None; // パリティ
            port.DataBits = 8; // 送信するデータのビット数 通常は8
            port.BaudRate = 115200; // baud rate
            port.StopBits = StopBits.One; // ストップビットの数
            port.DtrEnable = false; // DTRを用いるか
            port.RtsEnable = false; // RTSを用いるか

            try {
                port.Open(); // ポートをオープン
            }
            catch ( Exception ) { // 後を書かないことで例外原因を補足する変数を捨てられる
            // ポートオープンに失敗した場合
                Console.WriteLine($"Fail: Port open of {port.PortName}. Application stops.");
                Environment.Exit(1); // 終了コード1を発行して終了
            }

            port.DiscardInBuffer(); // 現段階で受信バッファに溜まっているデータをすべて破棄
            while (true) {// 無限ループで順次読んでいく
                char dat = (char)port.ReadByte(); // 1バイト分データを読む char型(ASCIIコード)にキャストして格納
                Console.Write(dat); // 読んだデータをそのままコンソールに書き込む
                // Console.Write((char)port.ReadByte()); // 上2行を縮めて書くとこんな感じ
            }
        }
    }
}

各種値を実行時に決められるようにする

これまでは決め打ちでやってましたが、いよいよ一般に流布しているターミナルソフトウェアのように実行時のコマンドライン引数から値を持ってくるように改造します。まずはそれ用の関数 assignPortAndBaud を作ります。

コマンドライン引数 string[] args の仕様ですが、sting 型配列に則って以下のように格納されています。

$ mono prog.txt aaa bbb ccc # 例
# args[0] => aaa
# args[1] => bbb
# args[2] => ccc

C言語とは違って、0番目に実行ファイル名が格納されているということはなく、イキナリ引数が入っていることに注意です。また、引数の数は args.Length で取得可能になっています。当然当てずっぽうでいきなり args[0] とソースコードに書いても、1つも引数を当てず実行したときには例外が発生してしまいますから、事前に .Length で分岐させておく必要があります。

今回の仕様としては、screen コマンドのように第1引数がポート名第2引数が baud rate とします。swich 文を用いて簡単に記述できます。

static string portName;
static int baudRate;
 public static void assignPortAndBaud(string[] args) {
    switch( args.Length ) {
        default:
            portName = "/dev/tty.usbmodem1412";
            baudRate = 115200;
            break;
        case 1:
            portName = args[0];
            break;
        case 2:
            portName = args[0];
            baudRate = Convert.ToInt32(args[1]);
            break;
    }
}

呼び出し源は素直に assignPortAndBaud(args) としておけば良い感じにメインクラス内のメンバ変数に格納されます。あとは各種 SerialPort インスタンスへの値代入を弄くれば良いのですね。

port.PortName = portName; // 通信するポート名
port.BaudRate = baudRate; // baud rate

実行はこんな感じに。当然ですが、ポート名と baud rate はターゲットに合わせてください。終了する時は Ctrl-C で SIGINT を突っ込めばいいです。

$ mono ser.exe /dev/tty.usbmodem1422 9600

とりあえず受信プログラムは出来ました。一番単純なポーリングのみによってこれがかけてしまいました。続いて目指すのは送受信両方に対応するプログラムですね。僕のコードで高速にデータ落ちがなく通信出来るのかは非常に怪しいのですが、とりあえず作っていきます。ちなみにここでキーとなるのはスレッドなのですが、これも次回にしましょう。

参考