JavaScriptを有効にしてください

WinFormsで質実剛健なシリアルモニタを作る

 ·  ☕ 8 分で読めます

    WinFormsでシリアルモニタ(Serial Monitor Essential)を自作しました。ソフトの使い方についてGitHubのReadMEに、ソフトの作り方のポイントについてこのブログにまとめてあります。


    作ったもの

    window

        再接続      送信機能  データレート
    Arduino IDE
    Tera Term
    Serial Monitor (VS Code Extension)
    Serial Monitor Essential
    • Arduino IDEは再接続に手間がかかる
    • Tera Termは送信用のテキストボックスがない
    • VS CodeのSerial Monitorは多くのデータを受信すると詰まる(≠baudrate)

    有名なシリアルモニタはそれぞれ使いやすい機能があるのですがどれも欠点があり、自分の欲しい機能が全部揃っているものがなかったので自作しました。

    技術選定

    タイトルを見て今時Windows Formsなのかと思われたかもしれませんが、以下の理由でWindows Formsを採用しました。

    • 軽量化のため、WindowsネイティブではないElectronなどは論外
    • UWPは配布がダルい
    • WPFのシリアルモニタがいい感じだけど高速受信には耐えられなかった
      • Serial Monitor Essentialと文字列操作は同じなのでWPFのUIのAPIなりなんなりがボトルネックっぽい
      • (UIはこのソフトを参考にさせていただきました)
    • ということでダサいけど伝統のWinForms

    参考:Windows アプリの開発手段の選択肢をまとめてみた

    ソフトの全体像

    金澤ソフト設計様のサンプルコードのC#でシリアル通信を行うをガッツリ参考にして、これに手を加える形で開発しました。

    • イベントドリブンでメインループは無く、UIの操作・タイマによって関数が呼び出される
    • 各種設定はグローバル変数ではなく、UIのcheckedなどの変数に直接アクセスして管理する

    Tips

    全部真面目に書くと大変なので、参考にしたサイトをまとめておきます。

    Git

    シリアルポート

    ユーザー設定

    TextBox

    • オートスクロール有効時にTextBox、無効時にRichTextBoxと使い分ける
      • Append系のメソッドで文字列を追加した場合、TextBoxでは自動でスクロールされる仕様であるのに対しRichTextBoxではスクロールされない仕様で、無理にスクロールさせたり止めたりすると処理が重くなってしまう
      • データ受信時はTextBoxにもRichTextBoxにも文字列を追加する
      • 文字列を選択している場合などRichTextBoxにフォーカスがある時は、邪魔にならないようにdata_queueに一時保存して文字列を追加しない
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        
        StringBuilder data_queue = new System.Text.StringBuilder();
        private void RcvDataToTextBox(string data)
        {
            if (data != null)
            {
                rcvTextBoxScroll.AppendText(data);
                if (rcvTextBox.Focused)
                {
                    data_queue.Append(data);
                }
                else
                {
                    rcvTextBox.AppendText(data_queue.ToString());
                    data_queue.Clear();
                    rcvTextBox.AppendText(data);
                }
            }
        }
        
      • オートスクロール有効時はTextBoxを、無効時はRichTextBoxを表に表示する
      • オートスクロールを無効にした瞬間だけ最新のデータまでスクロールする
      • RichTextBoxで最新のデータにスクロールさせるにはScrollToCaret()を呼び出す必要があるが、rcvTextBoxがフォーカスされてしまうのでsndTextBoxにフォーカスを逃がす
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        
        private void autoScroll_CheckedChanged(object sender, EventArgs e)
        {
            if (autoScroll.Checked)
            {
                rcvTextBoxScroll.BringToFront();
            }
            else
            {
                rcvTextBox.AppendText(data_queue.ToString());
                data_queue.Clear();
                rcvTextBox.BringToFront();
                rcvTextBox.ScrollToCaret();
                sndTextBox.Focus();
            }
        }
        
    • RichTextBoxの行間をTextBoxなどと同じ行間にする
    • [VB.net/C#] TextBoxのスクロール
    • (VB.Net)RichTextBoxで最下行にスクロールする
    • Windowsフォームでコントロールの配置や重なりを調整するには?

    文字列操作

    タイムスタンプ

    • シリアルモニタのパフォーマンス改善を考える
      • DateTime.NowよりDateTime.UtcNowの方が高速
    • バイナリモードでは改行コードの区別がないため一回のポーリングで受信したデータの先頭にタイムスタンプをつける
    • テキストモードでは改行の次の行にタイムスタンプをつける
      • デフォルトで改行コードの後にタイムスタンプをつけ、受信データが改行コードで終わっている場合にはそれを削除する実装
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        
        if (rawdata.EndsWith("\r") || rawdata.EndsWith("\n"))
        {
            last_is_newline = true;
            if (check_timestamp.Checked)
            {
                new_text.Remove(new_text.Length - (timestamp.Length), (timestamp.Length));
            }
        }
        else
        {
            last_is_newline = false;
        }
        
      • 起動してから最初の行である場合first_timestamp、前回の受信が改行で終わっている場合last_is_newlineは、先頭にタイムスタンプをつける
        1
        2
        3
        4
        5
        
        if ((( first_timestamp || last_is_newline )) && check_timestamp.Checked )
        {
            new_text.Append(timestamp);
            first_timestamp = false;
        }
        

    コマンド再送機能

    • グローバルの配列previous_send_dataに送信データdataを追加
      • このdataは実際に送信したデータではなく、テキストボックスに入力されたデータ
      • first_dataは配列外参照を防ぐために使用
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        
        List<string> previous_send_data = new List<string>();
        int previous_send_data_index = 0;
        private void btnSend_Click(object sender, EventArgs e)
        {
          ~~
          //送信履歴保存
          if (string.IsNullOrEmpty(data.Replace("\r", "").Replace("\n", ""))==false)
          {
              bool first_data = previous_send_data.Count == 0;
              bool different_data = false;
              if (!first_data)
              {
                  different_data = previous_send_data[previous_send_data.Count - 1] != data;
              }
              bool update = first_data || different_data;
        
              if (update)
              {
                  previous_send_data.Add(data);
              }
          }
          previous_send_data_index = previous_send_data.Count;
        }
        
    • 上下のキー入力でprevious_send_dataのデータを参照
      • C# テキストボックス KeyPress KeyDown KeyUp 使い方
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        
        private void sndTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Up)
            {
                if (previous_send_data_index >= 1 && previous_send_data.Count > previous_send_data_index - 1 && previous_send_data.Count != 0)
                {
                    previous_send_data_index--;
                    sndTextBox.Clear();
                    sndTextBox.AppendText(previous_send_data[previous_send_data_index]);
                }
            }
            if (e.KeyCode == Keys.Down)
            {
                if (previous_send_data_index >= -1 && previous_send_data.Count > previous_send_data_index + 1)
                {
                    previous_send_data_index++;
                    sndTextBox.Clear();
                    sndTextBox.AppendText(previous_send_data[previous_send_data_index]);
                }
                else if (previous_send_data.Count == previous_send_data_index + 1)
                {
                    previous_send_data_index++;
                    sndTextBox.Clear();
                }
            }
        }
        

    ファイル保存

    メッセージボックス

    テキストボックスの受信データをコピーするときのポップアップに使っています。

    見た目

    リリース

    インストーラー作成

    GitHub Release

    プラットフォームから認証を受けるという形で品質や安全性の保証はできませんが、個人ブログのサーバーから直接ダウンロードしたバイナリを実行したい人はいないと思うので、GitHubのリリース機能を使ってインストーラーを配布しています。バージョン管理もできるので便利です。

    アイコンデザイン

    色はArduino IDETeraTermの合成でこの色、全体はDevToysやWindows Terminalなどのアイコンを参考にしました。

    GitHubのReadMEをいい感じにする

    さいごに

    GitHubからインストールできるのでぜひ使ってみてください。

    window

    私が所属している(していた)鳥人間サークルTORICAやハイブリッドロケットサークルCORE、今回のアドベントカレンダーのCanSat団体FUSiONでも開発にこのソフトを用いています。

    鳥人間やロケットの他団体の電装班の方にもご使用いただいていて、リリースしてから1年ほど経過しておりバグは潰しているはずですがもしバグを見つけた場合はGitHubのIssueやTwitterのDMでご連絡いただけると幸いです。


    8bitマイコン
    著者
    8bitマイコン
    組み込み周りで遊ぶ宇宙好き