蓝牙设备对接文档

VxMi 步进电机协议

适用于 Vx/Mi/Amorlinkvex 系列设备的步进电机控制协议

概述

VxMi Protocol 用于控制 Vx / Mi / Amorlinkvex 系列步进电机设备,支持位置(position)和速度(speed)的精确控制。该协议使用 Nordic UART Service (NUS) 作为通信通道。

适用设备

通过设备型号名称(modelName)前缀判断是否使用此协议:

  • Vx 开头的设备
  • Mi 开头的设备
  • Amorlinkvex 开头的设备

BLE UUID

VxMi 使用 Nordic UART Service UUID:

特征UUID
Service6e400001-b5a3-f393-e0a9-e50e24dcca9e
Write (RX)6e400002-b5a3-f393-e0a9-e50e24dcca9e
Notify (TX)6e400003-b5a3-f393-e0a9-e50e24dcca9e
// VxMiProtocol.ts
public static readonly SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
public static readonly WRITE_UUID   = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'
public static readonly NOTIFY_UUID  = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'

帧格式

+--------+--------+------+-------------------------+----------+
| Header | Length | Cmd  | Payload                 | CRC16    |
| A5 5A  | 1 byte | A0   | 变长                    | 2 bytes  |
+--------+--------+------+-------------------------+----------+
字段长度说明
Header2 字节固定帧头 A5 5A
Length1 字节整帧字节长度(含 Header 和 CRC)
Cmd1 字节命令类型
Payload变长命令参数
CRC162 字节CRC16-CCITT-FALSE 校验(高低字节交换)

电机控制

参数映射

VxMi 接收两个主要参数,均为 0-100 的百分比值,内部映射为协议参数:

输入参数输入范围映射公式输出范围协议参数
amplitude(幅度)0-100(amplitude / 100) * 100000-10000Position
vibration(振动)0-100(vibration / 100) * 2550-255Speed

控制帧结构

A5 5A 0D A0 B0 [Speed] A0 01 0F [Pos_HI] [Pos_LO] [CRC_HI] [CRC_LO]
偏移长度说明
0-12A5 5A帧头
210D帧长度(13 字节)
31A0命令类型
41B0子命令标记(速度控制)
51Speed速度值 (0-255)
61A0参数标记
7101参数
810F参数
91Position High位置高字节
101Position Low位置低字节
111CRC16 High校验高字节
121CRC16 Low校验低字节

计算示例

amplitude=50, vibration=75:

Position = (50 / 100) * 10000 = 5000 = 0x1388
Speed    = (75 / 100) * 255   = 191  = 0xBF

帧数据(不含 CRC): A5 5A 0D A0 B0 BF A0 01 0F 13 88
CRC16 计算范围:      0D A0 B0 BF A0 01 0F 13 88
CRC16 值:           [CRC_HI] [CRC_LO]

完整帧: A5 5A 0D A0 B0 BF A0 01 0F 13 88 [CRC_HI] [CRC_LO]

字节序转换

Position 使用 Big-Endian 传输。内部计算时需要从 Little-Endian 转换:

function littleToBigEndian(hexStr: string): string {
  const bytes: string[] = []
  for (let i = 0; i < hexStr.length; i += 2) {
    bytes.push(hexStr.slice(i, i + 2))
  }
  return bytes.reverse().join('')
}

// 示例: "8813" -> "1388" (5000 的大端表示)

设备信息查询

查询设备固件版本和状态的命令:

A5 5A 07 00 01 1E 90

偏移 0-1: A5 5A   -- 帧头
偏移 2:   07      -- 帧长度(7 字节)
偏移 3:   00      -- 设备信息查询命令
偏移 4:   01      -- 参数
偏移 5-6: 1E 90   -- 预计算的 CRC16

CRC16-CCITT-FALSE

参数

参数
多项式0x1021
初始值0xFFFF
最终异或0x0000

计算范围

CRC16 的计算范围为 Length 字节开始,到 CRC 字段之前,不包含 A5 5A 帧头和 CRC 本身。

算法实现

function crc16CCITT(data: Uint8Array): number {
  let crc = 0xFFFF  // 初始值

  for (let i = 0; i < data.length; i++) {
    crc ^= (data[i] << 8)
    for (let j = 0; j < 8; j++) {
      if (crc & 0x8000) {
        crc = ((crc << 1) ^ 0x1021) & 0xFFFF
      } else {
        crc = (crc << 1) & 0xFFFF
      }
    }
  }

  return crc
}

高低字节交换

CRC16 写入帧时需要交换高低字节:

function crc16Swapped(data: Uint8Array): [number, number] {
  const crc = crc16CCITT(data)
  const high = (crc >> 8) & 0xFF
  const low = crc & 0xFF
  return [high, low]  // 注意:高字节在前
}

设备通知

通知帧结构

设备回复的数据帧同样以 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       // 原始数据
}

电量计算

根据电压线性插值计算电量百分比:

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
  )
}

// 示例:
// 电压 3.6V => ((3.6 - 3.0) / (4.2 - 3.0)) * 100 = 50%

完整使用示例

// 假设 VxMiProtocol 类已实现
import { VxMiProtocol } from '@/utils/bluetooth/protocol/VxMiProtocol'

// 1. 构造控制帧
// amplitude=80 (幅度80%), vibration=60 (振动60%)
// Position = 8000 = 0x1F40, Speed = 153 = 0x99
const controlFrame = VxMiProtocol.makeMotorControl(80, 60)
// 帧: A5 5A 0D A0 B0 99 A0 01 0F 1F 40 [CRC_HI] [CRC_LO]

// 2. 发送到设备
await unifiedBluetoothManager.write(controlFrame, connectedDevice)

// 3. 查询设备信息
const infoFrame = VxMiProtocol.makeDeviceInfoQuery()
// 帧: A5 5A 07 00 01 1E 90
await unifiedBluetoothManager.write(infoFrame, connectedDevice)

// 4. 停止电机
// amplitude=0, vibration=0
const stopFrame = VxMiProtocol.makeMotorControl(0, 0)
// Position = 0, Speed = 0
await unifiedBluetoothManager.write(stopFrame, connectedDevice)