我是一名嵌入式蓝牙工程师,平时大部分时间都在RTOS系统上进行蓝牙开发,最近因为工作需求要在Unix环境下搭建蓝牙开发环境,而我最熟悉的Unix系统莫过于Linux/Ubuntu,于是开始下载bluez的源代码,搭建蓝牙开发环境,这篇博客就是介绍如何在Ubuntu系统下进行HCI编程
搭建环境
我使用的系统是ubuntu-16.04
默认是已经安装了bluez
,并且bluez
是默认为开机启动的,大家可以用下面的命令测试下:
$ systemctl status bluetooth.service
● bluetooth.service - Bluetooth service
Loaded: loaded (/lib/systemd/system/bluetooth.service; disabled; vendor preset: enabled)
Active: active (running) since Fri 2021-08-13 15:42:22 CST; 2s ago
Docs: man:bluetoothd(8)
Main PID: 1213 (bluetoothd)
Status: "Running"
Tasks: 1 (limit: 4915)
CGroup: /system.slice/bluetooth.service
└─1213 /usr/lib/bluetooth/bluetoothd -C --noplugin=sap
如果输出这种结果,则说明bluez已经处于运行状态了
如果系统没有安装bluez
,安装命令也非常简单:
$ sudo apt-get install bluez -y
安装好之后,bluez
默认会安装一些实用工具,比如hcitool
,hciconfig
,gatttool
等,有兴趣的都可以试下这些命令
接着我们来安装bluez
的开发库,运行下面命令即可:
$ sudo apt-get install libbluetooth-dev -y
安装好之后会发现系统中多了一些bluetooth
的头文件,这些头文件默认安装在/usr/include/bluetooth/
目录下,另外还安装了一个libbluetooth
的库文件,可以查看下:
$ ls /usr/include/bluetooth/
bluetooth.h cmtp.h hci_lib.h l2cap.h sco.h sdp_lib.h
bnep.h hci.h hidp.h rfcomm.h sdp.h
$ pkg-config --libs bluez
-lbluetooth
HCI编程
蓝牙开发环境安装好之后,下面实现一个非常简单的小程序,就是调用HCI
的API
来获取蓝牙设备的ID:
#include <stdio.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
// filename: main.c
int main(int argc, char* argv[])
{
int dev_id;
dev_id = hci_devid("hci0"); // 函数hci_devid的声明在头文件bluetooth/hci_lib.h中
printf("dev_id: %d\n", dev_id);
}
然后运行下面的命令对其进行编译:
$ gcc main.c -o main -lbluetooth
$ ./main
dev_id: 0
因为我的系统上只有一个蓝牙设备,因此输出为:dev_id: 0
,也就是hci0
。
编程范式
由上面的小例子可以知,HCI编程就是调用bluez
提供的HCI接口来进行编程,只不过编译的时候要注意把蓝牙库链接进去,即-lbluetooth
。
其实HCI还有很多命令,可以查看下/usr/include/bluetooth/hci_lib.h
文件,可以试着调用这些函数,用得多了就会越来越熟悉。
int hci_open_dev(int dev_id);
int hci_close_dev(int dd);
int hci_send_cmd(int dd, uint16_t ogf, uint16_t ocf, uint8_t plen, void *param);
int hci_send_req(int dd, struct hci_request *req, int timeout);
int hci_create_connection(int dd, const bdaddr_t *bdaddr, uint16_t ptype, uint16_t clkoffset, uint8_t rswitch, uint16_t *handle, int to);
int hci_disconnect(int dd, uint16_t handle, uint8_t reason, int to);
int hci_inquiry(int dev_id, int len, int num_rsp, const uint8_t *lap, inquiry_info **ii, long flags);
int hci_devinfo(int dev_id, struct hci_dev_info *di);
int hci_devba(int dev_id, bdaddr_t *bdaddr);
int hci_devid(const char *str);
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);
int hci_read_remote_name(int dd, const bdaddr_t *bdaddr, int len, char *name, int to);
int hci_read_remote_name_with_clock_offset(int dd, const bdaddr_t *bdaddr, uint8_t pscan_rep_mode, uint16_t clkoffset, int len, char *name, int to);
int hci_read_remote_name_cancel(int dd, const bdaddr_t *bdaddr, int to);
int hci_read_remote_version(int dd, uint16_t handle, struct hci_version *ver, int to);
int hci_read_remote_features(int dd, uint16_t handle, uint8_t *features, int to);
int hci_read_remote_ext_features(int dd, uint16_t handle, uint8_t page, uint8_t *max_page, uint8_t *features, int to);
int hci_read_clock_offset(int dd, uint16_t handle, uint16_t *clkoffset, int to);
int hci_read_local_version(int dd, struct hci_version *ver, int to);
......
bluez
源码中tools
目录下的许多文件可以作为HCI编程的参考,例如tools/hcitool.c
和tools/hciconfig.c
等,这些代码可以有些可以直接用在自己的项目中。
下载bluez的源码:
- 最新版本可以在官网下载: http://www.bluez.org/
- 更多的历史版本可以在这里下载:https://mirrors.edge.kernel.org/pub/linux/bluetooth/
下面简要介绍几个hcitool
中的几个指令的实现
查看HCI设备的详细信息
当我们在命令行终端运行hcitool
的时候,经常会得到这样的输出:
$ hciconfig
hci0: Type: Primary Bus: UART
BD Address: E4:5F:01:3D:DA:11 ACL MTU: 1021:8 SCO MTU: 64:1
UP RUNNING PSCAN
RX bytes:2609 acl:0 sco:0 events:182 errors:0
TX bytes:10313 acl:0 sco:0 commands:182 errors:0
那么这个过程是如何实现的呢?我们可以查看下hcitool.c
的源码:
int main(int argc, char *argv[])
{
...
int ctl;
/* Open HCI socket */
if ((ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI)) < 0) {
perror("Can't open HCI socket.");
exit(1);
}
if (argc < 1) {
print_dev_list(ctl, 0);
exit(0);
}
...
}
首先打开HCI socket,我们知道现在的Linux发行版基本上都把蓝牙驱动整合进Linux内核了,而HCI就是应用程序跟蓝牙驱动进行交互的接口,Linux内核向应用程序提供一个AF_BLUETOOTH
的socket
来实现蓝牙Host和Controller的交互。因此,我们代码中首先构建一个蓝牙socket
static void print_dev_list(int ctl, int flags)
{
struct hci_dev_list_req *dl;
struct hci_dev_req *dr;
int i;
if (!(dl = malloc(HCI_MAX_DEV * sizeof(struct hci_dev_req) +
sizeof(uint16_t)))) {
perror("Can't allocate memory");
exit(1);
}
dl->dev_num = HCI_MAX_DEV;
dr = dl->dev_req;
if (ioctl(ctl, HCIGETDEVLIST, (void *) dl) < 0) {
perror("Can't get device list");
free(dl);
exit(1);
}
for (i = 0; i< dl->dev_num; i++) {
di.dev_id = (dr+i)->dev_id;
if (ioctl(ctl, HCIGETDEVINFO, (void *) &di) < 0)
continue;
print_dev_info(ctl, &di);
}
free(dl);
}
HCI_MAX_DEV
是内核支持的最大设备数,默认为16,struct hci_dev_list_req *dl
是要从内核返回的结果,因而首先为其分配空间,然后通过蓝牙socket进行ioctl
系统调用,其中HCIGETDEVLIST
是蓝牙驱动提供的命令,它表示获取蓝牙设备的数量,命令HCIGETDEVINFO
表示获取某个蓝牙设备的具体信息,最后调用print_dev_info
将结果打印出来
扫描周围的LE设备
现在低功耗蓝牙的应用非常广泛,hcitool
命令也可以扫描周围的低功耗蓝牙设备,命令行终端输入指令之后,输出是这样的:
$ sudo hcitool lescan
LE Scan ...
44:62:84:7A:DF:D7 (unknown)
44:62:84:7A:DF:D7 (unknown)
52:7A:77:FC:D4:79 (unknown)
括号中的是设备名字,如果没有名字则标记为unknown
。
那么我们可以看下这部分的实现是怎样的
static struct option lescan_options[] = {
{ "help", 0, 0, 'h' }, // 帮助信息
{ "static", 0, 0, 's' }, // 是否使用静态地址
{ "privacy", 0, 0, 'p' }, // 是否使用随机地址
{ "passive", 0, 0, 'P' }, // 是否进行被动扫描
{ "whitelist", 0, 0, 'w' }, // 是否使用白名单
{ "discovery", 1, 0, 'd' }, // Limited/General Discovery
{ "duplicates", 0, 0, 'D' }, // 是否允许重复的scan report
{ 0, 0, 0, 0 }
};
static void cmd_lescan(int dev_id, int argc, char **argv)
{
int err, opt, dd;
uint8_t own_type = LE_PUBLIC_ADDRESS;
uint8_t scan_type = 0x01;
uint8_t filter_type = 0;
uint8_t filter_policy = 0x00;
uint16_t interval = htobs(0x0010);
uint16_t window = htobs(0x0010);
uint8_t filter_dup = 0x01;
for_each_opt(opt, lescan_options, NULL) {
switch (opt) {
case 's':
own_type = LE_RANDOM_ADDRESS;
break;
case 'p':
own_type = LE_RANDOM_ADDRESS;
break;
case 'P':
scan_type = 0x00; /* Passive */
break;
case 'w':
filter_policy = 0x01; /* Whitelist */
break;
case 'd':
filter_type = optarg[0];
if (filter_type != 'g' && filter_type != 'l') {
fprintf(stderr, "Unknown discovery procedure\n");
exit(1);
}
interval = htobs(0x0012);
window = htobs(0x0012);
break;
case 'D':
filter_dup = 0x00;
break;
default:
printf("%s", lescan_help);
return;
}
}
helper_arg(0, 1, &argc, &argv, lescan_help);
if (dev_id < 0)
dev_id = hci_get_route(NULL);
dd = hci_open_dev(dev_id);
if (dd < 0) {
perror("Could not open device");
exit(1);
}
err = hci_le_set_scan_parameters(dd, scan_type, interval, window,
own_type, filter_policy, 10000);
if (err < 0) {
perror("Set scan parameters failed");
exit(1);
}
err = hci_le_set_scan_enable(dd, 0x01, filter_dup, 10000);
if (err < 0) {
perror("Enable scan failed");
exit(1);
}
printf("LE Scan ...\n");
err = print_advertising_devices(dd, filter_type);
if (err < 0) {
perror("Could not receive advertising events");
exit(1);
}
err = hci_le_set_scan_enable(dd, 0x00, filter_dup, 10000);
if (err < 0) {
perror("Disable scan failed");
exit(1);
}
hci_close_dev(dd);
}
首先lescan_options
静态配置了lescan
的参数,函数cmd_lescan
解析该参数,然后调用HCI的API函数hci_open_dev
获取可以和蓝牙设备通信的socket,紧接着调用hci_le_set_scan_parameters
设置扫描参数,调用hci_le_set_scan_enable
开始进行扫描,函数print_advertising_devices
会不断的从socket中读数据,这个数据是扫描的结果,即LE SCAN REPORT
,最后将扫描结果打印出来,如下所示:
static int print_advertising_devices(int dd, uint8_t filter_type)
{
...
while (1) {
evt_le_meta_event *meta;
le_advertising_info *info;
char addr[18];
while ((len = read(dd, buf, sizeof(buf))) < 0) {
if (errno == EINTR && signal_received == SIGINT) {
len = 0;
goto done;
}
if (errno == EAGAIN || errno == EINTR)
continue;
goto done;
}
...
printf("%s %s\n", addr, name);
...
}
}