uniapp 蓝牙工具

830 阅读6分钟

给蓝牙发送数据

  • 初始化蓝牙、检查蓝牙是否可用
  • 搜索蓝牙
  • 获取到搜索到的所有蓝牙信息
  • 连接指定蓝牙
  • 断开蓝牙
  • 蓝牙设备特征值变化时接收 notify(通知)
  • 给蓝牙发送数据(二进制数据)
let bluetoothOpen = false; // 手机蓝牙是否打开
let isSearch = false; // 是否正在搜索

let bluetoothArr = []; // 扫描到的蓝牙

let deviceId = null; // 设备id
let serviceId = null; // 服务id

let writeId = null; // 写入服务的特征值id
let readId = null; // 读取服务的特征值id
let indicateId = null; // 指示服务的特征值id
let notifyId = null; // 通知服务的特征值id

// 初始化蓝牙模块 并 获取本机蓝牙适配器状态(是否开启蓝牙)
const getBluetoothState = () => {
	return new Promise((resolve, reject) => {
		uni.openBluetoothAdapter({
			success: () => {
				console.log('蓝牙初始化成功');
				// 获取本机蓝牙适配器状态
				uni.getBluetoothAdapterState({
					success: function (res) {
						console.log('蓝牙是否正在搜索设备:', res.discovering);
						console.log('蓝牙适配器是否可用:', res.available);
						if (res.available) {
							bluetoothOpen = true;
							resolve();
						} else {
							uni.showToast({
								title: '请先打开蓝牙',
								icon: 'none',
							});
							bluetoothOpen = false;
							reject(new Error('蓝牙适配器不可用'));
						}
					},
					fail: function (err) {
						// 请开启蓝牙
						uni.showToast({
							title: '请先打开蓝牙',
							icon: 'none',
						});
						bluetoothOpen = false;
						reject(err);
					},
				});
			},
			fail: () => {
				uni.showToast({
					title: '请先打开蓝牙',
					icon: 'none',
				});
				bluetoothOpen = false;
				reject();
			},
		});
	});
};
// 开始搜索蓝牙设备
const startDiscoveryBluetooth = () => {
	return new Promise((resolve, reject) => {
		if (!bluetoothOpen) {
			uni.showToast({
				title: '请先打开蓝牙',
				icon: 'none',
			});
			return reject(new Error('蓝牙未打开'));
		}
		if (isSearch) {
			uni.showToast({
				title: '已经正在搜索蓝牙设备了',
				icon: 'none',
			});
			return reject(new Error('已在搜索中'));
		}
		uni.startBluetoothDevicesDiscovery({
			success(res) {
				console.log('开始搜寻附近的蓝牙外围设备成功', res);
				isSearch = true;
				uni.showToast({
					title: '搜索蓝牙设备完成',
					icon: 'none',
				});
				resolve(res);
			},
			fail(err) {
				console.log('开始搜寻附近的蓝牙外围设备失败', err);
				isSearch = false;
				reject(err);
			},
		});
	});
};
// 停止搜索蓝牙设备
const stopDiscoveryBluetooth = () => {
	return new Promise((resolve, reject) => {
		if (!bluetoothOpen) {
			uni.showToast({
				title: '请先打开蓝牙',
				icon: 'none',
			});
			return reject(new Error('蓝牙未打开'));
		}
		if (!isSearch) {
			uni.showToast({
				title: '当前未搜索蓝牙设备',
				icon: 'none',
			});
			return reject(new Error('未在搜索中'));
		}
		uni.stopBluetoothDevicesDiscovery({
			success(res) {
				console.log('停止搜寻附近的蓝牙外围设备成功', res);
				isSearch = false;
				resolve(res);
			},
			fail(err) {
				console.log('停止搜寻附近的蓝牙外围设备失败', err);
				reject(err);
			},
		});
	});
};
// 获取搜索到的蓝牙设备信息
const getBluetoothDevicesInfo = () => {
	return new Promise((resolve, reject) => {
		if (!bluetoothOpen) {
			uni.showToast({
				title: '请先打开蓝牙',
				icon: 'none',
			});
			return reject(new Error('蓝牙未打开'));
		}
		uni.getBluetoothDevices({
			success(res) {
				console.log('获取搜索到的蓝牙设备信息', res.devices);
				// 过滤掉name为空或者未知设备的设备
				let devices = res.devices.filter(function (obj) {
					return obj.name !== '' && obj.name !== '未知设备';
				});
				console.log('有名称的蓝牙列表', devices);
				bluetoothArr = devices;
				resolve(bluetoothArr);
			},
			fail(err) {
				console.log('获取搜索到的蓝牙设备信息失败');
				reject(err);
			},
		});
	});
};
/* 连接蓝牙
 * @param {string} deviceName 设备名称
 * @returns {Promise} 返回promise
 */
