.NET / C#で堅牢なSerialPortプログラミングを行う方法
-
22-07-2019 - |
質問
シリアルマグストライプリーダーおよびリレーボード(アクセス制御システム)と通信するためのWindowsサービスを作成しています。
別のプログラムが「中断」した後、コードが機能しなくなる(IOExceptionが表示される)問題が発生します。サービスと同じシリアルポートを開いてプロセスを実行します。
コードの一部は次のとおりです。
public partial class Service : ServiceBase
{
Thread threadDoorOpener;
public Service()
{
threadDoorOpener = new Thread(DoorOpener);
}
public void DoorOpener()
{
while (true)
{
SerialPort serialPort = new SerialPort();
Thread.Sleep(1000);
string[] ports = SerialPort.GetPortNames();
serialPort.PortName = "COM1";
serialPort.BaudRate = 9600;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.Parity = Parity.None;
if (serialPort.IsOpen) serialPort.Close();
serialPort.Open();
serialPort.DtrEnable = true;
Thread.Sleep(1000);
serialPort.Close();
}
}
public void DoStart()
{
threadDoorOpener.Start();
}
public void DoStop()
{
threadDoorOpener.Abort();
}
protected override void OnStart(string[] args)
{
DoStart();
}
protected override void OnStop()
{
DoStop();
}
}
私のサンプルプログラムはワークスレッドを正常に開始し、DTRを開いたり閉じたりして上げたりすると、Magストライプリーダーの電源が入り(1秒待機)、シャットダウン(1秒待機)などが発生します。
HyperTerminalを起動して同じCOMポートに接続すると、HyperTerminalはそのポートが現在使用中であることを通知します。ハイパーターミナルでEnterキーを繰り返し押して、再度開く 数回の再試行後に成功するポート。
これには、ワークスレッドでIOExceptionが発生するという効果がありますが、これは予想どおりです。ただし、ハイパーターミナルを閉じても、ワークスレッドで同じIOExceptionが発生します。唯一の解決策は、実際にコンピューターを再起動することです。
他のプログラム(ポートアクセスに.NETライブラリを使用していない)は、この時点では正常に動作しているようです。
これを引き起こしているものについて何か考えはありますか?
解決
@thomask
はい、ハイパーターミナルは実際にSetCommStateのDCBでfAbortOnErrorを有効にします。これは、SerialPortオブジェクトによってスローされるほとんどのIOExceptionについて説明します。一部のPC /ハンドヘルドには、デフォルトでオンになっているエラーフラグを無効にするUARTがあるため、シリアルポートの初期化ルーチンでクリアすることが必須です(Microsoftはこれを怠っています)。これをより詳細に説明するために、最近長い記事を書きました( this 興味がある場合)。
他のヒント
他の誰かのポートへの接続を閉じることはできません。次のコードは機能しません:
if (serialPort.IsOpen) serialPort.Close();
オブジェクトがポートを開かなかったため、ポートを閉じることはできません。
また、例外が発生した後でもシリアルポートを閉じて破棄する必要があります
try
{
//do serial port stuff
}
finally
{
if(serialPort != null)
{
if(serialPort.IsOpen)
{
serialPort.Close();
}
serialPort.Dispose();
}
}
プロセスを中断できるようにする場合は、ポートが開いているかどうかを確認し、一定期間戻ってからもう一度試してください。
while(serialPort.IsOpen)
{
Thread.Sleep(200);
}
アプリケーションでポートを開いたままにして、DtrEnableのオン/オフを切り替えてから、アプリケーションが閉じたらポートを閉じてみましたか?すなわち:
using (SerialPort serialPort = new SerialPort("COM1", 9600))
{
serialPort.Open();
while (true)
{
Thread.Sleep(1000);
serialPort.DtrEnable = true;
Thread.Sleep(1000);
serialPort.DtrEnable = false;
}
serialPort.Close();
}
DTRのセマンティクスに精通していないため、これが機能するかどうかはわかりません。
信頼できる非同期通信を行う方法
ブロッキングメソッドを使用しないでください。内部ヘルパークラスにはいくつかの微妙なバグがあります。
セッション状態クラス、呼び出し間で共有されるバッファとバッファカーソルを管理するインスタンス、および try ... catch で
EndRead
をラップするコールバック実装でAPMを使用しますコード>。通常の操作では、 try
ブロックが最後に行うべきことは、 BeginRead()
を呼び出して、次にオーバーラップするI / Oコールバックを設定することです。
問題が発生した場合、 catch
はrestartメソッドへのデリゲートを非同期に呼び出す必要があります。コールバックの実装は、 catch
ブロックの直後に終了する必要があります。これにより、再起動ロジックが現在のセッションを破棄し(セッション状態はほぼ確実に破損)、新しいセッションを作成できます。セッションを破棄して再作成することを防ぐため、再起動メソッドはセッション状態クラスに実装しないでください。
SerialPortオブジェクトが閉じられると(アプリケーションの終了時に発生します)、保留中のI / O操作が発生する可能性があります。その場合、SerialPortを閉じるとコールバックがトリガーされ、これらの条件下で EndRead
は一般的な通信shitfitと見分けがつかない例外をスローします。セッション状態にフラグを設定して、 catch
ブロックの再起動動作を禁止する必要があります。これにより、再起動方法が自然なシャットダウンを妨げなくなります。
このアーキテクチャは、SerialPortオブジェクトを予期せず保持しないように信頼できます。
restartメソッドは、シリアルポートオブジェクトのクローズと再オープンを管理します。 SerialPort
オブジェクトで Close()
を呼び出した後、 Thread.Sleep(5)
を呼び出して手放す機会を与えます。他の何かがポートを取得する可能性があるため、ポートを再度開くときにこれに対処する準備をしてください。
HyperTerminalはうまく機能しないという結論に達したと思います。次のテストを実行しました:
-
「コンソールモード」でサービスを開始すると、デバイスのオン/オフの切り替えが開始されます(LEDで確認できます)。
-
ハイパーターミナルを起動し、ポートに接続します。 デバイスはオンのままです(ハイパーターミナルはDTRを発生させます) サービスがイベントログに書き込み、ポートを開くことができない
-
ハイパーターミナルを停止し、タスクマネージャーを使用して適切に閉じられていることを確認します
-
デバイスはオフのままです(HyperTerminalはDTRを下げました)、アプリはポートを開けないというイベントログへの書き込みを続けます。
-
3番目のアプリケーション(共存する必要があるアプリケーション)を起動し、ポートに接続するように指示します。私はそうします。ここにエラーはありません。
-
上記のアプリケーションを停止します。
-
VOILA、私のサービスが再び作動し、ポートが正常に開き、LEDがオン/オフになります。
ワークスレッドをこのように変更してみましたが、まったく同じ結果になりました。ハイパーターミナルが「ポートのキャプチャ」に成功すると、 (スレッドがスリープしている間)、サービスはポートを再度開くことができません。
public void DoorOpener()
{
while (true)
{
SerialPort serialPort = new SerialPort();
Thread.Sleep(1000);
serialPort.PortName = "COM1";
serialPort.BaudRate = 9600;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.Parity = Parity.None;
try
{
serialPort.Open();
}
catch
{
}
if (serialPort.IsOpen)
{
serialPort.DtrEnable = true;
Thread.Sleep(1000);
serialPort.Close();
}
serialPort.Dispose();
}
}
このコードは正常に機能しているようです。 Procomm Plusを使用してポートを開いたり閉じたりするコンソールアプリケーションでローカルマシンでテストしましたが、プログラムは刻々と動作し続けます。
using (SerialPort port = new SerialPort("COM1", 9600))
{
while (true)
{
Thread.Sleep(1000);
try
{
Console.Write("Open...");
port.Open();
port.DtrEnable = true;
Thread.Sleep(1000);
port.Close();
Console.WriteLine("Close");
}
catch
{
Console.WriteLine("Error opening serial port");
}
finally
{
if (port.IsOpen)
port.Close();
}
}
}
この回答はコメントになるまでに長くなりました...
プログラムがThread.Sleep(1000)にあり、ハイパーターミナル接続を開くと、ハイパーターミナルがシリアルポートを制御すると信じています。その後、プログラムが起動してシリアルポートを開こうとすると、IOExceptionがスローされます。
メソッドを再設計し、別の方法でポートのオープンを処理しようとします。
編集: それについては、プログラムが失敗したときにコンピューターを再起動する必要があります...
おそらく、プログラムが実際に閉じられていないために、タスクマネージャーを開いて、プログラムサービスが見つかるかどうかを確認してください。アプリケーションを終了する前に、必ずすべてのスレッドを停止してください。
サービスを「所有」しないようにする正当な理由はありますか?港?組み込みのUPSサービスを見てください。たとえば、COM1にUPSが接続されていると通知すると、そのポートに別れを告げることができます。ポートを共有するための強力な運用要件がない限り、同じことを行うことをお勧めします。