Windows Formsにおけるシリアル通信の極意:計測器連携を「止まらない」システムにする設計術
現場のエンジニア諸君。計測器やバーコードリーダーをVB.NETで制御する際、最も多い失敗は何か知っているか?
「UIスレッドで同期的に受信待ちをして、画面がフリーズする」こと。そして「ケーブルが抜けた瞬間に例外を吐いてアプリが落ちる」ことだ。これらはプロの仕事ではない。
製造現場の過酷な環境下でも、計測データを取りこぼさず、切断されても静かに復旧する。そんな「堅牢な通信基盤」の作り方を伝授する。
—
1. なぜ「同期通信」は死を招くのか
`SerialPort.ReadLine()` をメインUIスレッドで実行してはいけない。これを行うと、デバイスからの応答がない限り、OSはUIの描画処理を停止させる。ユーザーから見れば「ただのフリーズ」だ。
我々が目指すべきは「イベント駆動型の非同期アーキテクチャ」である。`DataReceived` イベントを使い、受信バッファの処理をバックグラウンドスレッドに完全に委ねる。これが第一歩だ。
—
2. 堅牢なシリアル通信クラスの実装例
単に動くだけのコードではなく、切断検知と再接続のセーフティネットを備えた設計を紹介する。
Imports System.IO.Ports
Imports System.Threading
Public Class SerialManager
Private WithEvents _serialPort As SerialPort
Private _isClosing As Boolean = False
‘ 受信データを受け渡すためのイベント
Public Event DataReceivedEvent(data As String)
Public Event ConnectionStatusChanged(isConnected As Boolean)
Public Sub New(portName As String, baudRate As Integer)
_serialPort = New SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
‘ タイムアウトは非常に重要。無限待ちを避ける
_serialPort.ReadTimeout = 2000
_serialPort.WriteTimeout = 2000
End Sub
Public Sub Open()
Try
If Not _serialPort.IsOpen Then
_serialPort.Open()
RaiseEvent ConnectionStatusChanged(True)
End If
Catch ex As Exception
‘ ログ出力して再試行ロジックへ
RaiseEvent ConnectionStatusChanged(False)
End Try
End Sub
‘ シリアル通信のイベントハンドラ(別スレッドで動作する)
Private Sub _serialPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles _serialPort.DataReceived
If _isClosing Then Return
Try
‘ 読み込みは同期だが、別スレッドなのでUIは固まらない
Dim data As String = _serialPort.ReadLine()
RaiseEvent DataReceivedEvent(data)
Catch ex As Exception
‘ ここが重要:通信切断の検知
HandleCommunicationError()
End Try
End Sub
Private Sub HandleCommunicationError()
‘ 切断時はポートをリセットし、再接続を試みるロジックをここに組む
_serialPort.Close()
RaiseEvent ConnectionStatusChanged(False)
‘ 必要に応じてタイマーで再接続処理を起動する
End Sub
End Class
—
3. 実務における「落とし穴」と対策
タイムアウトとバッファの呪縛
シリアル通信において、`ReadTimeout` を設定しないのは自殺行為だ。相手側の機器がハングアップした場合、アプリは永遠に待ち続けることになる。また、`DataReceived` イベントは受信した瞬間に発火するとは限らない。大きなパケットが分割されて届くこともある。`ReadLine` を使うなら、必ず改行コード(CR/LF)が相手から送出されていることを確約させろ。
データベース連携の鉄則:スレッドセーフを忘れるな
受信データは別スレッドで発生する。そのデータをそのままテキストボックスに表示したり、データベースに書き込もうとすると、「スレッド間操作の例外」が発生する。
必ず `Control.Invoke` を使うこと。
‘ UI更新時の鉄則
Private Sub OnDataReceived(data As String) Handles Me.DataReceivedEvent
If Me.InvokeRequired Then
Me.Invoke(Sub() UpdateUI(data))
Else
UpdateUI(data)
End If
End Sub
ファイル出力の最適化
計測値をログファイルに書き込む際、毎回 `File.Open` / `File.Close` を繰り返すのはパフォーマンスの無駄だ。`StreamWriter` をインスタンス変数として保持し、アプリケーション終了時に `Flush` して閉じるのがベストプラクティスである。
—
4. リーダーからのアドバイス
「動けばいい」というコードは、現場では「一番困るコード」に化ける。
- 切断検知: `SerialPort` がオープン状態でも、物理的にケーブルが抜ければ例外が飛ぶ。`Try-Catch` で囲むのは基本中の基本だ。
- 保守性: 通信ロジックとUIロジックは明確に分けろ。`SerialManager` のようなクラスに切り出すことで、将来的に通信方式が USB-HID や LAN(Socket) に変わっても、UI側を書き直さずに済む。
優れたエンジニアは、正常系ではなく「異常系」を設計する。あなたの書いたシステムが、現場で誰にも気づかれず、ただ静かにデータを記録し続ける影の功労者となることを期待している。
健闘を祈る。
