大阪大学大学院医学系研究科 生体統御医学講座 麻酔集中治療医学教室

  • TOP
  • 麻酔学講座
  • 関連施設
  • 研究生・学生の方へ
  • 各種マニュアル
  • リンク集
  • 各種マニュアル
    大阪大学大学院医学系研究科 麻酔集中治療医学講座 TOP>各種マニュアル>モニタからのデータ取り込み(1)

    モニタからのデータ取り込み.1

    How to collect data from monitors

    Borland C++Builderを使用してリアルタイムデータ処理を行うソフトウェアを構築するために

    How to collect data from monitors

    Borland C++Builderを使用してリアルタイムデータ処理を行うソフトウェアを構築するために

    注意

    私の知識不足で,これまでこのページに記載していたC++のソースコードの一部が正しく表示されておりませんでした.この度改訂して訂正致しました.お詫びいたします.

    注意2

    ソースコードのうち以下のReadFile関数の第2変数はIOBufが正しく,以前の&IOBufは間違いでした今回の改訂で訂正致しました.

    ReadFile(ser_hdl, IOBuf, BUFFERSIZE-DataCount,
    &DataLength, NULL);

    last update 2006.07.15

    目次


    1.はじめに

    臨床研究をする際には各種の生体情報を記録する必要がありますが,これらを手動で記録したりモニターのプリントアウトを利用したりするのでは非常に煩雑ですし,記録ミスなど間違いの元となります.これらを自動的にコンピュータに取り込めれば記録の保管や後のデータ処理が非常に楽になります.学会発表や論文に使用するグラフもグラフ描画ソフトウェアを使用すれば美しく仕上げることができます.従ってモニターのデータをコンピュータへ自動で取り込むことができ るなら理想的なことです.
    ここではモニターからの情報をコンピュータに取り込むソフトウェアの構築法の実際について解説します.ここで紹介するソフトウェアはいずれも Windows95/98/NT/2000/XPで動作するものです.個人的には2004年からMacintoshを使用するようになりましたが現在のところMacintoshのソフトウェアは開発していないためここにはありません.開発環境が整えばMacintoshのソフトウェアも開発していく予定です.
    多くのモニターはデジタルもしくはアナログの外部出力を持っていますから,理論的にはデータを取り込むことは可能です.ただし,実際にはコンピュータとモ ニターの接続方法(ケーブル接続の方法)や通信プロトコルなどの知識が必要になります.これらの情報はメーカーから入手しなければなりません.
    ここに解説している実際のプログラムコードはBorland C++Builder Ver.5 Pro(Patch1)で私が開発した種々のソフトウェアで実際に実装しているものです.コードそのものはC++Builder Ver.3やVer.4でもそのまま利用できると思います.
    これまでは,background I/Oを使用しておらずWin95/98/98SE/MEでは動作するもののWinNT/2000/XPではうまく動作しませんでした.今回WinNT /2000/XPのOSにも対応したコードに改定しました.

    2.デジタルデータ取得の方法

    2-1.Serial connection (RS232C)

    現在のところモニター機器のデジタル出力の多くはシリアル(RS232C,RS422など)です.この場合に必要な情報は(1)通信パラメータ,(2) ケーブルの結線方法,(3)通信プロトコルなどです.

    2-1-1.通信パラメータ

    シリアル接続の通信パラメータにはbaud rate(300,600,1200,2400,4800,9600,19200,38400,57600,115200bps), bitサイズ(8bit, 7bit), stop bit (1,1.5,2), parity bit (none, even, odd)およびハンドシェイクの方法(no flow control, X-on/Xoff, RTS/CTS, DTR/DTR)が必要です.

    2-1-2.ケーブルの結線方法

    シリアル接続で使用されるコネクタは通常DSUB-9pinもしくはDSUB-25pinでWindowsマシンの場合9pinはケーブル側がメス,25pinはケーブル側がオスになっています.
    ケーブル接続には一般的に"クロス"と呼ばれるコンピュータ同士を接続する際に使用するケーブルと"ストレート"と呼ばれるコンピュータとモデムを接続す る際に使用するケーブルがあります.通常はどちらかを使用すればよいのですが,ケーブル接続には厳密な規格はありませんので,各モニターによってまちまちであるのが現状です.従ってメーカーからの情報が必須であることもあります.
    最もよく用いられているクロスケーブルではRxD-TxD, TxD-RxD, GND-GNDの3本を接続すれば無手順での通信が行えます.さらに通信制御する場合にはCTS-RTS, RTS-CTSの接続やDTR-DSR, DSR-DTRといった接続方法が取られますが,先に書きましたようにこの制御ラインの接続にはきっちりした規格がありません(どのように使っても構わな い)のでメーカーの情報を元に接続しなければなりません.尤も多くのモニターは無手順の垂れ流し式データ出力を行っていますのでデータラインの3本のみの 接続で動作することも多いと考えられます.ただこの場合にはデータの取りこぼしに注意しなければなりません.

    2-1-3.通信プロトコル

    通信プロトコルは完全にソフトウェアレベルの問題で各社まかせですからこれもモニターメーカーからの情報が必要です.いくつかのモニターについては業者か ら得た情報がありますので,これを利用してデータ取得ソフトを作成しました.
    現在以下のモニターのデータが取得可能です.(4,5,10については単体のソフトは作成していません.)
    いくつかのソフトウェアは私のソフトウェアライブラリからdownload可能です.
    統合型モニター
    1. BP-508 (日本コーリン).....血圧,心拍数,SpO2など.(波形データはアナログで取得可能)
    ガスモニター
    2. CAPNOMAC (DATEX).....PETCO2, 麻酔ガス
    パルスオキシメータ
    3. N6000 (ネルコア).....SpO2, PETCO2
    4. N200 (ネルコア)
    5. N550, N595 (Tyco health care)
    6. Masimo (マシモ)
    7. BIOX-3700 (Ohmeda)
    脳波モニター
    8. BISモニター (A-1050,A-2000).....各種脳波パラメータ,波形データ
    その他のモニター
    9. 連続血液ガス測定装置(Paratrend7; バイエルン).....血液ガスデータ(測定値,温度換算後の値 etc.) 現在プローブが入手できなくなり使用不能になっています.
    10. 血液凝固モニター(SonoClot; サイエンコ).....ClotSignal
    11. DDGアナライザー(DDG2001; 日本光電).....血中ICG濃度
    12. 連続心拍出量モニター(Vigilance; Baxter).....CO, SvO2
    13. 混合静脈血酸素飽和度モニター(Oximetrix3; Baxter)..... SvO2

    3.デジタルデータ取得ソフトウェアの構築方法

    Borland C++Builderでシリアルポートを介してデータ取得を行うにはWin32APIをコールする必要があります.また,画面操作と平行してデータ取得を 行うためにはThreadオブジェクトを作成するのが望ましいと考えられます.
    ここでは(1)シリアルコントロールを行うためのWin32APIの操作方法と(2)Threadオブジェクトの操作に関して解説します.

    3-1. シリアルコントロール関係のWin32APIの操作方法

    Windowsでは外部デバイスのopen,closeにはCreatFile, CloseHandle関数を用います.また,種々の設定やコントロールのためにいくつかの構造体を用います.私が使用しているのは _DCB,_COMSTAT, _COMMTIMEOUTS, OVERLAPPEDの4種です.またファイルハンドル(HANDLE)もデバイスのopen, closeに必要です.構造体へのデータの受け渡しを助ける関数にはGetCommState関数,GetCommTimeouts関数, BuildCommDCB関数,SetCommState関数,SetCommTimeouts関数,SetCommMask関数などがあります.さらに 実際のデータの入出力を担うのはReadFile関数とWriteFile関数です.エラーの後始末を行うのはPurgeComm関数です.これらの使い方について以下に解説します.
    少し工夫をすればSerial ControlをThreadの派生Classとして定義してしまえば,複数のシリアルポートを比較的簡単にコントロールできるようにもなりますがここで は割愛しています.これまでにComtrol社のボードを用いて8チャンネルまでコントロールした経験があります.

    3-1-1.初期設定

    最初にWin32APIのコントロールとデータ入出力に必要なデータ領域を確保します.
    #define BUFFERSIZE 1024
    #define LineMax 256
    #define TIMEOUT_MUL 50

    HANDLE ser_hdl;
    struct _DCB dcb;
    struct _COMSTAT comstat;
    struct _COMMTIMEOUTS timeout;
    DWORD SerialEvent;
    OVERLAPPED o_send, o_receive, o_event;

    unsigned long ByteRead;
    unsigned long ByteWrite;
    unsigned long DataLength;
    unsigned long ErrorMask;
    char IOBuf[BUFFERSIZE];
    char RingBuf[BUFFERSIZE];
    char DataBuf[BUFFERSIZE];
    char OutBuf[256];
    int WritePoint,ReadPoint;
    unsigned long DataCount;
    char ev_chr;
    ser_hdlはファイルハンドルです.dcbはデバイスコントロールブロック,comstatはシリアルポートステータスブロック,timeoutは通 信タイムアウトブロックを示します.

    void __fastcall COMsetup(void)
    {
      o_send.Internal = 0;
      o_send.InternalHigh = 0;
      o_send.Offset = 0;
      o_send.OffsetHigh = 0;
      o_send.hEvent = CreateEvent(NULL, true, false, NULL);

      o_receive.Internal = 0;
      o_receive.InternalHigh = 0;
      o_receive.Offset = 0;
      o_receive.OffsetHigh = 0;
      o_receive.hEvent = CreateEvent(NULL, true, false, NULL);

      o_event.Internal = 0;
      o_event.InternalHigh = 0;
      o_event.Offset = 0;
      o_event.OffsetHigh = 0;
      o_event.hEvent = CreateEvent(NULL, true, false, NULL);

    //  Initialize COM port
      ser_hdl = CreateFile("COM1",
      GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED, NULL);
      GetCommState(ser_hdl, &dcb);
      GetCommTimeouts(ser_hdl, &timeout);
      BuildCommDCB("baud=9600 parity=N data=8 stop=1",&dcb);
    //  BuildCommDCBAndTimeouts("baud=9600 parity=N data=8 stop=1",&dcb,
    //  &timeout);

      dcb.fRtsControl = RTS_CONTROL_DISABLE;
      dcb.fOutxDsrFlow = FALSE;
      dcb.fDsrSensitivity = FALSE;
      dcb.fAbortOnError = FALSE;
      dcb.EvtChar = ev_chr;

      timeout.WriteTotalTimeoutConstant = 0;
      timeout.WriteTotalTimeoutMultiplier = TIMEOUT_MUL;
      timeout.ReadIntervalTimeout = MAXDWORD;
      timeout.ReadTotalTimeoutConstant = 0;
      timeout.ReadTotalTimeoutMultiplier = TIMEOUT_MUL;

      SetCommState(ser_hdl,&dcb);
      SetCommTimeouts(ser_hdl,&timeout);

      ReadPoint = 0; WritePoint = 0; DataCount = 0;
    }

    //---------------------------------------------------------------------------
    __fastcall TSerialThread::TSerialThread(TThreadPriority StartPriority) : TThread(true)
    {
      Priority = StartPriority;
      FreeOnTerminate = true;
      Resume();
    }
    //---------------------------------------------------------------------------
    __fastcall TSerialThread::~TSerialThread(void)
    {
      if( ser_hdl != NULL ){
        COMclose();
        ser_hdl = NULL;
      }
    }
    //---------------------------------------------------------------------------

    最初にbackground I/OのためのOVERLAPPED構造体を定義します.ここで注意すべきことはOVERLAPPEDで用いるイベントをmanual resetにしなければならないことです.次に,CreateFile関数を使用してシリアルポートをオープンします.第1引数はポート名で す."COM1", "COM2"という名前を使用します.多チャンネルのシリアルポートのついたボードを使用して10個以上のポートを使用する場合には10番目以降のポート に対しては"\\\\.\\COM10"といった特殊な名前を用いる必要がありますので注意してください.それ以外のパラメータは上記の通りにして下さ い.OVERLAPPEDモードでは第6変数にFILE_FLAG_OVERLAPPEDを指定しておきます.
    次にGetCommState関数とGetCommTimeouts関数を用いて現在のパラメータをdcbとtimeoutに読み込みます.さらに BuildCommDCB関数を用いて通信パラメータをdcbに設定します.通信パラメータは通信速度(baud)の値,パリティ(parity)の値 (N=none, E=even, O=odd),データビット(data)の数(6,7,8),ストップビット(stop)の値(1,1.5,2)を1つの文字列で指定します.上記の例で は9600bps,non parity,8bit,1stopというものです.なお上記の例ではコメントアウトしていますがBuildCommDCBAndTimeouts関数を 用いてdcbとtimeout構造体に一度にデータを設定する方法もあります.
    その次はハンドシェークの設定です.dcb.fRtsControlはRTS/CTSのハードウェアハンドシェークを行うかどうかを指定します.RTS /CTS制御をしない場合にはRTS_CONTROL_DISABLEを,RTS/CTS制御をする場合にはRTS_CONTROL_HANDSHAKE を指定します.また,DSR/TDR制御をする場合にはdcb.fOutxDsrFlowと,dcb.fDsrSensitivityをTRUEに,しない場合にはFALSEに設定します.通信エラーが起こった時にAbortするようにしたい場合にはdcb.fAbortOnErrorをTRUEに,しな い場合にはFALSEに設定します.
    ハードウェアハンドシェイクではなくXon/Xoffによるソフトウェアハンドシェイクを行う場合にはdcb.fInXおよびdcb.fOutXを trueに設定しdcb.XonCharとdcb.XoffCharを設定しておく必要があります.この場合にはハードウェアハンドシェイクは disableしておきます.
    特定のデータが受信された場合にEventを発生させたい場合にはそのデータ(charコード)をdcb.EvtCharに設定します.これはASCII データを受信する場合に改行コードなどを基準にデータを取り込みたい場合に有用です.
    あとはtimeout関係の設定を行って構造体に設定したデータをシステムにSetCommState関数とSetCommTimeouts関数を使用してシステムに通知します.最後にアプリケーション側で用意しているRing Bufferのコントロール変数を初期化します.
    なおここには後述するThreadオブジェクトのコンストラクタデストラクタのリストも含まれています.

    3-1-2. ASCIIデータの受信

    データの読み込みにはReadFile関数を用います.OVERLAP構造体を用いたbackground読み込みのために Read_BackGround関数を作成しています.以下にはThreadオブジェクトのExecute関数も 示しています.

    // Get and Fill RingBuf from System Buffer
    void __fastcall Read_Background(DWORD cnt)
    {
      DWORD endtime;

      if(!ReadFile(ser_hdl, IOBuf, cnt, &ByteRead, &o_receive) ){
       ByteRead = 0;
       if( GetLastError() == ERROR_IO_PENDING ){
        endtime = GetTickCount() + 200;
        while( !GetOverlappedResult(ser_hdl, &o_receive, &ByteRead, FALSE) ){
         if( GetTickCount() > endtime ){
          break;
         }
        }
       }else{
        GetLastError();
       }
      }
      ResetEvent(o_receive.hEvent);
      return;
    }
    // Get and Fill RingBuf from System Buffer
    void __fastcall COMread_line(void)
    {
      unsigned int i;

      ClearCommError(ser_hdl, &ErrorMask, &comstat);
      if(comstat.cbInQue == 0){
        return;
      }
      if( comstat.cbInQue > BUFFERSIZE-DataCount ){
        Read_BackGround(BUFFERSIZE-DataCount);
      }else{
        Read_BackGround(comstat.cbInQue);
      } if(DataLength == 0){
        return;
      }
      for(i=0;i<DataLength;i++){
        RingBuf[WritePoint++] = IOBuf[i];
        DataCount++;
        if( WritePoint >= BUFFERSIZE){
          WritePoint = 0;
        }
      }
    }

    // Get 1 line from RingBuf if exist then return TRUE
    // else return FALSE
    int __fastcall get_COM_line(char *buf)
    {
      unsigned long i;
      int j;

      COMread_line();
      for(i=0,j=ReadPoint;i<DataCount;i++){
       if( (*buf = RingBuf[j]) == ev_chr ){
         *buf = 0;
         break;
       }
    // ignore control code ( <0x20 )
       if( *buf >= 0x20 ){
         buf++;
       }else{
         *buf = 0;
       }
       if(++j == BUFFERSIZE){
         j=0;
       }
      }
      if( i >= DataCount ){
         return(FALSE);
      }
      if( ++j == BUFFERSIZE ){
         j = 0;
      }
      ReadPoint = j;
      DataCount -= (i+1);
      return(TRUE);
    }
    //---------------------------------------------------------------------------
    void __fastcall TSerialThread::Execute(void)
    {
      ev_chr = 0x0a;

      COMsetup();
      if( ser_hdl == NULL ){
        return;
      }
      for(;;){
        SetCommMask(ser_hdl, EV_RXFLAG);
        WaitCommEvent(ser_hdl, &SerialEvent, &o_event);
        for(;;){
         if( WaitForSingleObject(o_event.hEvent,200) == WAIT_OBJECT_0 ){
          ClearCommError(ser_hdl, &ErrorMask, &comstat);
          ResetEvent(o_event.hEvent);
    //    if Serial communication error occured, then call PurgeComm
          if( ErrorMask|CE_IOE ){
            PurgeComm(ser_hdl,PURGE_REABORT);
          }
          break;
        }
        if(Terminated){
          return;
        }
       }
       for(;;){
        if( get_COM_line(DataBuf) == FALSE ){
          break;
        }
    //  Add DataBuf processing routines here !!
       }
      }
    }

    TSerialThreadのExecute関数ではdcb.EvtCharを設定した場合のEvent driven方式の場合の記述をしています.この方法はASCII文字列を行ごとに受信する場合に有用です.多くの場合改行コード(0x0a)が EvtCharに設定されると思います.WaitCommEvent関数をOVERLAPPED構造体を用いてコールしており,実際のEventは WaitForSingleEventでトラップしています.この例では手を抜いて200msecごとにTerminateをチェックして終了を確認する ようにしていますが,通信から抜ける際にもEventで行いたい場合にはEvent Handle2つの配列を用意します.そして,ひとつにはo_event.hEventを設定しもうひとつに終了用Eventを設定し WaitForMultipleObjectsで2つのEventを待つようにすれば完璧にEvent drivenになります.この場合WaitForMultipleObjectsの返す値がWAIT_OBJECT_0かWAIT_OBJECT_0+1 かでEventの種類を判別します.
    Eventが検知された場合にはWaitForSingleObjectの返り値がWAIT_OBJECT_0であることを確認 し,o_event.hEventをResetしてデータの読み込みに移ります.
    データの読み込みは以下の関数を用いて行います.COMread_line関数はClearCommError関数を用いてシステムバッファに読み込まれ ているデータのbyte数を調べ0でなければアプリケーションの一時バッファ(IOBuf)に読み込んだ後RingBufにセットし直しま す.get_COM_line関数はCOMread_line関数をコールした後,データに1行分のデータが読み込まれていれば,そのデータを指定された 文字配列に読み込みTRUEを返します.1行分のデータがなければそのままFALSEを返します.ClearCommError関数は通信エラーを解除す るのに用いられる他に上記のようにバッファ内のデータ数を得るためにも用いられます.ここの例ではASCII文字列を想定しておりSetCommMask 関数でEventタイプをデータ読み込み時にEventが発生するように指定した後WaitCommEventでEventが発生するまで待機するようにプログラムされています.これによって余分なCPU timeの消費を防ぐことができます.

    3-1-3. Binaryデータの受信

    バイナリデータを扱う場合にはこのようなEventを用いた読み込みを行うことが困難ですので,Event drivenではなくpollingによってデータを読む必要があります.また,データがASCIIでも区切り文字がないような場合にはバイナリデータと 同様の方法で行う必要があります.これらの場合にはSleep関数を使用してCPU timeの消費を抑えながらClearCommError関数で読み込まれたデータ数を監視しながら適宜データを読み込む処理を行います.以下の例では 100msec毎にバッファチェックを行うものです.Sleep関数の引数はmsec単位です.

    void __fastcall TSerialThread::Execute(void)
    {
      int i;

      COMsetup();
      if( ser_hdl == NULL ){
        return;
      }
      for(;;){
        for(;;){
          if(Terminated){
            return;
          }
          COMread_line();
          if( DataCount == 0 ){
            Sleep(100);
            continue;
          }
    // Add DataBuf processing routines here !!
        }
      }
    }

    3-1-4. データの送信

    モニターからのデータ取得は通常垂れ流しのデータを受けるのみの場合が多いのですが,中には受け取ったコマンドに対してレスポンスを返すものも存在します.例えば日本光電のDDGアナライザーなどはこれに当たります.BISモニターのA2000のようにバイナリパケットでのコマンドハンドシェイクが必要なモニターも存在します.次の例はDDG2001のデータ取得を行うプログラムの一部です.

    void __fastcall Write_Background(DWORD cnt, char *outbuf)
    {
      DWORD endtime;

      if(!WriteFile(ser_hdl, outbuf, cnt, &ByteWrite, &o_send) ){
        ByteWrite = 0;
        if( GetLastError() == ERROR_IO_PENDING ){
          endtime = GetTickCount() + 200;
          while( !GetOverlappedResult(ser_hdl, &o_send, &ByteWrite, FALSE) ){
            if( GetTickCount() > endtime ){
              break;
            }
          }
        }else{
          GetLastError();
        }
      }
      ResetEvent(o_send.hEvent);
      return;
    }

    void __fastcall TSerialThread::Execute(void)
    {
      COMsetup();
      if( ser_hdl == NULL ){
        return;
      }
      for(;;){
        if(Terminated){
          break;
        }
        if( get_COM_line(DataBuf) ){
          if( strcmp(DataBuf, "!D;SEND REQ,") == 0 ){
            Write_background(9, &Init_Str);
          }else{
            UpdateLine((String) DataBuf);
            if( get_token(DataBuf, tok)>=2 )
            { if( (strcmp(tok[0],"#D;") == 0)&&(strcmp(tok[1],"0") == 0)){
               return;
              }
            }
          }
        }
      }
      return;
    }

    このようにデータの送信にはWriteFile関数を使用します.ここでもbackground I/Oを行うためにWrite_background関数を作成しています.

    3-1-5. 通信回線のClose

    通信が終了したら回線を閉じます.回線を閉じるにはCloseHandle関数を用います.

    //---------------------------------------------------------------------------
    void __fastcall COMclose(void)
    {
      SetCommMask(ser_hdl, 0);
      PurgeComm(ser_hdl, PURGE_RXCLEAR);
      PurgeComm(ser_hdl, PURGE_TXCLEAR);
      PurgeComm(ser_hdl, PURGE_RXABORT);
      PurgeComm(ser_hdl, PURGE_TXABORT);
      GetCommState(ser_hdl, &dcb);

      dcb.fOutxCtsFlow = FALSE;
      dcb.fRtsControl = RTS_CONTROL_DISABLE;
      dcb.fAbortOnError = TRUE;
      dcb.fOutxDsrFlow = FALSE;

      SetCommState(ser_hdl, &dcb);
      CloseHandle(o_receive.hEvent);
      CloseHandle(o_send.hEvent);
      CloseHandle(o_event.hEvent);
      CloseHandle(ser_hdl);
    }

    まずEvent drivenを解除するためにSetCommMask関数を使用します.また各種エラーを解除するためにPurgeComm関数を呼び,念の為にdcbの 設定をハンドシェークなしにして最後にCloseHandle関数で回線を閉じて終了します.dcbの再設定などは必要ないかもしれませんが念のために 行っています.

    3-1-6. ToDo

    ここではモニターからのデータ取得を主眼にしているため,データ送信に関しては特に別Threadで処理を行ったりしていません.またエラー処理に関してもきっちり行っていないため場合によっては不都合が生じる可能性もあります.ただ,現実問題としてここに書いたルーチンで問題が生じたことはありません. これらの点については今後の検討課題としておきます.