蓝牙bluez进行HCI编程


我是一名嵌入式蓝牙工程师,平时大部分时间都在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默认会安装一些实用工具,比如hcitoolhciconfig ,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编程

蓝牙开发环境安装好之后,下面实现一个非常简单的小程序,就是调用HCIAPI来获取蓝牙设备的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.ctools/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_BLUETOOTHsocket来实现蓝牙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);
  ...
  }
}
  • 9
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值