web-dev-qa-db-ja.com

.NET / C#で堅牢なシリアルポートプログラミングを行う方法

シリアルマグストライプリーダーとリレーボード(アクセス制御システム)との通信用の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秒待機)などします。

ハイパーターミナルを起動して同じCOMポートに接続すると、ハイパーターミナルはポートが現在使用中であると通知します。ハイパーターミナルでEnterキーを繰り返し押すと、ポートを再度開こうとすると、数回の再試行後に成功します。

これには、ワークスレッドでIOExceptionsが発生するという影響があります。ただし、ハイパーターミナルを閉じても、ワークスレッドで同じIOExceptionが発生します。唯一の解決策は、実際にはコンピュータを再起動することです。

他のプログラム(ポートアクセスに.NETライブラリを使用していない)は、この時点では正常に動作しているようです。

これを引き起こしているものに関するアイデアはありますか?

26
Thomas Kjørnes

@thomask

はい、ハイパーターミナルは実際にはSetCommStateのDCBでfAbortOnErrorを有効にします。これは、SerialPortオブジェクトによってスローされるほとんどのIOExceptionについて説明します。一部のPC /ハンドヘルドには、デフォルトでオンになっているエラーフラグの中止を備えたUARTがあります。そのため、シリアルポートの初期化ルーチンがそれをクリアする必要があります(Microsoftはこれを無視しました)。私はこれをより詳細に説明するために最近長い記事を書きました(興味があれば this を参照してください)。

24
Zach Saw

他の誰かのポートへの接続を閉じることはできません。次のコードは機能しません。

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);
}
4
trampster

アプリケーションでポートを開いたままにし、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のセマンティクスに詳しくないので、これが機能するかどうかはわかりません。

2
FryGuy

信頼できる非同期通信を行う方法

ブロッキングメソッドを使用しないでください。内部ヘルパークラスにはいくつかの微妙なバグがあります。

APMをセッション状態クラスで使用します。そのインスタンスは、呼び出し間で共有されるバッファーとバッファーカーソルを管理し、EndReadを_try...catch_でラップするコールバック実装です。通常の操作では、tryブロックが最後に行うべきことは、BeginRead()を呼び出して、次の重複したI/Oコールバックを設定することです。

うまくいかない場合、catchは、restartメソッドへのデリゲートを非同期に呼び出す必要があります。コールバックの実装はcatchブロックの直後に終了する必要があります。これにより、再起動ロジックが現在のセッションを破壊し(セッション状態はほぼ確実に破損します)、新しいセッションを作成できます。再起動メソッドはnotをセッション状態クラスに実装する必要があります。これにより、セッションが破棄および再作成されなくなるためです。

SerialPortオブジェクトが閉じているとき(アプリケーションの終了時に発生します)は、保留中のI/O操作がある可能性があります。その場合、SerialPortを閉じるとコールバックがトリガーされ、これらの条件下ではEndReadは一般的な通信のシットフィットと区別がつかない例外をスローします。 catchブロックの再起動動作を禁止するには、セッション状態にフラグを設定する必要があります。これにより、再起動メソッドが自然なシャットダウンを妨害するのを防ぎます。

このアーキテクチャは、SerialPortオブジェクトを予期せずに保持しないように頼ることができます。

Restartメソッドは、シリアルポートオブジェクトのクローズと再オープンを管理します。 SerialPortオブジェクトでClose()を呼び出した後、Thread.Sleep(5)を呼び出して、手放す機会を与えます。何か他のものがポートをつかむ可能性があるので、ポートを再び開くときにこれに対処する準備をしてください。

2
Peter Wone

私はこのようにワークスレッドを変更しようとしましたが、まったく同じ結果が得られました。ハイパーターミナルが「ポートのキャプチャ」に成功すると(スレッドがスリープしている間)、サービスでポートを再び開くことができなくなります。

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();
    }
}
1
Thomas Kjørnes

このコードは正常に動作するようです。私はローカルアプリケーションのコンソールアプリケーションで、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();
            }
        }
    }
1
FryGuy

ハイパーターミナルがうまく機能しないという結論に達したと思います。次のテストを実行しました。

  1. 「コンソールモード」でサービスを開始すると、デバイスのオン/オフの切り替えが開始されます(LEDでわかります)。

  2. ハイパーターミナルを起動し、ポートに接続します。デバイスはオンのままです(ハイパーターミナルはDTRを発生させます)私のサービスはイベントログに書き込み、ポートを開けません

  3. ハイパーターミナルを停止します。タスクマネージャを使用して適切に閉じられていることを確認します

  4. デバイスがオフのまま(ハイパーターミナルがDTRを下げた)、アプリがポートを開くことができないと言って、イベントログへの書き込みを続けます。

  5. 3番目のアプリケーション(共存させる必要のあるアプリケーション)を起動し、ポートに接続するように指示します。私はそうします。ここにはエラーはありません。

  6. 上記のアプリケーションを停止します。

  7. VOILA、私のサービスが再び開始され、ポートが正常に開き、LEDがオン/オフになります。

1
Thomas Kjørnes

サービスがポートを「所有」しないようにする正当な理由はありますか?組み込みのUPSサービスを見てください。たとえば、COM1にUPSが接続されていると伝えたら、そのポートに別れを告げることができます。ポートを共有するという強い運用上の要件がない限り、同じことを行うことをお勧めします。

0
Coderer

この回答はコメントになるまで長くなりました...

プログラムがThread.Sleep(1000)にあり、ハイパーターミナル接続を開くと、ハイパーターミナルがシリアルポートを制御するようになると思います。プログラムが起動し、シリアルポートを開こうとすると、IOExceptionがスローされます。

メソッドを再設計し、ポートの開放を別の方法で処理してください。

編集:プログラムが失敗したときにコンピュータを再起動する必要があることについて...

それはおそらく、プログラムが実際に閉じられていないためです。タスクマネージャーを開いて、プログラムサービスが見つかるかどうかを確認してください。アプリケーションを終了する前に、必ずすべてのスレッドを停止してください。

0
Anders R