# bluetooth-config.js 快速接入与二开文档

| 项目 | 说明 |
|------|------|
| 模块文件 | `utils/bluetooth-config.js` |
| 协议类型 | 杰里 BLE + JL 自定义帧 |
| 适用端 | uni-app（微信小程序 / App）、任意支持 BLE 的 JS 端 |
| 文档版本 | V1.0.0 |
| 更新日期 | 2026-07-22 |

本模块只负责**协议层**：设备过滤、组包、CRC、Notify 拼包、状态解析。  
BLE 开关、扫描、连接、读写特征值由业务侧用 `uni` / 微信原生 API 完成。

---

## 1. 5 分钟接入

### 1.1 拷贝文件

将本仓库中的：

```text
utils/bluetooth-config.js
```

复制到你的工程（建议路径不变），按需修改广播名前缀等常量即可。

### 1.2 引入

```js
import {
  filterDevice,
  characteristicValues,
  resetRecvBuffer,
  recvBLEData,
  iotProtocolDecode,
  sendBLECharacterNotice,
  CRC16,
  bytesToArrayBuffer,
  BLE_CHUNK_SIZE,
  CONFIG_TIMEOUT_SEC
} from '@/utils/bluetooth-config.js'
```

> 若工程为 CommonJS，可改为：`const ble = require('@/utils/bluetooth-config.js')`（需自行补 `module.exports`，或继续用 ES Module）。

### 1.3 标准调用链路（必须按序）

```text
打开蓝牙 → 扫描 → filterDevice 过滤
→ 选设备 → 输入 SSID/密码
→ 停止扫描 → 断开再连接（约 2s）
→ 匹配 Service / Characteristic（characteristicValues）
→ 开启 Notify → resetRecvBuffer()
→ 组包 sendBLECharacterNotice + CRC16
→ 按 20 字节分包 write
→ Notify 回调里 recvBLEData → iotProtocolDecode
→ 按 status 处理；status==1 超时时重发带 net_status:"timeout"
```

---

## 2. 导出一览

### 2.1 常量

| 常量 | 默认值 | 说明 |
|------|--------|------|
| `DEVICE_NAME_PREFIXES` | `['QYAI_AC','XWAI_AC']` | 广播名过滤前缀，**二开常改项** |
| `UUID_PREFIXES` | `['0000AE80','0000AE81','0000AE82','6E40']` | 服务/特征值 UUID 前缀 |
| `BLE_CHUNK_SIZE` | `20` | 单次写入最大字节数 |
| `CONFIG_TIMEOUT_SEC` | `60` | 配网等待超时（秒），超时后建议重发 |

### 2.2 方法

| 方法 | 入参 | 返回 | 作用 |
|------|------|------|------|
| `filterDevice(devices)` | 扫描到的设备数组 | 过滤后的数组 | 按广播名过滤 |
| `characteristicValues(uuid)` | UUID 字符串 | `boolean` | 是否配网服务/特征 |
| `resetRecvBuffer()` | — | — | 清空 Notify 拼包缓存（**每次配网前必调**） |
| `recvBLEData(buffer)` | `ArrayBuffer` | `number[] \| undefined` | 拼包，满一帧返回 |
| `iotProtocolDecode(list)` | 完整帧字节数组 | `{ cell, content, err? }` | 解析 JSON 与文案 |
| `sendBLECharacterNotice(wifiData)` | `{ssid,pass,net_status?}` | CRC 输入字节数组 | 编码 JSON 载荷 |
| `CRC16(buf)` | CRC 输入字节数组 | 完整发送帧字节数组 | 加 JL 头 + CRC + `0xFF` |
| `bytesToArrayBuffer(bytes)` | `number[]` | `ArrayBuffer` | 供 `writeBLECharacteristicValue` |

---

## 3. 业务侧最小示例

### 3.1 扫描过滤

```js
uni.onBluetoothDeviceFound((res) => {
  const list = filterDevice(res.devices || [])
  // 将 list 去重后展示到 UI
})
```

### 3.2 匹配服务与特征值

