【実務・中級編】Windows Formsにおけるシリアル通信(RS-232C / USB)の非同期制御:計測器やバーコードリーダー連携でタイムアウトを防ぐ堅牢な設計 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

シリアル通信の「死」を回避せよ:VB.NETにおける非同期計測データ取得の極意

多くのVB.NET開発者が、`SerialPort`コンポーネントを甘く見て、UIスレッドを凍結させ、あるいは不安定なデータ欠損に頭を抱える。計測器やバーコードリーダーとの連携において、「イベントが発生したから読みに行く」という受動的な設計は、現場の過酷なノイズ環境では脆すぎる。

今日は、私がこれまで数々のFA(ファクトリーオートメーション)現場で培ってきた、「止まらない、見失わない、フリーズさせない」ためのシリアル通信アーキテクチャを伝授する。

—

1. なぜ「イベントハンドラ内での処理」が失敗するのか

初心者は `DataReceived` イベント内で直接データをパースしようとする。これが大罪だ。
このイベントは別スレッドで走る。ここで重いDB保存やファイル書き込みを行えば、確実にデッドロックかUIの応答停止を招く。また、シリアル通信特有の「データの断片化(数ミリ秒の遅延でデータが分割されて届く現象)」を考慮していないコードは、現場では使い物にならない。

鉄則:受信は「バッファリング」に徹し、パースと処理は別スレッド(あるいはタイマー)に分離せよ。

—

2. 実装の要諦:堅牢な非同期アーキテクチャ

以下のコードは、スレッドセーフを担保しつつ、データの断片化を吸収する「Producer-Consumerパターン」の簡易実装である。

Imports System.IO.Ports
Imports System.Collections.Concurrent
Imports System.Threading

Public Class SerialManager
‘ スレッドセーフなキューで受信データをバッファリング
Private ReadOnly _dataQueue As New ConcurrentQueue(Of String)
Private WithEvents _serialPort As SerialPort

Public Sub Initialize(portName As String)
_serialPort = New SerialPort(portName, 9600, Parity.None, 8, StopBits.One)
_serialPort.ReadTimeout = 500
_serialPort.Open()
End Sub

‘ 受信イベント:ここでは溜めるだけ
Private Sub _serialPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles _serialPort.DataReceived
Try
‘ 読み取り可能なバイト分を即座に回収
Dim data As String = _serialPort.ReadExisting()
_dataQueue.Enqueue(data)
Catch ex As Exception
‘ ログ出力のみに留める。ここでUIを触ってはいけない
Debug.WriteLine($”Error: {ex.Message}”)
End Try
End Sub

‘ 別スレッドで定期的にバッファを監視し、パースを行うメソッド
Public Sub ProcessData()
Dim rawBuffer As String = “”
While True
If _dataQueue.TryDequeue(Dim segment) Then
rawBuffer &= segment

‘ 改行コード(CR/LF)を区切り文字としてパース
If rawBuffer.Contains(vbCrLf) Then
Dim packets = rawBuffer.Split(New String() {vbCrLf}, StringSplitOptions.None)
‘ 最後が途切れている可能性があるため、最後尾をバッファに残す
rawBuffer = packets.Last()

For i As Integer = 0 To packets.Length – 2
ProcessPacket(packets(i))
Next
End If
End If
Thread.Sleep(10) ‘ CPU占有率を抑えるための微小待機
End While
End Sub

Private Sub ProcessPacket(packet As String)
‘ ここでデータベース保存やUI更新(Invoke経由)を行う
Console.WriteLine($”Validated Data: {packet}”)
End Sub
End Class

—

3. 実務で「詰む」ポイントと回避策

① ファイル・DB連携のボトルネック

`ProcessPacket` 内で直接データベースへのINSERT文を叩いていないか?
計測器の転送レートが高い場合、DBのI/O待ちが通信バッファを溢れさせる。DB書き込みは必ず非同期(Task.Run)またはバルクインサートで行え。

② シリアルポートの「お行儀」

アプリケーション終了時に `_serialPort.Close()` を呼び出さないと、OS側でポートが掴まれたままになり、再起動しないと復旧しない事態になる。
必ず `FormClosing` イベントで適切にクローズし、スレッドをAbort(あるいはキャンセル)する設計を徹底すること。

③ タイムアウトとハンドシェイク

計測器側が応答を返さない場合、`ReadTimeout` が発生する。これをキャッチして「再接続ロジック(一度ポートを閉じて、ウェイトを置いてから開く)」を実装しておくのがプロの仕事だ。

—

4. 最後に:エンジニアとしての矜持

「とりあえず動いた」コードは、ラボ環境では機能しても、工場のノイズや長期間の連続稼働では必ず破綻する。
シリアル通信は原始的だが、だからこそ「予期せぬ欠損」を前提とした防御的プログラミングが求められる。

  • 断片化を許容するバッファリング
  • UIと通信の明確なスレッド分離
  • リソースの確実な解放

この3つを骨の髄まで叩き込めば、君が作るツールは現場で「壊れないツール」として絶大な信頼を得るはずだ。
次のリリースでは、まずはログ出力の実装から見直してみるといい。健闘を祈る。

タイトルとURLをコピーしました