一 Bluetooth基本概念
蓝牙是无线数据和语音传输的开放式标准,它将各种通信设备、计算机及其终端设备、各种数字数据系统、甚至家用电器采用无线方式联接起来。它的传输距离为10cm~10m,如果增加功率或是加上某些外设便可达到100m的传输距离。它采用2.4GHzISM频段和调频、跳频技术,使用权向纠错编码、ARQ、TDD和基带协议。TDMA每时隙为0.625μs,基带符合速率为1Mb/s。蓝牙支持64kb/s实时语音传输和数据传输,语音编码为CVSD,发射功率分别为1mW、2.5mW和100mW,并使用全球统一的48比特的设备识别码。由于蓝牙采用无线接口来代替有线电缆连接,具有很强的移植性,并且适用于多种场合,加上该技术功耗低、对人体危害小,而且应用简单、容易实现,所以易于推广。
蓝牙技术的系统结构分为三大部分:底层硬件模块、中间协议层和高层应用。底层硬件部分包括无线跳频(RF)、基带(BB)和链路管理(LM)。无线跳频层通过2.4GHz无需授权的ISM频段的微波,实现数据位流的过滤和传输,本层协议主要定义了蓝牙收发器在此频带正常工作所需要满足的条件。基带负责跳频以及蓝牙数据和信息帧的传输。链路管理负责连接、建立和拆除链路并进行安全控制。
二 AndroidBluetooth架构
1. Android蓝牙系统分为四个层次,内核层、BlueZ库、BlueTooth的适配库、BlueTooth的JNI部分、Java框架层、应用层。下面先来分析Android的蓝牙协议栈。
framework层:实现了Headset/Handsfree 和A2DP/AVRCPprofile,但其实现方式不同Handset/Handfree是直接在bluez的RFCOMMSocket上开发的,没有利用bluez的audioplugin,而A2DP/AVRCP是在bluez的audioplugin基础上开发的,大大降低了实现的难度。
Android的蓝牙协议栈采用BlueZ来实现,BlueZ分为两部分:内核代码和用户态程序及工具集。
library层:libbluedroid.so等
bluez层:这是bluez用户空间的库,开源的bluetooth代码,包括很多协议,生成libbluetooth.so。
Linuxkernel层:bluez协议栈、uart驱动,h4协议,hci,l2cap, sco, rfcomm
2. Bluetooth代码层次结构
(1)JAVA层
frameworks/base/core/java/android/bluetooth/ 包含了bluetooth的JAVA类。
(2)JNI层
frameworks/base/core/jni/android_bluetooth_开头的文件, 定义了bluez通过JNI到上层的接口。
frameworks/base/core/jni/android_server_bluetoothservice.cpp 调用硬件适配层的接口system/bluetooth/bluedroid/bluetooth.c
(3)bluez库
external/bluez/ 这是bluez用户空间的库,开源的bluetooth代码,包括很多协议,生成libbluetooth.so。
(4)硬件适配层
system/bluetooth/bluedroid/bluetooth.c 包含了对硬件操作的接口
system/bluetooth/data/*一些配置文件,复制到/etc/bluetooth/。还有其他一些测试代码和工具。
内核代码主要由BlueZ核心协议和驱动程序组成;蓝牙协议实现在内核源代码net/bluetooth中,驱动程序位于内核源代码目录driver/bluetooth中。用户态程序及工具集主要包括应用程序接口和BlueZ工具集,位于Android源代码目录externel/bluetooth(注:Android版本不一样,有的在externel/bluez目录下)中。
三 Bluetooth协议栈分析
1. 蓝牙协议栈
蓝牙协议栈的体系结构由底层硬件模块、中间协议层和高端应用层三部分组成。
(1)底层硬件模块
组成:
链路管理协议(LinkManagerProtocol,LMP);
基带(BaseBand,BB);
射频(RadioFrequency,RF)。
功能:
射频(RF)通过2.4GHz的ISM频段实现数据流的过滤和传输。
基带(BB)提供两种不同的物理链路,即同步面向连接链路(SynchronousConnection Oriented,SCO)和异步无连接链路(AsynchronousConnectionLess,ACL),负责跳频和蓝牙数据,及信息帧的传输,且对所有类型的数据包提供不同层次的前向纠错码 (FrequencyError Correction,FEC)或循环冗余度差错校验(CyclicRedundancyCheck,CRC)。
链路管理协议(LMP)负责两个或多个设备链路的建立和拆除,及链路的安全和控制,如鉴权和加密、控制和协商基带包的大小等,它为上层软件模块提供了不同的访问入口。
主机控制器接口(HostControllerInterface,HCI)是蓝牙协议中软硬件之间的接口,提供了一个调用下层BB、LMP、状态和控制寄存器等硬件的统一命令,上下两个模块接口之间的消息和数据的传递必须通过HCI的解释才能进行。
(2)中间协议层
组成:
逻辑链路控制和适配协议(LogicalLinkControl and Adaptation Protocol,L2CAP);
服务发现协议(ServiceDiscoveryProtocol,SDP);
串口仿真协议(或称线缆替换协议RFCOMM);
二进制电话控制协议(TelephonyControlprotocolSpectocol,TCS)。
功能:
L2CAP位于基带(BB)之上,向上层提供面向连接的和无连接的数据服务,它主要完成数据的拆装、服务质量控制、协议的复用、分组的分割和重组,及组提取等功能。
SDP是一个基于客户/服务器结构的协议,它工作在L2CAP层之上,为上层应用程序提供一种机制来发现可用的服务及其属性,服务的属性包括服务的类型及该服务所需的机制或协议信息。
RFCOMM是一个仿真有线链路的无线数据仿真协议,符合ETSI标准的TS07.10串口仿真协议,它在蓝牙基带上仿真RS-232的控制和数据信号,为原先使用串行连接的上层业务提供传送能力。
TCS定义了用于蓝牙设备之间建立语音和数据呼叫的控制信令(CallControl Signalling),并负责处理蓝牙设备组的移动管理过程。
(3)高端应用层
组成:
点对点协议(Point-to-PointProtocol,PPP);
传输控制协议/网络层协议(TCP/IP);
用户数据包协议(UserDatagramProtocol,UDP);
对象交换协议(ObjectExchangProtocol,OBEX);
无线应用协议(WirelessApplicationProtocol,WAP);
无线应用环境(WirelessApplicationEnvironment,WAE);
功能:
PPP定义了串行点对点链路应当如何传输因特网协议数据,主要用于LAN接入、拨号网络及传真等应用规范。
TCP/IP、UDP定义了因特网与网络相关的通信及其他类型计算机设备和外围设备之间的通信。
OBEX支持设备间的数据交换,采用客户/服务器模式提供与HTTP(超文本传输协议)相同的基本功能。可用于交换的电子商务卡、个人日程表、消息和便条等格式。
WAP用于在数字蜂窝电话和其他小型无线设备上实现因特网业务,支持移动电话浏览网页、收取电子邮件和其他基于因特网的协议。
WAE提供用于WAP电话和个人数字助理(PersonalDigitalAssistant,PDA)所需的各种应用软件。
2. Android与蓝牙协议栈的关系
蓝牙系统的核心是BlueZ,因此JNI和上层都围绕跟BlueZ的沟通进行。JNI和Android应用层,跟BlueZ沟通的主要手段是D-BUS,这是一套被广泛采用的IPC通信机制,跟Android框架使用的Binder类似。BlueZ以D-BUS为基础,给其他部分提供主要接口。
四 Bluetooth之HCI层分析
蓝牙系统的HCI层是位于蓝牙系统的L2CAP(逻辑链路控制与适配协议)层和LMP(链路管理协议)层之间的一层协议。HCI为上层协议提供了进入LM的统一接口和进入基带的统一方式。在HCI的主机(Host)和HCI主机控制器(HostController)之间会存在若干传输层,这些传输层是透明的,只需完成传输数据的任务,不必清楚数据的具体格式。目前,蓝牙的SIG规定了四种与硬件连接的物理总线方式:USB、RS232、UART和PC卡。其中通过RS232串口线方式进行连接具有差错校验。蓝牙系统的协议模型如图3所示。
1. HCI层与基带的通信方式
HCI是通过包的方式来传送数据、命令和事件的。所有在主机和主机控制器之间的通信都以包的形式进行。包括每个命令的返回参数都通过特定的事件包来传输。HCI有数据、命令和事件三种包,其中数据包是双向的,命令包只能从主机发往主机控制器,而事件包始终是主机控制器发向主机的。主机发出的大多数命令包都会触发主机控制器产生相应的事件包作为响应。
命令包分为六种类型:
a.链路控制命令;
b.链路政策和模式命令;
c.主机控制和基带命令;
d.信息命令;
e.状态命令;
f.测试命令。
事件包也可分为三种类型:
a.通用事件,包括命令完成包(CommandComplete)和命令状态包(CommandStatus);
b.测试事件;
c.出错时发生的事件,如产生丢失(FlushOccured)和数据缓冲区溢出(DataBuffer Overflow)。
数据包则可分为ACL和SCO的数据包。
2. 包的分析及研究
命令包:命令包中的OCF(OpcodeCommand Field)和OGF(OpcodeGroup Field)是用于区分命令种的。ParameterLength表示所带参数的长度,以字节数为单位,随后就是所带的参数列表。下面以Inquiry命令为例对HCI的命令包做具体说明。
在Inquiry命令中,OGF=0x01表示此命令属于链路控制命令,同时OCF=0x0001则表示此命令为链路控制命令中的Inquiry命令。OCF与OGF共占2字节,又由于底位字节在前,则它们在命令包为0x0104。在Inquiry命令中,参数ParameterLength为5。Inquiry命令带3个参数,第一个参数为LAP(lowaddress part), 它将用来产生Baseband中查询命令包的包头中的AccessCode。第二个参数为Inquiry_Length,它时表示在Inquiry命令停止前所定义的最大时间,超过此时间,Inquiry命令将终止。第三个参数为NUM_Response,它的值为0X00表示设备响应数不受限制,只为0x00-0xff则表示在Inquiry命令终止前最大的设备响应数。因此,若LAP=0x9e8b00,Inquiry_Length=0x05,NUM_Response=0x05,则协议上层调用Inquiry命令是HCI向基带发的明令包将为:0x0104 05 00 8b 9e 05 05。
事件包:事件包的EventCode用来区分不同的事件包,ParameterLength表示所带参数的长度,以字节数为单位,随后就是所带的参数列表。以CommandStatus Event事件包为例对HCI的事件包进行具体说明。
当主机控制器收到主机发来的如上面所提到的Inquiry命令包并开始处理时,它就会向主机发送CommandStatus Event事件包,此事件包为:0x0f04 00 0a 01 04。0xOf表示此事件包为CommandStatusEvent事件包,0x04表示此事件包带4字节长度的参数,0x00为此事件包的第一个参数即Status,表示命令包正在处理。0x0a为事件包的第二个参数NUM_HCI_Command_Packets,表示主机最多可在向主机控制器发10个命令包。0x0104 为第三个参数Command_Opcode,表示此事件包是对Inquiry命令包的响应。
数据包:ACL和SCO数据包中的ConnectionHandle即连接句柄是一个12比特的标志符,用于唯一确认两台蓝牙设备间的数据或语音连接,可以看作是两台蓝牙设备间唯一的数据通道的标识。两台设备间只能有一条ACL连接,也就是只有一个ACL的连接句柄,相应L2CAP的信道都是建立在这个连接句柄表示的数据通道上;两台设备间可以有多个SCO的连接,则一对设备间会有多个SCO的连接句柄。连接句柄在两设备连接期间一直存在,不管设备处于什么状态。在ACL数据包中,Flags分为PBFlag和BCFlag,PBFlag为包的界限标志,PBFlag=0x00表示此数据包为上层协议包(如L2CAP包)的起始部分;PBFlag=0x01表示此数据包为上层协议包(如L2CAP包)的后续部分。BCFlag为广播发送的标志,BCFlag=0x00表示无广播发送,只是点对点的发送;BCFlag=0x01表示对所有处于激活状态的从设备进行广播发送,BCFlag=0x02表示对所有的从设备包括处于休眠状态的从设备进行广播发送。ACL和SCO数据包中的DataTotal Length 都表示所载荷的数据的长度,以字节位单位。
3. 通信过程的研究与分析
当主机与基带之间用命令的方式进行通信时,主机向主机控制器发送命令包。主机控制器完成一个命令,大多数情况下,它会向主机发出一个命令完成事件包(CommandComplete Packet),包中携带命令完成的信息。有些命令不会收到命令完成事件,而会收到命令状态事件包(CommandStatusPacket),当收到该事件则表示主机发出的命令已经被主机控制器接收并开始处理,过一段时间该命令被执行完毕时,主机控制器会向主机发出相应的事件包来通知主机。如果命令参数有误,则会在命令状态事件中给出相应错误码。假如错误出现在一个返回CommandComplete事件包的命令中,则此CommandComplete事件包不一定含有此命令所定义的所有参数。状态参数作为解释错误原因同时也是第一个返回的参数,总是要返回的。假如紧随状态参数之后是连接句柄或蓝牙的设备地址,则此参数也总是要返回,这样可判别出此CommandComplete事件包属于那个实例的一个命令。在这种情况下,事件包中连接句柄或蓝牙的设备地址应与命令包种的相应参数一致。假如错误出现在一个不返回CommandComplete事件包的命令中,则事件包包含的所有参数都不一定是有效的。主机必须根据于此命令相联系的事件包中的状态参数来决定它们的有效性。
五 Bluetooth之编程实现
1. HCI层编程
HostController Interface(HCI)是用来沟通Host和Module。Host通常就是PC,Module则是以各种物理连接形式(USB,serial,pc-card等)连接到PC上的bluetoothDongle。
在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。
2. 相关函数学习
(1) int hci_open_dev(int dev_id);
这个function用来打开一个HCISocket。它首先打开一个HCIprotocol的Socket,并将此Socket与deviceID=参数dev_id的Dongle绑定起来。只有bind后,它才将Socket句柄与Dongle对应起来。
注意,所有的HCICommand发送之前,都需要使用hci_open_dev打开并绑定。
(2) int hci_close_dev(int dd);
简单的关闭使用hci_open_dev打开的Socket。
(3) int hci_send_req(int dd, struct hci_request *r, int to);
向HCISocket发送request
BlueZ提供这个function非常有用,它可以实现一切Host向Modules发送Command的功能。当应用程序需要向Dongle(对应为一个bind后的Socket)发送Command时,调用此function. 下面详细解释此function和用法:
arg1: dd对应一个使用hci_open_dev()打开的Socket(Dongle)。
arg3: to则为等待Dongle执行并回复命令结果的timeout.以毫秒为单位。
arg2: hci_request* r 最为重要,首先看它的结构:
struct hci_request {
uint16_t ogf; //Opcode Group
uint16_t ocf; //Opcode Command
int event; //此Command产生的Event类型。
void *cparam; //Command 参数
int clen; //Command参数长度
void *rparam; //Response 参数
int rlen; //Response 参数长度
};
ogf,ocf就是GroupCode和CommandCode。这两项先确定下来,然后可以查HCISpec,察看输入参数(cparam)以及输出参数(rparam)含义。至于他们的结构以及参数长度,则在~/include/net/bluetooth/hci.h中有定义。
至于event,如果设置,它会被setsockopt设置于Socket。
(4) int hci_read_bd_addr(int dd, bdaddr_t *bdaddr, int to);
得到指定DongleBDAddr,
arg1:HCISocket,使用hci_open_dev()打开的Socket(Dongle)。
arg2:输出参数,其中会放置bdaddr.
arg3:以milliseconds为单位的timeout.
(5) 读写DongleName:
inthci_read_local_name(int dd, int len, char *name, int to);
inthci_write_local_name(int dd, const char *name, int to);
arg1:HCISocket,使用hci_open_dev()打开的Socket(Dongle)。
arg2:读取或设置Name。
arg3:以milliseconds为单位的timeout.
注意:这里的Name与IOCTLHCIGETDEVINFO 得到hci_dev_info中的name不同。
(6) 得到HCIVersion:
inthci_read_local_version(int dd, struct hci_version *ver, int to);
(7) 得到已经UP的DongleBDaddr
int hci_devba(int dev_id, bdaddr_t *bdaddr);
dev_id: Dongle Device ID.
bdaddr: 输出参数,指定Dongle如果UP,则放置其BDAddr。
(8) inquiry远程BluetoothDevice
int hci_inquiry(int dev_id, int len, int nrsp, const uint8_t *lap,inquiry_info **ii, long flags);
命令指定的Dongle去搜索周围所有bluetoothdevice. 并将搜索到的BluetoothDevice bdaddr 传递回来。
arg1:dev_id指定DongleDevice ID。如果此值小于0,则会使用第一个可用的Dongle。
arg2:len此次inquiry的时间长度(每增加1,则增加1.25秒时间)
arg3:nrsp此次搜索最大搜索数量,如果给0。则此值会取255。
arg4:lap BDADDR中LAP部分,Inquiry时这块值缺省为0X9E8B33.通常使用NULL。则自动设置。
arg5:ii存放搜索到BluetoothDevice的地方。给一个存放inquiry_info指针的地址,它会自动分配空间。并把那个空间头地址放到其中。
arg6:flags搜索flags.使用IREQ_CACHE_FLUSH,则会真正重新inquiry。否则可能会传回上次的结果。
返回值是这次Inquiry到的BluetoothDevice数目。
注意:如果*ii不是自己分配的,而是让hci_inquiry()自己分配的,则需要调用bt_free()来帮它释放空间。
(9) 得到指定BDAddr的reomtedevice Name:
int hci_read_remote_name(int dd, const bdaddr_t *bdaddr, int len, char*name, int to);
arg1:使用hci_open_dev()打开的Socket。
arg2:对方BDAddr.
arg3:name长度。
arg4:(out)放置name的位置。
arg5:等待时间。
(10)读取连接的信号强度
int hci_read_rssi(int dd, uint16_t handle, int8_t *rssi, int to);
注意,所有对连接的操作,都会有一个参数Handle。
3. L2CAP层编程
逻辑连接控制和适配协议(L2CAP)为上层协议提供面向连接和无连接的数据服务,并提供多协议功能和分割重组操作。L2CAP充许上层协议和应用软件传输和接收最大长度为64K的L2CAP数据包。
L2CAP基于通道(channel)的概念。通道(Channel)是位于基带(baseband)连接之上的逻辑连接。每个通道以多对一的方式绑定一个单一协议(singleprotocol)。多个通道可以绑定同一个协议,但一个通道不可以绑定多个协议。每个在通道里接收到的L2CAP数据包被传到相应的上层协议。多个通道可共享同一个基带连接。
L2CAP使用L2CAP连接请求(Connection Request)命令中的PSM字段实现协议复用。L2CAP可以复用发给上层协议的连接请求,这些上层协议包括服务发现协议SDP(PSM=0x0001)、RFCOMM(PSM=0x0003)和电话控制(PSM=0x0005)等。
L2CAP编程非常重要,它和HCI基本就是LinuxBluetooth编程的基础了。几乎所有协议的连接,断连,读写都是用L2CAP连接来做的。
创建L2CAPSocket:
socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP);
domain=PF_BLUETOOTH, type可以是多种类型, protocol=BTPROTO_L2CAP
绑定:
memset(&addr,0, sizeof(addr));
addr.l2_family= AF_BLUETOOTH;
bacpy(&addr.l2_bdaddr, &bdaddr); //bdaddr为本地DongleBDAddr
if(bind(sk, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("Can'tbind socket");
goto error;
}
连接:
memset(&addr,0, sizeof(addr));
addr.l2_family= AF_BLUETOOTH;
bacpy(addr.l2_bdaddr, src);
addr.l2_psm= xxx;
if(connect(sk, (struct sockaddr *)&addr, sizeof(addr)) < 0)
{
perror("Can'tconnect");
goto error;
}
注意:
structsockaddr_l2
{
sa_family_t l2_family; //必须为 AF_BLUETOOTH
unsignedshort l2_psm; //与前面PSM对应,这一项很重要
bdaddr_t l2_bdaddr; //Remote Device BDADDR
unsignedshort l2_cid;
};
发送数据到RemoteDevice:send()或write()都可以。接收数据: revc()或read()都可以
4. SDP层编程
服务发现协议(SDP或BluetoothSDP)在蓝牙协议栈中对蓝牙环境中的应用程序有特殊的含意,发现哪个服务是可用的和确定这些可用服务的特征。SDP定义了bluetoothclient发现可用bluetoothserver服务和它们的特征的方法。这个协议定义了客户如何能够寻找基于特定属性的服务而不需要客户知道可用服务的任何知识。SDP提供发现新服务的方法,在当客户登录到正在操作的蓝牙服务器的一个区域时是可用的时。Servicediscovery机制提供client应用程序侦测server应用程序提供的服务的能力,并且能够得到服务的特性。服务的品质包含服务type或服务class.
SDP也提供SDPserver与SDPclient之间的通讯。SDPserver维护着一个服务条目(servicerecord)列表. 每个服务条目描述一个单独的服务属性。SDPclient可以通过发送SDPrequest来得到服务条目。
如果一个client或者依附于client之上的应用程序决定使用某个service. 它创建一个单独的连接到service的提供者。SDP只提供侦测Service的机制,但不提供如何利用这些Service的机制。Sam觉得,这里其实是说:SDP只提供侦测Service的办法,但如何用,SDP不管。
每个Bluetooth Device最多只能拥有一个SDP Server。如果一个Bluetooth Device只担任Client,那它不需要SDPServer。但一个BluetoothDevice可以同时担当SDPServer和SDPclient.
(1) ServiceRecord(Service 条目):一个service是一个实体为另一个实体提供信息,执行动作或控制资源。一个service可以由软件,硬件或软硬件结合提供。所有的Service信息都包含于一个ServiceRecord内。一个ServiceRecord 包含一个Serviceattribute(Service属性)list.
(2) ServiceAttribute(Service 属性):每个Service属性描述servcie的特性.一个ServiceAttribute由2部分:AttributeID + Attribute Value。
(3) ServiceClass:每个Service都是某个ServiceClass的实例. Service Class定义了ServiceRecord中包含的Service属性. 属性ID,属性值都被定义好了。每个ServiceClass也有一个独特ID。这个ServiceClass标识符包含在属性值ServiceClassIDList属性中。
并描绘为UUID。自从ServiceRecord中的属性格式以及含义依赖于ServiceClass后,ServiceClassIDList属性变得非常重要。
(4) SearchingFor Service: ServiceSearch transaction允许client得到ServiceRecord Handle。一旦SDPClient得到ServiceRecord Handle,它就可以请求这个Record内具体属性的值。如果某个属性值UUID,则可以通过查找UUID查到这个属性。UUID:universally uniqueidentifier.(唯一性标识符)
总之,DP协议栈使用request/response模式工作,每个传输过程包括一个requestprotocol data unit(PDU)和一个responsePDU. SDP使用L2CAP连接传输数据。在发送RequestPDU但未收到ResponsePDU之前,不能向同一个server再发送RequestPDU。
六 Bluetooth之启动过程实现
对于蓝牙无论最底层的硬件驱动如何实现,都会在HCI层进行统一。也就是说,HCI在主机端的驱动主要是为上层提供统一接口,让上层协议不依赖于具体的硬件实现。HCI在硬件中的固件与HCI在主机端的驱动通信方式有多种,比如UART,USB和SDIO等。
所有的设备在HCI层面前都被抽象为一个hci_dev结构体,因此,无论实际的设备是哪种蓝牙设备、通过什么方式连接到主机,都需要向HCI层和蓝牙核心层注册一个hci_dev设备,注册过程由hci_registe_dev()函数来完成,同时也可以通过hci_unregister_dev()函数卸载一个蓝牙设备。
具体的蓝牙驱动有很多,常用的在linux内核都自带有驱动。比如:hci_vhci.c为蓝牙虚拟主控制器驱动程序,hci_uart.c(或者hci_ldisc.c)为串口接口主控制器驱动程序,btusb.c为USB接口主控制器驱动程序,btsdio.c为SDIO主控制器驱动程序。
1. Bluetooth启动步骤
(1) 串口驱动必须要先就绪(uart蓝牙而言),这是cpu和蓝牙模块之间的桥梁。
(2) 蓝牙初始化,模块上电和PSKEY的设置。
(3) 通过hciattach建立串口和蓝牙协议层之间的数据连接通道。
2. Bluetooth启动流程
(1) 打开蓝牙电源,通过rfkill来enable;(system/bluetooth/bluedroid/bluetooth.c)
(2) 启动servicehciattch -n -s 115200 /dev/ttyS2 bcm2035 115200;
(3) 检测HCI是否成功(接受HCIDEVUPsocket来判断或hciconfig hci0 up);
(4) hcid deamon start up。
3. Bluetooth数据流向
(1) uart口取得蓝牙模块的数据;
(2) uart口通过ldisc传给hci_uart;
(3) hci_uart传给在其上的bcsp;
(4) bcsp传给hci层;
(5) hci层传给l2cap层
(6) l2cap层再传给rfcomm;
蓝牙模块上电:一般是通过一个GPIO来控制的,通常是先高再低再高;
PSKEY的设置:通过串口发送命令给蓝牙模块,对于串口必须要知道的是要能通讯,必须得设好波特率,另外一方面蓝牙模块的晶振频率也必须要设,否则它不知道该怎么跳了;当然不同的芯片可能初始化的过程也不一样,也许还要下载firmware等等,一般是通过bccmd来完成的。
经过上面的设置基本上蓝牙模块以及可以正常工作了;
但是还没有和上面的协议层建立纽带关系,也就是说从uart收到的数据还没有传给hci层;如何把uart也就是蓝牙模块传上来的数据交给hci层,在驱动里面是通过一个叫做disc的机制完成的,这个机制本意是用来做过滤或者限制收上来的字符的,但是在蓝牙驱动里面则直接把数据传给了蓝牙协议层,再也不回到串口的控制了;
4. Bluez控制流程
class bluetoothsetting是UI的入口,通过按buttonscan进入搜索状态,applicaton层调用bluetoothdevice, 接着就是bluetoothservice的调用,bluetoothservice调用native方法,到此全部的java程序结束了。下面的调用都是JNI,cpp实现的。android_server_bluetoothservice.cpp里面实现了native方法,最终通过dbus封装,调用HCID deamon的functionDiscoverDevice。
5. Bluetooth启动过程分析
(1) 各协议层的注册
a.在af_bluetooth.c中首先调用bt_init()函数完成初始化,打印信息"Bluetooth: Core ver 2.16"随后调用函数sock_register()注册sock,打印信息"Bluetooth: HCI device and connection manager initialized"
b.接着调用函数hci_sock_init(), l2cap_init(), sco_init()实现各个协议的初始化
c.在hci_sock.c中,注册bt_sock协议,打印信息"Bluetooth: HCI socket layer initialized"
d.在L2cap_core.c中,调用l2cap_init_socks()函数,完成初始化,打印信息"Bluetooth: L2CAP socket layer initialized"
e.在sco.c中,注册BTPROTO_SCO协议,完成初始化,打印信息"Bluetooth: SCO socket layer initialized"
(2) 各硬件的初始化
a.在Hci_ldisc.c中,完成hci_uart_init的初始化,打印信息"Bluetooth: HCI UART driver ver 2.2"
b.在Hci_h4.c中,完成h4_init的初始化,打印信息"Bluetooth: HCI H4 protocol initialized"
c.在Hci_ath.c中,完成ath_Init的初始化,打印信息"Bluetooth: HCIATH3Kprotocol initialized"
d.在hci_ibs.c中,完成ibs_init的初始化,打印信息"Bluetooth: HCI_IBSprotocol initialized"
e.在Core.c中,完成rfcomm_init的初始化,其中调用函数rfcomm_init_ttys()、rfcomm_init_sockets(),实现rfcomm的初始化,打印信息"Bluetooth: RFCOMMver 1.11"
f.在TTY.C中,完成rfcomm_init_ttys的初始化,实现rfcomm_tty_driver的注册,打印信息"Bluetooth: RFCOMMTTY layer initialized"
g.在Sock.c中,完成rfcomm_init_sockets的初始化,实现BTPROTO_RFCOMM的注册,打印信息"Bluetooth: RFCOMMsocket layer initialized"
通过rfcomm完成uart串口的初始化代码在kernel\net\bluetooth\rfcomm
(3) bnep-蓝牙网络封装协议
在Core.c中,完成bnep_init的初始化,实现bnep的初始化,打印信息"Bluetooth: BNEP(Ethernet Emulation) ver 1.3/Bluetooth: BNEP filters: protocol" 完成蓝牙网络封装协议的初始化代码在kernel\net\bluetooth\bnep
七 Bluetooth之驱动移植
1. android系统配置
build\target\board\generic下面的generic.mk增加:
BOARD_HAVE_BLUETOOTH:= true 这个是由于编译相关蓝牙代码时需要这个宏,见system\bluetooth\android.mk
ifeq($(BOARD_HAVE_BLUETOOTH),true)
include$(all-subdir-makefiles)
endif
在 external\bluetooth也同样存在此宏起作用
2. 启动项修改
system/core/rootdir下init.rc文件增加:
service hciattach /system/bin/hciattach -n -s 115200 /dev/ttyS2 bcm2035115200
user bluetooth
group bluetooth net_bt_admin
disabled
oneshot
请放在 servicebluetoothd /system/bin/bluetoothd -n 类似这种语句的后面任意位置即可。
3. 电源管理rfkill驱动
kernel/driver/bluetooth/bluetooth-power.c 高通的这个文件基本上不用动。
在kernel/arch/arm/mach_msm7x27.c: static int bluetooth_power(int on)中实现:
上电:
把bt_resetpin和bt_reg_onpin拉低
mdelay(10);
把bt_resetpin和bt_reg_onpin拉高
mdelay(150);
掉电:
把bt_resetpin和bt_reg_onpin拉低
4. Rebuild Androidimage and reboot
命令行测试:
echo 0 >/sys/class/rfkill/rfkill0/state //BT下电
echo 1 >/sys/class/rfkill/rfkill0/state //BT上电
brcm_patchram_plus -d --patch ram/etc/firmware/BCM4329B1_002.002.023.0061.0062.hcd/dev/ttyHS0
hciattach -s 115200 /dev/ttyHS0 any 没任何错误提示是可以用以下测试
hciconfig hci0 up
hcitool scan
5. 实现BT睡眠唤醒机制
Kernel/drivers/bluetooth/bluesleep.c 一般来说这个文件改动比较少,但可能逻辑上会有些问题。需要小的改动(4.4内核中已经没有这个文件了)。
在kernel/arch/arm/mach_xxx/board_xxx.c:bluesleep_resources中定义gpio_host_wake(BT唤醒host脚)、gpio_ext_wake(host唤醒BT脚)、host_wake(BT唤醒host的中断号)。
注:各个平台的board_xxx.c文件名字不同,请客户确认(目前已经被设备树替代)
6.系统集成
(1)在init.qcom.rc中确认有下面的内容:
service hciattach /system/bin/sh /system/etc/init.qcom.bt.sh
user bluetooth
group qcom_oncrpc bluetooth net_bt_admin
disabled
oneshot
(2)修改init.qcom.bt.sh
确认有:
BLUETOOTH_SLEEP_PATH=/proc/bluetooth/sleep/proto(没有这个路径)
echo1 > $BLUETOOTH_SLEEP_PATH
/system/bin/hciattach-n/dev/ttyHS0 any 3000000 flow & 改为:
./brcm_patchram_plus--enable_lpm–enable_hci --patchram /system/etc/wifi/BCM4329BT.hcd--baudrate3000000 /dev/ttyHS0 &
注掉:高通下载firmware的命令。最后,重新编译system。此时BT应该能运行了。
八 Bluetooth之调试与编译
1. Bluetooth驱动调试
调试你的蓝牙实现,可以通过读跟蓝牙相关的logs(adblogcat)和查找ERROR和警告消息。Android使用Bluez,同时会带来一些有用的调式工具。下面的片段为了提供一个建议的例子:
1 hciconfig -a # print BT chipset address and features. Useful to check if you can communicate with your BT chipset.
2 hcidump -XVt # print live HCI UART traffic.
3 hcitool scan # scan for local devices. Useful to check ifRX/TX works.
4 l2ping ADDRESS # ping another BT device. Useful to check ifRX/TX works.
5 sdptool records ADDRESS # request the SDP records of another BTdevice.
守护进程日志
hcid(STDOUT)和hciattach(STDERR)的守护进程日志缺省是被写到/dev/null。编辑init.rc和init.PLATFORM.rc在logwrapper下运行这些守护进程,把它们输出到logcat。
hciconfig -a 和 hcitool
如果你编译你自己的system.img,除了hcitool扫描不行,hciconfig -a是可以工作的,尝试安装固件到蓝牙芯片。
2. Bluetooth 调试工具
BlueZ为调试和与蓝牙子系统通信提供很多设置命令行工具,包含下面这些:
(1) hciconfig
(2) hcitool
(3) hcidump
(4) sdptool
(5) dbus-send
dbus-monitor
九 Bluetooth之应用程序开发
1.Bluetooth的API开发
Android平台包含了对Bluetooth协议栈的支持,允许机器通过Bluetooth设备进行无线数据交换。应用框架通过AndroidBluetoothAPI访问Bluetooth功能模块。这些API能让应用无线连接其他Bluetooth设备,实现点对点和多点之间的通信。
运用蓝牙API,Android应用程序可以完成如下操作:
(1) 扫描其他Bluetooth设备。
(2) 查询配对Bluetooth设备的本地Bluetooth适配器。
(3) 建立RFCOMM通道。
(4) 通过服务探索连接到其他设备。
(5) 与其他设备进行数据传输。
(6) 管理多个连接
2. The Basics开发
本文描述如何使用AndroidBluetoothAPIs完成Bluetooth通讯的4个必要任务:设置Bluetooth,搜寻本地配对或者可用的Bluetooth设备,连接Bluetooth设备,与Bluetooth设备进行数据传输。
所有可用的Bluetooth APIs都包含在android.bluetooth包中。下面是建立Bluetooth连接需要用到的类和接口的总结:
(1) BluetoothAdapter
描述本地Bluetooth适配器(Bluetooth接收器)。BluetoothAdapter是所有Bluetooth相关活动的入口。运用BluetoothAdapter可以发现其他Bluetooth设备,查询连接(或配对)的设备列表,用已知MAC地址实例化一个BluetoothDevice对象,创建一个BluetoothServerSocket对象侦听其他设备的通信。
(2) BluetoothDevice
描述一个远程Bluetooth设备。可以用它通过一个BluetoothSocket请求一个远程设备的连接,或者查询远程设备的名称、地址、类、连接状态等信息。
(3) BluetoothSocket
描述一个BluetoothSocket接口(类似于TCPSocket)。应用通过InputStream和OutputStream与另外一个Bluetooth设备交换数据,即它是应用与另外一个设备交换数据的连接点。
(4) BluetoothServerSocket
BluetoothServerSocket是一个开放的socket服务器,用来侦听连接进来的请求(类似于RCPServerSocket)。为了连接两个Android设备,一个设备必须使用该类来开启一个socket做服务器,当另外一个设备对它发起连接请求时并且请求被接受时,BluetoothServerSocket会返回一个连接的BluetoothSocket对象。
(5) BluetoothClass
BluetoothClass是用来定义设备类和它的服务的只读属性集。然而,它并不是可靠的描述设备支持的所有Bluetooth配置和服务,而只是一些设备类型的有用特征。
(6) BluetoothProfile
BluetoothProfile是两个设备基于蓝牙通讯的无线接口描述。Profile定义了设备如何实现一种连接或者应用,你可以把Profile理解为连接层或者应用层协议。比如,如果一家公司希望它们的Bluetooth芯片支持所有的Bluetooth耳机,那么它只要支持HeadSetProfile即可,而无须考虑该芯片与其它Bluetooth设备的通讯与兼容性问题。如果你想购买Bluetooth产品,你应该了解你的应用需要哪些Profile来完成,并且确保你购买的Bluetooth产品支持这些Profile。
(7) BluetoothHeadset
提供移动电话的Bluetooth耳机支持。包括Bluetooth耳机和Hands-Free(v1.5) profiles。
(8) BluetoothA2dp
定义两个设备间如何通过Bluetooth连接进行高质量的音频传输。A2DP(AdvancedAudio Distribution Profile):高级音频传输模式。
(9) BluetoothProfile.ServiceListener
一个接口描述,在与服务连接或者断连接的时候通知BluetoothProfileIPC(这是内部服务运行的一个特定的模式<profile>)。
3. BluetoothPermissions开发
要使用Bluetooth功能,至少需要2个Bluetooth权限:BLUETOOTH 和 BLUETOOTH_ADMIN.
BLUETOOTH:用来授权任何Bluetooth通信,如请求连接,接受连接,传输数据等。
BLUETOOTH_ADMIN:用来授权初始化设备搜索或操作Bluetooth设置。大多数应用需要它的唯一场合是用来搜索本地Bluetooth设备。本授权的其他功能不应该被使用,除非是需要修改Bluetooth设置的“powermanager(电源管理)”应用。
注意:需要BLUETOOTH_ADMIN权限的场合,BLUETOOTH权限也是必需的。需要在manifest文件中声明Bluetooth权限,示例如下:
<manifest... >
<uses-permissionandroid:name="android.permission.BLUETOOTH" />
...
</manifest>
4. Setting Up Bluetooth服务
在用Bluetooth通讯之前,需要确认设备是否支持Bluetooth,如果支持,还得确保Bluetooth是可用的。如果设备不支持Bluetooth,需要优雅的将Bluetooth置为不可用。如果支持Bluetooth,但没有开启,可以在应用中请求开启Bluetooth。该设置使用BluetoothAdapter. 通过两个步骤完成。
(1) 获取BluetoothAdapter
BluetoothAdapter是每个Bluetooth的Activity都需要用到的。用静态方法getDefaultAdapter()获取BluetoothAdapter,返回一个拥有Bluetooth适配器的BluetoothAdapter对象。如果返回null,说明设备不支持Bluetooth,关于Bluetooth的故事到此就结束了(因为你干不了什么了)。示例:
BluetoothAdaptermBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if(mBluetoothAdapter == null) {
//Device does not support Bluetooth
}
(2) EnableBluetooth
接下来,就是确保Bluetooth功能是开启的。调用isEnabled()来检查Bluetooth当前是否是开启的。用ACTION_REQUEST_ENABLEactionIntent调用startActivityForResult()来请求开启Bluetooth,这会通过系统设置发出一个Bluetooth使能请求(并且不会停止本应用程序)。示例:
if(!mBluetoothAdapter.isEnabled()) {
IntentenableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent,REQUEST_ENABLE_BT);
}
用户请求使能Bluetooth时,会显示一个对话框。选择“Yes”,系统会使能Bluetooth,并且焦点会返回你的应用程序。如果使能Bluetooth成功,你的Activity会在onActivityResult()回调函数中收到RESULT_OK的结果码。如果Bluetooth使能因发生错误(或用户选择了“No”)而失败,收到的结果码将是RESULT_CANCELED。
作为可选项,应用也可以侦听ACTION_STATE_CHANGED broadcastIntent,这样无论Bluetooth状态何时被改变系统都会发出broadcast(广播)。该广播包含附加的字段信息EXTRA_STATE和EXTRA_PREVIOUS_STATE分别代表新的和旧的Bluetooth状态,该字段可能的值为STATE_TURNING_ON,STATE_ON, STATE_TURNING_OFF,和STATE_OFF。应用运行时,侦听ACTION_STATE_CHANGED广播来检测Bluetooth状态的改变是很有用的。
提示:启用Bluetooth可被发现功能能够自动开启Bluetooth。如果在完成Activity之前需要持续的使能Bluetooth可被发现功能,那么上面的第2步就可以忽略。
5. Finding Devices服务
使用BluetoothAdapter可以通过设备搜索或查询配对设备找到远程Bluetooth设备。
Devicediscovery(设备搜索)是一个扫描搜索本地已使能Bluetooth设备并且从搜索到的设备请求一些信息的过程(有时候会收到类似“discovering”,“inquiring”或“scanning”)。但是,搜索到的本地Bluetooth设备只有在打开被发现功能后才会响应一个discovery请求,响应的信息包括设备名,类,唯一的MAC地址。发起搜寻的设备可以使用这些信息来初始化跟被发现的设备的连接。
一旦与远程设备的第一次连接被建立,一个pairing请求就会自动提交给用户。如果设备已配对,配对设备的基本信息(名称,类,MAC地址)就被保存下来了,能够使用BluetoothAPI来读取这些信息。使用已知的远程设备的MAC地址,连接可以在任何时候初始化而不必先完成搜索(当然这是假设远程设备是在可连接的空间范围内)。
需要记住,配对和连接是两个不同的概念:
配对意思是两个设备相互意识到对方的存在,共享一个用来鉴别身份的链路键(link-key),能够与对方建立一个加密的连接。连接意思是两个设备现在共享一个RFCOMM信道,能够相互传输数据。目前AndroidBluetooth API's要求设备在建立RFCOMM信道前必须配对(配对是在使用BluetoothAPI初始化一个加密连接时自动完成的)。
下面描述如何查询已配对设备,搜索新设备:
注意:Android的电源设备默认是不能被发现的。用户可以通过系统设置让它在有限的时间内可以被发现,或者可以在应用程序中要求用户使能被发现功能。
(1) Queryingpaired devices
在搜索设备前,查询配对设备看需要的设备是否已经是已经存在是很值得的,可以调用getBondedDevices()来做到,该函数会返回一个描述配对设备BluetoothDevice的结果集。例如,可以使用ArrayAdapter查询所有配对设备然后显示所有设备名给用户:
Set<BluetoothDevice>pairedDevices = mBluetoothAdapter.getBondedDevices();
//If there are paired devices
if(pairedDevices.size() > 0) {
//Loop through paired devices
for(BluetoothDevice device : pairedDevices) {
//Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName()+ "\n" + device.getAddress());
}
}
BluetoothDevice对象中需要用来初始化一个连接唯一需要用到的信息就是MAC地址。
(2) Discoveringdevices
要开始搜索设备,只需简单的调用startDiscovery()。该函数时异步的,调用后立即返回,返回值表示搜索是否成功开始。搜索处理通常包括一个12秒钟的查询扫描,然后跟随一个页面显示搜索到设备Bluetooth名称。
应用中可以注册一个带CTION_FOUND Intent的BroadcastReceiver,搜索到每一个设备时都接收到消息。对于每一个设备,系统都会广播ACTION_FOUND Intent,该Intent携带着额外的字段信息EXTRA_DEVICE和EXTRA_CLASS,分别包含一个BluetoothDevice和一个BluetoothClass。下面的示例显示如何注册和处理设备被发现后发出的广播:
//Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
publicvoid onReceive(Context context, Intent intent) {
Stringaction = intent.getAction();
//When discovery finds a device
if(BluetoothDevice.ACTION_FOUND.equals(action)) {
//Get the BluetoothDevice object from the Intent
BluetoothDevicedevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName()+ "\n" + device.getAddress());
}
}
};
//Register the BroadcastReceiver
IntentFilterfilter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
警告:完成设备搜索对于Bluetooth适配器来说是一个重量级的处理,要消耗大量它的资源。一旦你已经找到一个设备来连接,请确保你在尝试连接前使用了cancelDiscovery()来停止搜索。同样,如果已经保持了一个连接的时候,同时执行搜索设备将会显著的降低连接的带宽,所以在连接的时候不应该执行搜索发现。
(3) Enabling discover ability
如果想让本地设备被其他设备发现,可以带ACTION_REQUEST_DISCOVERABLEaction Intent调用startActivityForResult(Intent,int)方法。该方法会提交一个请求通过系统刚设置使设备出于可以被发现的模式(而不影响应用程序)。默认情况下,设备在120秒后变为可以被发现的。可以通过额外增加EXTRA_DISCOVERABLE_DURATIONIntent自定义一个值,最大值是3600秒,0表示设备总是可以被发现的(小于0或者大于3600则会被自动设置为120秒)。下面示例设置时间为300:
IntentdiscoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION,300);
startActivity(discoverableIntent);
询问用户是否允许打开设备可以被发现功能时会显示一个对话框。如果用户选择“Yes”,设备会在指定时间过后变为可以被发现的。Activity的onActivityResult()回调函数被调用,结果码等于设备变为可以被发现所需时长。如果用户选择“No”或者有错误发生,结果码会是Activity.RESULT_CANCELLED。
提示:如果Bluetooth没有启用,启用Bluetooth可被发现功能能够自动开启Bluetooth。
在规定的时间内,设备会静静的保持可以被发现模式。如果想在可以被发现模式被更改时受到通知,可以用ACTION_SCAN_MODE_CHANGEDIntent注册一个BroadcastReceiver,包含额外的字段信息EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE分别表示新旧扫描模式,其可能的值为SCAN_MODE_CONNECTABLE_DISCOVERABLE(discoverablemode),SCAN_MODE_CONNECTABLE(notin discoverable mode but still able to receiveconnections),SCAN_MODE_NONE(notin discoverable mode and unable to receive connections)。
如果只需要连接远程设备就不需要打开设备的可以被发现功能。只在应用作为一个服务器socket的宿主用来接收进来的连接时才需要使能可以被发现功能,因为远程设备在初始化连接前必须先发现了你的设备。
6. ConnectingDevices服务
为了建立两个设备之间的应用的连接,需要完成服务器端和客户端,因为一个设备必须打开一个服务器socket而另外一个设备必须初始化连接(用服务器端的MAC地址)。服务器和客户端在各自获得一个基于同一个RFCOMM信道的已连接的BluetoothSocket对象后就被认为连接已经建立。这个时候,双方设备可以获取输入输出流,数据传输可以开始了。本节描述如何在两个设备之间初始化连接。
服务器设备和客户端设备用不同的方式获取各自需要的BluetoothSocket对象。服务器端的在接收一个进来的连接时获取到,客户端的在打开一个与服务器端的RFCOMM信道的时候获取到。
一个实现技巧是自动把每个设备作为服务器,这样就拥有了一个打开的socket用来侦听连接。然后任一设备就能够发起与另一个设备的连接,并成为客户端。另外,一个设备也可以明确的成为“host”,并打开一个服务端socket,另一个设备可以简单的发起连接。
注意:如果两个设备之前没有配对,那么在连接处理过程中Android应用框架会自动显示一个配对请求的通知或对话框给用户。因此,当尝试连接设备时,应用不需要关心设备是否已经配对。RFCOMM连接会阻塞直到用户成功将设备配对(如果用户拒绝配对或者配对超时了连接会失败)。
(1)Connectingas a server
如果要连接两个设备,其中一个必须充当服务器,通过持有一个打开的BluetoothServerSocket对象。服务器socket的作用是侦听进来的连接,如果一个连接被接受,提供一个连接好的BluetoothSocket对象。从BluetoothServerSocket获取到BluetoothSocket对象之后,BluetoothServerSocket就可以(也应该)丢弃了,除非你还要用它来接收更多的连接。
下面是建立服务器socket和接收一个连接的基本步骤:
①通过调用listenUsingRfcommWithServiceRecord(String, UUID)得到一个BluetoothServerSocket对象。
该字符串为服务的识别名称,系统将自动写入到一个新的服务发现协议(SDP)数据库接入口到设备上的(名字是任意的,可以简单地是应用程序的名称)项。UUID也包括在SDP接入口中,将是客户端设备连接协议的基础。也就是说,当客户端试图连接本设备,它将携带一个UUID用来唯一标识它要连接的服务,UUID必须匹配,连接才会被接受。
② 通过调用accept()来侦听连接请求。
这是一个阻塞的调用,知道有连接进来或者产生异常才会返回。只有远程设备发送一个连接请求,并且携带的UUID与侦听它socket注册的UUID匹配,连接请求才会被接受。如果成功,accept()将返回一个连接好的BluetoothSocket对象。
③ 除非需要再接收另外的连接,否则的话调用close()。
close()释放serversocket和它的资源,但不会关闭连接accept()返回的连接好的BluetoothSocket对象。与TCP/IP不同,RFCOMM同一时刻一个信道只允许一个客户端连接,因此大多数情况下意味着在BluetoothServerSocket接受一个连接请求后应该立即调用close()。
accept()调用不应该在主ActivityUI线程中进行,因为这是个阻塞的调用,会妨碍其他的交互。经常是在在一个新线程中做BluetoothServerSocket或BluetoothSocket的所有工作来避免UI线程阻塞。注意所有BluetoothServerSocket或BluetoothSocket的方法都是线程安全的。
下面是一个简单的接受连接的服务器组件代码示例:
privateclass AcceptThread extends Thread {
privatefinal BluetoothServerSocket mmServerSocket;
publicAcceptThread() {
//Use a temporary object that is later assigned to mmServerSocket,
//because mmServerSocket is final
BluetoothServerSockettmp = null;
try{
//MY_UUID is the app's UUID string, also used by the client code
tmp= mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME,MY_UUID);
}catch (IOException e) { }
mmServerSocket= tmp;
} publicvoid run() {
BluetoothSocketsocket = null;
//Keep listening until exception occurs or a socket is returned
while(true) {
try{
socket= mmServerSocket.accept();
}catch (IOException e) {
break;
}
//If a connection was accepted
if(socket != null) {
//Do work to manage the connection (in a separate thread)
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
} /**Will cancel the listening socket, and cause the thread to finish */
publicvoid cancel() {
try{
mmServerSocket.close();
}catch (IOException e) { }
}
}
本例中,仅仅只接受一个进来的连接,一旦连接被接受获取到BluetoothSocket,就发送获取到的BluetoothSocket给一个单独的线程,然后关闭BluetoothServerSocket并跳出循环。
注意:accept()返回BluetoothSocket后,socket已经连接了,所以在客户端不应该调用connnect()。
manageConnectedSocket()是一个虚方法,用来初始化线程好传输数据。
通常应该在处理完侦听到的连接后立即关闭BluetoothServerSocket。在本例中,close()在得到BluetoothSocket后马上被调用。还需要在线程中提供一个公共的方法来关闭私有的BluetoothSocket,停止服务端socket的侦听。
(2) Connecting as a client
为了实现与远程设备的连接,你必须首先获得一个代表远程设备BluetoothDevice对象。然后使用BluetoothDevice对象来获取一个BluetoothSocket来实现来接。
下面是基本的步骤:
①用BluetoothDevice调用createRfcommSocketToServiceRecord(UUID)获取一个BluetoothSocket对象。
这个初始化的BluetoothSocket会连接到BluetoothDevice。UUID必须匹配服务器设备在打开BluetoothServerSocket时用到的UUID(用listenUsingRfcommWithServiceRecord(String,UUID))。可以简单的生成一个UUID串然后在服务器和客户端都使用该UUID。
② 调用connect()完成连接
当调用这个方法的时候,系统会在远程设备上完成一个SDP查找来匹配UUID。如果查找成功并且远程设备接受连接,就共享RFCOMM信道,connect()会返回。这也是一个阻塞的调用,不管连接失败还是超时(12秒)都会抛出异常。
注意:要确保在调用connect()时没有同时做设备查找,如果在查找设备,该连接尝试会显著的变慢,慢得类似失败了。
下面是一个完成Bluetooth连接的样例线程:
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
//Use a temporary object that is later assigned to mmSocket,
//because mmSocket is final
BluetoothSockettmp = null;
mmDevice= device;
//Get a BluetoothSocket to connect with the given BluetoothDevice
try{
//MY_UUID is the app's UUID string, also used by the server code
tmp= device.createRfcommSocketToServiceRecord(MY_UUID);
}catch (IOException e) { }
mmSocket= tmp;
} publicvoid run() {
//Cancel discovery because it will slow down the connection
mBluetoothAdapter.cancelDiscovery();
try{
//Connect the device through the socket. This will block
//until it succeeds or throws an exception
mmSocket.connect();
}catch (IOException connectException) {
//Unable to connect; close the socket and get out
try{
mmSocket.close();
}catch (IOException closeException) { }
return;
}
//Do work to manage the connection (in a separate thread)
manageConnectedSocket(mmSocket);
} /**Will cancel an in-progress connection, and close the socket */
publicvoid cancel() {
try{
mmSocket.close();
}catch (IOException e) { }
}
}
注意到cancelDiscovery()在连接操作前被调用。在连接之前,不管搜索有没有进行,该调用都是安全的,不需要确认(当然如果有要确认的需求,可以调用isDiscovering())。manageConnectedSocket()是一个虚方法,用来初始化线程好传输数据。
在对BluetoothSocket的处理完成后,记得调用close()来关闭连接的socket和清理所有的内部资源。
7. Managing aConnection服务
如果已经连接了两个设备,他们都已经拥有各自的连接好的BluetoothSocket对象。那就是一个有趣的开始,因为你可以在设备间共享数据了。使用BluetoothSocket,传输任何数据通常来说都很容易了:
(1) 通过socket获取输入输出流来处理传输(分别使用getInputStream()和getOutputStream())。
(2) 用read(byte[])和write(byte[])来实现读写。仅此而已。
当然,还是有很多细节需要考虑的。首要的,需要用一个专门的线程来实现流的读写。只是很重要的,因为read(byte[])和write(byte[])都是阻塞的调用。read(byte[])会阻塞直到流中有数据可读。write(byte[])通常不会阻塞,但是如果远程设备调用read(byte[])不够快导致中间缓冲区满,它也可能阻塞。所以线程中的主循环应该用于读取InputStream。线程中也应该有单独的方法用来完成写OutputStream。
下面是一个如上面描述那样的例子:
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream; public ConnectedThread(BluetoothSocket socket) {
mmSocket= socket;
InputStreamtmpIn = null;
OutputStreamtmpOut = null;
//Get the input and output streams, using temp objects because
//member streams are final
try{
tmpIn= socket.getInputStream();
tmpOut= socket.getOutputStream();
}catch (IOException e) { }
mmInStream= tmpIn;
mmOutStream= tmpOut;
} public void run() {
byte[]buffer = new byte[1024]; // buffer store for the stream
intbytes; // bytes returned from read()
//Keep listening to the InputStream until an exception occurs
while(true) {
try{
//Read from the InputStream
bytes= mmInStream.read(buffer);
//Send the obtained bytes to the UI Activity
mHandler.obtainMessage(MESSAGE_READ,bytes, -1, buffer)
.sendToTarget();
}catch (IOException e) {
break;
}
}
} /*Call this from the main Activity to send data to the remote device */
public void write(byte[] bytes) {
try{
mmOutStream.write(bytes);
}catch (IOException e) { }
}
/*Call this from the main Activity to shutdown the connection */
public void cancel() {
try{
mmSocket.close();
}catch (IOException e) { }
}
}
构造函数中得到需要的流,一旦执行,线程会等待从InputStream来的数据。当read(byte[])返回从流中读到的字节后,数据通过父类的成员Handler被送到主Activity,然后继续等待读取流中的数据。向外发送数据只需简单的调用线程的write()方法。
线程的cancel()方法时很重要的,以便连接可以在任何时候通过关闭BluetoothSocket来终止。它应该总在处理完Bluetooth连接后被调用。
9. Working withProfiles服务
从Android3.0开始,Bluetooth API就包含了对Bluetoothprofiles的支持。Bluetoothprofile是基于蓝牙的设备之间通信的无线接口规范。例如Hands-Freeprofile(免提模式)。如果移动电话要连接一个无线耳机,他们都要支持Hands-Freeprofile。
你在你的类里可以完成BluetoothProfile接口来支持某一Bluetoothprofiles。AndroidBluetooth API完成了下面的Bluetoothprofile:
Headset:Headsetprofile提供了移动电话上的Bluetooth耳机支持。Android提供了BluetoothHeadset类,它是一个协议,用来通过IPC(interprocess communication)控制BluetoothHeadset Service。BluetoothHeadset既包含BluetoothHeadset profile也包含Hands-Freeprofile,还包括对AT命令的支持。
A2DP:AdvancedAudio Distribution Profile (A2DP)profile,高级音频传输模式。Android提供了BluetoothA2dp类,这是一个通过IPC来控制BluetoothA2DP的协议。
下面是使用profile的基本步骤:
① 获取默认的Bluetooth适配器。
② 使用getProfileProxy()来建立一个与profile相关的profile协议对象的连接。在下面的例子中,profile协议对象是BluetoothHeadset的一个实例。
③ 设置BluetoothProfile.ServiceListener。该listener通知BluetoothProfileIPC客户端,当客户端连接或断连服务器的时候。
④ 在onServiceConnected()内,得到一个profile协议对象的句柄。
⑤ 一旦拥有了profile协议对象,就可以用它来监控连接的状态,完成于该profile相关的其他操作。
例如,下面的代码片段显示如何连接到一个BluetoothHeadset协议对象,用来控制Headsetprofile:
BluetoothHeadsetmBluetoothHeadset;
//Get the default adapter
BluetoothAdaptermBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//Establish connection to the proxy.
mBluetoothAdapter.getProfileProxy(context,mProfileListener, BluetoothProfile.HEADSET);
private BluetoothProfile.ServiceListener mProfileListener = newBluetoothProfile.ServiceListener() {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if(profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset= (BluetoothHeadset) proxy;
}
}
public void onServiceDisconnected(int profile) {
if(profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset= null;
}
}
};
//... call functions on mBluetoothHeadset
//Close proxy connection after use.
mBluetoothAdapter.closeProfileProxy(mBluetoothHeadset);