C#

C#で医療用検査機器とシリアル通信 – パート2

さて、前回で流れは大まかにつかめましたが、実際に手をつけるとなると他にも気をつけなければならないこともあります。

通信処理であれば当たり前と言えるでしょうが、必ずしも1度の受信で1レコードが完成するわけではないという点です。
長い電文になると、1度で受けきれずに数回にの受信で1レコード分が完成することもあるため、それを念頭に入れておかなければなりません。
これは、RS-232Cのデバイスにも関係するかと思われます。試しにシリアルをUSBに変換するケーブルで試すと、メチャメチャ細かく別れて受信していました。1レコードが完成するまでに何回もデータを受ける必要があります。

そのため、レコードの終わりである[CR・LF]の文字列を受信するでは、じっと我慢の子です。それまでに受信した文字列はどこかにに別途保持しておきましょう。
[CR・LF]を見たら、[ACK]を返して次のレコードを受信します。
基本的にデータの受診時はこの繰り返しとなります。

他にも、非同期で通信処理をしないと色々と不都合が出てきますし、ASTM1381の決まりでASCII以外の文字は使用できません。

大変そうではありますが、早速コードを書いていきましょう。
※長くなるので「結果エクスポート」などの処理はここでは省略します。

とりあえず、以下を定義しておきます。

// 主要な制御コード
// 扱いづらいので分かりやすく
static string STX = char.ConvertFromUtf32(2); // 電文のテキストの開始
static string ETX = char.ConvertFromUtf32(3); // 電文のテキストの終了
static string EOT = char.ConvertFromUtf32(4); // 伝送が終了
static string ENQ = char.ConvertFromUtf32(5); // 伝送の開始
static string ACK = char.ConvertFromUtf32(6); // 応答(OK)
static string LF = char.ConvertFromUtf32(10); // 改行コード 伝文の終わり
static string CR = char.ConvertFromUtf32(13); // 改行コード 伝文の終わり
static string NAK = char.ConvertFromUtf32(21); // 否定(NG)
static string ETB = char.ConvertFromUtf32(23); // 伝送ブロックが終了

// シリアルポート
static SerialPort Port;
// 医療機器からの応答待ちなどで活用
static EventWaitHandle Wh;
// 試験オーダー要求の受信フラグ <LF>を受信するまでは true
static bool OrderFlg = false;

// 現在処理中(受信中)のサンプルID(バーコード)
static string NowSampleID = null;
// 電文内に存在した全サンプルID(バーコードのリスト)
static List<string> SampleIDList = null;
// 直前の電文のサンプルID(直前の検体識別コード)
// 結果データの登録時に利用する
static string BeforeSampleID = null;
// 検査結果の報告の種類 "Oレコード" の結果パラメータ
static string ResultReportState = null;
// 直前に送信したデータ(メッセージ) NAKが帰ってきた時に再送する
static string SendData = null;
// ETBでの受診時にデータ(メッセージ)
static string EtbData = null;
// 受信データがバラけてしまった時の退避用
static string ApartData = null;
// NAKを連続で受け取ったカウント
static Int32 NakCnt = 0;
// メッセージ送信スレッド
static Thread Thread;       
// スレッド用のフラグ(終了の制御)
static bool ThreadRun;

「コンストラクタ」や「フォーム読込時」などで、

SP = new SerialPort();
// データ受信イベントのデリゲート
SP.DataReceived += new SerialDataReceivedEventHandler(SP_DataReceived);

ボタンクリックでシリアル接続といった感じで呼んで下さい。

