/** * 蓝牙秤工具模块 - BT-YH * Service UUID: 49535343-FE7D-4AE5-8FA9-9FAFD205E455 * Characteristic UUID: 49535343-1E4D-4BD9-BA61-23C647249616 * 数据格式: ASCII 字符串, 正则 /=(\d+\.\d+)/ 提取重量(kg) */ const DEVICE_NAME = 'BT-YH'; const SERVICE_UUID = '49535343-FE7D-4AE5-8FA9-9FAFD205E455'; const CHARACTERISTIC_UUID = '49535343-1E4D-4BD9-BA61-23C647249616'; // ArrayBuffer 转 ASCII 字符串 function ab2str(buffer) { const arr = new Uint8Array(buffer); let str = ''; for (let i = 0; i < arr.length; i++) { str += String.fromCharCode(arr[i]); } return str; } // 从原始字符串中提取重量值 function parseWeight(str) { const match = str.match(/=(\d+\.\d+)/); if (match) { const val = parseFloat(match[1]); if (!isNaN(val)) return val; } return null; } // UUID 统一转大写,兼容 iOS/Android 差异 function normalizeUUID(uuid) { return uuid.toUpperCase(); } class BluetoothScale { constructor() { this._deviceId = null; this._serviceId = null; this._characteristicId = null; this._buffer = ''; // 分包拼接缓冲 this._lastWeight = null; // 上一次稳定值 this._stableCount = 0; // 连续相同次数 this._onWeight = null; // 稳定重量回调 this._onStatus = null; // 状态变更回调 this._onSelectDevice = null; // 多设备选择回调 this._scanning = false; this._connected = false; } // 注册回调 onWeight(fn) { this._onWeight = fn; } onStatus(fn) { this._onStatus = fn; } // 多设备时触发,参数为设备列表,调用方需 resolve(deviceId) 或 reject() onSelectDevice(fn) { this._onSelectDevice = fn; } _setStatus(msg) { console.log('[BT]', msg); if (this._onStatus) this._onStatus(msg); } // 处理收到的数据包 _handleData(buffer) { const chunk = ab2str(buffer); this._buffer += chunk; // 避免缓冲区过大 if (this._buffer.length > 200) { this._buffer = this._buffer.slice(-100); } const weight = parseWeight(this._buffer); if (weight !== null) { if (weight === this._lastWeight) { this._stableCount++; // 连续2次相同认为稳定 if (this._stableCount >= 2 && this._onWeight) { this._onWeight(weight); } } else { this._lastWeight = weight; this._stableCount = 1; } } } // 初始化蓝牙适配器 _openAdapter() { return new Promise((resolve, reject) => { wx.openBluetoothAdapter({ success: resolve, fail: (err) => { if (err.errCode === 10001) { reject(new Error('请先开启手机蓝牙')); } else { reject(new Error('蓝牙初始化失败: ' + err.errMsg)); } } }); }); } // 扫描目标设备,固定扫5秒收集所有 BT-YH,多台时交给调用方选择 _scanDevice() { return new Promise((resolve, reject) => { this._scanning = true; this._setStatus('正在搜索蓝牙秤...'); const foundDevices = []; // 收集所有找到的目标设备 const done = () => { wx.stopBluetoothDevicesDiscovery(); this._scanning = false; if (foundDevices.length === 0) { reject(new Error('未找到蓝牙秤,请确认秤已开机并在附近')); return; } if (foundDevices.length === 1) { // 只有一台,直接连 resolve(foundDevices[0].deviceId); return; } // 多台,交给调用方弹选择框 if (this._onSelectDevice) { this._onSelectDevice(foundDevices, (deviceId) => { if (deviceId) { resolve(deviceId); } else { reject(new Error('已取消选择')); } }); } else { // 没注册回调时默认取第一台 resolve(foundDevices[0].deviceId); } }; // 5秒后结束扫描 const timer = setTimeout(done, 5000); wx.startBluetoothDevicesDiscovery({ services: [SERVICE_UUID], allowDuplicatesKey: false, success: () => { wx.onBluetoothDeviceFound((res) => { const devices = res.devices || []; for (const device of devices) { const name = (device.name || device.localName || '').trim(); if (name === DEVICE_NAME) { // 去重后加入列表 const exists = foundDevices.some(d => d.deviceId === device.deviceId); if (!exists) { foundDevices.push({ deviceId: device.deviceId, name: name, rssi: device.RSSI || 0, }); this._setStatus('已发现 ' + foundDevices.length + ' 台蓝牙秤...'); } } } }); }, fail: (err) => { clearTimeout(timer); this._scanning = false; reject(new Error('启动扫描失败: ' + err.errMsg)); } }); }); } // 连接设备 _connect(deviceId) { return new Promise((resolve, reject) => { this._setStatus('正在连接蓝牙秤...'); wx.createBLEConnection({ deviceId, timeout: 10000, success: () => { this._deviceId = deviceId; this._connected = true; resolve(); }, fail: (err) => { reject(new Error('连接失败: ' + err.errMsg)); } }); }); } // 获取服务列表,找到目标 Service _getService() { return new Promise((resolve, reject) => { // iOS 需要延迟一点再获取服务 setTimeout(() => { wx.getBLEDeviceServices({ deviceId: this._deviceId, success: (res) => { const target = res.services.find( s => normalizeUUID(s.uuid) === normalizeUUID(SERVICE_UUID) ); if (target) { this._serviceId = target.uuid; resolve(); } else { reject(new Error('未找到目标蓝牙服务')); } }, fail: (err) => reject(new Error('获取服务失败: ' + err.errMsg)) }); }, 500); }); } // 获取特征值,找到目标 Characteristic _getCharacteristic() { return new Promise((resolve, reject) => { wx.getBLEDeviceCharacteristics({ deviceId: this._deviceId, serviceId: this._serviceId, success: (res) => { const target = res.characteristics.find( c => normalizeUUID(c.uuid) === normalizeUUID(CHARACTERISTIC_UUID) ); if (target) { this._characteristicId = target.uuid; resolve(); } else { // 未精确匹配时,取第一个支持 notify 的特征值 const notifyChar = res.characteristics.find( c => c.properties && c.properties.notify ); if (notifyChar) { this._characteristicId = notifyChar.uuid; resolve(); } else { reject(new Error('未找到可订阅的特征值')); } } }, fail: (err) => reject(new Error('获取特征值失败: ' + err.errMsg)) }); }); } // 开启 notify 订阅 _subscribeNotify() { return new Promise((resolve, reject) => { this._setStatus('正在订阅数据...'); wx.notifyBLECharacteristicValueChange({ deviceId: this._deviceId, serviceId: this._serviceId, characteristicId: this._characteristicId, state: true, success: () => { // 注册数据接收回调 wx.onBLECharacteristicValueChange((res) => { if ( res.deviceId === this._deviceId && normalizeUUID(res.characteristicId) === normalizeUUID(this._characteristicId) ) { this._handleData(res.value); } }); resolve(); }, fail: (err) => reject(new Error('订阅失败: ' + err.errMsg)) }); }); } /** * 启动蓝牙秤连接 * 返回 Promise,resolve 后可通过 onWeight 回调接收数据 */ async start() { try { this._buffer = ''; this._lastWeight = null; this._stableCount = 0; await this._openAdapter(); const deviceId = await this._scanDevice(); await this._connect(deviceId); await this._getService(); await this._getCharacteristic(); await this._subscribeNotify(); this._setStatus('已连接,等待读数...'); } catch (err) { this._setStatus(err.message); this.stop(); throw err; } } /** * 断开并清理 */ stop() { // 关闭 notify if (this._connected && this._deviceId && this._serviceId && this._characteristicId) { wx.notifyBLECharacteristicValueChange({ deviceId: this._deviceId, serviceId: this._serviceId, characteristicId: this._characteristicId, state: false, fail: () => {} }); } if (this._deviceId) { wx.closeBLEConnection({ deviceId: this._deviceId, fail: () => {} }); } if (this._scanning) { wx.stopBluetoothDevicesDiscovery(); } wx.offBLECharacteristicValueChange && wx.offBLECharacteristicValueChange(); this._deviceId = null; this._serviceId = null; this._characteristicId = null; this._connected = false; this._scanning = false; this._buffer = ''; this._lastWeight = null; this._stableCount = 0; } } export default BluetoothScale;