前言

断更许久,也是因为没有什么好的素材可以写,最近学了一点鸿蒙开发,因为手头上有一块蓝牙模块(JDY-31),就想着说结合stm32,还有手机app实现一个类似智能家居的系统。说干就干,32的代码比较容易写,很快就完成了,但是鸿蒙蓝牙调试软件的开发让我犯了难。起初,我想要去应用商店找一下有没有现成的软件可以用,找了很久发现要么不好用,没有我想要的功能,要么就是半成品软件,空有其表。于是,我就打算开发一个自己的蓝牙调试器,在开发过程中,我也遇到了许多问题。所有我就写下了这篇文章来跟大家分享一下我的经验,如果有写的不好的地方还请各位大佬批评指正。

正文

首先我们需要清楚在鸿蒙当中怎样去实现蓝牙的功能

1.申请蓝牙权限

这里贴上文档地址:向用户申请授权-申请应用权限-应用权限管控-程序访问控制-安全-系统 - 华为HarmonyOS开发者

通过查看华为官方文档我们可以知道申请蓝牙权限有这些步骤:

image-20250626224549706

首先,我们需要在配置文件当中申请蓝牙权限

如图,我们需要在module.json5配置文件中加入这么一个配置

"requestPermissions": [
      {
        "name" : "ohos.permission.ACCESS_BLUETOOTH",
        "reason": "$string:blooth_reason",
        "usedScene": {
          "abilities": [
            "FormAbility"
          ],
          "when":"inuse"
        }
      }
    ],

image-20250626225251815

其中reason一项很容易让人搞不懂鼠标移动到reason后,按住control,鼠标左键单击

image-20250626225139038

单击后我们会发现跳转到了string.json,然后在这里仿造格式写上原因就行啦,注意这里name属性对应的就是上面那个配置当中$string:blooth_reason的blooth_reason,这里随意起名字就好,但是要规范,自己能看得懂

image-20250626225204132

在配置文件声明完权限后还需要添加向用户申请权限的代码,代码如下

  // 用户申请权限
  async reqPermissionsFromUser(): Promise<number[]> {
    let context = getContext() as common.UIAbilityContext;
    let atManager = abilityAccessCtrl.createAtManager();
    let grantStatus = await atManager.requestPermissionsFromUser(context, ['ohos.permission.ACCESS_BLUETOOTH']);
    return grantStatus.authResults;
  }

  // 用户申请蓝牙权限
  async requestBlueToothPermission() {
    let grantStatus = await this.reqPermissionsFromUser();
    for (let i = 0; i < grantStatus.length; i++) {
      if (grantStatus[i] === 0) {
        // 用户授权,可以继续访问目标操作
      }
    }
  }

需要 申请权限的时候就调用一下 requestBlueToothPermission ,这里要注意因为 requestBlueToothPermission 是异步函数,所以要用await来进行调用,像这样:

await this.requestBlueToothPermission()

这里给点小建议,可以在页面初始化的时候调用这个函数,这样就可以直接获取授权了

在这里我定义了一个init函数

image-20250626231123260

代码如下

async init() {
    try {
      await this.requestBlueToothPermission()
    } catch (err) {
      promptAction.showToast({
        message: 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message//弹窗内容
      });
      console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
    }
  }

我们可以在页面顶端调用该函数,如图所示:

image-20250626231408151

如果一切顺利打开软件到调用init函数所在页面就会跳出授权弹窗f981d094b52d0e41a8ad1604d21c730

至此授权的过程也就完成了,如果用户选择拒绝授权还需要涉及二次授权,因为博主也没有二次授权的相关经验,还请大家自行参考官方文档进行调试

这里贴上二次授权的文档地址: 二次向用户申请授权-申请应用权限-应用权限管控-程序访问控制-安全-系统 - 华为HarmonyOS开发者

2.蓝牙相关操作开发

这里我定义了一个模型,用来存储扫描到的蓝牙设备数据,方便在连接界面展示

GlobalBlueTooth.ets

export interface BlueToothItem {
  mac: string,
  name: string
}

@ObservedV2
export class GlobalBlueTooth {
  @Trace blueList: BlueToothItem[] = [] //蓝牙设备数据列表

  //重置数据方法
  rest()
  {
    this.blueList = [] //蓝牙设备数据列表
  }
}

1.蓝牙开与关

BlueToothManager.ets

//开启蓝牙
async blueToothOn()
{
  try {
    await this.requestBlueToothPermission()
    await access.enableBluetooth();
  } catch (err) {
    promptAction.showToast({
      message: 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message//弹窗内容
    });
    console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  }
}

