【BlueZ】【蓝牙】跨平台实现Ble Master&Slave&Mesh 之Linux篇-1

一,前述

1,上一篇讲述了如何通过Windows实现蓝牙应用,该方式可适用于Windows/Linux/Macos等场景。[https://blog.csdn.net/u014028690/article/details/113943287?spm=1001.2014.3001.5502],该方式比较特殊的一点,用了指定的蓝牙dongle设备,也规避了不同平台产生的差异而导致的问题。
2,但是市面上更多的是WIFI+BT/BLE combo的芯片。针对该类设备,各个厂商都遵循统一的HCI接口以及适配协议,不同的是针对各自设备都有各自的驱动程序。所以contorller部分协议以及硬件都是由原厂提供,host部分的协议栈有我们熟知的linux官方蓝牙协议栈bluez。(http://www.bluez.org/

二、设计思路

1,模型框图

当前我们利用通用设备(如市面上的USB蓝牙dongle/系统蓝牙等)实现ble master/ble slave。

基础模型

2,具体设计

2.1 我们采用MGMT方式实现ble master/ble slave。为什么应用MGMT方式实现主从机。

1,很重要的一点,占用资源少。(实测Dbus环境动态库接近2M,bin接近1M,还是strip之后的。对于flash很小的设备而言内存占用压力很大,而用MGMT加起来不到1M)
2,便于纠错,直接和kernel通信的接口,避免过多的进程间交互,导致问题出来,不需要花大量时间纠错。(也是避免了内核与用户空间没有冲突的风险)
3,剩下的优点不提了,对于我当前简单实现ble slave,单点连接设备而言,用的比较少。
// 感兴趣可以看一下,来自官方blog
Command queues and synchronization
Since the kernel is now responsible for all HCI traffic there’s no risk of conflicts between kernel and userspace.

Blocking operations
With the management interface there are simple asynchronous messages that are used to power on and off adapters: blocking problem solved.

Unnecessary HCI event processing
No raw HCI sockets means no promisc flag on the kernel side. So extra processing of these packets isn’t needed anymore.

Distributed security policy and logic
With the management interface only user interaction (PIN code/pass key requests, etc) and link key storage is handled on the user space side. User space will feed the kernel with all stored link keys, including the key types, upon adapter initialization. After that the kernel is responsible for handling link key requests.

An additional benefit with having an abstracted interface for security is that it can be used for the Security Manager Protocol (SMP) that’s part of the Bluetooth Low Energy (LE) specification. SMP has a similar user interaction model as SSP so the same messages between user space and the kernel can be reused.

As long as SMP is implemented on the kernel side there’d be a big problem with dealing with it from user space using the existing kernel interface since unlike SSP, SMP uses L2CAP and not HCI for messaging.

Lack of early-tracing capability
The management interface will offer a special type of tracing socket which can be used to get the HCI traffic of all connected adapters. This will allow a userspace process to catch all traffic to and from an adapter from the first moment that it is plugged in.

这里可以参考另一篇文章。([https://blog.csdn.net/u014028690/article/details/107246633]
2.2 基础篇,我们注重从源码出发,便于理解以及应用。详细的多设备控制,我们后续补充。

三、代码理解

1,Ble Master之扫描设备

源码信息:btmgmt.c

由于我们采用MGMT方式实现ble master,故我们应用btmgmt工具集实现扫描设备。

// 扫描设备分为两个步骤

// 第一步设定扫描参数:
// 对应命令行:btmgmt scan-params 0x30 0x30
// 对应代码如下:
static void cmd_scan_params(int argc, char **argv)
{
	struct mgmt_cp_set_scan_params cp;
	uint16_t index;

	index = mgmt_index;
	if (index == MGMT_INDEX_NONE)
		index = 0;

	cp.interval = strtol(argv[1], NULL, 0);
	cp.window = strtol(argv[2], NULL, 0);

	if (mgmt_send(mgmt, MGMT_OP_SET_SCAN_PARAMS, index, sizeof(cp), &cp,
					scan_params_rsp, NULL, NULL) == 0) {
		error("Unable to send set_scan_params cmd");
		return bt_shell_noninteractive_quit(EXIT_FAILURE);
	}
}

// 第二步扫描设备:
// 对应命令行:btmgmt find-service -u 0xa201 -l
// 注:如需多个设备扫描可在后面追加service uuid,例如:find-service -u 0xa201 -u 0x1827 -u 0x1828 -l (这里包含了mesh 设备的unprov service uuid和 proxy service uuid)
// 对应代码如下:
static void cmd_find_service(int argc, char **argv)
{
	struct mgmt_cp_start_service_discovery *cp;
	uint8_t buf[sizeof(*cp) + 16 * MAX_UUIDS];
	uuid_t uuid;
	uint128_t uint128;
	uuid_t uuid128;
	uint8_t type = SCAN_TYPE_DUAL;
	int8_t rssi;
	uint16_t count;
	int opt;
	uint16_t index;

	index = mgmt_index;
	if (index == MGMT_INDEX_NONE)
		index = 0;

...//省略很多行

	argc -= optind;
	argv += optind;
	optind = 0;

	cp = (void *) buf;
	cp->type = type;
	cp->rssi = rssi;
	cp->uuid_count = cpu_to_le16(count);

	if (mgmt_send(mgmt, MGMT_OP_START_SERVICE_DISCOVERY, index,
				sizeof(*cp) + count * 16, cp,
				find_service_rsp, NULL, NULL) == 0) {
		error("Unable to send start_service_discovery cmd");
		return bt_shell_noninteractive_quit(EXIT_FAILURE);
	}
}
对应到kernel的源码如下:
static int start_service_discovery(struct sock *sk, struct hci_dev *hdev,
				   void *data, u16 len)
{
	struct mgmt_cp_start_service_discovery *cp = data;
	struct mgmt_pending_cmd *cmd;
	struct hci_request req;
	const u16 max_uuid_count = ((U16_MAX - sizeof(*cp)) / 16);
	u16 uuid_count, expected_len;
	u8 status;
	int err;

	BT_DBG("%s", hdev->name);

	hci_dev_lock(hdev);

...//省略很多行

	if (!trigger_discovery(&req, &status)) {
		err = mgmt_cmd_complete(sk, hdev->id,
					MGMT_OP_START_SERVICE_DISCOVERY,
					status, &cp->type, sizeof(cp->type));
		mgmt_pending_remove(cmd);
		goto failed;
	}

	err = hci_req_run(&req, start_discovery_complete);
	if (err < 0) {
		mgmt_pending_remove(cmd);
		goto failed;
	}

	hci_discovery_set_state(hdev, DISCOVERY_STARTING);

failed:
	hci_dev_unlock(hdev);
	return err;
}
// 扫描开启成功后,会设定对应设定扫描Timeout
// 这里很重要,决定了你的设备是否需要短期多次被扫到或者扫描开启周期等。
static void start_discovery_complete(struct hci_dev *hdev, u8 status,u16 opcode);

以下贴图,便于理解,如果不好理解,可省略。。。

scan流程

扫描设备实际结果:

结果

注:这里我们选取一个设备(BC:23:00:00:FF:02)。

2,Ble Master之设备连接

源码信息:btgatt-client.c

// 蓝牙子设备连接包含两个主要步骤:
// 第一步:L2CAP链路连接:
// 这里的connect是否成功很大程度取决于蓝牙设备的性能,否则会一直出现conn_fail()的情况,或者conn_fail()之后connected的情况,设备占用,但是无法操作等等状况。

static int l2cap_le_att_connect(bdaddr_t *src, bdaddr_t *dst, uint8_t dst_type,
									int sec)
{
	int sock;
	struct sockaddr_l2 srcaddr, dstaddr;
	struct bt_security btsec;

...//省略很多行

	/* Set up destination address */
	memset(&dstaddr, 0, sizeof(dstaddr));
	dstaddr.l2_family = AF_BLUETOOTH;
	dstaddr.l2_cid = htobs(ATT_CID);
	dstaddr.l2_bdaddr_type = dst_type;
	bacpy(&dstaddr.l2_bdaddr, dst);

	printf("Connecting to device...");
	fflush(stdout);

	if (connect(sock, (struct sockaddr *) &dstaddr, sizeof(dstaddr)) < 0) {  // 这里如果连接失败或者errno出现busy/not implement等情况,fd会失效
		perror(" Failed to connect");
		close(sock);
		return -1;
	}

	printf(" Done\n");

	return sock;
}

这里仅仅是ATT连接成功流程。
ATT

// 第二步便是注册GATT Client。
// 上一步connect后的fd在这一步中进行数据交互。
// 包含有:断开事件注册,服务获取
static struct client *client_create(int fd, uint16_t mtu)
{
	struct client *cli;

	cli = new0(struct client, 1);
	if (!cli) {
		fprintf(stderr, "Failed to allocate memory for client\n");
		return NULL;
	}

	cli->att = bt_att_new(fd, false);
	if (!cli->att) {
		fprintf(stderr, "Failed to initialze ATT transport layer\n");
		bt_att_unref(cli->att);
		free(cli);
		return NULL;
	}

	if (!bt_att_set_close_on_unref(cli->att, true)) {
		fprintf(stderr, "Failed to set up ATT transport layer\n");
		bt_att_unref(cli->att);
		free(cli);
		return NULL;
	}

	if (!bt_att_register_disconnect(cli->att, att_disconnect_cb, NULL,
								NULL)) {
		fprintf(stderr, "Failed to set ATT disconnect handler\n");
		bt_att_unref(cli->att);
		free(cli);
		return NULL;
	}

	cli->fd = fd;
	cli->db = gatt_db_new();
	if (!cli->db) {
		fprintf(stderr, "Failed to create GATT database\n");
		bt_att_unref(cli->att);
		free(cli);
		return NULL;
	}

	cli->gatt = bt_gatt_client_new(cli->db, cli->att, mtu, 0);
	if (!cli->gatt) {
		fprintf(stderr, "Failed to create GATT client\n");
		gatt_db_unref(cli->db);
		bt_att_unref(cli->att);
		free(cli);
		return NULL;
	}

	gatt_db_register(cli->db, service_added_cb, service_removed_cb,
								NULL, NULL);

	if (verbose) {
		bt_att_set_debug(cli->att, BT_ATT_DEBUG_VERBOSE, att_debug_cb,
								"att: ", NULL);
		bt_gatt_client_set_debug(cli->gatt, gatt_debug_cb, "gatt: ",
									NULL);
	}

	bt_gatt_client_ready_register(cli->gatt, ready_cb, cli, NULL);
	bt_gatt_client_set_service_changed(cli->gatt, service_changed_cb, cli,
									NULL);

	/* bt_gatt_client already holds a reference */
	gatt_db_unref(cli->db);

	return cli;
}

连接后,我们便可以获取到设备的服务及特征值信息了。
设备获取

注册一下notify cb,便于接收对端数据
register notify

3,Ble Master之设备透传
// 连上之后,数据透传就比较简单了,可以按需选择。
// 当前我连的这个设备,发什么数据就回什么数据。
static void cmd_write_value(struct client *cli, char *cmd_str)
{
	int opt, i, val;
	char *argvbuf[516];
	char **argv = argvbuf;
	int argc = 1;
	uint16_t handle;
	char *endptr = NULL;
	int length;
	uint8_t *value = NULL;
	bool without_response = false;
	bool signed_write = false;

	if (!bt_gatt_client_is_ready(cli->gatt)) {
		printf("GATT client not initialized\n");
		return;
	}

	if (!parse_args(cmd_str, 514, argv + 1, &argc)) {
		printf("Too many arguments\n");
		write_value_usage();
		return;
	}

	optind = 0;
	argv[0] = "write-value";
	while ((opt = getopt_long(argc, argv, "+ws", write_value_options,
								NULL)) != -1) {
		switch (opt) {
		case 'w':
			without_response = true;
			break;
		case 's':
			signed_write = true;
			break;
		default:
			write_value_usage();
			return;
		}
	}
 ... //省略很多行  
	if (without_response) {
		if (!bt_gatt_client_write_without_response(cli->gatt, handle,
						signed_write, value, length)) {
			printf("Failed to initiate write without response "
								"procedure\n");
			goto done;
		}

		printf("Write command sent\n");
		goto done;
	}

	if (!bt_gatt_client_write_value(cli->gatt, handle, value, length,
								write_cb,
								NULL, NULL))
		printf("Failed to initiate write procedure\n");

done:
	free(value);
}

对应write的notify的handle value

数据透传

发送给设备端数据(对应handle=0x10):0x00 0x01 0x02 0x03 0x04 0x05
在这里插入图片描述

4,Ble Master补充说明

1,真正投入产品,这些远远是不够的,所以这里要求我们对基本原理了解的同时,衍生出对多个设备的思考,以及如何维护多个设备fd和连接/断开/透传等状态机。

2,关于如何剥离bluez源码,需要实现跨平台裸代码无第三方编译及使用的可以私聊我。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tim-Cheng

你的鼓励是我最大的动力,奥利给

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值