基于BlueZ的C语言蓝牙编程 有很多理由促使我们选用C替代其他高级语言来例如Python来开发蓝牙应用程序。Python环境可能并不适合于嵌入式系统。因为嵌入式系统对程序的大小,运行速度,和占用的存储空间有严格的限制,这些都使得像Python之类的解释性语言无法在嵌入式系统上应用。程序员需要对本地的蓝牙适配器进行更好的控制,或者需要建立一套动态链接库以便于其他应用程序的链接以取代单一的应用程序。就像上述描述的这些,BlueZ是一款强大的蓝牙通信协议栈,它扩展的API使得用户方便操纵大量的蓝牙资源。但是BlueZ没有官方的描述文档,甚至非官方的文档也寥寥无几。初学者在BlueZ的官方邮件列表上请求相关的文档,通常的得到的回复是被告知请通过仔细阅读源代码来了解API的功能。阅读BlueZ的源代码对于初学者来说是一项相当费时的工作,在短期内取得的进展是相当有限的,很可能成为很多蓝牙编程初学者的拦路虎。 本章简要叙述了基于BlueZ的C语言蓝牙编程的方法。本章为C程序员进一步阐述了第二章中涉及的知识点。 4.1 选择一个通信的对象 Example 4-1是一个查找周边蓝牙设备的简单应用程序。程序首先获取系统的蓝牙设备号,扫描周边的蓝牙设备,然后查找每一个被搜索到的蓝牙设备的名称。后边有对数据结构和函数的详细描述。
Example 4-1. simplescan.c #include
#include #include #include #include #include #include int main(int argc, char **argv) { 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); sock = hci_open_dev( dev_id ); if (dev_id < 0 || sock < 0) { perror("opening socket"); exit(1); } 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)); 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; } 4.1.1 编译 编译需要使用gcc链接libbluetooth这个库。 # gcc -o simplescan simplescan.c -lbluetooth 4.1.2. 解释 typedef struct { uint8_t b[6]; } __attribute__((packed)) bdaddr_t; 蓝牙设备的地址采用结构体bdaddr_t来描述,BlueZ中队蓝牙地址的存储和操纵都使用bdaddr_t结构体,BlueZ提供两个函数来进行字符串到蓝牙地址的转换。 int str2ba( const char *str, bdaddr_t *ba ); int ba2str( const bdaddr_t *ba, char *str ); str2ba把形如XX:XX:XX:XX:XX:XX(XX标识48位蓝牙地址的16进制的一个字节)的字符串转化6字节的bdaddr_t结构, ba2str完成相反的功能。 本地蓝牙适配器被分配一个从0开始的识别号码。程序在分配系统资源时必须指定使用那一个蓝牙适配器,通常的话系统只有一个蓝牙适配器,把参数NULL传给hci_get_route可以获得第一个有效的蓝牙适配器识别号。 int hci_get_route( bdaddr_t *bdaddr ); int hci_open_dev( int dev_id ); [note]将适配器的设备号指定为0是不恰当的,因为它并不总代表第一个可用的蓝牙适配器。例如系统有两个蓝牙适配器,第一个被disable掉了,那么第一个有效的设备号就是2。 如果存在多个蓝牙适配器,选择"01:23:45:67:89:AB"作为蓝牙适配器的地址, 将指示这个地址的指针char *representation传给hci_devid函数,用这个函数替代hci_get_route。 很多蓝牙操作都需要打开一个套接口, hci_open_dev函数可以打开特定资源号的一个套接口,确切的说hci_open_dev打开的套接字建立了一条和本地蓝牙适配器控制器的连接,而不是和远端蓝牙设备的连接。使用这个套接口发送命令到蓝牙控制器可以实现底层的蓝牙操作,这部分在4.5中有详细的讨论。 选择好本地蓝牙适配器并进行系统资源分配后,程序就可以开始扫描周边的蓝牙设备了,在这个例程中,hci_inquiry函数完成对蓝牙设备的搜寻,并将返回的设备信息数据记录在变量ii中。遇到错误时,它将返回-1并设置errno变量。 int hci_inquiry(int dev_id, int len, int max_rsp, const uint8_t *lap, inquiry_info **ii, long flags); hci_inquiry 的参数需要使用设备资源号而非套接口,所以我们使用hci_get_route函数的返回值dev_id传递给它。查询时间最长持续1.28 * len秒。max_rsp个设别返回的信息都被存储在变量ii中,这个变量必须有足够的空间来存储max_rsp返回的结果。我们推荐max_rsp取值 255来完成标准10.24秒的查询工作。 如果标志位flag设置为IREQ_CACHE_FLUSH,那么在进行查询操作时会把先前一次查询记录的cache刷新,否则flag设置为0的话,即便先前查询的设备已经不处于有效范围内,先前查询的记录也将被返回。 inquiry_info结构体定义如下 typedef struct { bdaddr_t bdaddr; uint8_t pscan_rep_mode; uint8_t pscan_period_mode; uint8_t pscan_mode; uint8_t dev_class[3]; uint16_t clock_offset; } __attribute__ ((packed)) inquiry_info; 在大多数场合,我们仅用到成员bdaddr,它标识了设备的蓝牙地址。有些场合我们也会用到成员dev_class,它标识了被检测到的蓝牙设备的一些信息(例如,识别这个设备是打印设备,电话,个人电脑等),详细地对应关系可以参见蓝牙设备分配号[3]。其余的成员在用于底层通信,一般情况并不常用。感兴趣的读者可以阅读蓝牙内核规范[4]获取更多的信息。一旦周围的蓝牙设备和其蓝牙地址被检测到,程序可以将此设备的名称提供给用户,hci_read_remote_name函数可以完成这个功能。 int hci_read_remote_name(int sock, const bdaddr_t *ba, int len, char *name, int timeout) hci_read_remote_name函数在规定的超时时间内使用套接口通过蓝牙地址ba去获取蓝牙设备的名称,成功返回0,并将获取的蓝牙设备名称存入name中;失败时返回-1并设置相应的errno。 Notes [1]http://www.bluez.org/lists.html [2] for the curious, it makes a call to socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI), followed by a call to bind with the specified resource number. [3]https://www.bluetooth.org/foundry/assignnumb/document/baseband [4]http://www.bluetooth.org/spec