シリアル通信アプリ開発の落とし穴

· · シリアル通信, RS-232, C#, .NET, Windows開発, 装置連携

まず結論

シリアル通信アプリで本当に難しいのは、送受信 API そのものではありません。難しいのは境界、タイムアウト、状態遷移、再接続、観測可能性です。

押さえたいポイント:

  • シリアル通信は順序付き byte streamであって、メッセージ境界は勝手には付かない
  • Read(100) したからといって 100 byte ぴったり返るとは限らない
  • .NET の DataReceived は受信 byte ごとに発火するとは限らず、UI スレッドでもない
  • タイムアウトは1個では足りない。open、inter-byte、response、reconnect で分ける
  • 送信はどこからでも Write できるようにするより、single writer に寄せる

シリアル通信は「メッセージ」ではなく「順序付き byte stream」

アプリ側からは「コマンドを1つ送り、応答を1つ受ける」ように見えても、下の層では byte 列が流れているだけです。1回 Write した内容が相手側で2回に分かれて届いたり、他のデータと連結して届いたりします。

よくある思い込み 実際
Read(16) なら 16 byte 返る 到着状況次第で途中までしか取れない
DataReceived = 1メッセージ到着 byte ごと保証されず、UI スレッドでもない
Write が返った = 相手が処理完了 送信側バッファに積めただけ
COM 一覧 = 現在の接続状態 列挙順は不定、結果が古いことも

このため、メッセージ境界をプロトコルとして自分で定義する必要があります(固定長、区切り文字、長さ+payload+checksum など)。

最初に決めるべきこと

  1. フレーム境界 - どの byte 列を1メッセージと見なすか
  2. テキストかバイナリか - 両方混ざる場合は境界ルールを明示
  3. タイムアウトの意味 - open, inter-byte, response, reconnect で分ける
  4. フロー制御とライン状態 - BaudRate, DataBits, Parity, StopBits, Handshake, DTR/RTS
  5. 責務分離 - 読む人、書く人、パースする人、業務状態へ反映する人を分ける
  6. 状態遷移 - Closed, Opening, Ready, WaitingResponse, Fault, Reconnecting
  7. ログと調査性 - open/close 時刻、ポート設定、送受信 hex dump、エラー情報

よくある落とし穴

1. 「1回の Read = 1メッセージ」だと思う

最も多いミス。受信はまずバッファに蓄積し、そこから parser がフレームを切り出す形に分ける。

2. DataReceived をそのまま業務イベントにする

DataReceived は「何か来たらしい」の通知と割り切り、ハンドラ内では重い処理をしない。UI 更新は必ず UI スレッドに戻す。

3. どこからでも Write してよいと思う

UI ボタン、監視タイマー、再接続処理がそれぞれ直接 Write する構成は崩れやすい。single writer に寄せる。

4. ReadLine()/WriteLine() で全部通す

行ベースのテキストプロトコルのときだけ便利。NewLine 不一致、ペイロード中の改行、バイナリ混在があると境界が壊れる。

5. タイムアウトを設計せず既定のままにする

同期 read の無限待ち、1個の timeout ですべてを表現しようとする、UI スレッドで同期 read する、は詰まりやすい。

6. RTS/CTS、DTR/RTS を軽く見る

ハンドシェイクや制御線の設定不一致で、送信がたまに止まる、一定量を超えると取りこぼす、といった症状が出る。

7. Open() のやり直しだけで再接続した気になる

特に USB-シリアルでは、session 無効化、pending request fail、reader/writer 停止、backoff 後の reopen、装置初期化の再実行まで含めて扱う。

8. COM ポート列挙を真実だと思う

一覧に出たことと open できることは別。前回の COM7 を盲信したり、先頭を自動選択したりしない。

ベストプラクティス

責務を分けるのが最も効きます:

  • reader: port から byte 列を読むだけ
  • writer: outbound queue から順番に書くだけ
  • parser: byte 列から frame を切り出すだけ
  • protocol: request と response の対応、checksum を扱う
  • app state: 業務状態を更新するだけ

受信は buffer に蓄積してから parser が frame を切り出す。送信は single writer に集約する。再接続は単なる reopen ではなく session 再生成として扱う。生ログ(raw hex dump)と要約ログの両方を持つ。

チェックリスト

  • メッセージ境界は明文化されているか
  • 受信は byte 蓄積 → frame 切り出しになっているか
  • DataReceived をメッセージ到着扱いしていないか
  • UI スレッドで同期 I/O していないか
  • 送信は single writer か
  • timeout は意味ごとに分かれているか
  • Handshake / DTR / RTS が明示されているか
  • reconnect で session を作り直しているか
  • raw hex dump を残しているか
  • 実機の抜き差しや途中切断を試験しているか

まとめ

  • シリアル通信はメッセージではなく byte stream
  • Read 単位とメッセージ単位は一致しない
  • 境界はプロトコルとして自分で定義する
  • 送受信は責務を分離し、送信は single writer に寄せる
  • timeout は意味ごとに分割、再接続は session 単位で設計
  • raw hex dump を含むログが後の調査をかなり楽にする

シリアル通信アプリでは、ポートを開けることより、byte 列をどう解釈し、時間と状態をどう制御するかのほうがずっと大事です。

参考資料

関連する記事

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

FileSystemWatcherの安全な使い方

FileSystemWatcher のイベントは完了通知ではないという前提に立ち、取りこぼし、重複通知、完了判定の落とし穴を整理し、再スキャン要求への畳み込み、原子的 claim、idempotency までの設計指針をまとめます。

記事を読む

ClickOnce 入門:配布・更新・選定基準

ClickOnce の仕組みと向き不向きを実務目線で整理します。マニフェスト、自動更新、キャッシュ分離、署名、配布経路の注意点に加え、社内業務アプリで強い理由と MSI/MSIX が適するケースの見分け方が分かります。

記事を読む

関連トピック

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

このテーマがつながるサービス

この記事は次のサービスページにつながります。近い入口からご覧ください。

ブログ一覧に戻る