private void Connect()
{
    // 初期化
    OrderFlg = false;
    SendData = null;
    EtbData = null;
    WriteLogFlg = true;

    SP.PortName = "COM3";
    // ボーレートをコンボボックスから取り出す.
    SP.BaudRate = 9600;
    // データビットをセットする. (データビット = 8ビット)
    SP.DataBits = 8;
    // パリティビットをセットする. (パリティビット = なし)
    SP.Parity = Parity.None;
    // ストップビットをセットする. (ストップビット = 1ビット)
    SP.StopBits = StopBits.One;
    // フロー制御
    SP.Handshake = Handshake.None;
    // 文字コードをセットする.
    SP.Encoding = Encoding.ASCII;
  
    try
    {
        // シリアルポートをオープンする.
        SP.Open();
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

データ受診時の処理です。受信するたびに受け取ったデータの検証を行い、試験オーダー要求であれば検体ID(バーコード)を保持しておきます。

private void SP_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    string msg = SP.ReadExisting();

    if (msg == null || msg.Length == 0)
    {
        return;
    }

    // =======================================
    // 1.受信したデータの評価を行う
    // =======================================
    if (msg == ENQ || msg == EOT || msg == ACK || msg == NAK)
    {
        // パラメータであるかを確認する
        if(msg == NAK)
        {
            NakCnt++;
            if (NakCnt >= 2)
            {
                NakCnt = 0;
                MessageBox.Show("否定応答を連続で受信しました。", "データ受信", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                ThreadRun = false; // メッセージ送信スレッドを終了させる
                WH.Set();
            }
        }
        else
        {
            NakCnt = 0;
        }
    }
    else
    {
        // 受け取ったメッセージがSTXデータの体をなしているか評価する
        if(CompareMsg(msg) == true)
        {
            // 正常な形式であれば問題ないので保持用変数を初期化
            ApartData = null;
        }
        else
        {
            // 正常で無ければ受信データが欠損していると判断して処理
            // 保持用変数に受信データを加えて再評価する
            ApartData = ApartData + msg;
            if (CompareMsg(ApartData) == false)
            {
                // 未完成なら続きのデータを受信する
                return;
            }
            else
            {
                // STXデータ完成
                msg = ApartData;
                ApartData = null;
            }
        }

    }

    // =======================================
    // 2.STXが存在する場合の処理
    // =======================================
    if (msg.Contains(STX) == true)
    {
        // ETBが存在する場合はデータが途中であるため、
        // 一旦保持しておいて次のデータ受信を待機する
        if (msg.Contains(ETB) == true)
        {
            EtbData = EtbData + ExtractData(msg, ETB);
            SendAck();
            return;
        }

        // ETXが存在する場合
        else if (msg.Contains(ETX) == true)
        {
            // 前のデータ受信でETBが存在していた形跡があれば、
            // そのデータを結合して完成形のデータを生成する
            if(string.IsNullOrEmpty(EtbData) == false)
            {
                string data = ExtractData(msg, ETX);
                data = EtbData + data;

                string frame = STX + data + CR + ETX;
                string chksum = 頑張ってチェックサム値を入れて下さい。;
                msg = frame + chksum + CR + LF; // 完成形のデータ
            }
            // 保持しておいたETBデータを初期化する
            EtbData = null;
        }
    }

    // =======================================
    // 3.受信データを処理する
    // =======================================
    EvalReceiveData(msg);

    return;
}

// 受信データの処理
private static void EvalReceiveData(string msg)
{
    if (msg == null || msg.Length == 0)
    {
        return;
    }

    if (msg == ACK)
    {
        // TCPプロトコルにおける応答確認パケット
        // 次の電文を検査機器に送信する
        // ※実際には SendMsg メソッド内の電文送信のループが止まっているので進行させる
        WH.Set();
    }

    else if (msg == ENQ)
    {
        // メッセージの受信勧誘または応答督促
        SendAck();
        SampleIDList = new List<string>();
        EtbData = null;
    }

    else if (msg == EOT)
    {
        // 伝送制御の終了処理
        if (OrderFlg == true)
        {
            // フラグを戻す
            OrderFlg = false;

            // 試験オーダ情報の要求を医療機器から受け取った為、
            // 保持した全サンプルID(検体識別コード)分のオーダ情報を返す
            Thread = new Thread(new ThreadStart(ReplyOrderData));
            ThreadRun = true;
            Thread.Start();
        }

        EtbData = null;
    }

    else if(msg == NAK)
    {
        // 伝送メッセージの否定応答
        // 直前に送信したメッセージレコードが存在すれば再送する
        if(SendData != null)
        {
            Send(SendData);
        }
    }

    else
    {
        // レコードの先頭から3文字目を取得する
        // ※1文字目は制御コード、2文字目のフレーム番号は無視
        string recordType = null;
        try
        {
            recordType = msg[2].ToString();
        }
        catch
        {
            return;
        }

        if(recordType == "H")
        {
            // ヘッダーレコード
            SendAck();
            break;
        }
        elseif(recordType == "Q")
        {
            // 試験オーダ要求レコード
            string[] fields = msg.Split('|');
            string[] comps = fields[2].Split('^');
            NowSampleID = comps[1];
            SampleIDList.Add(NowSampleID);
            OrderFlg = true;
            SendAck();
        }
        elseif(recordType == "R")
        {
            // 結果レコード
            string[] fields = msg.Split('|');
            string assay = fields[2].ToString();
            string result = fields[3].ToString();
            string result_state = fields[8].ToString();
            // 上記のデータを使って結果情報の処理を行って下さい。
            SendAck();
        }
        elseif(recordType == "O")
        {
            // 試験オーダレコード(結果受取時)
            string[] prms = ExtractData(msg, ETX).Split('|');
            NowSampleID = prms[2];
            assay = prms[4].ToString().Split('^').GetValue(3).ToString(); // [CT/GC]など
            ResultReportState = prms[25]; // [F]や[X]など
            SendAck();
        }
        elseif(recordType == "C")
        {
            // コメントレコード 必要に応じて取得して下さい。
            string comment = msg.Split('|').GetValue(3).ToString();
            SendAck();
        }
        elseif(recordType == "L")
        {
            // 終了レコード
            SendAck();
        }
        else
        {
            SendAck();
        }
    }

    return;
}

とりあえず、きちんと受信できたかを返さないと医療機器から全てのデータが送られてこないので、OK(NG)の送信処理を。

// 応答
private static void SendAck()
{
    Thread.Sleep(Wait);
    SP.Write(ACK);
}
// 否定
private static void SendNak()
{
    Thread.Sleep(Wait);
    SP.Write(NAK);
}

これで検査機器からの受信で全データの取得はできたかなといったところです。
延ばすつもりはなかったのですが、長くなってしまったので受信したオーダーに対して検査内容を伝える送信処理は次回とさせて下さい。
次回で終わるはずです。多分。。。