HarmonyOS开发实战( Beta5.0)蓝牙实现服务端和客户端通讯详解

鸿蒙HarmonyOS开发往期必看:

HarmonyOS NEXT应用开发性能实践总结

最新版!“非常详细的” 鸿蒙HarmonyOS Next应用开发学习路线!(从零基础入门到精通)


介绍

本示例分为服务端和客户端两个功能模块。
服务端创建蓝牙服务实例,添加心率跳动服务。以心率跳动值作为特征值,通过notifyCharacteristicChanged接口将心率跳动特征值广播发送给连接到本服务端并订阅了该特征值变动通知的蓝牙客户端设备。
客户端以特定服务UUID作为过滤条件扫描服务端,连接到扫描的设备后通过setCharacteristicChangeNotification接口向服务端发送‘通知心率跳动特征值变动’的请求,以便收到服务端该特征值变动的通知消息。

主要有以下几点功能:

  1. 发现具有特定服务的设备。
  2. 连接到设备。
  3. 发现服务。
  4. 发现服务的特征、读取给定特征的值、为特征设置通知等。

相关概念:

  1. BLE扫描:通过BLE扫描接口startBLEScan实现对BLE设备的搜索。
  2. BLE连接:通过BLE的GattClientDevice实现对BLE设备的连接、断连等操作。
  3. 接收数据:通过BLECharacteristicChange接收特征值的改变。

效果图预览

使用说明

  1. 该功能需要两台设备,进入BLE通讯场景页面,选择当前设备是作为BLE服务端还是BLE客户端。
  2. 点击“BLE服务端”,进入服务端页面。点击“开启BLE心率广播”,打开蓝牙服务,向订阅了心率跳动值通知的客户端广播发送实时心率值。
  3. 点击“BLE客户端”,进入客户端页面。点击“搜索设备”,搜索开启了心率跳动服务的BLE服务端,连接搜索到的蓝牙设备。连接成功后,点击设备右边的“已连接”,进入心率波动图页面查看实时心率。

实现思路

