WinFormsでシリアルモニタ(Serial Monitor Essential)を自作しました。ソフトの使い方についてGitHubのReadMEに、ソフトの作り方のポイントについてこのブログにまとめてあります。
新しく書く体力は無かったので下書きで眠っていた記事を公開します。
人工衛星やCanSatには関係ないですが電子工作関連です。
作ったもの
いい感じの(※1)シリアルモニタが無くて(※2)キレたので自作しました
— 8bitマイコン (@771_8bit) December 28, 2022
↓でインストールできますhttps://t.co/z0Cqbk35lh
再接続 | 送信機能 | データレート | |
---|---|---|---|
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
- Gitを使ってVisual Studioのプロジェクトをバージョン管理する
- .gitignoreがいい感じになるので、手動ではなくこれで設定すべき
シリアルポート
- SerialPort クラス
- 受信のタイミング
- シリアルポートのデータ受信により発火するイベントを実装することもできるが、これでは高速にデータが来ると耐えられない
- 20msごとにポーリングを行い、
timerReadSerial_Tick()
でバッファを読む - タイマにより一定時間間隔で処理を行うには?(Windowsタイマ編)
- 自動接続は200msごとに
timerReconnect_Tick()
で再接続を試行して実装 - DtrとRtsに注意
ユーザー設定
- Visual Studioでアプリケーションの設定を保存する
- バージョンアップ後の設定の引き継ぎ(リンク切れ)
1 2 3 4 5
if (Properties.Settings.Default.setting_first) { Properties.Settings.Default.Upgrade(); Properties.Settings.Default.setting_first = false; }
- Control.Enabled プロパティ
- バイナリモードのときに改行コード関連のオプションを無効にするときに使う
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フォームでコントロールの配置や重なりを調整するには?
文字列操作
- 文字列を置換する
- 改行コード処理
- 先にNULL文字を置換して
\0
を改行コードの置換に使用 - 改行はタイムスタンプも含めた
newline
で置換1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
if (checkNULL.Checked) { new_text.Replace("\0", "[NULL]"); } else { new_text.Replace("\0", ""); } if (checkCRLF.Checked) { new_text.Replace("\r\n", "[CRLF]\0"); new_text.Replace("\r", "[CR]\0"); new_text.Replace("\n", "[LF]\0"); new_text.Replace("\0", newline); } else { new_text.Replace("\r\n", "\n"); new_text.Replace("\r", "\n"); new_text.Replace("\n", newline); }
- 先にNULL文字を置換して
- バイナリモード
タイムスタンプ
- シリアルモニタのパフォーマンス改善を考える
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(); } } }
- C# テキストボックス KeyPress KeyDown KeyUp 使い方
ファイル保存
メッセージボックス
テキストボックスの受信データをコピーするときのポップアップに使っています。
見た目
- UI/UX
- Auto ScrollとShow Timestampの配置はArduino IDEに合わせる
- 送信文字列は入力ミスを防ぐためフォントサイズを大きくする
- 受信データの消去ボタンは中途半端な位置ではなく一番右下に配置し、CLEARと全て大文字で表記
- 高DPI対応
- とくにTextBoxのバグは要注意
- 等幅フォント対応
リリース
インストーラー作成
GitHub Release
プラットフォームから認証を受けるという形で品質や安全性の保証はできませんが、個人ブログのサーバーから直接ダウンロードしたバイナリを実行したい人はいないと思うので、GitHubのリリース機能を使ってインストーラーを配布しています。バージョン管理もできるので便利です。
アイコンデザイン
色はArduino IDEとTeraTermの合成でこの色、全体はDevToysやWindows Terminalなどのアイコンを参考にしました。
GitHubのReadMEをいい感じにする
さいごに
GitHubからインストールできるのでぜひ使ってみてください。
私が所属している(していた)鳥人間サークルTORICAやハイブリッドロケットサークルCORE、今回のアドベントカレンダーのCanSat団体FUSiONでも開発にこのソフトを用いています。
鳥人間やロケットの他団体の電装班の方にもご使用いただいていて、リリースしてから1年ほど経過しておりバグは潰しているはずですがもしバグを見つけた場合はGitHubのIssueやTwitterのDMでご連絡いただけると幸いです。