```js
uni.getBLEDeviceServices({
  deviceId,
  success(res) {
    const service = (res.services || []).find(
      (s) => s.isPrimary && characteristicValues(s.uuid)
    )
    if (!service) return // 未找到配网服务

    uni.getBLEDeviceCharacteristics({
      deviceId,
      serviceId: service.uuid,
      success(r) {
        let notifyId = ''
        let writeId = ''
        ;(r.characteristics || []).forEach((c) => {
          if (!characteristicValues(c.uuid)) return
          const p = c.properties || {}
          if (p.notify || p.indicate) notifyId = c.uuid
          if (p.write || p.writeNoResponse) writeId = c.uuid
        })
        // 保存 service.uuid / notifyId / writeId，再开 Notify
      }
    })
  }
})
```

### 3.3 开启 Notify 并解析回包

```js
resetRecvBuffer()

uni.notifyBLECharacteristicValueChange({
  deviceId,
  serviceId,
  characteristicId: notifyId,
  state: true,
  success() {
    uni.onBLECharacteristicValueChange((res) => {
      const frame = recvBLEData(res.value)
      if (!frame) return // 半包，继续等

      const { cell, content, err } = iotProtocolDecode(frame)
      if (err || !cell) {
        // 提示 content
        return
      }

      if (cell.status === 0) {
        // 成功：可使用 cell.mac / cell.uuid / cell.batch_num 做绑定
      } else if (cell.status === 1) {
        // 进行中：启动 60s 超时，超时后重发（见 3.5）
      } else {
        // 失败：提示 content
      }
    })
  }
})
```

### 3.4 组包并分包写入

```js
function sendWifi({ ssid, pass }, isTimeout = false) {
  const wifiData = { ssid, pass }
  if (isTimeout) wifiData.net_status = 'timeout'

  const payload = sendBLECharacterNotice(wifiData) // [LEN_H,LEN_L,T1,T2,...]
  const frame = CRC16(payload)                     // 完整 JL 帧
  writeByChunk(frame.slice())
}

function writeByChunk(array) {
  if (!array.length) return

  const chunk = array.splice(0, BLE_CHUNK_SIZE)
  const value = bytesToArrayBuffer(chunk)

  uni.writeBLECharacteristicValue({
    deviceId,
    serviceId,
    characteristicId: writeId,
    value,
    success() {
      // iOS 建议间隔约 500ms 再写下一段
      const delay = uni.getSystemInfoSync().platform === 'ios' ? 500 : 0
      setTimeout(() => writeByChunk(array), delay)
    }
  })
}
```

### 3.5 超时重发（推荐）

当收到 `status === 1`（正在配网）后启动计时，默认 `CONFIG_TIMEOUT_SEC`（60）秒仍无成功，则重发一次：

```js
sendWifi({ ssid, pass }, true)
// 等价于 JSON: { "ssid":"...", "pass":"...", "net_status":"timeout" }
```

首次下发**不要**带 `net_status` 字段。

---

## 4. 设备回包 status

| status | 含义 | UI 建议 |
|--------|------|---------|
| `0` | 配网成功 | 结束流程；可绑定 `mac` / `uuid` / `batch_num` |
| `1` | 正在配网 | 继续等待；可启超时重发 |
| `2` | 配网失败 | 提示重启设备重试 |
| `3` | Wi‑Fi 名称或密码错误 | 检查后重启设备重试 |
| `4` | Wi‑Fi 密码错误 | 检查密码后重启设备重试 |
| 其他 | 未知错误 | 提示出错并重启设备 |

`iotProtocolDecode` 返回示例：

```js
{
  cell: { status: 0, mac: '...', uuid: '...', batch_num: '...' },
  content: '配网成功'
}
```

---

## 5. 推荐时序与延时

| 步骤 | 动作 | 建议延时 |
|------|------|----------|
| 1 | `openBluetoothAdapter` + 扫描 | 扫描超时建议 60s |
| 2 | 用户选设备并输入 SSID/密码 | — |
| 3 | `stopBluetoothDevicesDiscovery` | — |
| 4 | 先 `closeBLEConnection` 再连 | 断开后约 **2s** |
| 5 | `createBLEConnection` | 成功后约 **1s** |
| 6 | 取 Service / Characteristic | 取特征前约 **1s** |
| 7 | 开启 Notify | 开启后再等约 **1s** 再监听/发送 |
| 8 | 分包写入 | 单包 ≤ **20** 字节；iOS 包间约 **500ms** |

