[KEKB Bunch Feedback Group]

Embarcadero RAD Studio XE2 (Delphi XE2)用EPICSユーザーインターフェース


by とびやま まこと(Makoto Tobiyama)/KEKB ビームモニターグループ
帯名 崇(Takashi Obina)/加速器7系モニター・制御
以下の戯れ言の文責は飛山にあります。

警告
以下の記述に関しては、意図する、しないに関わらず悪意に基づく多くの誤り、誤解が含まれていると思われますので、 決して信用してはいけません。これを信じて起きた損害に関しては、当方は一切責任を持ちません。


If you want to contact with the author, please E-mail to makoto.tobiyama@kek.jp.
目次

1.はじめに

EPICSコントロールシステムのユーザーインターフェース(OPI)として、 EPICSツールセットで用意されているもの(medmとかstrip chart)や、 インタープリンタ+TKを使った物(Pythonとかsad)など、それぞれの特徴に 応じて使われています。KEKBコントロールでは主にSAD、ところにより Python、ハードに近い部分にmedmと使われていました。私が担当している フィードバックシステム(および関連システム)では、私のひねくれた性格 もあり、Delpiを使ったコントロールパネルを多用してきました。もちろん、 直接ハードウエアを使う部分にはmedm(dm2k)とかedmを使ったシンプルな パネルも併用していました。

DelphiからEPICSチャンネルアクセスを行う手法については、基本的には 10年以上前に準備された手法 を使い続けてきました。開発当初のDelphiのバージョンは多分4か5、 EPICSもR312の時代、WindowsはやっとWindows98になったばかりだったと思います。 その後、Delphiのバージョンは 上がってゆき(実用運転にはDelphi2007まで使用した)、EPICSのバージョンも これを書いている現在R314まで来ており、windowsも制御用とはいえWindowsXPではなく Windows7-64bit版になりつつありますが、このインターフェースや 開発されたアプリケーションはずっとほぼオリジナルのまま、特に問題なく使ってきました。

しかしながら、近年になり、以下の3つの課題が気になってきました。

  1. EPICS R314から可能になった長いwaveformに対するchannel accessが出来ない。 subArrayあるいはcompactsubArrayに分割して何度かアクセスしないとwaveform 全体を持ってくることが出来ない。
  2. RAD Studio XE以降、再コンパイルしたコードでchannel accessが動かなく なった。
  3. (まあ無くてもいいんだけれど)たまにEPICSのモニター機能を使いたく なることがある。

このうち、(1)については、windows上で新しいEPICSシステムをコンパイルして 出来たca.dllなどをインターフェースする試みは幾人もの方たちによって試み られて来ましたが、DLLとの接続がうまくいかず、あきらめられていました。 また、(2)についても、資料が極めて少なく、DelphiからはDLL内のデバッグ が出来ない(というより、やり方が分からない)ため、こちらも頓挫して いました。

最近になって、(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の文法などについては、私が教えて ほしい位なので私に聞いても無駄です。

もちろん、今や(昔も)絶滅危惧種であるDelphi の情報はあまりweb上にもありませんし、くされhelpシステム(これはEmbarcadero さまに限ったことではなく、最近のソフトはまともなマニュアルは一切無く(しかも RAD Studioなど昔に比べてめちゃ高額になっているのに)、 ヘルプをかけると凶悪な悪意を感じるほどどーしてここまで関係ない情報しか出てこないのかと 驚愕するほどのくそ情報しか出ないが、別に驚くには当たらない。特に Officeさまのヘルプは感激するほど糞情報しか出ない)に期待するより、 ぐーぐるさまなどにお伺いを立てた方がまだマシでしょう。

2.PC環境

ライブラリ、コードなどのご希望は直接飛山あるいは帯名氏までお問い合わせ 下さい。 検証はWindows7 Professional-64bitおよびWindows7 Professional-32bitで 行っています。WindowsXPやWindows Vistaで動かない理由はないと思います が、動かない時はこっそりお教え下さい。

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で 動かしていますので、別段何の設定もしていません。


3.Delphi側の構成

RAD studio XE2側の設定は今のところ特に必要ありません。開発を行うディレクトリに
ca.dll
Com.dll
caRepeater.exe

EPICStypes.pas
CA_iN.pas
があるようにして下さい。ターゲットプラットフォーム(Win32)としては 32ビット Windowsと64ビット Windowsで検証を行い、どちらでも テストプログラムに関しては 問題なく動作していることを確認しています。末期頓首のOSX Machでは そもそもchannel access手法が異なりますのでたとえクロスコンパイルしても動作するわけがないと 思います。

KEK所内の方は、以下のところから該当ファイルをダウンロード出来ます(いずれも 対象をファイルに保存して下さい)。所外の 方で、ご興味のある方は、直接帯名あるいは 飛山までご連絡頂きますようお願い致します。

Sorry for restricted acess (kek.jp or jparc.jp only).


4.通常のchannel access

通常のchannel access(cagetとかcaput)をする場合の使い方は、以前と同じです。具体的には、 はじめにユニット登録
unit なんとか interface uses Windwos, Messages, SysUtils,..... EPICSTypes,....
と、implementationのところに
uses CA_iN;
としておきます。また、form初期化のところで
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);
と、ca_context_createとca_connectをEPICSレコードに 対して行っておきます。