//关闭蓝牙
async blueToothOff()
{
  try {
    await this.requestBlueToothPermission()
    await access.disableBluetooth();
  } catch (err) {
    promptAction.showToast({
      message: 'errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message//弹窗内容
    });
    console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  }
}

2.蓝牙设备的搜索

BlueToothManager.ets

//检查扫描到的设备mac是否重复
//重复则不添加到表中
blueToothDeviceChecker(data: Array<string>)
{
  for(let i = 0; i < this.blueToothData.blueList.length; i++)
  {
    if(this.blueToothData.blueList[i].mac == data[0])
    {
      //当检查到重复则跳出循环返回0
      return 0;
    }
  }
  //当循环结束还没有找到重复则返回1
  return 1;
}

//蓝牙搜索方法
// 定义扫描结果上报回调函数
onReceiveEvent = (data: Array<string>) => {
  let deviceName = connection.getRemoteDeviceName(data[0])
  console.info('bluetooth device: '+ JSON.stringify(data));
  if(this.blueToothDeviceChecker(data))
  {
    //当没有扫描到重复时添加数据
    if(deviceName == '')
    {
      console.info('bluetooth device name: 未知设备');
      this.blueToothData.blueList.unshift({
        mac: data[0],
        name: "未知设备"
      }) //在共享的设备数据末位添加一条扫描到的设备数据
    }
    else
    {
      console.info('bluetooth device name: '+ deviceName);
      this.blueToothData.blueList.unshift({
        mac: data[0],
        name: deviceName
      })
    }
  }
};

public startDiscovery() {
  try {
    connection.on('bluetoothDeviceFind', this.onReceiveEvent);
  } catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  }
  try {
    // 判断本机设备是否正在进行扫描
    let scan = connection.isBluetoothDiscovering();
    if (!scan) {
      // 若当前不处于扫描过程,则开始扫描设备
      connection.startBluetoothDiscovery();
    }
  } catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  }
}

public stopDiscovery() {
  try {
    // 判断本机设备是否正在进行扫描
    let scan = connection.isBluetoothDiscovering();
    if (scan) {
      // 若当前处于扫描过程,则停止扫描设备
      connection.stopBluetoothDiscovery();
    }
    // 若不再需要使用扫描,可以取消订阅扫描上报结果
    connection.off('bluetoothDeviceFind', this.onReceiveEvent);
  } catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  }
}

3.获取本机的扫描模式

BlueToothManager.ets

public setScanMode() {
  try {
    // 获取当前本机的扫描模式
    let scanMode: connection.ScanMode = connection.getBluetoothScanMode();
    console.info('scanMode: ' + scanMode);
    if (scanMode != connection.ScanMode.SCAN_MODE_CONNECTABLE_GENERAL_DISCOVERABLE) {
      // 将本机设备的扫描模式设为可被发现和可被连接
      connection.setBluetoothScanMode(connection.ScanMode.SCAN_MODE_CONNECTABLE_GENERAL_DISCOVERABLE, 0);
    }
  } catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  }
}

4.获取已经配对的设备信息

public getPairedDevices() {
  try {
    // 获取已配对设备信息
    let devices = connection.getPairedDevices();
    console.info('pairedDevices: ' + JSON.stringify(devices));
    // 若已知道设备地址,可主动查询该设备是否是已配对的
    if (devices.length > 0) {
      let pairState = connection.getPairState(devices[0]);
      console.info('device: '+ devices[0] + ' pairState is ' + pairState);
    }
  } catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
  }
}

5.与设备连接

这里采用的是spp连接的方式,我定义了一个sppClientManager的类来进行管理

SppClientManager.ets

import { socket } from '@kit.ConnectivityKit'
import { BusinessError } from '@kit.BasicServicesKit';
import { GlobalType } from '../models/globalType';
import { AppStorageV2 } from '@kit.ArkUI';
import { connection } from '@kit.NetworkKit';


class SppClientManager {
  // 定义客户端的socket id
  clientNumber: number = -1;

  //共享连接数据
  typeData: GlobalType = AppStorageV2.connect(GlobalType, 'TYPE_KEY', () => new GlobalType())!