---

## 6. 权限与平台注意

### 微信小程序

- 需定位权限：`scope.userLocation`（系统限制，用于 BLE 扫描）
- `manifest.json` / 小程序后台配置位置用途说明
- **必须真机调试**，模拟器通常无可用 BLE
- 含 `input` 的弹层建议用**底部弹窗**，避免键盘顶起后居中弹层消失

### App（Android）

- 定位权限 + 系统位置服务
- Android 12+：`BLUETOOTH_SCAN` / `BLUETOOTH_CONNECT`
- 仅支持 **2.4G Wi‑Fi**，勿开 AP 隔离 / 防蹭网

### iOS

- 蓝牙使用说明（Privacy - Bluetooth）
- 分包写入务必加间隔，降低丢包

---

## 7. 二开常见改动点

### 7.1 改设备广播名

编辑 `bluetooth-config.js`：

```js
export const DEVICE_NAME_PREFIXES = ['QYAI_AC', 'XWAI_AC', 'YOUR_PREFIX']
```

### 7.2 改 UUID 前缀

```js
export const UUID_PREFIXES = ['0000AE80', '0000AE81', '0000AE82', '6E40']
```

> 若设备固件 UUID 变更，需与设备侧约定后同步修改。

### 7.3 改超时时间

```js
export const CONFIG_TIMEOUT_SEC = 60
```

### 7.4 接入自有绑定接口

配网成功（`status === 0`）后：

```js
const { cell } = iotProtocolDecode(frame)
// cell.mac / cell.uuid / cell.batch_num
await yourBindApi({
  mac: cell.mac,
  uuid: cell.uuid,
  batch_num: cell.batch_num
})
```

### 7.5 不要改动的部分（除非懂协议）

- JL 帧结构、`T1=0x10` / `T2=0x01`
- CRC16 查表与计算范围
- 中文 SSID 的百分号编码规则
- `recvBLEData` 拼包长度算法（`frameLen = payloadLen + 7`）

协议细节见：`杰里蓝牙配网协议文档.md`。

---

## 8. 完整配网伪代码

```js
async function startProvision({ deviceId, ssid, pass }) {
  resetRecvBuffer()

  // 1. 断开 → 等待 2s → 连接 → 等待 1s
  // 2. 找 service / notify / write（characteristicValues）
  // 3. notifyBLECharacteristicValueChange + onBLECharacteristicValueChange
  // 4. 等待 1s 后发送：

  sendWifi({ ssid, pass }, false)

  // 5. 在 Notify 回调中：
  //    frame = recvBLEData(buffer)
  //    { cell, content } = iotProtocolDecode(frame)
  //    status 0 → 成功
  //    status 1 → 启 60s 定时器，超时 sendWifi(..., true)
  //    status 2/3/4 → 失败提示
}
```

可直接参考本仓库页面实现：`pages/index/index.vue`。

---

## 9. 排查清单

| 现象 | 排查 |
|------|------|
| 扫不到设备 | 蓝牙/定位权限；广播名是否含约定前缀；设备是否在配网态 |
| 连上无服务 | UUID 是否匹配 `UUID_PREFIXES` |
| 写入无回包 | 是否先开 Notify；是否 20 字节分包；iOS 是否加包间隔 |
| 一直 `status=1` | 是否 2.4G；密码是否正确；是否做超时重发 |
| `status=3/4` | SSID/密码错误，重启设备后重试 |
| JSON 解析失败 | 是否半包未拼齐；配网前是否 `resetRecvBuffer()` |
| 小程序输入框弹层消失 | 勿用居中 fixed + 键盘；改用底部弹层 |

---

## 10. 交付清单（给客户）

| 文件 | 用途 |
|------|------|
| `utils/bluetooth-config.js` | 协议核心（必接） |
| `docs/bluetooth-config接入文档.md` | 本文档 |
| `pages/index/index.vue` | 完整 Demo 参考实现（可选） |
| `杰里蓝牙配网协议文档.md` | 协议原文（可选深入） |

接入最少只需 **1 个 JS 文件** + 按本文第 3 节接 BLE API。
