ハードウェアのシリアル通信を試験するとき、汎用の端末ソフトでは手数が多い、と感じていました。同じ命令を残しておいて、送り直すことができない。秒数を指定して、同じ命令を送り続けることもできない。相手は独自のハードウェアだったので、その試験に合った機能が欲しくなりました。
そこで10年以上前に、自分に必要な最低限だけを入れた試験ツールを自作しました。C#のWindowsフォームです。
いま見ると、画面のデザインはかなり古びています。そこで、AI(Claude Code)にWPFへ置き換えてもらいました。頼んだのは「同じような機能を、WPFで」です。AIは元のソースを読んで画面を作り直し、ついでに、内部のバグも見つけて直してきました。
この記事に書くのは3つです。自分用に絞った仕様、AIへ頼んだ内容と出てきたもの、AIが直した内部のバグ。AIが書いたコードの側にも不具合が1つあったので、それも書きます。
載せているのは、置き換えた版の画面と実測です
業務で扱った機器の命令やログは載せていません。画面に出てくる VER? や STATUS? は、説明用に決めた架空の命令です。元のツールのソースコードも載せていません。
この記事の前提
- 対象読者:機器の試験用に小さなツールを自作したことがある方、これから作る方。古い自作ツールをAIに作り直させることを考えている方
- 経験時点:元のツールを作ったのは10年以上前です。置き換えは2026年11月です
- 対象範囲:自分用に絞った仕様、AIへの頼み方と結果、AIが直したログまわりのバグ、AIのコードにあったWPFの不具合
- 扱わない範囲:シリアル通信そのものの説明(ボーレート、パリティなど)。フロー制御。AIツールの使い方の手順
- 確認の範囲:機器をつながない折り返し(ループバック)で確かめています。実際のシリアルポートでは、まだ確かめていません(末尾に書きました)
置き換えた版のソースは、GitHubに置いています。
自分用の最低限だけを入れた
機器の試験では、公開されているシリアル通信ツールを使っていました。使っているうちに、かゆいところを自分で補いたくなりました。考えていたのは3つです。
- 送ったものが履歴に残り、コピーして送り直すのが楽なこと。履歴は、見やすくなるように工夫しました
- マクロのような操作が、簡単にできること
- 難しい機能が無く、送受信の試験だけを手早くできること
多機能な端末ソフトの代わりを作るつもりは、ありませんでした。試験でやることは限られています。その限られた操作を、少ない手数でできれば足ります。置き換えた版で言うと、次のとおりです。
| 試験でやること | このツールでの操作 |
|---|---|
| 同じ命令を、少し変えて送り直す | 履歴をクリックすると入力欄に戻る。入力欄では ↑↓ キーでもたどれる |
| よく使う命令を送る | 登録リストに置いておき、ダブルクリックで送る |
| 同じ命令を、一定間隔で送り続ける | 「繰り返し送信」に間隔を入れて開始する |
| 決まった順に、命令を送る | 「登録リストを順に送信」を開始する |
| 受信に応じて、決めた内容を返す | 条件の文字列と、返す内容を登録する |
| 送受信の記録を残す | ログの行を選んでコピーする。またはファイルへ保存する |
入れなかったものも、決めてあります。スクリプト、条件分岐のあるマクロ、画面のカスタマイズ、文字コードの選択です。そこまで入れると、手早く試すためのツールではなくなります。
AIに、WPFへ置き換えてもらった
AIへ渡したのは、元のツールのソースと、上の3つの考え方です。画面の細かい指定はしていません。
置き換え前と、置き換え後の画面です。
元の画面は、黒い背景のログと、原色のボタンでできています。いま見ると、かなり古びています。置き換えた版は、左が履歴、右がログ、下が送信欄です。
登録した命令、一定間隔での送信、エラー応答を受けたときの再送といった元の機能は、形を変えて残っています。出来上がったものを見て、実務で使いやすかった機能は引き継がれている、と感じました。
マクロにあたる機能は、1つのタブにまとまりました。
頼んでいないのに付いてきたものもあります。
- 登録リストを順に送信する機能。 元のツールには無かったものです。登録した命令を上から1件ずつ、間をあけて送ります。こういう機能も便利です。頼んでいない機能まで提案して作ってくるところに、AIの進化を感じました
- 自動テスト64件。 画面に依存しない処理(HEXの変換、受信の区切り、自動応答)を別のライブラリに分け、そこに50件。画面の裏側(ViewModel)を動かすものが14件
- 機器なしで試せる折り返し。 送ったものがそのまま受信として返る、仮の接続先。試験用のポートが無いPCでも、画面とマクロを動かせる
- 元のソースを使わない書き直し。 元のコードは流用せず、読んで意図を取ったうえで、新しく書いている
私がしたのは、考え方を伝えることと、出来上がったものを見て、これで進めると決めることでした。
ついでに直してもらった、内部のバグ
AIは、置き換えの前に元のソースを読んでいます。そのうえで「元の作りから変えた点」として挙げてきたのが、次の3つでした。どれもログまわりです。
当時の私は、どれにも気付いていませんでした。10年以上そのままだったものを、AIがソースを読んで見つけたことになります。これも、いまの技術の進化です。
受信が、電文の途中で割れてログに出る
元のツールは、受信の通知(SerialPort の DataReceived イベント)が来るたびに、その時点で読めた分をログへ1行足していました。
これは、通知が電文の切れ目で来る、という前提に寄りかかった作りです。実際には、通知は切れ目と関係なく来ます。.NETの資料にも、DataReceived は受信したバイトごとに発生するとは限らない、と書かれています。1件の電文が2回に分かれて届くことも、2件がまとめて届くこともあります。
置き換えた版では、届いた断片をためておき、改行コードが来たところで1件として確定します。
図の元になった記録です。確認用の小さなプログラムで、同じ受信データを2通りに表示しました。
接続先: ループバック
送信: 4 行、128 バイト(1行 32 バイト、30 ミリ秒おき)
(1) 通知が来た単位のまま、1行ずつ書いた場合
18.1 ms 16 バイト LINE-001 AAAAAAA
30.3 ms 16 バイト AAAAAAAAAAAAAA\r\n
61.4 ms 16 バイト LINE-002 BBBBBBB
77.0 ms 16 バイト BBBBBBBBBBBBBB\r\n
92.6 ms 16 バイト LINE-003 CCCCCCC
108.0 ms 16 バイト CCCCCCCCCCCCCC\r\n
123.6 ms 16 バイト LINE-004 DDDDDDD
139.2 ms 16 バイト DDDDDDDDDDDDDD\r\n
(2) 改行コード(CR+LF)で区切り直した場合
18.1 ms 30 バイト LINE-001 AAAAAAAAAAAAAAAAAAAAA
61.4 ms 30 バイト LINE-002 BBBBBBBBBBBBBBBBBBBBB
92.6 ms 30 バイト LINE-003 CCCCCCCCCCCCCCCCCCCCC
123.6 ms 30 バイト LINE-004 DDDDDDDDDDDDDDDDDDDDD
通知の回数: 8(うち、行の終わりでちょうど切れていたもの: 4)
区切り直した件数: 4(送った行数: 4)
この記録は、実機の測定ではありません
手元に試験用のシリアルポートが無かったので、機器の代わりに「1行を2つに分けて返す」折り返しで確かめました。実際のポートで、どこで、いくつに分かれるかは、通信速度や電文の長さで変わります。通知が切れ目で来るとは限らない、という点は.NETの資料の記述です。
区切り直す処理の中心は、次の十数行です。届いたバイトを1つずつためていき、末尾が改行コードになったら、そこまでを1件として取り出します。
public IReadOnlyList<Frame> Push(ReadOnlySpan<byte> chunk, DateTime now)
{
var frames = new List<Frame>();
var terminator = Terminator.ToBytes();
foreach (var b in chunk)
{
if (_pending.Count == 0)
{
_startedAt = now; // 1件の時刻は、最初のバイトが届いた時刻にする
}
_pending.Add(b);
if (terminator.Length > 0 && EndsWith(terminator))
{
frames.Add(Take(terminated: true, strip: terminator.Length));
}
else if (_pending.Count >= MaxFrameLength)
{
frames.Add(Take(terminated: false, strip: 0));
}
}
_lastReceivedAt = now;
return frames;
}
改行コードが来ないまま続きが途絶えたときは、決めた時間(既定は50ミリ秒)で打ち切って1件にし、行の末尾に「区切りなし」と添えます。待ち続けると、その行がいつまでもログに出ないためです。区切りの文字を持たないバイナリの電文は、この時間だけで区切ります。
HEXの表示から、元のバイト列を読み戻せない
元のツールは、受信した1バイトずつを16進にして、文字列の前へ足していく作りでした。桁をそろえる指定も付けていません。4バイトの 01 02 0A FF で確かめると、こうなります。
1桁を詰めず、前に足していく書き方 : ffa21
2桁にそろえ、届いた順に並べる書き方: 01 02 0A FF
上の書き方では、届いた順と逆になり、0A が a の1文字になります。ffa21 から元の4バイトを読み戻すことはできません。置き換えた版は、必ず2桁、届いた順、空白区切りです。
ログの時刻に、月と日が無い
元のツールの書式は、年のあとに時・分・秒を並べたものでした。月と日が無く、時は12時間表記です。2つの時刻で確かめました。
2026年11月11日 15時04分05秒 → 2026:03:04:05
2026年11月12日 03時04分05秒 → 2026:03:04:05
別の日の、午後と午前が、同じ文字列になります。置き換えた版は、画面ではミリ秒までの時刻を出し、ファイルへ保存するときは年月日を含めて 2026-11-11 15:04:05.678 の形にしています。
バグではないが、変わったところ
バグとは別に、作りが変わったところが2つあります。
送信と受信を、1つの時系列に並べる。 元のツールは、送信ログと受信ログを別の欄に分けていました。置き換えた版は1つに並べます。「この送信に、この応答が返った」を、行の順番で読めます。文字列とHEXの表示は後から切り替えられ、切り替えると既にあるログも書き換わります。
自動応答を、自分で登録できる。 元のツールの自動の動作は、受信に NG か ERR が含まれていたら送信欄の内容を送り直す、という固定のものでした。置き換えた版では「受信にこの文字列が含まれていたら、これを送る」という決まりを登録します。
自由に登録できると、止まらなくなる組み合わせが作れます。「PING を含んでいたら PING を送る」という決まりで、相手が受け取ったものをそのまま返す場合です。そこでAIは、自動応答が1秒に20回を超えたら自動応答そのものを止め、ログに理由を残す歯止めを入れていました。この組み合わせはテストにも入っていて、最初の1回を含めて21〜22回送ったところで止まります。
AIが書いたコードにも、不具合が1つあった
直してもらう話ばかりではありません。置き換えの途中で、AIが書いたアプリ自身が、ときどき異常終了しました。
見つけたのもAIです。説明用の画面の画像を作る操作を繰り返していて気付き、Windowsのイベントログから原因をたどっていました。AIが数えた結果では、同じ操作30回のうち4回です。毎回ではないので、人が数回触っただけでは気付きにくい種類の不具合でした。
イベントログに残っていた例外は、次の内容です。
System.InvalidOperationException: ItemsControl が項目のソースと一致していません。
(中略)
累計カウント 6 が実際のカウント 7 と異なります。
(中略)
at System.Windows.Controls.ItemContainerGenerator.Verify()
at System.Windows.Controls.VirtualizingStackPanel.MeasureChild(...)
at System.Windows.ContextLayoutManager.UpdateLayout()
at System.Windows.Controls.ItemsControl.OnBringItemIntoView(ItemInfo info)
at System.Windows.Controls.ListBox.ScrollIntoView(Object item)
一覧(ListBox)が数えていた行数は6、実際の行数は7、という食い違いです。原因は、ログに行が増えたら最新の行まで送る、という次の処理でした。
// 直す前:ログのコレクションが「1行増えた」と知らせてきた、その場で送る
private void Log_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add && Vm.AutoScroll && LogList.Items.Count > 0)
{
LogList.ScrollIntoView(LogList.Items[^1]);
}
}
「1行増えた」という知らせは、登録した順に、複数の受け手へ届きます。受け手は、この処理と、一覧の2つです。届く順番を調べると、この処理が先、一覧が後でした。
一覧がまだ7行目を知らないうちに、7行目まで送れと頼んでいたことになります。それでも多くの場合は何も起きず、画面を計算し直す途中で食い違いが検出されたときだけ落ちていました。どういうときに検出されるのかまでは、突き止めていません。
直し方は、知らせの中では送らず、知らせがすべて届き終わってから送ることです。
// 直した後:知らせが終わってから送る。続けて何行も来たときは1回にまとめる
private void Log_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Add || !Vm.AutoScroll || _scrollPending)
{
return;
}
_scrollPending = true;
Dispatcher.BeginInvoke(DispatcherPriority.Background, () =>
{
_scrollPending = false;
if (Vm.AutoScroll && LogList.Items.Count > 0)
{
LogList.ScrollIntoView(LogList.Items[^1]);
}
});
}
直したあとは、同じ操作30回で異常終了0回でした。
この不具合は、シリアル通信に固有のものではありません。行が増えるたびに最新の行まで送る作りなら、どのアプリでも起こり得ます。このツールはログを自動では保存しないので、落ちると画面のログを失います。
確認した範囲と、まだ確かめていないこと
- 自動テスト64件が通ること。繰り返し送信、順に送信、自動応答とその歯止めも、折り返しにつないで実際に動かしています
- 画面は、3つのタブの表示と、通常の起動・終了・設定の保存と復元
- 実際のシリアルポートにつないだ送受信は、まだ確かめていません。 置き換えに使ったPCにあるのが、Bluetooth用のポートだけだったためです。AIが直したと言っている内容も、実機での確認はこれからです
- 不具合の「30回中4回」は、説明用の画面を作って終了する操作を繰り返した回数です。手で操作したときの頻度ではありません
- フロー制御と、DTR・RTSの操作は入っていません
確認環境は次のとおりです。
| 項目 | 版 |
|---|---|
| OS | Windows 11 |
| .NET SDK | 10.0.402(net8.0-windows 向けにビルド) |
| 実行環境 | .NET 8.0.32(Windows Desktop) |
置き換えを頼んで分かったこと
- 汎用のソフトで手数が多いなら、自分に必要な最低限だけで作る手がある。入れないものも先に決める
- AIへ渡したのは、元のソースと、3つの考え方。画面の細かい指定はしなくても、機能は引き継がれた
- AIは元のソースを読んで、当時の私が気付いていなかった内部のバグ(受信が割れる、HEXを読み戻せない、時刻に月日が無い)も直してきた。頼んでいない機能も足してきた
- AIが書いたコードにも不具合はあった。数回触るだけでは出ないものは、繰り返し動かして数える
- 実機での確認は、まだ残っている


