蓝牙设备对接文档

设备通知

设备状态上报与通知处理

概述

点对点设备通过 BLE Notify 特征主动上报数据,包括认证信息、电量、电机状态等。不同协议的通知格式各异,系统通过统一的通知处理机制自动路由和解析。

通知订阅

Web Bluetooth 订阅

// WebBleManager 中的通知订阅
private async enableNotifications(): Promise<void> {
  this.notifyCharacteristic.addEventListener(
    'characteristicvaluechanged',
    (event: Event) => {
      const characteristic = event.target as BluetoothRemoteGATTCharacteristic
      const value = new Uint8Array(characteristic.value!.buffer)

      // 调用通知回调
      if (this.notifyCallback) {
        this.notifyCallback(value)
      }
      if (this.notifyListener) {
        this.notifyListener.onData(value)
      }
    }
  )

  await this.notifyCharacteristic.startNotifications()
}

Native 订阅

// NativeBleManager 中的通知订阅
await BleClient.startNotifications(
  deviceId,
  serviceUUID,
  notifyCharacteristicUUID,
  (data: DataView) => {
    const value = new Uint8Array(data.buffer)
    if (this.notifyCallback) {
      this.notifyCallback(value)
    }
  }
)

PrivateProtocol 通知

PrivateProtocol 的通知数据经过 FrameCodec 编码,接收后需要先解码再解析。

通知帧结构

所有通知帧以 0xBA 开头:

[0xBA] [通知类型] [数据...]

认证通知 (类型 0x00)

设备连接后首先发送认证通知。详见 设备认证

偏移    长度    说明
0       1       0xBA -- 帧标记
1       1       0x00 -- 认证类型
2-3     2       客户编号 (ClientID)
4-5     2       硬件版本
6-11    6       软件版本
12      1       电池电量 (0-100)

解析后的数据结构:

interface AuthNotification {
  type: 'auth'
  clientId: number           // 客户编号
  hardwareVersion: string    // 格式: "MAT3_V5.6"
  softwareVersion: string    // 格式: "3.1.240115"
  battery: number            // 电量 (0-100)
  rawData: Uint8Array        // 原始数据
}

状态通知 (类型 0x01)

设备在运行期间周期性上报运行状态:

偏移    长度    说明
0       1       0xBA -- 帧标记
1       1       0x01 -- 状态类型
2       1       电池电量 (0-100)
3       1       Motor1 强度 (0-10)
4       1       Motor2 强度 (0-10)
5       1       Motor3 强度 (0-10)

解析后的数据结构:

interface StatusNotification {
  type: 'status'
  battery: number      // 电量 (0-100)
  motors: number[]     // [Motor1, Motor2, Motor3],各 0-10
}

PrivateProtocol 解析代码

public static parseDeviceNotification(data: Uint8Array): DeviceNotification | null {
  if (!data || data.length < 2) return null

  const frame = data[0] & 0xFF
  const type = data[1] & 0xFF

  // 认证通知: 0xBA 0x00
  if (frame === 0xBA && type === 0x00) {
    const clientId = (data[2] << 8) | data[3]

    // 硬件版本解析
    const hwVersionNum = (data[4] << 8) | data[5]
    const boardType = Math.floor(hwVersionNum / 100)
    const hwVer = hwVersionNum % 100
    const major = Math.floor(hwVer / 10)
    const minor = hwVer % 10
    const hardwareVersion = `MAT${boardType}_V${major}.${minor}`

    // 软件版本解析
    const swBoardType = (data[6] << 8) | data[7]
    const swNumber = data[8]
    const year = data[9].toString().padStart(2, '0')
    const month = data[10].toString().padStart(2, '0')
    const day = data[11].toString().padStart(2, '0')
    const softwareVersion = `${swBoardType}.${swNumber}.${year}${month}${day}`

    const battery = data[12]

    return {
      type: 'auth',
      clientId,
      hardwareVersion,
      softwareVersion,
      battery,
      rawData: data
    }
  }

  // 状态通知: 0xBA 0x01
  if (frame === 0xBA && type === 0x01) {
    return {
      type: 'status',
      battery: data[2],
      motors: [data[3], data[4], data[5]]
    }
  }

  return { type: 'unknown', frame, typeCode: type, rawData: data }
}

