实战Linux Bluetooth编程(三) HCI层编程
作者:Sam (甄峰) sam_code@hotmail.com
(HCI协议简介,HCI在BlueZ中的实现以及HCI编程接口)
1. HCI层协议概述:
HCI提供一套统一的方法来访问Bluetooth底层。如图所示:
从图上可以看出,Host ControllerInterface(HCI)
在Host这一端:application,SDP,L2cap等协议都是软件形式提出的(Bluez中是以kernel层程序)。在Module这一端:LinkManager, BB, 等协议都是硬件中firmware提供的。
而HCI则比较特殊,它一部分在软件中实现,用来给上层协议和程序提供访问接口(Bluez中,hci.chci_usb.c,hci_sock.c等).另一部分也是在Firmware中实现,用来将软件部分的指令等用底层协议明白的方式传递给底层。
居于PC的上层程序与协议和居于Modules的下层协议之间通过HCI沟通,有4种不同形式的传输:Commands,Event, ACL Data, SCO/eSCO Data。
1.1. HCI Command:
HCI Command是Host向Modules发送命令的一种方式。HCI Command Packet结构如下:
OpCode用来唯一标识HCI Command.它由2部分组成,10bit的Opcode Command.6bit的Opcode Group。
1.1.1: OpCode Group:
Linux Kernel(BlueZ)中,~/include/net/bluetooth/hci.h中定义了OpCodeGroup。
#define OGF_LINK_CTL
#defineOGF_LINK_POLICY
#define OGF_HOST_CTL
#defineOGF_INFO_PARAM
#defineOGF_STATUS_PARAM
它们代表了不同的Command Group:
OGF_LINK_CTL: Link control,这个CommandGroup中的Command允许Host控制与其它bluetooth device 的连接。
OGF_LINK_POLICY
OGF_HOST_CTL: Control and Baseband.
1.1.2: Opcode Command:
用来在同一个Group内唯一识别Command。~/include/net/bluetooth/hci.h中定义。
1.2: HCI Event:
Modules向Host发送一些信息,使用HCI Event。Event Packet结构如下:
HCI Event分3种:Command complete Event, Command StatesEvent,Command Subsequently Completend.
Command complete Event:如果Host发送的Command可以立刻有结果,则会发送此类Event。也就是说,如果发送的Command只与本地Modules有关,不与remote设备打交道,则使用Commandcomplete Event。例如:HCI_Read_Buffer_Size.
Command StatesEvent:如果Host发送的Command不能立刻得知结果,则发送此类Event。Host发送的Command执行要与Remote设备打交道,则必然无法立刻得知结果,所以会发送CommandStates Event.例如:
HCI Connect。
Command SubsequentlyCompletend:Command延后完成Event。例如:连接已建立。
下图是一个Command-Event例子:
从这里可以看出,如果Host发送的Command是与Remote device有关的,则会先发送CommandStates Event
HCI ACL与SCO数据,这里就不多讲了。只需要明白,l2cap数据是通过ACL数据传输给remotedevice的。
下图很明白的展示了l2cap数据如何一步一步转化为USB数据并传递给底层协议的。
很明显,一个l2cap包会按照规则先切割为多个HCI数据包。HCI数据包再通过HCI-usb这一层传递给USB设备。每个包又通过USBdriver发送到底层。
2. HCIprotocol的实现:
(稍后添加)
3. HCI 层的编程:
正如上一节所说,HCI是沟通上层协议以及程序与底层硬件协议的通道。所以,通过HCI发送的Command都是上层协议或者应用程序发送给BluetoothDongle的。它命令Bluetooth Dongle(或其中的硬件协议)去做什么何种动作。
3.0:得到Host上插入Dongle数目以及Dongle信息:
我们先复习一下socket的概念:
使用函数socket()建立一个Socket,就如同你有一部电话.bind()则是把这个电话和某个电话号码(网络地址)对应起来。
类似的,我们可以把Host理解为一个房间,这个房间有多部电话(Dongle)。
当使用socket() 打开一个HCIprotocol的socket,表明得到这个房间的句柄。HOST可能会有多个Dongle。换句话说,这个房间可以有多个电话号码。所以HCI会提供一套指令去得到这些Dongle。
//0.
struct hci_dev_list_req *dl;
struct hci_dev_req *dr;
struct hci_dev_info di;
int i;
//1. 打开一个HCI socket.此socket相当于一个房间。
if ((ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI))< 0) {
// 2. 使用HCIGETDEVLIST,得到所有dongle的DeviceID。存放在dl中。
// 3 使用HCIGETDEVINFO,得到对应Device ID的Dongle信息。
di.dev_id = (dr+i)->dev_id;
ioctl(ctl, HCIGETDEVINFO, (void *) &di);
这样就能得到所有Dongle信息。
struct hci_dev_info {
};
3.0.1: UP和Down Bluetooth Dongle:
ioctl(ctl, HCIDEVUP, hdev)
ioctl(ctl, HCIDEVDOWN, hdev)
ctl:为使用socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI)打开的Socket.
hdev: Dongle Device ID.(所以上面的Socket不需要bind,因为这边指定了)
3.1BlueZ提供的HCI编程接口一(针对本地Dongle的API系列):
3.1。1 打开一个HCI Socket---int hci_open_dev(intdev_id):
这个function用来打开一个HCI Socket。它首先打开一个HCIprotocol的Socket(房间),并将此Socket与deviceID=参数dev_id的Dongle绑定起来。只有bind后,它才将Socket句柄与Dongle对应起来。
注意,所有的HCI Command发送之前,都需要使用hci_open_dev打开并绑定。
3.1.2: 关闭一个HCI Socket:
int hci_close_dev(int dd)//简单的关闭使用hci_open_dev打开的Socket。
3.1.3: 向HCI Socket(对应一个Dongle)发送request:
int hci_send_req(int dd, structhci_request *r, int to)
BlueZ提供这个function非常有用,它可以实现一切Host向Modules发送Command的功能。
参数1:HCI Socket。
参数2:Command内容。
参数3:以milliseconds为单位的timeout.
下面详细解释此function和用法:
当应用程序需要向Dongle(对应为一个bind后的Socket)发送Command时,调用此function.
其中,参数一dd对应一个使用hci_open_dev()打开的Socket(Dongle)。
参数三to则为等待Dongle执行并回复命令结果的timeout.以毫秒为单位。
参数二hci_request * r 最为重要,首先看它的结构:
struct hci_request {
};
ogf,ocf不用多说,对应前面的图就明白这是Group Code和Command Code。这两项先确定下来,然后可以查HCISpec。察看输入参数(cparam)以及输出参数(rparam)含义。至于他们的结构以及参数长度,则在~/include/net/bluetooth/hci.h中有定义。
至于event.如果设置,它会被setsockopt设置于Socket。
例1:得到某个连接的Policy Setting.
HCISpec以及~/include/net/bluetooth/hci.h中均可看到,OGF=OGF_LINK_POLICY(0x02).OCF=OCF_READ_LINK_POLICY(0x0C).
因为这个Command用来读取某个ACL连接的Policy Setting。所以输入参数即为此连接Handle.
返回参数则包含3部分,status(Command是否顺利执行), handle(连接Handle)。policy(得到的policy值)
这就又引入了一个新问题,如何得到某个ACL连接的Handle。
可以使用ioctl HCIGETCONNINFO得到ACL 连接Handle。
ioctl(dd, HCIGETCONNINFO, (unsigned long) cr);
Connect_handle =htobs(cr->conn_info->handle);
所以完整的过程如下:
struct hci_request HCI_Request;
// 1.得到ACL Connect Handle
if (ioctl(dd, HCIGETCONNINFO, (unsigned long) cr)< 0)
memset(&HCI_Request, 0,sizeof(HCI_Request));
// 2.填写Command输入参数
HCI_Request.ogf = OGF_LINK_POLICY;
if (hci_send_req(dd, &HCI_Request, to)< 0)
//如果返回值状态不对
//得到当前policy
3.1.4:几个更基础的function:
static inline void bacpy(bdaddr_t *dst, const bdaddr_t *src)//bdaddr copy
static inline int bacmp(const bdaddr_t *ba1, const bdaddr_t*ba2)//bdaddr 比较
3.1.5: 得到指定Dongle BDAddr:
int hci_read_bd_addr(int dd, bdaddr_t *bdaddr, int to);
参数1:HCISocket,使用hci_open_dev()打开的Socket(Dongle)。
参数2:输出参数,其中会放置bdaddr.
参数3:以milliseconds为单位的timeout.
3.1.6: 读写Dongle Name:
int hci_read_local_name(int dd, int len, char *name, int to)
int hci_write_local_name(int dd, const char *name, int to)
参数1:HCISocket,使用hci_open_dev()打开的Socket(Dongle)。
参数2:读取或设置Name。
参数3:以milliseconds为单位的timeout.
注意:这里的Name与IOCTL HCIGETDEVINFO 得到hci_dev_info中的name不同。
3.1.7:得到HCI Version:
int hci_read_local_version(int dd, struct hci_version *ver, intto)
3.1.8:得到已经UP的Dongle BDaddr:
int hci_devba(int dev_id, bdaddr_t *bdaddr);
dev_id: Dongle Device ID.
bdaddr:输出参数,指定Dongle如果UP, 则放置其BDAddr。
3.1.9: 得到Dongle Info:
int hci_devinfo(int dev_id, struct hci_dev_info *di)
dev_id: Dongle Device ID.
di: 此Dongle信息。
出错返回 -1。
注意,这个Function的做法与3.0的方法完全一致。
3.1.10:从hciX中得到X:
int hci_devid(const char *str)
str: 类似 hci0这样的字串。
如果hciX对应的Device ID(X)是现实存在且UP。则返回此设备DeviceID。
3.1.11:得到BDADDR不等于参数bdaddr的DongleDevice ID:
int hci_get_route(bdaddr_t *bdaddr)
查找Dongle,发现Dongle Bdaddr不等于参数bdaddr的第一个Dongle,则返回此Dongle DeviceID。
所以,如果: int hci_get_route(NULL),则得到第一个可用的Dongle Device ID。
3.1.12: 将BDADDR转换为字符串:
int ba2str(const bdaddr_t *ba, char *str)
3.1.13: 将自串转换为BDADDR:
int str2ba(const char *str, bdaddr_t *ba)
3.2BlueZ提供的HCI编程接口二(针对Remote Device的API系列):
3.2.1
int hci_inquiry(int dev_id, int len, int nrsp, const uint8_t*lap, inquiry_info **ii, long flags)
hci_inquiry()用来命令指定的Dongle去搜索周围所有bluetoothdevice.并将搜索到的Bluetooth Device bdaddr 传递回来。
参数1:dev_id:指定Dongle Device ID。如果此值小于0,则会使用第一个可用的Dongle。
参数2:len: 此次inquiry的时间长度(每增加1,则增加1.25秒时间)
参数3:nrsp:此次搜索最大搜索数量,如果给0。则此值会取255。
参数4:lap:BDADDR中LAP部分,Inquiry时这块值缺省为0X9E8B33.通常使用NULL。则自动设置。
参数5:ii:存放搜索到BluetoothDevice的地方。给一个存放inquiry_info指针的地址,它会自动分配空间。并把那个空间头地址放到其中。
参数6:flags:搜索flags.使用IREQ_CACHE_FLUSH,则会真正重新inquiry。否则可能会传回上次的结果。
返回值是这次Inquiry到的Bluetooth Device 数目。
注意:如果*ii不是自己分配的,而是让hci_inquiry()自己分配的,则需要调用bt_free()来帮它释放空间。
3.2.2:得到指定BDAddr的reomte device Name:
int hci_read_remote_name(int dd, const bdaddr_t *bdaddr, intlen, char *name, int to)
参数1:使用hci_open_dev()打开的Socket。
参数2:对方BDAddr.
参数3:name 长度。
参数4:(out)放置name的位置。
参数5:等待时间。
3.2.3: 读取连接的信号强度:
int hci_read_rssi(int dd, uint16_t handle, int8_t *rssi, intto)
注意,所有对连接的操作,都会有一个参数,handle.这个参数是连接的Handle。前面讲过如何得到连接Handle的。
对于hci层的编程 是相当复杂的 功能也是强大的,建议学习者 去读bluez-lib中的hci和sdp
这里简要介绍 我在开发公司的项目中的 一个小的基础动作. 该编程的过程基本理清了hci和sdp的衔接和应用.希望对学习者有帮助
说明有不到之处,望大家多多研究,不要局限,每个人有自己的方式,也许你的在逻辑算法上比我的好!!
//scan是一个利用hci层协议 获得远程蓝牙设备的蓝牙地址和蓝牙昵称的函数;
int scan()
{
inquiry_info *ii = NULL;
int max_rsp, num_rsp;
int dev_id, sock, len, flags;
int i;
char addr[19] = { 0 };
char name[248] = { 0 };
dev_id = hci_get_route(NULL);
printf("\nhci%d is scanning......\n",dev_id);
//dev_id = lc[scannum].name;
sock = hci_open_dev( dev_id );
if (dev_id < 0 || sock < 0) {
system("reboot");
perror("opening socket");
return 0;
}
bdaddr_t src;
bacpy(&src, BDADDR_ANY);
len = 8;
max_rsp = 255;
flags = IREQ_CACHE_FLUSH;
ii = (inquiry_info*)malloc(max_rsp * sizeof(inquiry_info));
num_rsp = hci_inquiry(dev_id, len, max_rsp, NULL, &ii, flags);
if( num_rsp < 0 ) perror("hci_inquiry");
for (i = 0; i < num_rsp; i++) {
ba2str(&(ii+i)->bdaddr, addr);
memset(name, 0, sizeof(name));
intc=0;
if(sdp_get_channel_opush(&src, &(ii+i)->bdaddr, &c))
//printf("\n%d\n",c);
if(c>0)addadr(addr,c); //此处是我在做项目中做的一个与应用程序连接的接口函数,功能是完成远程蓝牙设备的地址和文件传输通道号添加到 我自己建的地址列表中.
//printf("chinal=%d\n",c);
// if (hci_read_remote_name(sock, &(ii+i)->bdaddr, sizeof(name),name, 0) < 0)
// strcpy(name, "[unknown]");
// printf("%s %s\n",addr, name);
}
free( ii );
close( sock );
return 0;
}
//sdp_get_channel_opush,这是一个我自己编写的通过 hci获得的远程蓝牙地址和初始化为零的 channel来获取远程蓝牙设备有无文件传输功能,并获得文件传输通道的过程.
intsdp_get_channel_opush(bdaddr_t *src, bdaddr_t *dst, int *channel)
{
uuid_t service;
sdp_session_t *session;
sdp_list_t *search, *attrs, *rsp;
uint16_t attr;
interr;
/* build search request */
sdp_uuid16_create(&service,OBEX_OBJPUSH_SVCLASS_ID);
search = sdp_list_append(0, &service);
attr= SDP_ATTR_PROTO_DESC_LIST;
attrs = sdp_list_append(NULL, &attr);
/*connect */
session = sdp_connect(src, dst,SDP_RETRY_IF_BUSY);
if(!session) return(-1);
/*send request */
err= sdp_service_search_attr_req(session, search, SDP_ATTR_REQ_INDIVIDUAL, attrs,&rsp);
/*close connection */
sdp_close(session);
printf("sdp_service_search_attr_req-return=%d\n",err);
if(err) return(0);
/*get rfcomm channel */
for(; rsp; rsp = rsp->next) {
sdp_record_t *rec = (sdp_record_t *) rsp->data;
sdp_list_t *protos;
if(!sdp_get_access_protos(rec, &protos)) {
int ch = sdp_get_proto_port(protos, RFCOMM_UUID);
printf("channel=%d/n",ch);
if (ch > 0) {
*channel = ch;
return(1);
}
}
}
return(0);
}
更多推荐
所有评论(0)