Linux蓝牙之使用C开发轻量级BLE应用

linux蓝牙协议栈bluez(https://www.bluez.org/),对于开发蓝牙应用提供了丰富的蓝牙开发工具和示例。

在Linux下面使用C开发蓝牙应用主要分为以下2种方式:

  • 基于bluez的hci/mgmt接口,可更精细控制蓝牙硬件模块,适合开发轻量级BLE应用。
  • 基于bluez的DBUS接口,提供了大量的蓝牙上层协议,能更好管理蓝牙,更适合开发需要丰富接口的重应用,比如音频、文件传输等。

这里主要讨论不使用dbus(DBUS对于一些简单的BLE应用,一些简单的Linux设备来说还是太“厚重”了),而是基于hci接口来开发简单的轻量级BLE应用。

具体实现

这里主要实现BLE最重要的两类工作模式:

  • 中心设备/主机 (Central)
    中心设备可以扫描外围设备,并在发现有外围设备存在后与之建立连接,之后就可以使用外围设备提供的服务(Service)。
    一般而言,手机会担任中心设备的角色,利用外围设备提供的数据进行处理或展示等等。小程序提供低功耗蓝牙接口是默认设定手机为中心设备的。
  • 外围设备/从机 (Peripheral)
    外围设备一直处于广播状态,等待被中心设备搜索和连接,不能主动发起搜索。例如智能手环、传感器等设备。
    如果外围设备广播时被设置为不可连接的状态,也被称为广播模式 (Broadcaster),常见的例子是蓝牙信标 (Beacon) 设备。

好像核外bluez协议栈没有针对BLE有独立的c语言api,所以主要思路就是参考核外协议栈bluez里面测试、工具的相关代码实现。
目前参考的是:

  • Central端对应tools/btgatt-client.c
  • Peripheral端对应tools/btgatt-server.c

以上都已验证通过,并已在正常使用。

Central端

Central端主要功能包括,对从设备的连接,查到对应UUID的服务,并对相关的特征值进行读写,或者注册notify操作。
整个流程伪代码实现如下,主要都是通过回调方式实现:

static int *ble_client_task_thread(void *arg) 
{
    int sec = BT_SECURITY_LOW;
	uint16_t mtu = 0;
	uint8_t dst_type = BDADDR_LE_PUBLIC;
	bdaddr_t src_addr, dst_addr;
	int dev_id = -1;
	int fd = -1;
	
	ble_client_is_running = true;
    bacpy(&src_addr, BDADDR_ANY);

	if (str2ba("xx:xx:xx:xx:xx:xx", &dst_addr) < 0) {
		LOG_ERROR("Invalid remote address: %s\n",
							optarg);
		return EXIT_FAILURE;
	}

    //mainloop初始化
	mainloop_init();

    //连接从机
    fd = l2cap_le_att_connect(&src_addr, &dst_addr, dst_type, sec);
	if (fd < 0)
		return EXIT_FAILURE;
	
    //创建客户端
	cli = client_create(fd, mtu);
	if (!cli) {
		LOG_ERROR("client_create error!\n");
		close(fd);
		return EXIT_FAILURE;
	}

	LOG_INFO("Running GATT Client\n");
	//print_services(cli);

    //运行mainloop
	//mainloop_run_with_signal(signal_cb, NULL);
	mainloop_run();

	LOG_INFO("Shutting down...\n");
	
    //注销操作
	client_destroy(cli);
	close(fd);

    return EXIT_SUCCESS;
}

client_create()主要是ATT、GATT初始化,注册一些GATT的回调函数,

static struct client *client_create(int fd, uint16_t mtu)
{
	struct client *cli;

	LOG_INFO("ble client_create.\n");
	cli = new0(struct client, 1);
	if (!cli) {
		LOG_ERROR("Failed to allocate memory for client\n");
		return NULL;
	}

	cli->att = bt_att_new(fd, false);
	if (!cli->att) {
		LOG_ERROR("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)) {
		LOG_ERROR("Failed to set up ATT transport layer\n");
		bt_att_unref(cli->att);
		free(cli);
		return NULL;
	}

	if (!bt_att_register_disconnect(cli->att, client_att_disconnect_cb, NULL,
								NULL)) {
		LOG_ERROR("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) {
		LOG_ERROR("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) {
		LOG_ERROR("Failed to create GATT client\n");
		gatt_db_unref(cli->db);
		bt_att_unref(cli->att);
		free(cli);
		return NULL;
	}

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

	ret = bt_gatt_client_ready_register(cli->gatt, ready_cb, cli, NULL);
	ret = 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;
}

上面最重要的就是bt_gatt_client_ready_register注册的回调函数ready_cb,在该函数中可以通过gatt_db_foreach_service函数遍历查找相应UUID的服务,找到后并执行对应的服务函数mpa_service, 在mpa_service就可以对相关特征值进行读写,或者通过bt_gatt_client_register_notify注册notify,这里就不赘述了。

static void ready_cb(bool success, uint8_t att_ecode, void *user_data)
{
	struct client *cli = user_data;

	if (!success) {
		LOG_ERROR("GATT discovery procedures failed - error code: 0x%02x\n",
								att_ecode);
		return;
	}

	LOG_INFO("GATT discovery procedures complete\n");

	print_services(cli);
	
	bt_uuid_t tmp, uuid;
	bt_string_to_uuid(&tmp, "0000fff0-0000-1000-8000-008022221111");
	bt_uuid_to_uuid128(&tmp, &uuid);
	gatt_db_foreach_service(cli->db, &uuid, mpa_service, cli);
}

Peripheral端

Peripheral端主要功能包括,一直处于广播状态,等待被中心设备搜索和连接,被中心设备进行读写操作或注册通知主动上报特征值。
整个流程伪代码实现如下,主要也都是通过回调方式实现:

static int *ble_server_task_thread(void *arg) 
{
	bdaddr_t src_addr;
    int sec = BT_SECURITY_LOW;
	uint8_t src_type = BDADDR_LE_PUBLIC;
    int fd;
    uint16_t mtu = 0;
    struct server *server;

	cmd_le_adv(0, 0, NULL);

	bacpy(&src_addr, BDADDR_ANY);

    //监听等待连接
	fd = l2cap_le_att_listen_and_accept(&src_addr, sec, src_type);
	if (fd < 0) {
		LOG_ERROR("Failed to accept L2CAP ATT connection\n");
		return EXIT_FAILURE;
	}

    //mainloop初始化
	mainloop_init();

	server = server_create(fd, mtu);
	if (!server) {
		close(fd);
		LOG_ERROR("server_create error!!\n");
		return EXIT_FAILURE;
	}

	gatt_db_foreach_service(server->db, NULL, print_service, server);
	LOG_INFO("Running GATT server\n");

    //mainloop运行
	mainloop_run();

    //注销操作
	server_destroy(server);

	return EXIT_SUCCESS;
}

server_create()主要是ATT、GATT初始化,构建相关服务,

static struct server *server_create(int fd, uint16_t mtu)
{
	struct server *server;
	size_t name_len = strlen(test_device_name);

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

	server->att = bt_att_new(fd, false);
	if (!server->att) {
		fprintf(stderr, "Failed to initialze ATT transport layer\n");
		goto fail;
	}

	if (!bt_att_set_close_on_unref(server->att, true)) {
		fprintf(stderr, "Failed to set up ATT transport layer\n");
		goto fail;
	}

	if (!bt_att_register_disconnect(server->att, server_att_disconnect_cb, NULL,
									NULL)) {
		fprintf(stderr, "Failed to set ATT disconnect handler\n");
		goto fail;
	}

	server->name_len = name_len + 1;
	server->device_name = malloc(name_len + 1);
	if (!server->device_name) {
		fprintf(stderr, "Failed to allocate memory for device name\n");
		goto fail;
	}

	memcpy(server->device_name, test_device_name, name_len);
	server->device_name[name_len] = '\0';

	server->fd = fd;
	server->db = gatt_db_new();
	if (!server->db) {
		fprintf(stderr, "Failed to create GATT database\n");
		goto fail;
	}

	server->gatt = bt_gatt_server_new(server->db, server->att, mtu, 0);
	if (!server->gatt) {
		fprintf(stderr, "Failed to create GATT server\n");
		goto fail;
	}

	//构建服务
	populate_db(server);

	return server;

fail:
	gatt_db_unref(server->db);
	free(server->device_name);
	bt_att_unref(server->att);
	free(server);

	return NULL;
}
static void populate_db(struct server *server)
{
    //构建生成GAP服务
	populate_gap_service(server);
    //构建生成GATT服务
	populate_gatt_service(server);
    //构建生成配置服务
	populate_config_service(server);
}

以上三个具体操作都比较类似,主要就是gatt_db_add_service新增服务,然后通过gatt_db_service_add_characteristic去增加特征值,通过gatt_db_service_add_descriptor去增加描述,最后gatt_db_service_set_active激活服务,其中特征值和描述都注册相应的读写回调函数。
这里就不再赘述,感兴趣的可自行去查看代码。

后续计划

  • 基于mgmt接口来实现ble基本操作

参考

https://www.cnblogs.com/embedded-linux/p/18332058
https://blog.csdn.net/u014028690/article/details/107246633
https://blog.csdn.net/u014028690/article/details/114132989