传输子系统的协议打算采用TCP来完成,开发板充当服务器,PC机充当客户机。传输视频其实也就是传输一幅幅图片,因此接下来的任务就时在服务器和客户机之间传输图片。这里面又涉及到了传输协议,我们采用申请式的协议,客户机发送一个图片请求,服务器就传送一副图片,如果没有任何请求,服务器将什么也不做。
1、协议设计
为了把Epoll用起来我们定义2个事件,分别是对可以对socket读和写的事件。当socket创建之后,显然是立马就可以写的(发送数据),因此我们初始化之后先添加读事件,等待客户机的请求,当收到请求之后,开始处理请求,这个时候把读事件关闭(挂起接收请求的任何),把写事件打开(开始传输数据),当传输结束后再把读事件打开、写事件关闭,这样就可以实现有序的传输图片。
对于请求包的头部包含3个字节,可以是请求一帧图片、获取图像格式、设置图像格式等,这3个字节又分为请求包长度、命令1(请求类型)、命令2(命令ID)。接下来是数据部分,数据部分的前4个字节又是数据包的长度,随后是数据。把这些构思好之后,就根据这个协议来传输图片,为了方便,可以把一些宏定义,比如说构造头部,获取请求ID。写出的头文件如下:
#ifndef __PROTOCOL_H__
#define __PROTOCOL_H__
#define VID_FRAME_MAX_SZ (0xFFFFF - FRAME_MAX_SZ)
#define FRAME_MAX_SZ 253
#define FRAME_DAT_MAX 253
#define FRAME_HDR_SZ 3
#define FRAME_ERR_SZ 3
#define TYPE_MASK 0xE0
#define TYPE_BIT_POS 5
#define SUBS_MASK 0x1F
#define LEN_POS 0
#define CMD0_POS 1
#define CMD1_POS 2
#define DAT_POS 3
/* Error codes */
#define ERR_SUCCESS 0 /* success */
#define ERR_SUBS 1 /* invalid subsystem */
#define ERR_CMD_ID 2 /* invalid command ID */
#define ERR_PARAM 3 /* invalid parameter */
#define ERR_LEN 4 /* invalid length */
#define TYPE_SREQ 0x1
#define TYPE_SRSP 0x2
#define SUBS_ERR 0x0
#define SUBS_SYS 0x1
#define SUBS_VID 0x3
#define SUBS_MAX 0x4
#define REQUEST(len, type, subs, id) (((len) << (8*LEN_POS)) | \
(((type) << TYPE_BIT_POS | (subs)) << (8*CMD0_POS)) | ((id) << (8*CMD1_POS)))
enum request {
SYS_VERSION = REQUEST(0x0, TYPE_SREQ, SUBS_SYS, 0x0),
/**
* VID SubSystem
*/
VID_GET_UCTLS = REQUEST(0x0, TYPE_SREQ, SUBS_VID, 0x0),
VID_GET_UCTL = REQUEST(0x4, TYPE_SREQ, SUBS_VID, 0x1),
VID_SET_UCTL = REQUEST(0x8, TYPE_SREQ, SUBS_VID, 0x2),
VID_SET_UCS2DEF = REQUEST(0x0, TYPE_SREQ, SUBS_VID, 0x3),
VID_GET_FRMSIZ = REQUEST(0x0, TYPE_SREQ, SUBS_VID, 0x10),
VID_GET_FMT = REQUEST(0x0, TYPE_SREQ, SUBS_VID, 0x11),
VID_REQ_FRAME = REQUEST(0x0, TYPE_SREQ, SUBS_VID, 0x20),
};
#define REQUEST_ID(req) (((req) >> (8*CMD1_POS)) & 0xFF)
#define REQUEST_TYPE(req) (((req) >> (8*CMD0_POS + TYPE_BIT_POS)) & TYPE_MASK)
#define REQUEST_SUBS(req) (((req) >> (8*CMD0_POS)) & SUBS_MASK)
#define REQUEST_LEN(req) (((req) >> (8*LEN_POS)) & 0xFF)
#endif
2、结构设计
为了方便代码框架的设计,定义一些结构:
传输子系统的结构,成员包括:服务器的socke文件,Epoll的fd,以及Epoll需要使用到的参数。
struct tcp_srv {
int sock;
int epfd;
void *arg;
};
在和客户机建立连接之后还需要保存一些客户机的信息,因此定义一个结构:
struct tcp_cli
{
int sock;//客户机的sockfd
struct sockaddr_in addr;//客户机的地址
struct tcp_srv *srv;//保存服务器的相关信息
struct event_ext *ev_tx;//发送数据的epoll事件
struct event_ext *ev_rx;//接收数据的epoll事件
char *buf;
int len;
unsigned char req[FRAME_MAX_SZ];//存放请求数据包
unsigned char rsp[FRAME_MAX_SZ + VID_FRAME_MAX_SZ];//存放发送数据包
};
3、代码设计
首先是传输子系统的初始化,包括建立TCP的socket,以及添加socket读事件到Epoll,函数如下:
int net_sys_init()
{
struct sockaddr_in addr;
struct sockaddr_in sin;
struct tcp_srv *s = calloc(1, sizeof(struct tcp_srv));
struct tcp_cli *c = calloc(1, sizeof(struct tcp_cli));
int new_sock;
int len;
//初始化传输子系统
s->epfd = srv_main->epfd;
//socket
s->sock = socket(AF_INET, SOCK_STREAM, 0);
//bind
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_family = AF_INET;
addr.sin_port = htons(DEF_TCP_SRV_PORT);
bind(s->sock, (struct sockaddr*)&addr, sizeof(struct sockaddr));
//listen
listen(s->sock, 5);
//accept
new_sock = accept(s->sock, (struct sockaddr*)&sin, &len);
c->sock = new_sock;
memcpy(&c->addr, &sin, len);
c->srv = s;
//将传输子系统的事件加入Epoll池,tcp_cli作为事件的参数
c->ev_rx = epoll_event_create(c->sock, EPOLLIN, rx_app_handler, c);
c->ev_tx = epoll_event_create(c->sock, EPOLLOUT, tx_app_handler, c);
//先加入rx的事件
epoll_add_event(c->srv->epfd, c->ev_rx);
//保存数据到srv_main中
srv_main->srv = s;
//return s;
return 0;
}
接下来就阻塞起来了,等到请求包的到了,当有了一个请求之后触发rx_app_handler函数,这个函数设计如下:
static void rx_app_handler(int sock, void *arg)
{
struct tcp_cli *c = arg;
int res = 0;
unsigned char *pbuf;
pbuf = &c->req[0];
res = read(c->sock, pbuf, FRAME_HDR_SZ);//读取头部数据
process_incoming(c);//根据请求ID进行处理
}
然后调用process_incoming进行处理,这个函数定义如下
/*请求包处理函数
形参:
c:连接到的客户机的结构*/
int process_incoming(struct tcp_cli *c)
{
struct cam *v = srv_main->cam;
__u8 *req = c->req;
__u8 *rsp = c->rsp;
__u8 id = req[CMD1_POS];
__u8 fmt_data[FRAME_DAT_MAX];
__u8 status = ERR_SUCCESS;
__u32 pos,len,size;
switch(id){
//获取图像格式
case REQUEST_ID(VID_GET_FMT):
//获取图像格式
cam_get_fmt(v, fmt_data);
//构造返回数据
build_ack(rsp, (TYPE_SRSP << TYPE_BIT_POS) | SUBS_VID, id, 4, fmt_data);
//发送返回数据
net_send(c, rsp, 4 + FRAME_HDR_SZ);
break;
//获取一帧图像
case REQUEST_ID(VID_REQ_FRAME):
pos = FRAME_HDR_SZ + 4;
//获取一帧图像
size = cam_get_trans_frame(v, &rsp[pos]);
//构造返回数据
build_ack(rsp, (TYPE_SRSP << TYPE_BIT_POS) | SUBS_VID, id, 4, (__u8*)&size);
//发送返回数据
net_send(c, rsp, pos + size);
break;
default:
status = ERR_CMD_ID;
break;
}
return status;
}
这里对获取一帧图像的流程做简要介绍,在摄像头子系统的设计中,如果获取到了一帧图像,会把图像数据保存在缓冲里面,而这个缓冲出队后,它的首地址和长度又保存到一个buf结构中,因此获取一帧图片,主要就是根据地址和长度把图片数据拷贝到返回数据包中,然后调用net_send发送数据。net_send函数如下:
void net_send(struct tcp_cli *tc, void *buf, int len)
{
struct tcp_cli *c = tc;
struct tcp_srv *s = c->srv;
epoll_del_event(s->epfd, c->ev_rx);
c->buf = buf;
c->len = len;
epoll_add_event(s->epfd, c->ev_tx);
}
这个函数非常简单,他只是把socket读事件删除(挂起接收请求包),然后保存发送数据包的信息,再添加socket写事件,什么时候可以写socke交给Epoll,让系统来判断,当可以写socket的时候会唤醒tx_app_handler,这个函数如下:
static void tx_app_handler(int sock, void *arg)
{
struct tcp_cli *c = arg;
struct tcp_srv *s = c->srv;
int res = 0;
res = send(sock, c->buf, c->len, 0);
if(res > 0)
{
c->len -= res;
if(c->len == 0)
{
epoll_del_event(s->epfd, c->ev_tx);
epoll_add_event(s->epfd, c->ev_rx);
}
}
}
这里面除了发送数据之外,还要判断是否发送成功,如果发送成功,可以添加读事件而关闭写事件,从而开始写一个轮回。如果写失败了可以返回相应的错误编码,这里没有设计。
到这里整个框架和代码就设计完成,至于客户机播放器方面,我们先提供一个编写好的程序用于测试,具体代码的设计后面再学。
代码和可执行程序在https://github.com/dayL-W/Video-capture-system.git
下载后,分别在开发板和PC机上运行wcamsrv和wcamclient,在运行PC机的客户端是会提示需要输入开发板的IP地址。
更多Linux资料及视频教程点击这里