业务功能·

实现蓝牙设备测量完整流程

本文剖析前端蓝牙设备测量的代码实现,介绍其模块化架构。包含蓝牙连接模块(初始化适配器、检查权限)、设备连接模块(发现、连接设备)、设备测量模块(发送指令、处理结果)、设备上报模块(处理并上报数据)。各模块分工明确,通过错误处理机制保障稳定性,还封装为 BluetoothPlugin 工具供调用,可满足不同蓝牙设备测量需求。

整体架构

采用模块化的设计思想,将蓝牙连接、设备连接、设备测量和设备上报等功能拆分成独立的模块,每个模块负责特定的任务,提高代码的可维护性和可扩展性。以下是主要的模块及其功能:

1. 蓝牙连接模块(bluetoothConnection.js

此模块主要负责蓝牙适配器的初始化和权限检查。代码中通过 getRuntime 函数判断当前运行环境是 uni 还是 wx,并获取相应的平台 API。checkPermissions 函数用于检查蓝牙权限,如果未授权则请求授权。initBluetoothAdapter 函数用于初始化蓝牙适配器,并设置监听事件,当蓝牙适配器状态变化时会触发相应的处理逻辑。

const getRuntime = () => {
  if (typeof uni !== 'undefined') return uni
  if (typeof wx !== 'undefined') return wx
  throw new Error('Unsupported runtime')
}
const platformApi = getRuntime()

const checkPermissions = () =>
  new Promise((resolve, reject) => {
    platformApi.getSetting({
      success: ({ authSetting }) => {
        if (authSetting["scope.bluetooth"] === true) return resolve();

        platformApi.authorize({
          scope: "scope.bluetooth",
          success: resolve,
          fail: () => {
            platformApi.showModal({
              title: "权限要求",
              content: "需要蓝牙权限以连接设备",
              success: ({ confirm }) =>
                confirm ? platformApi.openSetting() : reject(),
            });
          },
        });
      },
      fail: reject,
    });
  });

const initBluetoothAdapter = () => {
  return new Promise((resolve, reject) => {
    platformApi.openBluetoothAdapter({
      success: () => {
        setupBluetoothAdapterListeners();
        resolve();
      },
      fail: (err) => {
        if (err.errMsg === "openBluetoothAdapter:fail already opened") {
          setupBluetoothAdapterListeners();
          resolve();
        } else {
          reject(err);
        }
      },
    });
  });
};

2. 设备连接模块(deviceConnection.js

该模块负责设备的发现、连接和断开连接等操作。startBluetoothDiscovery 函数用于启动设备发现,会在一定时间后自动停止扫描,并根据连接模式决定是否自动连接设备。handleDeviceFound 函数处理发现的设备,过滤掉不符合条件的设备,并更新设备列表。connectDevice 函数是设备连接的核心逻辑,包括创建 BLE 连接、获取服务和特征值、启用通知等步骤。

const startBluetoothDiscovery = () => {
  return new Promise(async (resolve, reject) => {
    const { available } = await getBluetoothAdapterState();

    if (!available) {
      errorHandler(
        ErrorTypes.BLUETOOTH_CONNECTION_FAILED,
        "蓝牙适配器不可用",
        new Error("蓝牙适配器不可用")
      );
      reject(new Error("蓝牙适配器不可用"));
      return;
    }

    if (scanTimer) {
      clearTimeout(scanTimer);
    }
    scanTimer = setTimeout(async () => {
      await stopBluetoothDiscovery();
      if (config.connectionMode === "auto") {
        autoConnectDevice(false);
      }
      resolve();
    }, scanTime * 1000);

    await new Promise((resolve, reject) => {
      platformApi.startBluetoothDevicesDiscovery({
        services: serviceId ? [serviceId] : void 0,
        allowDuplicatesKey: false,
        success: () => {
          platformApi.onBluetoothDeviceFound(handleDeviceFound);
          resolve();
        },
        fail: (err) => {
          console.log("扫描错误", err);
          if (err.errMsg.includes("already started")) {
            resolve();
          } else {
            errorHandler(
              ErrorTypes.DEVICE_CONNECTION_FAILED,
              `扫描设备失败: ${err.errMsg}`,
              err
            );
            reject(err);
          }
        },
      });
    });
  });
};

const connectDevice = (device) => {
  return new Promise(async (resolve, reject) => {
    try {
      await createBLEConnection(device.deviceId);
      const services = await getBLEDeviceServices(device.deviceId);
      const primaryService = services.find((s) => s.uuid === serviceId);
      if (!primaryService) throw new Error("未找到指定服务");
      const characteristics = await getBLEDeviceCharacteristics(
        device.deviceId,
        primaryService.uuid
      );
      const { writeChar, notifyChar } = characteristicMatch(characteristics);
      if (!writeChar || !notifyChar) throw new Error("缺少必要特征值");
      selectedDevice = device;
      writeChar = writeChar;
      notifyChar = notifyChar;
      await enableNotifications(
        device.deviceId,
        primaryService.uuid,
        notifyChar.uuid
      );
      setupDeviceListeners(device.deviceId);
      resolve();
    } catch (error) {
      if (error.message === "未找到指定服务") {
        errorHandler(ErrorTypes.SERVICE_NOT_FOUND, error.message, error);
      } else if (error.message === "缺少必要特征值") {
        errorHandler(
          ErrorTypes.CHARACTERISTIC_NOT_FOUND,
          error.message,
          error
        );
      } else {
        errorHandler(
          ErrorTypes.DEVICE_CONNECTION_FAILED,
          error.message,
          error
        );
      }
      reject(error);
    }
  });
};

3. 设备测量模块(deviceMeasurement.js

该模块负责向蓝牙设备发送测量指令并处理测量结果。startMeasurement 函数会检查设备是否连接和特征值是否获取,如果条件满足则生成通信数据并发送给设备,同时监听设备返回的测量结果。handleDataReceived 函数处理接收到的测量数据。

const startMeasurement = () => {
  if (!selectedDevice || !writeChar || !notifyChar) {
    errorHandler(ErrorTypes.DEVICE_CONNECTION_FAILED, '设备未连接或特征值未获取', new Error('设备未连接或特征值未获取'));
    return;
  }

  const buffer = communicationData();
  platformApi.writeBLECharacteristicValue({
    deviceId: selectedDevice.deviceId,
    serviceId,
    characteristicId: writeChar.uuid,
    value: buffer,
    success: res => {
      console.log('发送指令成功', res);
      platformApi.onBLECharacteristicValueChange(handleDataReceived);
    },
    fail: res => {
      errorHandler(res);
    }
  });
};

const handleDataReceived = res => {
  console.log('测量结果', res);
  measureData = res
};

4. 设备上报模块(deviceReport.js

该模块负责将测量数据上报到指定的位置。setMeasureData 函数用于设置测量数据,reportData 函数会检查是否有测量数据,如果有则对数据进行处理并调用上报方法。

const setMeasureData = (data) => {
  measureData = data;
};

const reportData = () => {
  if (!measureData) {
    errorHandler(ErrorTypes.NO_MEASUREMENT_DATA, '没有测量数据可供上报', new Error('没有测量数据可供上报'));
    return;
  }

  const processedData = reportDataHandler(measureData);
  try {
    reportMethod(processedData);
  } catch (error) {
    errorHandler(ErrorTypes.NO_MEASUREMENT_DATA, error.message, error);
  }
};

错误处理

项目中定义了各种错误类型(ErrorTypes),并在各个模块中进行了相应的错误处理。当出现错误时,会调用 errorHandler 函数,将错误类型、错误信息和错误对象传递给该函数进行统一处理,提高了代码的健壮性。

const ErrorTypes = {
  BLUETOOTH_CONNECTION_FAILED: 'BLUETOOTH_CONNECTION_FAILED',
  DEVICE_CONNECTION_FAILED: 'DEVICE_CONNECTION_FAILED',
  DEVICE_NOT_FOUND: 'DEVICE_NOT_FOUND',
  SERVICE_NOT_FOUND: 'SERVICE_NOT_FOUND',
  CHARACTERISTIC_NOT_FOUND: 'CHARACTERISTIC_NOT_FOUND',
  NOTIFICATION_ENABLE_FAILED: 'NOTIFICATION_ENABLE_FAILED',
  WRITE_DATA_FAILED: 'WRITE_DATA_FAILED',
  NO_MEASUREMENT_DATA: 'NO_MEASUREMENT_DATA'
};

// 在各个模块中使用 errorHandler 处理错误
errorHandler(ErrorTypes.DEVICE_CONNECTION_FAILED, error.message, error);

初始化

通过 BluetoothPlugin 函数将各个模块的功能进行整合,用户可以传入自定义的配置项,函数会合并默认配置和用户配置,并返回可调用的方法。用户可以通过这些方法实现蓝牙连接、设备发现、测量和上报等功能。

function BluetoothPlugin(config) {
  const defaultConfig = {
    connectionMode: 'auto',
    serviceId: '',
    onDeviceFound: (devices, deviceList) => {
      console.log('设备发现:', devices);
      console.log('设备列表:', deviceList);
    },
    characteristicMatch: (characteristics) => {
      const writeChar = characteristics.find(c => c.properties.write);
      const notifyChar = characteristics.find(c => c.properties.notify);
      return { writeChar, notifyChar };
    },
    communicationData: () => {
      let buffer = new ArrayBuffer(6);
      let dataView = new DataView(buffer);
      dataView.setUint8(0, 0x02);
      dataView.setUint8(1, 0x40);
      dataView.setUint8(2, 0xdc);
      dataView.setUint8(3, 0x01);
      dataView.setUint8(4, 0xa1);
      dataView.setUint8(5, 0x3c);
      return buffer;
    },
    reportDataHandler: (data) => {
      return data;
    },
    reportMethod: (data) => {
      console.log('Reporting data:', data);
    },
    errorHandler: (err) => {
      console.log('Error:', err);
    }
  };
  config = Object.assign({}, defaultConfig, config);

  const { initBluetooth, disconnectBluetoothAdapter } = bluetoothConnection(config);
  const { connectDevice, startBluetoothDiscovery, stopBluetoothDiscovery, getDeviceInfo, disconnectDevice } = deviceConnection(config);
  const { startMeasurement, setDeviceInfo, getMeasureData } = deviceMeasurement(config);
  const { reportData, setMeasureData } = deviceReport(config);

  return {
    connectBluetooth: initBluetooth,
    disconnectBluetoothAdapter,
    startBluetoothDiscovery,
    stopBluetoothDiscovery,
    connectDevice,
    disconnectDevice,
    startMeasurement: () => {
      setDeviceInfo(getDeviceInfo())
      startMeasurement()
    },
    reportData: () => {
      setMeasureData(getMeasureData())
      reportData()
    },
  };
}

使用

import { ref, computed, onMounted, onUnmounted } from "vue";
import { BluetoothPlugin, ErrorTypes } from "bluetooth-pressure-measure";

const config = {
  connectionMode: 'auto', // 'auto' or 'manual'
  serviceId: '',
  onDeviceFound: (devices, deviceList) => {
    console.log('设备发现:', devices);
    console.log('设备列表:', deviceList);
  },
  characteristicMatch: (characteristics) => {
    const writeChar = characteristics.find(c => c.properties.write);
    const notifyChar = characteristics.find(c => c.properties.notify);
    return { writeChar, notifyChar };
  },
  communicationData: () => {
    let buffer = new ArrayBuffer(6);
    let dataView = new DataView(buffer);
    dataView.setUint8(0, 0x02);
    dataView.setUint8(1, 0x40);
    dataView.setUint8(2, 0xdc);
    dataView.setUint8(3, 0x01);
    dataView.setUint8(4, 0xa1);
    dataView.setUint8(5, 0x3c);
    return buffer;
  },
  reportDataHandler: (data) => {
    return data;
  },
  reportMethod: (data) => {
    // Implement your reporting logic here
    console.log('Reporting data:', data);
  },
  errorHandler: (errorType, message, error) => {
    switch (errorType) {
      case ErrorTypes.BLUETOOTH_CONNECTION_FAILED:
        console.log('蓝牙连接失败:', message, error);
        break;
      case ErrorTypes.DEVICE_CONNECTION_FAILED:
        console.log('设备连接失败:', message, error);
        break;
      case ErrorTypes.DEVICE_NOT_FOUND:
        console.log('未找到设备:', message, error);
        break;
      // 其他异常类型处理
      default:
        console.log('未知异常:', message, error);
        break;
    }
  }
};

完整代码

代码地址

总结

通过分析这个前端蓝牙设备测量的代码示例,我们可以看到实现蓝牙设备交互和测量功能需要经过多个步骤,包括蓝牙适配器初始化、设备发现、连接、数据发送和接收以及数据上报等。模块化的设计思想使得代码结构清晰,易于维护和扩展。同时,合理的错误处理机制提高了代码的健壮性,确保在出现异常情况时能够及时处理。在实际开发中,我们可以根据具体需求对代码进行进一步的优化和扩展,以满足不同蓝牙设备的测量需求。