VxMi Protocol 通知

VxMi 设备返回 JSON 格式的状态数据,封装在 A5 5A 帧中。

通知帧结构

A5 5A [Length] ... [Payload] ... [CRC16]

解析流程

  1. 验证帧头 A5 5A
  2. 检查响应类型标记(偏移 8-10 包含 02
  3. 提取有效载荷(跳过帧头 10 字节和尾部 CRC 4 字节)
  4. 将 Hex 载荷转为 UTF-8 字符串
  5. 解析 JSON

返回数据

interface VxMiNotification {
  type: 'status'
  battery: number          // 电量百分比 (0-100)
  voltage: number          // 电压值
  firmwareVersion: string  // 固件版本
  mcu1Firmware: string     // MCU1 固件版本
  mcu2Firmware: string     // MCU2 固件版本
  mtu: number              // MTU 值
  rawData: Uint8Array      // 原始数据
}

电量计算

VxMi 设备根据电压值线性插值计算电量:

function voltageToPercent(voltage: number): number {
  const MIN_VOLTAGE = 3.0  // 对应 0%
  const MAX_VOLTAGE = 4.2  // 对应 100%

  if (voltage <= MIN_VOLTAGE) return 0
  if (voltage >= MAX_VOLTAGE) return 100

  return Math.round(
    ((voltage - MIN_VOLTAGE) / (MAX_VOLTAGE - MIN_VOLTAGE)) * 100
  )
}

统一通知处理

系统通过 parseDeviceNotification 函数根据设备类型自动路由通知解析:

// deviceModes.ts
export const parseDeviceNotification = (
  modelName: string,
  isPrivate: any,
  data: any
): any => {
  if (isPrivate) {
    // 私有协议设备:先解码再解析
    let payload: Uint8Array
    try {
      payload = FrameCodec.decode(data)
    } catch {
      // 解码失败,使用原始数据
      payload = data
    }
    return PrivateProtocol.parseDeviceNotification(payload)
  }

  // 其他协议(VxMi、EL 等)返回空对象
  return {}
}

通知处理流程

BLE Notify 特征收到数据
  |
  v
判断设备协议类型
  |
  +-- PrivateProtocol --+
  |                      |
  |                      v
  |              FrameCodec.decode(data)
  |                      |
  |                      v
  |              PrivateProtocol.parseDeviceNotification(payload)
  |                      |
  |                      +-- type='auth' --> 处理认证
  |                      |
  |                      +-- type='status' --> 更新状态
  |
  +-- VxMi Protocol ----+
  |                      |
  |                      v
  |              解析 A5 5A 帧
  |              提取 JSON 数据
  |              计算电量
  |
  +-- EL Protocol ------+
                         |
                         v
                  协议特定解析

通知监听完整示例

import { unifiedBluetoothManager } from '@/utils/bluetooth/UnifiedBluetoothManager'
import { parseDeviceNotification } from '@/utils/deviceModes'

// 设置通知监听
unifiedBluetoothManager.setNotifyListener({
  onData: (data: Uint8Array) => {
    const connectedDevice = useDeviceStore.getState().connectedDevice
    const modelName = connectedDevice?.deviceModel?.modelName
    const isPrivate = connectedDevice?.deviceModel?.isPrivate

    // 统一解析通知
    const notification = parseDeviceNotification(modelName, isPrivate, data)

    if (!notification) return

    switch (notification.type) {
      case 'auth':
        console.log('认证信息:', {
          clientId: notification.clientId,
          hardware: notification.hardwareVersion,
          software: notification.softwareVersion,
          battery: notification.battery
        })
        // 自动处理认证握手(由 connectAuth 内部完成)
        break

      case 'status':
        console.log('设备状态:', {
          battery: notification.battery,
          motors: notification.motors
        })
        // 更新 UI 显示
        updateBatteryDisplay(notification.battery)
        updateMotorDisplay(notification.motors)
        break

      default:
        console.log('未知通知类型:', notification)
    }
  }
})