  // 发起连接
  public startConnect(peerDevice: string, deviceName: string): void {
    // 配置连接参数
    let option: socket.SppOptions = {
      uuid: '00001101-0000-1000-8000-00805F9B34FB', // 需要连接的服务端UUID服务,确保服务端支持
      secure: false,
      type: socket.SppType.SPP_RFCOMM
    };
    console.info('startConnect ' + peerDevice);
    socket.sppConnect(peerDevice, option, (err, num: number) => {
      if (err) {
        console.error('startConnect errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
      } else {
        //连接成功
        this.typeData.connectType = 1; //修改显示的连接状态
        this.typeData.connectedDeviceName = deviceName; //修改显示的连接设备名称
        console.info('startConnect clientNumber: ' + num);
        this.clientNumber = num;
      }
    });
    console.info('startConnect after ' + peerDevice);
  }


  // 发送数据
  public sendData(sendData: number) {
    console.info('sendData ' + this.clientNumber);
    let arrayBuffer = new ArrayBuffer(1);
    let data = new Uint8Array(arrayBuffer);
    data[0] = sendData;
    try {
      socket.sppWrite(this.clientNumber, arrayBuffer);
    } catch (err) {
      console.error('sppWrite errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
      return -1; //发现异常,也就是有可能断开连接就返回-1
    }
    return 1; //正常发送数据代表还在连接就返回1
  }


  // 定义接收数据的回调函数
  read = (dataBuffer: ArrayBuffer) => {
    let data = new Uint8Array(dataBuffer);
    console.info('client data: ' + JSON.stringify(data));
  };


  // 接收数据
  public readData() {
    try {
      // 发起订阅
      socket.on('sppRead', this.clientNumber, this.read);
    } catch (err) {
      console.error('readData errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
    }
  }


  // 断开连接
  public stopConnect() {
    console.info('closeSppClient ' + this.clientNumber);
    try {
      // 取消接收数据订阅
      socket.off('sppRead', this.clientNumber, this.read);
    } catch (err) {
      console.error('off sppRead errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
    }
    try {
      // 从client端断开连接
      socket.sppCloseClientSocket(this.clientNumber);

    } catch (err) {
      console.error('stopConnect errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
    }
  }
}


export const sppClientManager: SppClientManager = new SppClientManager()

BlueToothManager.ets

public connectDevice(device: string) {
  let pairState = connection.getPairState(device); //查询设备是否配对
  if(pairState == 2)
  {
    //设备已配对
    let deviceName = connection.getRemoteDeviceName(device); //获取设备名称
    sppClientManager.startConnect(device, deviceName) //发起连接
  }
  else
  {
    //设备没有配对
    try {
      // 发起配对
      connection.pairDevice(device).then(() => {
        console.info('pairDevice');
      }, (error: BusinessError) => {
        console.error('pairDevice: errCode:' + error.code + ',errMessage' + error.message);
      });
    } catch (err) {
      console.error('startPair: errCode:' + err.code + ',errMessage' + err.message);
      console.info('mac:' + device);
    }
  }
}

当连接设备的时候调用connectDevice函数,传入连接设备的地址一般为 XX:XX:XX:XX:XX:XX ,该地址可以在步骤二蓝牙设备的搜索当中获取

connectDevice函数会先检查该地址设备是否已配对,如果没有配对则会开始配对设备,如果已配对就会调用SppClientManager类中的startConnect开始连接设备,连接成功后就可以开始收发数据了

6.数据的收发

SppClientManager.ets

// 发送数据
  public sendData(sendData: number) {
    console.info('sendData ' + this.clientNumber);
    let arrayBuffer = new ArrayBuffer(1);
    let data = new Uint8Array(arrayBuffer);
    data[0] = sendData;
    try {
      socket.sppWrite(this.clientNumber, arrayBuffer);
    } catch (err) {
      console.error('sppWrite errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
      return -1; //发现异常,也就是有可能断开连接就返回-1
    }
    return 1; //正常发送数据代表还在连接就返回1
  }


  // 定义接收数据的回调函数
  read = (dataBuffer: ArrayBuffer) => {
    let data = new Uint8Array(dataBuffer);
    console.info('client data: ' + JSON.stringify(data));
  };


  // 接收数据
  public readData() {
    try {
      // 发起订阅
      socket.on('sppRead', this.clientNumber, this.read);
    } catch (err) {
      console.error('readData errCode: ' + (err as BusinessError).code + ', errMessage: ' + (err as BusinessError).message);
    }
  }


在这里我定义的sendData函数只能发送数字数据,需要发送其他数据的小伙伴请自行修改

结尾

相信看到这里你已经掌握了鸿蒙蓝牙的常规开发,学会搜索,配对,连接设备,并且收发数据。很感谢你能看到这里,如果觉得我的文章对你有帮助的话请给一个点赞还有收藏谢谢,如果有什么意见欢迎在评论区留言,我们下期再见!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