V1 蓝牙对接代码实现
以下示例代码按实际接入流程组织,供小程序开发参考。
打开蓝牙适配器
ts
async function openBluetooth(): Promise<void> {
const setting = await wx.getSetting();
if (setting.authSetting['scope.bluetooth'] === false) {
wx.showModal({
title: '需要蓝牙权限',
content: '请在设置中允许使用蓝牙,以便搜索设备',
confirmText: '去设置',
success: (res) => { if (res.confirm) wx.openSetting(); },
});
throw new Error('蓝牙权限被拒绝');
}
await new Promise<void>((resolve, reject) => {
wx.openBluetoothAdapter({
success: () => resolve(),
fail: (err) => reject(new Error(err.errMsg || '打开蓝牙失败')),
});
});
wx.onBluetoothAdapterStateChange((res) => {
if (!res.available) { /* 提示用户开启系统蓝牙 */ }
});
}扫描与过滤
ts
function startScan(): Promise<void> {
return new Promise((resolve, reject) => {
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: () => resolve(),
fail: (err) => reject(new Error(err.errMsg || '开始扫描失败')),
});
});
}
wx.onBluetoothDeviceFound((res) => {
for (const d of res.devices || []) {
const name = d.localName || d.name || '';
if (!name.startsWith('DTXZ_')) continue; // ① 广播名前缀
if (!hasBlufiService(d.advertisData)) continue; // ② 服务 UUID 0xFFFF
const online = parseOnlineStatus(d.advertisData); // ③ 状态字节(用于分流,可选)
// 加入列表展示:设备名(含 MAC 尾缀)、RSSI、联网状态
}
});
// Service Data(AD type 0x16)= [0xFF, 0xFF, 状态字节](UUID 小端 + 状态)
function hasBlufiService(advertisData?: ArrayBuffer): boolean {
if (!advertisData) return false;
const bytes = new Uint8Array(advertisData);
let i = 0;
while (i + 1 < bytes.length) {
const len = bytes[i];
if (len === 0) break;
const type = bytes[i + 1];
// 128 位 UUID 列表(type 0x07)中的 0xFFFF 服务(小端)
if (type === 0x07 && len >= 4) {
const uuid = bytes[i + 2] | (bytes[i + 3] << 8);
if (uuid === 0xffff) return true;
}
i += 1 + len;
}
return false;
}
function stopScan(): Promise<void> {
return new Promise((resolve) => {
wx.stopBluetoothDevicesDiscovery({ complete: () => resolve() });
});
}
function parseOnlineStatus(advertisData?: ArrayBuffer): boolean | undefined {
if (!advertisData) return undefined;
const bytes = new Uint8Array(advertisData);
let i = 0;
while (i + 1 < bytes.length) {
const len = bytes[i];
if (len === 0) break;
const type = bytes[i + 1];
if (type === 0x16 && len >= 4) {
const uuid = bytes[i + 2] | (bytes[i + 3] << 8); // 小端
if (uuid === 0xffff) {
const status = bytes[i + 4];
if (status === 0x01) return true; // 已联网:连接后约 1s 内回 AT+ID=
if (status === 0x02) return false; // 配网模式:需先下发 WiFi 凭据
}
}
i += 1 + len;
}
return undefined; // 未知:连接后用探测判定
}连接、服务发现与订阅通知
ts
async function connectAndSubscribe(deviceId: string) {
await stopScan(); // 连接前先停扫描(部分平台连接中不允许扫描)
await new Promise<void>((resolve, reject) => {
wx.createBLEConnection({
deviceId, timeout: 10000,
success: () => resolve(),
fail: (err) => reject(new Error(err.errMsg || '连接失败')),
});
});
// 定位 BLUFI 服务/特征(UUID 大小写不敏感)
const { services } = await wx.getBLEDeviceServices({ deviceId });
const svc = services.find((s) => s.uuid.toUpperCase().includes('FFFF'));
const { characteristics } = await wx.getBLEDeviceCharacteristics({
deviceId, serviceId: svc.uuid,
});
const writeChar = characteristics.find((c) => c.uuid.toUpperCase().includes('FF01'));
const notifyChar = characteristics.find((c) => c.uuid.toUpperCase().includes('FF02'));
// Android 协商 MTU(iOS 自动协商,此调用会失败,可忽略)
wx.setBLEMTU({ deviceId, mtu: 512, complete: () => {} });
// 先订阅通知,再发命令
await new Promise<void>((resolve, reject) => {
wx.notifyBLECharacteristicValueChange({
deviceId, serviceId: svc.uuid, characteristicId: notifyChar.uuid,
state: true,
success: () => resolve(),
fail: (err) => reject(new Error(err.errMsg || '订阅通知失败')),
});
});
wx.onBLECharacteristicValueChange((res) => {
if (res.deviceId === deviceId && res.characteristicId === notifyChar.uuid) {
onNotify(new Uint8Array(res.value)); // → 帧解析,见“探测设备与帧解析”
}
});
}BLUFI 帧构造与写入
帧格式:type(1) | fc(1) | seq(1) | data_len(1) | data。
type:高 6 位 subtype,低 2 位包类型(0x0控制帧 /0x1数据帧);fc:明文模式ENC=0、CHECK=0;seq:每次连接从 0 开始,每帧 +1(分片每片 +1),设备按序校验;- 单次
wx.writeBLECharacteristicValue上限 = MTU - 3(默认 20 字节),超长必须分片。
ts
let seq = 0;
const PACKAGE_CTRL = 0x00, PACKAGE_DATA = 0x01;
const SUBTYPE_STA_SSID = 0x02, SUBTYPE_STA_PASSWD = 0x03;
const SUBTYPE_CONN_TO_AP = 0x03, SUBTYPE_CUSTOM_DATA = 0x13;
const FC_FRAG = 0x10;
function buildFrame(pkgType: number, subType: number, data: number[]): number[] {
const type = (subType << 2) | pkgType;
return [type, 0x00, seq++ & 0xff, data.length, ...data];
}
// 分片:首片及中间片 fc 置 FRAG,data 区前 2 字节为整帧数据总长(大端);
// 末片 FRAG=0、不带总长。packetLimit = MTU - 3。
async function writeData(conn, pkgType: number, subType: number, data: number[],
packetLimit = 20): Promise<void> {
const contentLimit = packetLimit - 4 - 2; // 帧头 4 字节 + 分片总长头 2 字节
let offset = 0;
while (true) {
const remaining = data.length - offset;
const frag = remaining > contentLimit;
const piece = frag
? [(data.length >> 8) & 0xff, data.length & 0xff,
...data.slice(offset, offset + contentLimit)]
: data.slice(offset);
const frame = buildFrame(pkgType, subType, piece);
if (frag) frame[1] |= FC_FRAG;
await new Promise<void>((resolve, reject) => {
wx.writeBLECharacteristicValue({
deviceId: conn.deviceId, serviceId: conn.serviceId,
characteristicId: conn.writeCharId, value: frame as unknown as ArrayBuffer,
success: () => resolve(),
fail: (err) => reject(new Error(err.errMsg || '写入失败')),
});
});
offset += frag ? contentLimit : remaining;
if (!frag) break;
}
}
const ascii = (s: string) => Array.from(s, (c) => c.charCodeAt(0) & 0xff);探测设备与帧解析
ts
// 连接成功、订阅完成后:
// ① 状态字节 = 0x02 → 直接进入配网流程(见“WiFi 配网”)
// ② 否则发送 AT+GETID,等待 AT+ID=<device_id>
await writeData(conn, PACKAGE_DATA, SUBTYPE_CUSTOM_DATA, ascii('AT+GETID'));
// 2 秒未收到回复 → 判定配网模式,进入“WiFi 配网”(已联网设备连接后约 1s 内会自动上报一轮,
// 此处的 GETID 只是双保险)收到通知后的帧解析:
ts
function onNotify(bytes: number[]): void {
const type = bytes[0], fc = bytes[1], seq = bytes[2], len = bytes[3];
const pkgType = type & 0x03, subType = (type >> 2) & 0x3f;
let payload = bytes.slice(4, 4 + len);
if ((fc & FC_FRAG) !== 0) payload = payload.slice(2); // 分片帧剥掉总长头(拼合略)
if (pkgType === PACKAGE_DATA && subType === SUBTYPE_CUSTOM_DATA) {
const text = String.fromCharCode(...payload);
if (text.startsWith('AT+ID=')) {
const deviceId = text.slice(6).trim();
// → 进入“云端绑定与触发重启”
}
}
}WiFi 配网
ts
// 依次下发(SSID ≤31 字节、密码 ≤63 字节,按 UTF-8 字节计):
await writeData(conn, PACKAGE_DATA, SUBTYPE_STA_SSID, ascii(ssid));
await writeData(conn, PACKAGE_DATA, SUBTYPE_STA_PASSWD, ascii(password));
await writeData(conn, PACKAGE_CTRL, SUBTYPE_CONN_TO_AP, []); // 设备原地连接
// 设备联网(一般 2~10 秒)后自动授权并上报 AT+ID=;也可再发一次 AT+GETID 主动拉取。
// 失败无上报:设整体超时 30~40 秒,超时提示“请检查 WiFi 密码或信号后重新下发”。
// 注意:设备仅支持 2.4GHz WiFi,5G 热点无法连接。云端绑定与触发重启
ts
// ① 调云端绑定接口(携带 Bearer mac_auth_key + external_user_id)
await bindDevice(deviceId, clientKey, externalUserId);
// ② 通知设备立即重启:再次发送 AT+GETID
await writeData(conn, PACKAGE_DATA, SUBTYPE_CUSTOM_DATA, ascii('AT+GETID'));
// 设备检测到已绑定 → 自动重启 → BLE 断开(属正常现象,勿提示“连接失败”)
wx.showToast({ title: '设备正在重启,请稍候', icon: 'none' });
wx.closeBLEConnection({ deviceId });
// 设备重启后进主程序;即使未发通知,设备 60 秒轮询兜底也会重启