const connectBluetooth = (deviceName) => {
	return new Promise((resolve, reject) => {
		if (!bluetoothOpen) {
			uni.showToast({
				title: '请先打开蓝牙',
				icon: 'none',
			});
			return reject(new Error('蓝牙未打开'));
		}
		if (deviceId) {
			uni.showToast({
				title: '请先断开已连接的蓝牙',
				icon: 'none',
			});
			return reject(new Error('已有设备连接'));
		}
		let isHaveDevice = false; // 是否存在该设备
		let lookupDeviceId = null; // 查找到的设备id
		for (let index = 0; index < bluetoothArr.length; index++) {
			const e = bluetoothArr[index];
			if (e.name === deviceName) {
				lookupDeviceId = e.deviceId;
				isHaveDevice = true;
				break;
			}
		}
		if (!isHaveDevice) {
			uni.showToast({
				title: '未找到该设备',
				icon: 'none',
			});
			return reject(new Error('未找到该设备'));
		}
		if (lookupDeviceId == deviceId) {
			uni.showToast({
				title: '已连接该设备',
				icon: 'none',
			});
			return reject(new Error('已连接该设备'));
		}
		deviceId = lookupDeviceId;
		uni.createBLEConnection({
			deviceId: deviceId, // 设备id
			success() {
				console.log('连接蓝牙设备成功', {
					蓝牙设备名称: deviceName,
					蓝牙设备id: deviceId,
				});
				if (isSearch) {
					stopDiscoveryBluetooth();
				}
				resolve();
				// 获取服务id
				getServiceId().catch((err) => {
					console.error('获取服务ID失败', err);
				});
			},
			fail(err) {
				console.log('蓝牙连接失败');
				reject(err);
			},
		});
	});
};
// 断开蓝牙
const disconnectBluetooth = () => {
	return new Promise((resolve, reject) => {
		if (!bluetoothOpen) {
			uni.showToast({
				title: '请先打开蓝牙',
				icon: 'none',
			});
			return reject(new Error('蓝牙未打开'));
		}
		if (!deviceId) {
			uni.showToast({
				title: '请先连接蓝牙',
				icon: 'none',
			});
			return reject(new Error('未连接蓝牙'));
		}
		uni.closeBLEConnection({
			deviceId: deviceId,
			success(res) {
				console.log('蓝牙断开成功', res);
				deviceId = null;
				resolve(res);
			},
			fail(err) {
				console.log('蓝牙断开失败', err);
				reject(err);
			},
		});
	});
};
// 获取服务id
const getServiceId = () => {
	return new Promise((resolve, reject) => {
		uni.getBLEDeviceServices({
			deviceId: deviceId,
			success(res) {
				console.log('获取服务Id成功', res);
				/*
				res.services是一个数组,数组中的每一项代表一个蓝牙服务
				找到你需要的蓝牙服务,将其uuid赋值给serviceId
				*/
				let found = false;
				for (let index = 0; index < res.services.length; index++) {
					const service = res.services[index];
					if (service.uuid == '改成对接文档上提供的服务id') {
						serviceId = service.uuid;
						getCharacteristics().then(resolve).catch(reject);
						found = true;
						break;
					}
				}
				if (!found) {
					reject(new Error('未找到指定服务ID'));
				}
			},
			fail(err) {
				console.log('获取服务Id失败', err);
				reject(err);
			},
		});
	});
};
// 获取蓝牙设备某个服务中所有特征值
const getCharacteristics = () => {
	return new Promise((resolve, reject) => {
		uni.getBLEDeviceCharacteristics({
			deviceId: deviceId, // 蓝牙设备id
			serviceId: serviceId, // 蓝牙服务UUID
			success(res) {
				console.log('服务中所有特征值', res);
				/*
				特征值的 properties 中包含了 read、write、notify、indicate 四个属性
				read: 表示特征值可以读取
				write: 表示特征值可以写入
				notify: 表示特征值可以通知
				indicate: 表示特征值可以指示
				*/
				res.characteristics.forEach((item) => {
					// 通知
					if (item.properties.notify === true) {
						notifyId = item.uuid;
					}
					// 指示
					if (item.properties.indicate === true) {
						indicateId = item.uuid;
					}
					// 读取
					if (item.properties.read === true) {
						readId = item.uuid;
					}
					// 写入
					if (item.properties.write === true) {
						writeId = item.uuid;
					}
				});
				if (notifyId) {
					startNotice().then(resolve).catch(reject);
				} else {
					resolve();
				}
			},
			fail(err) {
				console.log('获取服务中所有特征值失败', err);
				reject(err);
			},
		});
	});
};
// 启用低功耗蓝牙设备特征值变化时的notify功能(通知)
const startNotice = () => {
	return new Promise((resolve, reject) => {
		uni.notifyBLECharacteristicValueChange({
			deviceId: deviceId,
			serviceId: serviceId,
			characteristicId: notifyId,
			state: true,
			success() {
				// 监听低功耗蓝牙设备的特征值变化
				uni.onBLECharacteristicValueChange((result) => {
					console.log('监听低功耗蓝牙设备的特征值变化', result);
					// result.value 是 ArrayBuffer 类型
					if (result.value) {
						// 1. 转为 16 进制字符串
						const hexArr = Array.prototype.map.call(new Uint8Array(result.value), (x) =>
							('00' + x.toString(16)).slice(-2)
						);
						const hexStr = hexArr.join(' ');
						console.log('收到蓝牙数据(16进制):', hexStr);

						// 2. 尝试转为 UTF-8 字符串
						let str = '';
						try {
							str = String.fromCharCode.apply(null, new Uint8Array(result.value));
							console.log('收到蓝牙数据(UTF-8 字符串):', str);
						} catch (e) {
							console.log('收到蓝牙数据无法转为UTF-8 字符串');
						}

						// 3. 如果你有设备协议,可以在这里进一步解析成更友好的信息
					}
				});
				resolve();
			},
			fail(err) {
				console.log('启用低功耗蓝牙设备特征值变化时的notify功能失败', err);
				reject(err);
			},
		});
	});
};
/**
 * 向低功耗蓝牙设备特征值中写入二进制数据(分片+重试)
 * @param {ArrayBuffer} ArrayBuffer 二进制数据
 * @param {number} [maxRetries=3] 最大重试次数
 * @returns {Promise}
 */