服务端
  1. 开启或关闭蓝牙广播服务。源码参考BluetoothAdvertiser.ets及startAdvertiser。

    toggleAdvertiser(): void {if (this.startAdvertiserState) {//  TODO: 知识点 关闭蓝牙广播服务advertiserBluetoothViewModel.stopAdvertiser();this.toggleHeartRate(false);this.startAdvertiserState = false;} else {//  TODO: 知识点 开启蓝牙广播服务let ret = advertiserBluetoothViewModel.startAdvertiser();if (ret) {this.localName = advertiserBluetoothViewModel.getLocalName();// 模拟心率跳动this.toggleHeartRate(true);this.startAdvertiserState = true;} else {Log.showError(TAG, `toggleAdvertiser: ret = ${ret}`);}}
    }
    
     // TODO: 知识点 创建蓝牙服务实例this.mGattServer = ble.createGattServer();let descriptors: Array<ble.BLEDescriptor> = [];const arrayBuffer = ArrayBufferUtils.byteArray2ArrayBuffer([11]);const descriptor: ble.BLEDescriptor = {serviceUuid: BleConstants.UUID_SERVICE_HEART_RATE, //  特定服务(service)的 UUIDcharacteristicUuid: BleConstants.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT, // 特定特征(characteristic)的 UUIDdescriptorUuid: BleConstants.UUID_DESCRIPTOR_HEART_RATE, // 描述符(descriptor)的 UUIDdescriptorValue: arrayBuffer  // 描述符对应的二进制值};descriptors[0] = descriptor;let characteristics: Array<ble.BLECharacteristic> = [];const arrayBufferC = ArrayBufferUtils.byteArray2ArrayBuffer([1]);const characteristic: ble.BLECharacteristic = {serviceUuid: BleConstants.UUID_SERVICE_HEART_RATE, // 特定服务(service)的 UUIDcharacteristicUuid: BleConstants.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT, // 特定特征(characteristic)的 UUIDcharacteristicValue: arrayBufferC, // 特征对应的二进制值descriptors: descriptors  // 特定特征的描述符列表};characteristics[0] = characteristic;// 定义心率跳动服务const service: ble.GattService = {serviceUuid: BleConstants.UUID_SERVICE_HEART_RATE,isPrimary: true, // 主服务characteristics: characteristics,includeServices: []};try {// 添加服务this.mGattServer.addService(service);Log.showInfo(TAG, `startAdvertiser: addService suc`);} catch (err) {Log.showError(TAG, `startAdvertiser: addService err = ${err}`);}try {// 订阅连接服务状态this.onConnectStateChange();// 设置广播发送的参数let setting: ble.AdvertiseSetting = {interval: DurationConstants.ADVERTISE_INTERVAL, // 广播间隔,最小值设置160个slot表示100mstxPower: 1, // 发送功率,最小值设置-127,最大值设置1,默认值设置-7connectable: true  // 是否是可连接广播};// BLE广播包内容let advData: ble.AdvertiseData = {serviceUuids: [BleConstants.UUID_SERVICE_HEART_RATE], // 要广播的服务 UUID 列表manufactureData: [], // 广播的制造商信息列表serviceData: [], // 广播的服务数据列表};// BLE回复扫描请求回复响应let advResponse: ble.AdvertiseData = {serviceUuids: [BleConstants.UUID_SERVICE_HEART_RATE],manufactureData: [],serviceData: [],};// TODO: 知识点 开始广播ble.startAdvertising(setting, advData, advResponse);Log.showInfo(TAG, `startAdvertiser: startAdvertising success`);return true;} catch (err) {Log.showError(TAG, `startAdvertiser: startAdvertising err = ${err}`);}
    
  2. 服务开启状态下,广播通知特征值变动。源码参考BluetoothAdvertiser.ets及notifyCharacteristicChanged。

       this.mIntervalId = setInterval(() => {this.heartRate = MathUtils.getRandomInt(MIN_HEART_RATE, MAX_HEART_RATE);if (this.deviceId) {// TODO: 知识点 通知客户端心率特征值变动advertiserBluetoothViewModel.notifyCharacteristicChanged(this.deviceId, this.heartRate);} else {Log.showWarn(TAG, `toggleHeartRate: deviceId is null, heartRate = ${this.heartRate}`);}}, DurationConstants.NOTIFY_DELAY_TIME)
    
         // 构造BLECharacteristiclet arrayBufferC = ArrayBufferUtils.byteArray2ArrayBuffer([0x00, heartRate]);let characteristic: CharacteristicModel = {serviceUuid: BleConstants.UUID_SERVICE_HEART_RATE,characteristicUuid: BleConstants.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT,characteristicValue: arrayBufferC,descriptors: descriptors};// 通知的特征值消息let notifyCharacteristic: NotifyCharacteristicModel = {serviceUuid: BleConstants.UUID_SERVICE_HEART_RATE,characteristicUuid: BleConstants.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT,characteristicValue: characteristic.characteristicValue,confirm: false  // 对端不需要确认};// TODO: 知识点 server端特征值发生变化时,主动通知已连接的client设备。this.mGattServer.notifyCharacteristicChanged(deviceId, notifyCharacteristic, (err: BusinessError) => {if (err) {Log.showError(TAG, 'notifyCharacteristicChanged callback failed, err.code = ' + err.code + ", err.message =" + err.message);} else {Log.showInfo(TAG, 'notifyCharacteristicChanged callback success');}});
    
客户端
  1. 启动时请求蓝牙权限。源码参考BluetoothClient.ets。

    // 所需蓝牙权限
    const PERMISSION_LIST: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION','ohos.permission.LOCATION'
    ];// TODO 知识点: 获取蓝牙相关权限
    function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();atManager.requestPermissionsFromUser(context, permissions).then((data) => {const granStatus: Array<number> = data.authResults;const length: number = granStatus.length;for (let i = 0; i < length; i++) {if (granStatus[i] === 0) {} else {return;}}})
    }
    
  2. 扫描设备。源码参考BluetoothClientModel.ets

    startBLEScan(): boolean {   if (!this.isBluetoothEnabled()) {Log.showInfo(TAG, `startBLEScan: bluetooth is disable.`);// 启动蓝牙服务this.enableBluetooth();promptAction.showToast({message: $r('app.string.ble_toast_enable_bluetooth'),duration: DurationConstants.DURATION_TIME});return false;}// 订阅搜索蓝牙服务this.onBLEDeviceFind(); // 扫描蓝牙设备const ret = this.startBLEScanInner();return ret;
    }
    
  3. 连接设备。源码参考BluetoothClientModel.ets及connectInner。

    .onClick(() => {if (this.bluetoothDevice.connectionState === ConnectionState.STATE_DISCONNECTED) {// 连接蓝牙设备bluetoothViewModel.connect(this.bluetoothDevice);} else if (this.bluetoothDevice.connectionState === ConnectionState.STATE_CONNECTED) { // 断开与蓝牙设备的连接bluetoothViewModel.disconnect();}
    })
    
    private connectInner(gattClientDevice: ble.GattClientDevice): boolean {try {if (!gattClientDevice) {Log.showWarn(TAG, `connectInner: mGattClientDevice is null`);return false;}// 订阅连接状态改变消息this.onBLEConnectionStateChange();// 订阅特征值改变消息this.onBLECharacteristicChange();// 开始连接gattClientDevice.connect();this.mConnectBluetoothDevice.connectionState = ConnectionState.STATE_CONNECTING;AppStorage.setOrCreate('connectBluetoothDevice', this.mConnectBluetoothDevice);return true;} catch (err) {Log.showError(TAG, `connectInner: err = ${err}`);}return false;
    }
    
  4. 向服务端发送‘通知心率跳动’特征值请求,侦听特征值变化数据。源码参考BLECharacteristicChange。

    // connect success, Starts discovering services.
    let services: Array<ble.GattService> = await this.mGattClientDevice!.getServices();
    Log.showInfo(TAG, `onBLEConnectionStateChange: services = ${JSON.stringify(services)}`);// Characteristic enable/disable indicate/notify
    let service: ble.GattService | undefined =
    services.find(item => item.serviceUuid === BleConstants.UUID_SERVICE_HEART_RATE);
    let characteristics: Array<ble.BLECharacteristic> = service!.characteristics;
    let characteristic: ble.BLECharacteristic | undefined =
    characteristics.find(item => item.characteristicUuid ===
    BleConstants.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT);
    Log.showInfo(TAG, `onBLEConnectionStateChange: characteristic = ${JSON.stringify(characteristic)}`);
    // TODO 知识点: 向服务端发送设置通知此特征值请求
    this.mGattClientDevice!.setCharacteristicChangeNotification(characteristic, true);
    let descriptors: Array<ble.BLEDescriptor> = characteristic!.descriptors;
    let descriptor: ble.BLEDescriptor | undefined =
    descriptors.find(item => item.descriptorUuid === BleConstants.UUID_DESCRIPTOR_HEART_RATE);
    Log.showInfo(TAG, `onBLEConnectionStateChange: descriptor = ${JSON.stringify(descriptor)}`);
    descriptor!.descriptorValue = ArrayBufferUtils.byteArray2ArrayBuffer([0x01, 0x00]);
    this.mGattClientDevice!.writeDescriptorValue(descriptor);
    
    // TODO 知识点: 订阅特征值变化事件
    this.mGattClientDevice.on('BLECharacteristicChange', (data: ble.BLECharacteristic) => {
    Log.showInfo(TAG, `onBLECharacteristicChange: data = ${JSON.stringify(data)}`);
    let characteristicValue: ArrayBuffer = data.characteristicValue;
    Log.showInfo(TAG,
    `onBLECharacteristicChange: characteristicValue.length = ${characteristicValue.byteLength}, characteristicValue = ${JSON.stringify(new Uint8Array(characteristicValue))}`);
    let byteArr = ArrayBufferUtils.arrayBuffer2ByteArray(characteristicValue);
    Log.showInfo(TAG, `byteArr = ${byteArr}`);
    let heartRate = byteArr[1];
    AppStorage.setOrCreate('heartRate', heartRate);
    })
    

高性能知识点

不涉及

工程结构&模块类型

bluetooth                                  // har类型
src/main/ets/
|---constants
|   |---BleConstants.ts                    // BLE常量
|   |---StyleConstants.ts                  // Style样式常量
|   |---DurationConstants.ts.ts            // 定时、延迟类常量
|---model
|   |---BluetoothDevice.ets                // 蓝牙设备model
|---pages
|   |---BluetoothView.ets                  // 场景首页,可选择进入客户端、服务端
|   |---BluetoothAdvertiser.ets            // 广播者角色(作为服务端)
|   |---BluetoothClient.ets                // 客户端连接页面
|   |---HeartRate.ets                      // 连接成功后,侦听到服务端的心率数据   
|---uicomponents
|   |---HeartRateGraph.ets                 // 实时心率图表
|   |---NavigationBar.ets                  // 顶部导航栏
|---uitls
|   |---ArrayBufferUtils.ts                // ArrayBuffer工具
|   |---DateUtils.ts                       // 日期工具
|   |---Log.ts                             // 日志工具
|   |---MathUtils.ts                       // Math工具,用于生成随机数
|---viewmodel
|   |---BluetoothClientModel.ets           // 开启蓝牙、扫描BLE、连接、断连等BLE接口
|   |---AdvertiserBluetoothViewModel.ets   // 开启蓝牙、开启蓝牙心率广播等

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为体系杂乱无章,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)路线、视频、文档用来跟着学习是非常有必要的。

如果你是一名有经验的资深Android移动开发、Java开发、前端开发、对鸿蒙感兴趣以及转行人员

鸿蒙 NEXT 全栈开发学习笔记 希望这一份鸿蒙学习文档能够给大家带来帮助~


 鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频教程+学习PDF文档

鸿蒙语法ArkTS、TypeScript、ArkUI教程……)

 纯血版鸿蒙全套学习文档(面试、文档、全套视频等)

                   

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线


http://www.ppmy.cn/news/1527858.html

相关文章

F12抓包08:查看网站Cookie

课程大纲 1、查看Cookie 1. 应用界面查看&#xff1a;按F12进入浏览器的开发者模式 - “应用”&#xff08;Application&#xff09; - Cookie&#xff0c;可查看Cookie并进行增、删、改、查操作。 2. 控制台命令行查看&#xff1a;按F12进入浏览器的开发者模式 - “控制台”&…

Spring-bean的生命周期-尾篇

上回说到阶段9&#xff0c;现在我们接着往下说 阶段10&#xff1a;所有单例bean初始化完成后阶段 所有单例bean实例化完成之后&#xff0c;spring会回调下面这个接口&#xff1a; package org.springframework.beans.factory;public interface SmartInitializingSingleton {…

150+个流行的Instagram标签(及如何找到并正确使用它们)

早在2007年8月&#xff0c;当你还在收音机里唱着“Umbrella”&#xff08;ella…ella&#xff09;的时候&#xff0c;一位名叫Chris Messina的产品设计师向Twitter提出了“使用#&#xff08;井号&#xff09;来分组”的想法。 Twitter的回应是什么&#xff1f;太书呆子气&#…

Cassandra 和 ScyllaDB

Cassandra 和 ScyllaDB 详解 Cassandra 和 ScyllaDB 是现代分布式数据库系统中非常受欢迎的两个选择&#xff0c;它们在性能、可扩展性和高可用性方面有着显著优势&#xff0c;适合大规模、高吞吐量的应用场景。这两者都是面向列的分布式数据库&#xff08;Columnar Store&…

详解TCP的三次握手

TCP&#xff08;三次握手&#xff09;是指在建立一个可靠的传输控制协议 (TCP) 连接时&#xff0c;客户端和服务器之间的三步交互过程。这个过程的主要目的是确保连接是可靠的、双方的发送与接收能力是正常的&#xff0c;并且可以开始数据传输。下面是对每个步骤的详细解释&…

C++ | Leetcode C++题解之第406题根据身高重建队列

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {sort(people.begin(), people.end(), [](const vector<int>& u, const vector<int>& v) …

VirtualBox增加磁盘并给docker用

在VirtualBox新增磁盘 在虚拟机停止的情况下依次选择&#xff0c;然后创建新磁盘 虚拟机新磁盘创建分区、格式化、挂载分区 开机自动挂载新磁盘分区/dev/sdb1&#xff1a; nano /etc/fstab末尾添加一行&#xff1a; /dev/sdb1 /disk02 e…

深入理解ConcurrentHashMap

HashMap为什么线程不安全 put的不安全 由于多线程对HashMap进行put操作&#xff0c;调用了HashMap的putVal()&#xff0c;具体原因&#xff1a; 假设两个线程A、B都在进行put操作&#xff0c;并且hash函数计算出的插入下标是相同的&#xff1b; 当线程A执行完第六行由于时间片…

linux-Linux 内核与模块管理-内核基础

Linux 内核是操作系统的核心&#xff0c;它负责管理硬件资源和提供系统调用接口供用户程序使用。Linux 内核的设计极为灵活和模块化&#xff0c;它允许开发者通过加载和卸载模块来动态地扩展内核的功能。 一、Linux 内核概述 1.1 内核的基本功能 Linux 内核的主要功能可以分…

Python基础语法(3)下

列表和元组 列表是什么&#xff0c;元组是什么 编程中&#xff0c;经常需要使用变量&#xff0c;来保存/表示数据。变量就是内存空间&#xff0c;用来表示或者存储数据。 如果代码中需要表示的数据个数比较少&#xff0c;我们直接创建多个变量即可。 num1 10 num2 20 num3…

【计算机网络】数据链路层深度解析

概述三个重要问题封装成帧差错检测可靠传输 使用广播信道的数据链路层数据链路层的互连设备 媒体接入MAC地址集线器与交换机区别以太网交换机生成树协议STP 概述 链路就是从一个结点到相邻结点的一段物理线路&#xff0c;而中间没有任何其他的交换结点。数据链路是指把实现通信…

数据结构-3.3.栈的链式存储实现

一.链栈的定义&#xff1a; 二.总结&#xff1a;

【C++算法】模拟算法

替换所有的问号 题目链接 替换所有的问号https://leetcode.cn/problems/replace-all-s-to-avoid-consecutive-repeating-characters/description/ 算法原理 代码步骤 class Solution { public:string modifyString(string s) {int n s.size();for(int i 0; i < n; i){…

面试爱考 | 设计模式

一、概述二、创建型 1. 单例&#xff08;Singleton&#xff09; IntentClass DiagramImplementationExamplesJDK 2. 简单工厂&#xff08;Simple Factory&#xff09; IntentClass DiagramImplementation 3. 工厂方法&#xff08;Factory Method&#xff09; IntentClass Diagr…

Halo 开发者指南——项目运行、构建

准备工作 环境要求 OpenJDK 17 LTSNode.js 20 LTSpnpm 9IntelliJ IDEAGitDocker&#xff08;可选&#xff09; 名词解释 工作目录 指 Halo 所依赖的工作目录&#xff0c;在 Halo 运行的时候会在系统当前用户目录下产生一个 halo-next 的文件夹&#xff0c;绝对路径为 ~/ha…

钻机、塔吊等大型工程设备,如何远程维护、实时采集运行数据?

在建筑和工程领域&#xff0c;重型设备的应用不可或缺&#xff0c;无论是在道路与桥梁建设、高层建筑施工&#xff0c;还是在风电、石油等能源项目的开发中&#xff0c;都会用到塔吊、钻机等大型机械工程设备。 随着数字化升级、工业4.0成为行业发展趋势&#xff0c;为了进一步…

MacOS安装homebrew,jEnv,多版本JDK

1 安装homebrew homebrew官网 根据官网提示&#xff0c;运行安装命令 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"安装后&#xff0c;bash会提示执行两条命令 (echo; echo eval "$(/opt/homebrew/b…

海外问卷调查是做什么的,适合什么人做

疫情结束后的市场大环境萧条&#xff0c;特别是实体经济&#xff0c;许多中小微企业要么倒闭&#xff0c;要么垂死挣扎&#xff0c;等待重生。于是有一些老板&#xff0c;就转型做起了互联网生意。 抖音这几年的直播带货、短视频带货等等飞速崛起&#xff0c;成就了多少百万富…

如何用站群服务器做抢购秒杀平台

随着各种电商购物节的开幕&#xff0c;全球外贸、直播电商抢购活动愈发火热&#xff0c;外贸行业容纳了海量的公司、组织和个人。为了营销&#xff0c;人们使用海外站群服务器抢货的做法已经不再稀奇&#xff0c;因为使用海外站群服务器操作抢购秒杀商品&#xff0c;可以拥有多…

爬坑--docker构建容器ssh连接容器环境变量会发生变化

问题 通过 Dockerfile 语句在镜像中安装了 openssh, 但是在创建容器之后, 通过 docker exec -it 容器 bash 和通过远程 ssh, 会发现两边终端的 PATH 不一致, ssh 连接明显缺少一些东西. 解决方案 在 Dockerfile 最后添加: RUN echo "export PATH${PATH}" >>…