DelphiからEPICSチャンネルアクセスを行う手法については、基本的には 10年以上前に準備された手法 を使い続けてきました。開発当初のDelphiのバージョンは多分4か5、 EPICSもR312の時代、WindowsはやっとWindows98になったばかりだったと思います。 その後、Delphiのバージョンは 上がってゆき(実用運転にはDelphi2007まで使用した)、EPICSのバージョンも これを書いている現在R314まで来ており、windowsも制御用とはいえWindowsXPではなく Windows7-64bit版になりつつありますが、このインターフェースや 開発されたアプリケーションはずっとほぼオリジナルのまま、特に問題なく使ってきました。
しかしながら、近年になり、以下の3つの課題が気になってきました。
最近になって、(1)については、ca.dllとのインターフェースの呼び出し名 が変わったため、と判明し少なくともDelphi2007までならR314での長い waveform読み出しが(条件付きで:後で判明したむごいバグが他にもあった)可能と なりました。しかし、RAD Studioでの問題は依然残ったままでした。
Delphi2007は、少なくとも昔Windows Vistaで使っていた時はそれなりに使えていたと いう気がしますが(嘘かも知れない)、現時点で私が使用しているWindows7-64bit環境下では、 IDEでのデバッグが ほぼ使えないといえるほど致命的なバグにとりつかれていて(手動でprocessをデタッチ せずにターゲットプロセスの実行を止めると延々とスレッドエラーが出続け、 あきらめてキャンセルするとIDEごと落ちる。もちろん、何のファイルもセーブしては くれない。これはEPICSとは関係ない模様)、もはや使い続けるのは(心理的に)不可能です。(なお、 この問題については、web上で何の情報も得られないことから、私の環境だけで 起きている可能性も高い)。
そこで、 RAD Studio XE2でのEPICS使用を可能とすることとEPICSのモニター機能を生かすことを 主な目的とし、東日技研さまにEPICS-Delphiインターフェース作成を依頼しました。 本稿では、開発されたEPICS-Delphiユーザーインターフェースの使い方を 紹介します。
EPICS自体への入門出家入道については自活努力をお願い致します。 RAD Studioの「正しい」使い方とか、Delphiの文法などについては、私が教えて ほしい位なので私に聞いても無駄です。
EPICS R314から長いwaveformを使うことが出来るようになりましたが、
そのためにはPC側の環境変数として
EPICS_CA_MAX_ARRAY_BYTESをたとえば1048576などwaveformの長さに応じた大きな数字にしておく必要があります。設定 していない場合、短くてもwaveformの読み取りに問題が発生します。
ca.dllとかcom.dll、caRepeater.exeが 実行パスにある必要があるのは以前と同じですが、今回はこれらをMicrosoft Visual Studio 2010 Expressで作成しましたので、このランタイムライブラリがinstallされていないと 動作しません。具体的には、 Microsoft Visual C++ 2010 再頒布可能パッケージ(x86)を インストールする必要があります(dllのコピーだけでは動作しない模様です)。また、いっそ EPICS環境をインストールする元気のある方は、別に引き留めませんので ご随意にどうぞ。ただ、違う作り方をしたca.dll、com.dllで動くとは思えませんけど。
そのほかのEPICS環境変数については、必要な物をちゃんとセットして下さい。 私の現テスト環境では、EPICS IOCは同じPCで動かしているVMWare上のSL6で 動かしていますので、別段何の設定もしていません。
ca.dll Com.dll caRepeater.exe EPICStypes.pas CA_iN.pasがあるようにして下さい。ターゲットプラットフォーム(Win32)としては 32ビット Windowsと64ビット Windowsで検証を行い、どちらでも テストプログラムに関しては 問題なく動作していることを確認しています。末期頓首のOSX Machでは そもそもchannel access手法が異なりますのでたとえクロスコンパイルしても動作するわけがないと 思います。
KEK所内の方は、以下のところから該当ファイルをダウンロード出来ます(いずれも 対象をファイルに保存して下さい)。所外の 方で、ご興味のある方は、直接帯名あるいは 飛山までご連絡頂きますようお願い致します。
と、implementationのところにunit なんとか interface uses Windwos, Messages, SysUtils,..... EPICSTypes,....
としておきます。また、form初期化のところでuses CA_iN;
と、ca_context_createとca_connectをEPICSレコードに 対して行っておきます。procedure TCA_NEW_TEST.FormCreate(Sender: TObject); var s : PAnsiChar; i : integer; begin i := 1; irtc := 0; irtc := ca_context_create(i); s := 'FBTEMP:TEST:AI1'; ai1_ptr := ca_connect(s); s := 'FBTEMP:TEST:AI2'; ai2_ptr := ca_connect(s); s := 'FBTEMP:TEST:LI1'; li1_ptr := ca_connect(s); s := 'FBTEMP:TEST:BI1'; bi1_ptr := ca_connect(s); s := 'FBTEMP:TEST:C1'; c1_ptr := ca_connect(s); s := 'FBTEMP:TEST:W1'; w1_ptr := ca_connect(s); s := 'FBTEMP:TEST:W1'; s := 'bill'; w1r_ptr := ca_connect(s);
注意:RAD Studioから 文字列がディフォルトで2バイトUNICODEになってしまいました。このため、 たとえ日本語を使っていないつもりでも、 通常のstringとかPcharの設定では自動的に2バイトコードに文字が変換 されてしまいますので、PAnsiCharなど、1バイトコードだと明示する 必要があります
なお、ca_connectは互換性のために 残してあるだけなので、本来的な使い方ではconnectで 作ったほうがよいと思われます。
レコードの読み書きは、たとえばタイマーeventが起きたとき (たとえば1秒ごとに)読むようにしたときは
のようにします。EPICSレコードに値を書くときも同じように、 たとえばprocedure TCA_NEW_TEST.Timer1Timer(Sender: TObject); var s : AnsiString; begin timer1.Enabled := false; ai1 := ca_get_ai(ai1_ptr); label1.Caption := floattostr(ai1); ai2 := ca_get_ai(ai2_ptr); label4.Caption := floattostr(ai2); li1 := ca_get_li(li1_ptr); label6.caption := inttostr(li1); bi1 := ca_get_li(bi1_ptr); label8.caption := inttostr(bi1); c1 := ca_get_ai(c1_ptr); label10.Caption := floattostr(c1); timer1.Enabled := true; end;
のようにします。for i := 0 to 16383 do begin w1[i] := 20*sin((i+gv1)/512*pi); end; putwaveformdouble(w1_ptr,16384,w1); getwaveformdouble(w1_ptr,16384,w1);
はじめにevent handlerとconnection hanlderの中身のprocedureを定義します。 event handlerはモニターイベントが起きたとき実行する内容で、ここで 絵を描かせるとか文字を書かせるなど面倒なことはせずに、フラグを立てる だけにしておくことがお勧めです。
コネクションハンドラーは、 最初に接続したときに動作する部分です。どちらも(pascalなので) connectをする前に定義してある必要があります(あるいはforward宣言するか)。 なお、モニター動作時点で該当レコードの値はargs.dbp^にはいっていますので もう一度ca_get_aiなどするのはちょい無駄です。
上の例では、モニターイベントが起きたとき、monitor_flag(グローバル変数)をtrueにしています。 また、eventとしては値が変わったとき(DBE_VALUE)に起きるよう設定しています。もしもアラーム (こんなもんまじめに使っている人がいるかどうか不明)でも応答するようにするときは、var pvs : pv; eventMask : longint; procedure event_handler(args : event_handler_args) cdecl; begin m1 := double(args.dbr^); monitor_flag := true; end; procedure connection_handler(args : connection_handler_args) cdecl; var rtn : integer; begin rtn := 0; if args.op= CA_OP_CONN_UP then begin pvs.chid.chid := args.chid.chid; pvs.onceConnected := AnsiChar(1); pvs.dbfType := ca_field_type(pvs.chid.chid); pvs.dbrType := ca_field_type(pvs.chid.chid); pvs.nElems := ca_element_count(pvs.chid.chid); eventMask := DBE_VALUE; rtn := ca_create_subscription(pvs.dbrType, pvs.nElems,pvs.chid.chid, eventMask, @event_handler, @pvs, nil); end; end;
と設定することになります。eventMask := DBE_VALUE or DBE_ALARM;
初期化のところ(多分、formcreateでやるのが一番良いと思われる)では、以下の様にします。
この例では、FBTEMP:TEST:C1をモニターするように設定しています。procedure TCA_NEW_TEST.FormCreate(Sender: TObject); var s : PAnsiChar; i : integer; pCaCh : Pointer; pPvs : Pointer; iPriority : integer; dCaTimeout : double; callselect : ca_preemptive_callback_select; select : integer; rtn : integer; begin i := 1; irtc := 0; irtc := ca_context_create(i); s := 'FBTEMP:TEST:AI1'; ai1_ptr := ca_connect(s); s := 'FBTEMP:TEST:AI2'; ai2_ptr := ca_connect(s); s := 'FBTEMP:TEST:LI1'; li1_ptr := ca_connect(s); s := 'FBTEMP:TEST:BI1'; bi1_ptr := ca_connect(s); s := 'FBTEMP:TEST:C1'; c1_ptr := ca_connect(s); s := 'FBTEMP:TEST:W1'; w1_ptr := ca_connect(s); s := 'FBTEMP:TEST:W1'; s := 'bill'; w1r_ptr := ca_connect(s); s := 'FBTEMP:TEST:C1'; callselect := ca_preemptive_callback_select.ca_enable_preemptive_callback; select := integer(callselect); rtn := contextcreate(select); pvs.name := PAnsiChar(s); pCaCh := @connection_handler; pPvs := @pvs; iPriority := CA_PRIORITY_DEFAULT; m1_ptr := connect(pvs, pCaCh, pPvs, iPriority); rtn := ca_pend_event(1.0); gv1 := 0; end;
モニターに引き続いて起きるeventはタイマールーチンの中で実行することにし、 たとえば
のようにし、timer間隔をモニターイベントが起きうる間隔より 短く(たとえば10msとか)にしておけば(あまり美しくはないが)良さそうです。procedure TCA_NEW_TEST.Timer1Timer(Sender: TObject); var a : double; s : AnsiString; begin if monitor_flag then begin timer1.Enabled := false; ai1 := ca_get_ai(ai1_ptr); label1.Caption := floattostr(ai1); ai2 := ca_get_ai(ai2_ptr); label4.Caption := floattostr(ai2); li1 := ca_get_li(li1_ptr); label6.caption := inttostr(li1); bi1 := ca_get_li(bi1_ptr); label8.caption := inttostr(bi1); c1 := m1; label10.Caption := floattostr(c1); timer1.Enabled := true; monitor_flag := false; end; end;
(付記)
複数のモニターレコードがあり、動作がそのモニターのorで良い場合は
と定義し、event_handlerとconnection_hanlderではimplementation {$R *.dfm} uses CA_iN, EPICStypes; var pvs : array[0..9] of pv; // array of PV pevid : array[0..9] of Pointer; // array of EventID (used to clear monitor event) clear monitorしないならば不要。 monval: array[0..9] of Double; // array of monitor value 実データ保存用; ここではdouble決め打ち。 npv : integer = 0; // number of connected PVs (init 0; max 10) monitor_flag : boolean; // become true when camonitor event occured 配列にして各レコードごとにする手もあるが、実用上はこれで十分か。
として、初期化のときはprocedure event_handler(args : event_handler_args) cdecl; var i : integer; begin for i:=0 to npv-1 do begin if (args.chid.chid = pvs[i].chid.chid) then begin monval[i] := double(args.dbr^); //doubleだと決めうちしている end; end; monitor_flag := true; end; procedure connection_handler(args : connection_handler_args) cdecl; var rtn : integer; eventMask : longint; begin if args.op= CA_OP_CONN_UP then begin pvs[npv].chid.chid := args.chid.chid; pvs[npv].onceConnected := AnsiChar(1); pvs[npv].dbfType := ca_field_type(pvs[npv].chid.chid); pvs[npv].dbrType := ca_field_type(pvs[npv].chid.chid); pvs[npv].nElems := ca_element_count(pvs[npv].chid.chid); eventMask := DBE_VALUE or DBE_ALARM; // ca_create_subscription must be called after CA_OP_CONN_UP rtn := ca_create_subscription(pvs[npv].dbrType, pvs[npv].nElems,pvs[npv].chid.chid, eventMask, @event_handler, @pvs[npv], @pevid[npv]); //CA_iN.ca_pend_io(10.0); npv := npv + 1; // increase number of monitored channels end; end;
のような形ですれば良さそうです。callselect := ca_preemptive_callback_select.ca_enable_preemptive_callback; rtn := ca_context_create(Integer(callselect)); // Add monitor records pvs[npv].name := PAnsiChar('jane'); rtn := ca_create_channel(pvs[npv].name, @connection_handler, @pvs[npv], CA_PRIORITY_DEFAULT, pvs[npv].chid.chid); pvs[npv].name := PAnsiChar('fred'); rtn := ca_create_channel(pvs[npv].name, @connection_handler, @pvs[npv], CA_PRIORITY_DEFAULT, pvs[npv].chid.chid);
モニターするレコードがwaveformの時も、使い方はほぼ同じです。モニターeventが起きたとき、 データをコピーする方法として、waveformの中身をglobal変数として定義しておき
event handlerの中ではvar Chan1 : array[0..16383] of double;
としてやれば、eventが起きたときに同時にデータコピーも完了します。procedure event_hanlder(args : event_hanlder_args) cdecl; begin system.move(args.dbr^,chan1, sizeof(chan1)); monitor_flag := true; end;
IOC側で、casrで接続状況を確認したところ
と、ちゃんと接続(通常の接続、モニター接続)されていることが分かります。 このコードを数時間にわたって実行し続けましたが、特に落ちるとか、 メモリーリークが起きるとかいったことは無いようでした。CPUの 占有については、Core-i7マシンから見るとカスのようなもので、 ほぼ無視できる状態でした。epics> casr,2 Channel Access Server V4.13 Connected circuits: TCP 192.168.111.1:53360(tobiyama-c7): User="tobiyama", V4.13, 5 Channels, Priority=0Task Id=0x7f74ec00a730, Socket FD=9 Secs since last send 0.19, Secs since last receive 0.19 Unprocessed request bytes=0, Undelivered response bytes=0 State=up jumbo-send-buf jumbo-recv-buf 1832 bytes allocatedTCP 192.168.111.1:53362(tobiyama-c7): User="tobiyama", V4.13, 1 Channels, Priority=0
FBTEMP:TEST:AI1(0rw) FBTEMP:TEST:AI2(0rw) FBTEMP:TEST:LI1(0rw) FBTEMP:TEST:BI1(0rw) FBTEMP:TEST:W1(0rw) Task Id=0x7f74ec00a0e0, Socket FD=3 Secs since last send 0.46, Secs since last receive 38.42 Unprocessed request bytes=0, Undelivered response bytes=0 State=up 656 bytes allocated FBTEMP:TEST:C1(1rw)UDP Server:UDP 192.168.111.1:64577(): User="", V4.13, 0 Channels, Priority=0Task Id=0x7f74f0000970, Socket FD=7 Secs since last send 38.42, Secs since last receive 4.44 Unprocessed request bytes=0, Undelivered response bytes=16 State=up 272 bytes allocatedThere are currently 332296 bytes on the server's free list 5 client(s), 506 channel(s), 511 event(s) (monitors) 0 putNotify(s) 14 small buffers (16384 bytes ea), and 0 jumbo buffers (1048600 bytes ea) The server's resource id conversion table: Bucket entries in use = 6 bytes in use = 32992 Bucket entries/hash id - mean = 0.001465 std dev = 0.038245 max = 1 The server's array size limit is 1048600 bytes max Channel Access Address List 192.168.111.255:5065
モニター機能はさておき、長いwaveformが直接扱えるようになりました ので、イマドキのADCとかスコープなど大データのものの扱いがずいぶん 楽になるのではないかと思います。また、モニター機能も、たくさん使う のはいささか面倒ですが、少数のものだけなら、コードをそれほど複雑に せずにネットワークトラフィックを低減し、より実時間に近い動作を するのに寄与するのではないかと思います。