function writeBLEValueLoop(ArrayBuffer, maxRetries = 3) {
	return new Promise(async (resolve, reject) => {
		if (!bluetoothOpen) {
			uni.showToast({ title: '请先打开蓝牙', icon: 'none' });
			return reject(new Error('蓝牙未打开'));
		}
		if (!deviceId) {
			uni.showToast({ title: '请先连接设备', icon: 'none' });
			return reject(new Error('未连接设备'));
		}
		let bufferList;
		try {
			bufferList = getSliceBufferList(ArrayBuffer);
		} catch (e) {
			return reject(e);
		}
		for (let i = 0; i < bufferList.length; i++) {
			const buffer = bufferList[i];
			let retries = 0;
			while (retries <= maxRetries) {
				try {
					await writeData(buffer);
					console.log(`分片${i + 1}/${bufferList.length}发送成功`);
					break;
				} catch (error) {
					retries++;
					console.error(`分片${i + 1}发送失败, 重试${retries}/${maxRetries}`, error);
					if (retries > maxRetries) {
						return reject(new Error(`分片${i + 1}达到最大重试次数, 发送失败`));
					}
					await new Promise((res) => setTimeout(res, 500 * retries));
				}
			}
		}
		console.log('全部数据发送完成');
		resolve();
	});
}
/**
 * 获取分片数组
 * @param {ArrayBuffer} buffer - 要分片的二进制数据缓冲区
 * @param {number} [maxChunk=20] - 每包控制大小,默认为 20 字节
 * @returns {Array<ArrayBuffer>} - 分片后的缓冲区数组
 * @throws {Error} - 当输入缓冲区无效时抛出异常
 */
function getSliceBufferList(buffer, maxChunk = 20) {
	if (!(buffer instanceof ArrayBuffer)) {
		throw new Error('Invalid input: expected ArrayBuffer');
	}
	if (typeof maxChunk !== 'number' || maxChunk < 1 || maxChunk > 512) {
		throw new Error('maxChunk 必须在 1~512 之间');
	}
	if (buffer.byteLength === 0) return [];
	const result = [];
	let start = 0;
	while (start < buffer.byteLength) {
		const end = Math.min(start + maxChunk, buffer.byteLength);
		result.push(buffer.slice(start, end));
		start = end;
	}
	return result;
}
/**
 * 写入单个分片
 * @param {ArrayBuffer} buffer
 * @returns {Promise}
 */
const writeData = (buffer) => {
	return new Promise((resolve, reject) => {
		if (!deviceId || !serviceId || !writeId) {
			return reject(new Error('蓝牙参数未初始化'));
		}
		uni.writeBLECharacteristicValue({
			deviceId,
			serviceId,
			characteristicId: writeId,
			value: buffer,
			success(res) {
				console.log('写入成功', res);
				resolve();
			},
			fail(err) {
				console.log('写入失败', err);
				reject(err);
			},
		});
	});
};

export default {
	getBluetoothState,
	startDiscoveryBluetooth,
	stopDiscoveryBluetooth,
	getBluetoothDevicesInfo,
	connectBluetooth,
	disconnectBluetooth,
	writeBLEValueLoop,
};