提纲
1) 通道访问概念
2)通道访问API
3) 简单的CA客户端
4)使用回调的简单CA客户端
EPICS概要
搜索和连接过程
搜索请求
1)搜索请求由一系列UDP包组成
- 只发送给EPICS_CA_ADDR_LIST
- 从短时间间隔开始,每次加倍
- 直到它变得大于5秒,接着它保持在5s
- 在100个包后停止或者它得到了响应
- 在它见到一个异常beacon或者创建一个新PV前,不再尝试
- 发完所有100个包的全部时间大约8分钟
2)服务程序必须为每个包进行一次存在测试
3)通常在首个活前几个包时连接
4)不存在的PVs产生大量流量:尝试消除它们
Beacons
1) 一个beacon是一个由服务程序发送的UDP广播包。
2)当它健康时,每个服务程序以规则间隔发送一个UDP广播包(像一个心跳)
EPICS_CA_BEACON_PERIOD默认15秒。
3)当它启动时,每个服务程序广播一个启动序列的UDP beacons
- 从短间隔开始(25ms, 对于vxWorks 75ms)
- 每次间隔加倍
- 直到它变得大于15s,接着它保持在15s。发出大于10个beacons和40s变成准备状态
4) 客户端监视这些beacons
- 确定连接状态,是否再次发送搜索
虚电路断开
1) 3.13和早期3.14
- 挂断消息或者30秒内无来自服务程序的响应。
- 如果不是一个挂断,则客户端发送"你在吗"的请求。
- 如果5秒无响应,TCP连接被关闭
- MEDM窗口变白。
- 客户端再次发送搜索请求
2)3.14.5以及以后
- 来自服务程序的挂断消息。
- TCP连接被关闭
- MEDM变白
- 客户端再次发送搜索请求
虚拟电路无响应
3.14.5以及以后
1)30秒内无来自服务程序的响应。
2)客户端接着发送"你在吗"的请求
3)如果5秒内无响应,TCP连接不被关闭,至少若干小时
4)MEDM窗口变白
5)客户端不重新发送搜索请求,有利于网络风暴
6)不调用ca_poll的客户端经常见到虚电路断开,即使服务程序可能是正常的。
- 为3.13编写但使用3.14的客户端会遇到问题
- 在以后版本中可能被修改。
重要的环境变量
1) EPICS_CA_ADDR_LIST
a.决定搜索哪里
b.是一个列表(由空格分隔)
如:"192.168.1.255 172.16.2.20 10.30.55.50"
c.默认是主机上所有网卡的广播地址
当服务器和客户端在相同子网时有效
d.广播地址
- 发送给一个子网上所有服务器
- 示例:192.168.1.255
- 在UNIX上使用ifconfig -a查找它
2) EPICS_CA_AUTO_ADDR_LIST
- YES:在搜索中包含以上默认地址
- NO:不要再默认地址上搜索
- 如果你设置了EPICS_CA_ADDR_LIST, 通常设置这个为NO。
其它重要的环境变量
1)CA客户端
- EPICS_CA_ADDR_LIST
- EPICS_CA_AUTO_ADDR_LIST
- EPICS_CA_CONN_TMO
- EPICS_CA_BEACON_PERIOD
- EPICS_CA_REPEATER_PORT
- EPICS_CA_SERVER_PORT
- EPICS_CA_MAX_ARRAY_BYTES
- EPICS_TS_MIN_WEST
2)CA服务器
- EPICS_CAS_SERVER_PORT
- EPICS_CAS_AUTO_BEACON_ADDR_LIST
- EPICS_CAS_BEACON_PERIOD
- EPICS_CAS_BEACON_PORT
- EPICS_CAS_INFT_ADDR_LIST
- EPICS_CAS_IGNORE_ADDR_LIST
3.13和3.14类似
- 为了使为3.13编写的客户端在没有编码修改下对3.14有效,付出了非常大的努力
- 甚至像MEDM的大型程序只需要进行少量的修改
- 这表示已有程序一般不需要被重新编写。
- 相比较,在切换到3.14中通道访问服务器需要很多更改
3.13和3.14不同
1) 3.14是线程的
你的程序不是必须是线程的。
2)3.14对某些功能有不同的名称。
- ca_context_create对应ca_task_initialize
- ca_context_destroy对应ca_task_exit
- ca_create_channel对应ca_search_and_connect
- ca_create_subscription对应ca_add_event
- ca_clear_subscription对应ca_clear_event
- 新函数可能有更多功能,通常与线程相关
- 我们将使用新名称。
3)3.14对丢失连接有不同机制
- 虚电路无响应(3.13中不可用)。
- 虚电路断开
编写一个通道访问客户端的基本过程
1)初始化通道访问
ca_task_initialize或ca_context_create
2)搜索
ca_search_and_conntect或ca_create_channel
3)进行读或写
ca_get或ca_put
4)监视
ca_add_event或ca_create_subscription
5)让通道访问有机会运行
ca_poll, ca_pend_io, ca_pend_event
6)关闭通道访问
ca_task_exit或ca_context_destroy
所有C或C++程序必须包含cadef.h:#include <cadef.h>
库函数介绍
1)ca_context_create
enum ca_preemptive_callback_select{
ca_disable_preemptive_callback,
ca_enable_preemptive_callback
};
int ca_context_create(enum ca_preemptive_callback_select SELECT);
- 在任何其它调用前被调用一次
- 设置通道访问
- 除非你想要使用多线程,否则SELECT=ca_disable_preemptive_callback
- 为了3.13兼容性,也可以使用ca_task_initialize()
2) ca_context_destroy
void ca_context_destroy()
- 在退出你的程序前,应该被调用。
- 关闭通道访问。
- 为了3.13兼容性,也可以使用ca_task_exit()
3) ca_create_channel
typedef void caCh(struct connection_handler_args ARGS);
int ca_create_channel(
const char * PVNAME,
caCh * CALLBACK,
void * PUSER,
capri PRIORITY,
chid * PCHID
);
a) 设置一个通道访问并且开始搜索过程。
b) PVNAME是过程变量的名称。
c) CALLBACK是你连接回调(或者NULL)
- 当连接状态变化时,包括首次连接,将调用这个回调
- 有关这个通道的消息被包含在ARGS中
- 如果你不需要一个回调,使用NULL
d) PUSER是一种传递其它参数的方式
- 你拥有的任何东西被存储在这个地址中
- 它被存储在chid中
- 在C++中,它经常是对应一个类的this指针
- 如果你不需要它,使用NULL
e) 使用PRIORITY=CA_PRIORITY_DEFAULT
f) 一个chid是一个指向一个不透明struct的指针(地址),通道访问使用它存储有关这个通道大部分信息。chanId与chid相同(typdef chid chanId)
g) PCHID是chid指针的地址(使用&CHID)
- 在进行调用前,你需要为chid分配空间
- 通道访问将为这个struct分配空间并且返回它的地址
h) 使用宏访问chid中的信息
- ca_name(CHID):获取这个过程变量的名称
- ca_state(CHID):获取连接状态
- ca_puser(CHID):获取你指定的PUSER
i) ARGS struct在连接回调中包含chid
j) 为了3.13兼容性也可以使用ca_search_and_connect()
4) ca_clear_channel
int ca_clear_channel(chid CHID);
- 关闭一个通道访问并且回收资源。
- 在退出这个程序前,应该被调用。
- CHID与在ca_create_channel中使用相同的chid
5) ca_array_get
int ca_array_get(
chtype TYPE,
unsigned long COUNT,
chid CHID,
void * PVALUE
);
a) 从过程变量请求一个标量或者值的数组
b) 一般之后为ca_pend_io
c) TYPE是你变量的外部类型
- 使用db_access.h中DBR_XXX类型之一
- 例如:DBR_DOUBLE或DBR_STRING
d) COUNT:要读取的数组元素数目。
e) CHID:来自ca_create_channel的通道标识符。
f) PVALUE:你想要获取值存入的位置。必须有足够空间保存这些值。
6) ca_array_get_callback
typedef void (*pCallback)(struct event_handler_args ARGS);
int ca_array_get_callback(
chtype TYPE,
unsigned long COUNT,
chid CHID,
pCallback USERFUNC,
void * USERARG
);
a) 使用一个回调,从一个过程变量请求一个标量或者值的数组。
b) TYPE:是你变量的外部类型。
- 使用db_access.h中DBR_XXXX类型之一。
- 例如:DBR_DOUBLE或DBR_STRING
c) COUNT:要读取的数组元素数目
d) CHID:是来自ca_create_channel的通道标识符。
e) USERFUNC:在操作结束时要被运行的你的回调的名称。
f) USERARGS:一种传递其它信息给这个回调的方式
- struct event_handler_args有一个void * usr成员
7) ca_array_put
int ca_array_put(
chtype TYPE,
unsigned long COUNT,
chid CHID,
const void * PVALUE
);
a) 请求写一个标量或者值数组到一个过程变量
b) 一般之后为ca_pend_io
c) TYPE是你提供的外部类型
- 使用db_access.h中DBR_XXXX类型之一
- 例如:DBR_DOUBLE或DBR_STRING
d) COUNT是要写的数组元素的数目。
e) CHID是来自ca_create_channel的通道标识符。
f) PVALUE是从哪里寻找要被写的值。
8) ca_array_put_callback
typedef void (*pCallback)(struct event_handler_args ARGS);
int ca_array_put_callback(
chtype TYPE,
unsigned long COUNT,
chid CHID,
const void * PVALUE,
pCallback USERFUNC,
void * USERARG
);
a) 请求写一个标量或值数组到一个过程变量
b) TYPE是你变量的外部类型
- 使用db_access.h中DBR_XXXX类型之一
- 例如:DBR_DOUBLE或DBR_STRING
c) COUNT:要写的数组元素数目。
d)CHID:来自ca_create_channel的通道标识符。
e) PVALUE:查找要被写值的位置。
f) USERFUNC:当操作结束时,要被运行的你的回调的名称。
g) USERARG:一种传递其它信息给这个回调的方法。
- struct event_handler_args有一个void * usr成员。
9) ca_create_subscription
typedef void (*pCallback)(struct event_handler_args ARGS);
int ca_create_subscriptino(
chtype TYPE,
unsigned long COUNT,
chid CHID,
unsigned long MASK,
pCallback USERFUNC,
void * USERARG,
evid * PEVID
);
a) 指定一个回调函数,当这个过程变量经历显著状态变化时,被调用。
- 值,警报状态,警报严重性
- 这是监视一个过程变量的方法
b) TYPE是你想要返回的外部类型
- 使用db_access.h中DBR_XXXX类型之一
- 例如:DBR_DOUBLE或DBR_STRING
c) COUNT是要监视的数组元素数目
d) CHID是来自ca_create_channel的通道标识符。
e) MASK有对应于请求的事件触发类型的位集合
- DBE_VALUE:值变化。
- DBE_LOG:超过存档死区。
- DBE_ALARM:警报状态变化
f) USERFUNC在状态变化发生时要被运行的你的回调的名称。
g) USERARG是一种传递其它信息给回调的方法。
- struct event_handler_args有一个void *成员。
h) PEVID是一个evid(event id)的地址。
- 在进行调用前你需要为evid分配空间
- 类似chid
- 仅用于清理订阅(如果不需要,可以是NULL)
10) ca_clear_subscription
int ca_clear_subscription(evid EVID);
- 用于移除一个monitor回调
- EVID是来自ca_create_subscription的evid
11)ca_add_exception_event
typedef void (* pCallback)(struct exception_handler_args ARGS);
int ca_add_exception_event(
pCallback USERFUNC,
void *USERARG
);
a)用于替代默认的exception处理程序。
b) USERFUNC是在一个异常发生时要被运行的你的回调的名称。
- 使用NULL移除回调。
c) USERARG是一种传递其它信息给这个回调的方式。
- struct exception_handler_args有一个void * usr成员
请求处理
1) 先前的例程是请求
- 它们仅排队这个操作
- 它们基本不出错。返回值基本总是ECA_NORMAL,但它们应该被检查。
2)仅在一下之一被调用时,这些请求才被处理。
- ca_pend_io:在请求被处理前阻塞。
- ca_pend_event:阻塞指定的时间。
- ca_poll:仅处理当前工作。
3)如果这些例程没有被调用,请求不被处理并且后台任务也不被处理。
4)规则是这些之一应该每100ms被调用:允许后台任务运行(beacons等)。
1) ca_pend_io
int ca_pend_io(double TIMEOUT);
a)清空发送缓存。
b) 在以下发生前,最多阻塞TIMEOUT秒
- 待处理gets结束
- 没有回调已经连接的搜索
c) 当gets和搜索结束时,返回ECA_NORMAL
d)返回ECA_TIMEOUT,否则
- 表示某过程出错
- get请求可以被再次发出
- 在ca_clear_channel后,搜索请求可以被再次发出。
e) 通道访问后台任务被执行
- 除非没有待处理的I/O请求
f) 与不使用回调的搜索,gets和puts一起使用。
2) ca_pend_event
int ca_pend_event(double TIMEOUT);
a) 清空发送缓存。
b) 在TIMEOUT秒内运行后台任务。
- 在TIMEOUT秒耗尽时才返回。
c) 当你的程序不是必须做任何其它事情时使用这个函数。
d) 使用ca_pend_event替代sleep
3) ca_poll
int ca_poll();
a) 清空发送缓存。
b) 仅运行待处理任务。
- 当没有待处理任务时退出,否则类似于ca_pend_event
c) 当你的程序有要做的其它事情时,使用这个函数。
- 例如:大多数GUI程序。
d)确认它至少每100ms被调用。
4) CHID宏
chtype ca_field_type(CHID);
unsigned ca_element_count(CHID);
char * ca_name(CHID);
void * ca_puser(CHID);
void ca_set_puser(chid CHID, void * puser);
enum channel_state{
cs_never_conn, //有效chid,服务器未找到或者不可用
cs_prev_conn, //有效chid,先前连接到服务器
cs_conn, //有效chid,连接了服务器
cs_closed //用户删除了通道
};
char * ca_host_name(CHID);
char * ca_read_access(CHID);
char * ca_write_access(CHID)
5) ca_connection_handler_args
struct ca_connection_handler_args{
chidId chid; // 通道id
long op; // CA_OP_CONN_UP/CA_OP_CONN_DOWN
};
- 在连接回调中使用
- 注意:使用了chanId而不是chid,某些编译器不认chid chid
6) event_handler_args
typedef struct event_handler_args{
void * usr; // 提供给请求的用户参数
chanId chid; //通道ID
long type; //返回项的类型
long count; //返回项的元素数目
const void * dbr; //指向返回项的指针
int status; //请求op的ECA_XXX状态
};
- 在get, put和monitor回调中使用
- 如果status不是ECA_NORMAL,不要使用dbr中的值。
7) 通道访问API函数
简单的CA客户端
1) 使用以下数据库文件,此数据库文件由两个记录组成一个wavefrom记录和一个stringin记录。
record(waveform, "$(USER):wfin") {
field(DESC, "A Example Waveform")
field(SCAN, "Passive")
field(NELM, "10")
field(FTVL, "LONG")
}
record(stringin, "$(USER):StrIn") {
field(DESC, "A Example StringIn")
field(SCAN, "Passive")
field(VAL, "HelloWorld")
field(PINI, "YES")
}
2)将以上数据库加载到一个IOC中产生一个两个记录实例:
epics> dbl
TEST:StrIn
TEST:wfin
3) 用EPICS base自带的通道访问命令测试以上两个记录:
orangepi@orangepi4-lts:~/host_program/host/hostApp$ caget TEST:StrIn
TEST:StrIn HelloEveryOne
orangepi@orangepi4-lts:~/host_program/host/hostApp$ caget TEST:wfin
TEST:wfin 10 1 2 3 4 5 6 7 8 9 10
4) 用以上讲解的通道访问API函数写一个自己的通道访问程序,源代码如下:
orangepi@orangepi4-lts:~/host_program/host/hostApp$ cat simpleget.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* EPICS header */
#include "cadef.h"
#define epicsAlarmGLOBAL
#include "epicsEvent.h"
#include "epicsMutex.h"
#define TIMEOUT 1.0
#define SCA_OK 1
#define SCA_ERR 0
#define MAX_STRING 40
int main(int argc, char **argv)
{
int stat;
chid pCh;
char pvname[20] = {0};
char svalue[100];
int ivalue[20];
void * pvalue;
if (argc != 2){
printf("Usage: %s channelname\n", argv[0]);
exit(1);
}
strcpy(pvname, argv[1]);
printf("PVNAME: %s\n", pvname);
/* Initialize channel access */
stat = ca_context_create(ca_disable_preemptive_callback);
if (stat != ECA_NORMAL){
printf("ca_context_create failed:\n%s\n",ca_message(stat));
exit(1);
}
else{
printf("channel accesss initialized successfully\n");
}
/* create the pv */
stat = ca_create_channel(pvname, NULL, NULL, CA_PRIORITY_DEFAULT, &pCh);
if (stat != ECA_NORMAL){
printf("ca_create_channel failed:\n%s\n", ca_message(stat));
goto EXIT;
}
else{
printf("PV for channel name %s created successfully\n", pvname);
}
/* call ca_pend_io to process the search */
stat = ca_pend_io(TIMEOUT);
if (stat != ECA_NORMAL)
{
printf("search for PV:[%s] timed out after %g sec", pvname, TIMEOUT);
goto DESTROY;
}
/* Macro TEST */
printf("MACRO TEST:\n");
int request_type = ca_field_type(pCh);
printf("ca_field_type(CHID): %d\n", request_type);
long request_count = ca_element_count(pCh);
printf("ca_element_count(CHID): %ld\n", request_count);
printf("ca_name(CHID):%s\n", ca_name(pCh));
printf("ca_state(CHID): %d\n", ca_state(pCh));
printf("ca_host_name(CHID):%s\n",ca_host_name(pCh));
printf("ca_read_access(CHID):%d\n", ca_read_access(pCh));
printf("ca_write_access(CHID):%d\n", ca_write_access(pCh));
printf("DBR_STRING: %d\n", DBR_STRING);
printf("DBR_LONG: %d\n", DBR_LONG);
/* Request Get */
if (request_type == DBR_STRING){
pvalue = svalue;
}
else if(request_type == DBR_LONG){
pvalue = ivalue;
}
else{
printf("NO SUPPORT TYPE: %d\n", request_type);
goto DESTROY;
}
/* Request the get */
stat = ca_array_get(request_type, request_count, pCh, pvalue);
if (stat != ECA_NORMAL){
printf("ca_array_get failed:\n%s\n", ca_message(stat));
goto DESTROY;
}
/* call ca_pend_io to get the values */
stat = ca_pend_io(TIMEOUT);
if (stat != ECA_NORMAL){
printf("get %s timed out after %g sec\n%s\n", pvname, TIMEOUT, ca_message(stat));
goto DESTROY;
}
if (request_type == DBR_STRING){
printf("PV[%s] ===> VAlUE[%s]\n", pvname, svalue);
}
else if (request_type == DBR_LONG){
int i, ret = 0;
for (i = 0; i < request_count; i++){
ret += sprintf(svalue + ret, "%d ", ivalue[i]);
}
printf("PV[%s] ==> VALUE[%s]\n", pvname, svalue);
}
DESTROY:
stat = ca_clear_channel(pCh);
if (stat != ECA_NORMAL){
printf("ca_clear_channel failed [%s] \n%s\n",pvname,ca_message(stat));
goto EXIT;
}
/* clean up channel */
EXIT: ca_context_destroy();
printf("channel access context destroyed and exits the program\n");
return 0;
}
编译以上程序代码,并且对以上记录执行,结果如下:
orangepi@orangepi4-lts:~/host_program/host/hostApp$ O.linux-aarch64/simpleget TEST:StrIn
PVNAME: TEST:StrIn
channel accesss initialized successfully
PV for channel name TEST:StrIn created successfully
MACRO TEST:
ca_field_type(CHID): 0
ca_element_count(CHID): 1
ca_name(CHID):TEST:StrIn
ca_state(CHID): 2
ca_host_name(CHID):192.168.50.184:5064
ca_read_access(CHID):1
ca_write_access(CHID):1
DBR_STRING: 0
DBR_LONG: 5
PV[TEST:StrIn] ===> VAlUE[HelloEveryOne]
channel access context destroyed and exits the program
orangepi@orangepi4-lts:~/host_program/host/hostApp$ O.linux-aarch64/simpleget TEST:wfin
PVNAME: TEST:wfin
channel accesss initialized successfully
PV for channel name TEST:wfin created successfully
MACRO TEST:
ca_field_type(CHID): 5
ca_element_count(CHID): 10
ca_name(CHID):TEST:wfin
ca_state(CHID): 2
ca_host_name(CHID):192.168.50.184:5064
ca_read_access(CHID):1
ca_write_access(CHID):1
DBR_STRING: 0
DBR_LONG: 5
PV[TEST:wfin] ==> VALUE[1 2 3 4 5 6 7 8 9 10 ]
channel access context destroyed and exits the program
通过以上实例程序编写和测试,可以进一步加强我们对EPICS通道访问函数的理解。