注意:RAD Studioから 文字列がディフォルトで2バイトUNICODEになってしまいました。このため、 たとえ日本語を使っていないつもりでも、 通常のstringとかPcharの設定では自動的に2バイトコードに文字が変換 されてしまいますので、PAnsiCharなど、1バイトコードだと明示する 必要があります

なお、ca_connectは互換性のために 残してあるだけなので、本来的な使い方ではconnectで 作ったほうがよいと思われます。

レコードの読み書きは、たとえばタイマーeventが起きたとき (たとえば1秒ごとに)読むようにしたときは

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;
のようにします。EPICSレコードに値を書くときも同じように、 たとえば
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);
のようにします。

5.モニター機能

今回のバージョンから、EPICSのモニター機能を使うことが出来ます。但し、 まだ「カンタンに」使える状態ではないし、どのような逆襲をうけるかも 分かりませんので、人柱がなんぼのもんじゃい、わしゃーどーしても使いたいんじゃけぇ、という 場合お使い下さい。

はじめにevent handlerとconnection hanlderの中身のprocedureを定義します。 event handlerはモニターイベントが起きたとき実行する内容で、ここで 絵を描かせるとか文字を書かせるなど面倒なことはせずに、フラグを立てる だけにしておくことがお勧めです。

コネクションハンドラーは、 最初に接続したときに動作する部分です。どちらも(pascalなので) connectをする前に定義してある必要があります(あるいはforward宣言するか)。 なお、モニター動作時点で該当レコードの値はargs.dbp^にはいっていますので もう一度ca_get_aiなどするのはちょい無駄です。

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;
上の例では、モニターイベントが起きたとき、monitor_flag(グローバル変数)をtrueにしています。 また、eventとしては値が変わったとき(DBE_VALUE)に起きるよう設定しています。もしもアラーム (こんなもんまじめに使っている人がいるかどうか不明)でも応答するようにするときは、
eventMask := DBE_VALUE or DBE_ALARM;
と設定することになります。

初期化のところ(多分、formcreateでやるのが一番良いと思われる)では、以下の様にします。

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;
この例では、FBTEMP:TEST:C1をモニターするように設定しています。

モニターに引き続いて起きるeventはタイマールーチンの中で実行することにし、 たとえば

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;
のようにし、timer間隔をモニターイベントが起きうる間隔より 短く(たとえば10msとか)にしておけば(あまり美しくはないが)良さそうです。

(付記)
複数のモニターレコードがあり、動作がそのモニターのorで良い場合は

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 配列にして各レコードごとにする手もあるが、実用上はこれで十分か。
と定義し、event_handlerとconnection_hanlderでは
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変数として定義しておき

var Chan1 : array[0..16383] of double;
event handlerの中では
procedure event_hanlder(args : event_hanlder_args) cdecl; begin system.move(args.dbr^,chan1, sizeof(chan1)); monitor_flag := true; end;
としてやれば、eventが起きたときに同時にデータコピーも完了します。

6.サンプル例

サンプルのプロジェクトを示します。

ここでは、
  1. FBTEMP:TEST:C1 (ai)をモニターし、値が変わったら AI1、AI2、LI1、BI1を読み込みラベルに表示する
  2. 上とは非同期に0.5秒ごとに FBTEMP:TEST:W1 (16382の長さのdouble型waveform)を 書き込み、次に同じレコードを読み出し、グラフに 表示する
ということを行っています。FBTEMP:TEST:C1はcalcレコードで、 レコード自体のscan設定で0.5秒〜の間隔で値を更新して います。calcレコードのscanを変えたとき、追随して 更新が変わることが確認出来ています。

IOC側で、casrで接続状況を確認したところ

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=0
Task 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 allocated
FBTEMP:TEST:AI1(0rw)FBTEMP:TEST:AI2(0rw)FBTEMP:TEST:LI1(0rw)
FBTEMP:TEST:BI1(0rw)FBTEMP:TEST:W1(0rw)
TCP 192.168.111.1:53362(tobiyama-c7): User="tobiyama", V4.13, 1 Channels, Priority=0
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=0
Task 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 allocated
There 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
と、ちゃんと接続(通常の接続、モニター接続)されていることが分かります。 このコードを数時間にわたって実行し続けましたが、特に落ちるとか、 メモリーリークが起きるとかいったことは無いようでした。CPUの 占有については、Core-i7マシンから見るとカスのようなもので、 ほぼ無視できる状態でした。

7.おわりに

最新のDelphi開発環境、Embarcadero RAD Studio XE2 (Delphi XE2)で EPICS制御を行うためのインターフェースの使い方について説明しました。 このルーチンについては、株式会社東日本技術研究所(東日技研)殿に作成を依頼し、 無事納品されたもの を一部変更して使用しています。東日技研さまには、Delphiのような 超マイナーで絶滅危惧種のソフト環境でもご対応頂き、大変助かりました。

モニター機能はさておき、長いwaveformが直接扱えるようになりました ので、イマドキのADCとかスコープなど大データのものの扱いがずいぶん 楽になるのではないかと思います。また、モニター機能も、たくさん使う のはいささか面倒ですが、少数のものだけなら、コードをそれほど複雑に せずにネットワークトラフィックを低減し、より実時間に近い動作を するのに寄与するのではないかと思います。


Makoto Tobiyama
7/Jun/2012

Return to FB Home Page...