/** * 蓝牙称重设备服务模块(新版:支持智能数据发送与健壮连接管理) * 功能: * - 自动连接已配置的称重设备 * - 主动读取重量:发送 'R' 命令,设备返回一次重量数据(readWeight方法) * - 实时监听并解析重量数据(支持ASCII/二进制协议) * - 发送控制命令(去皮、校准、单位切换等) * - 连接状态管理与事件通知 * - 支持从 Vuex 或 localStorage 加载配置 * * 使用方法: * import scaleService from '@/utils/BluetoothScaleService.new.js'; * await scaleService.autoConnect(); * * // 主动读取重量(发送一次命令,返回一次数据) * const result = await scaleService.readWeight(); * console.log(result.raw); // 十六进制原始数据 * console.log(result.rawAscii); // ASCII原始数据 * console.log(result.value); // 解析后的重量值 * * // 监听重量变化(设备主动推送模式) * scaleService.onWeightChange(this.handleWeightChange); */ let store = null; try { store = require('@/store/index.js').default; } catch (e) { console.warn('[称重服务] Vuex store 加载失败,将使用 localStorage'); } class BluetoothScaleService { constructor() { this.adapterOpen = false; this.isConnected = false; this.connecting = false; this.deviceId = null; this.deviceName = ''; this.serviceId = null; this.notifyCharId = null; this.writeCharId = null; this.pendingRead = null; this.readTimeout = 5000; this.connectionChangeCallbacks = []; this.weightChangeCallbacks = []; this.errorCallbacks = []; this.logCallbacks = []; this._connectionListener = null; this._dataListener = null; this._writeType = null; this._mtu = 20; this.currentWeight = { value: 0, unit: 'kg', stable: false, raw: null, timestamp: null }; this.protocolType = 'auto'; this.protocols = { ascii: { pattern: /^([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?$/, parser: (str) => { const match = str.match(/([+-]?\d+\.?\d*)/); const unitMatch = str.match(/(kg|g|lb|oz)/i); return { value: match ? parseFloat(match[1]) : null, unit: unitMatch ? unitMatch[1].toLowerCase() : 'kg' }; } }, A12: { parser: (buffer) => { if (!buffer || buffer.length < 5) return null; const data = new Uint8Array(buffer); const sign = (data[0] & 0x80) ? -1 : 1; const stable = (data[0] & 0x40) !== 0; const decimal = (data[0] & 0x30) >> 4; const weight = ((data[1] << 8) | data[2]) & 0x3FFF; const value = sign * weight / Math.pow(10, decimal); const units = ['kg', 'g', 'lb', 'oz']; const unitIndex = data[3] & 0x03; return { value: value, unit: units[unitIndex] || 'kg', stable: stable }; } }, Sartorius: { parser: (buffer) => { if (!buffer || buffer.length < 6) return null; const data = new Uint8Array(buffer); const value = ((data[2] << 16) | (data[3] << 8) | data[4]) / 1000; return { value: value, unit: 'kg', stable: data[1] === 0x01 }; } }, Mettler: { parser: (buffer) => { if (!buffer || buffer.length < 8) return null; const data = new Uint8Array(buffer); const value = parseFloat(String.fromCharCode.apply(null, data.slice(1, 7))); return { value: value, unit: 'kg', stable: data[7] === 0x0D }; } } }; this.commands = { // 读取重量命令:0x52 = ASCII 'R' READ_WEIGHT: [0x52], TARE: [0x1B, 0x54], CALIBRATE_1KG: [0x1B, 0x43, 0x01], CALIBRATE_2KG: [0x1B, 0x43, 0x02], CALIBRATE_5KG: [0x1B, 0x43, 0x03], UNIT_KG: [0x1B, 0x55, 0x01], UNIT_G: [0x1B, 0x55, 0x02], UNIT_LB: [0x1B, 0x55, 0x03], ZERO: [0x1B, 0x5A], RESET: [0x1B, 0x52] }; } /** * 初始化蓝牙适配器 */ async init() { if (this.adapterOpen) return; return new Promise((resolve, reject) => { uni.openBluetoothAdapter({ success: () => { this.adapterOpen = true; this._registerListeners(); resolve(); }, fail: (err) => { console.error('[称重服务] 蓝牙初始化失败', err); reject(err); } }); }); } /** * 注册蓝牙事件监听 */ _registerListeners() { if (this._connectionListener) return; this._connectionListener = (res) => { if (!res.connected && this.deviceId === res.deviceId) { this.isConnected = false; this._log('设备连接断开'); this._notifyConnectionChange(false); } }; this._dataListener = (res) => { if (res.deviceId === this.deviceId) { this._handleDataReceived(res.value); } }; uni.onBLEConnectionStateChange(this._connectionListener); uni.onBLECharacteristicValueChange(this._dataListener); } /** * 销毁,清理资源 */ destroy() { if (this._connectionListener) { try { uni.offBLEConnectionStateChange(this._connectionListener); } catch (e) { console.warn('[称重服务] 移除连接监听失败', e); } this._connectionListener = null; } if (this._dataListener) { try { uni.offBLECharacteristicValueChange(this._dataListener); } catch (e) { console.warn('[称重服务] 移除数据监听失败', e); } this._dataListener = null; } if (this.pendingRead) { clearTimeout(this.pendingRead.timer); this.pendingRead.reject(new Error('服务已销毁')); this.pendingRead = null; } this.disconnect(); } /** * 处理接收到的数据 */ _handleDataReceived(buffer) { try { if (this.pendingRead) { const pending = this.pendingRead; this.pendingRead = null; clearTimeout(pending.timer); const rawHex = this._bufferToHex(buffer); const rawAscii = this._bufferToString(buffer); this._log(`读取响应 - HEX: ${rawHex}, ASCII: ${rawAscii}`); try { const weight = this._parseCustom(buffer); pending.resolve({ raw: rawHex, rawAscii: rawAscii, ...weight }); } catch (parseError) { this._log(`解析响应失败: ${parseError.message}`, 'error'); pending.resolve({ raw: rawHex, rawAscii: rawAscii, value: null, unit: 'kg', stable: false, rawHex: rawHex, rawAscii: rawAscii.trim() }); } return; } let weight = null; if (this.protocolType === 'auto') { weight = this._autoParse(buffer); } else if (this.protocolType === 'ascii') { weight = this._parseAscii(buffer); } else { weight = this._parseBinary(buffer); } if (weight) { this.currentWeight = { ...weight, raw: this._bufferToHex(buffer), timestamp: new Date().toISOString() }; this._log(`重量: ${weight.value} ${weight.unit} ${weight.stable ? '(稳定)' : '(变化中)'}`); this._notifyWeightChange(this.currentWeight); } } catch (error) { this._log(`数据解析错误: ${error.message}`, 'error'); this._notifyError(error); } } /** * 自动检测并解析数据格式 */ _autoParse(buffer) { const asciiResult = this._parseAscii(buffer); if (asciiResult) { return asciiResult; } return this._parseBinary(buffer); } /** * 解析ASCII格式数据 */ _parseAscii(buffer) { try { const text = this._bufferToString(buffer); const trimmed = text.trim(); const match = trimmed.match(/^([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?$/i); if (match) { return { value: parseFloat(match[1]), unit: match[2] ? match[2].toLowerCase() : 'kg', stable: true }; } const statusMatch = trimmed.match(/^(ST|US|OL),?([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?$/i); if (statusMatch) { return { value: parseFloat(statusMatch[2]), unit: statusMatch[3] ? statusMatch[3].toLowerCase() : 'kg', stable: statusMatch[1].toUpperCase() === 'ST' }; } // 自定义格式: wn0000.00kg (wn前缀 + 数字 + 单位) const customMatch = trimmed.match(/^[a-zA-Z]*([+-]?\d+\.?\d*)(kg|g|lb|oz)$/i); if (customMatch) { return { value: parseFloat(customMatch[1]), unit: customMatch[2].toLowerCase(), stable: true }; } return null; } catch (error) { return null; } } /** * 解析二进制格式数据 */ _parseBinary(buffer) { for (const [name, protocol] of Object.entries(this.protocols)) { if (name === 'ascii') continue; try { const result = protocol.parser(buffer); if (result && typeof result.value === 'number' && !isNaN(result.value)) { return result; } } catch (e) { } } return this.protocols.A12.parser(buffer); } /** * 自定义协议解析(主动读取模式使用) */ _parseCustom(buffer) { const data = new Uint8Array(buffer); const hexStr = this._bufferToHex(buffer); const asciiStr = this._bufferToString(buffer); const autoResult = this._autoParse(buffer); if (autoResult && autoResult.value !== null && !isNaN(autoResult.value)) { this._log(`[自定义协议] 自动解析成功: ${autoResult.value} ${autoResult.unit}`); return { ...autoResult, rawHex: hexStr, rawAscii: asciiStr.trim() }; } this._log(`[自定义协议] 未识别的格式 - HEX: ${hexStr}, ASCII: ${asciiStr.trim()}`); return { value: null, unit: 'kg', stable: false, rawHex: hexStr, rawAscii: asciiStr.trim() }; } /** * Buffer转16进制字符串 */ _bufferToHex(buffer) { const data = new Uint8Array(buffer); return Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' '); } /** * Buffer转字符串 */ _bufferToString(buffer) { const data = new Uint8Array(buffer); return String.fromCharCode.apply(null, data); } /** * 设置协议类型 */ setProtocol(protocol) { if (protocol === 'auto' || protocol === 'ascii' || this.protocols[protocol]) { this.protocolType = protocol; this._log(`协议已切换: ${protocol}`); } } /** * 获取当前配置 */ getConfig() { return { deviceId: this.deviceId, deviceName: this.deviceName, serviceId: this.serviceId, notifyCharId: this.notifyCharId, writeCharId: this.writeCharId, protocol: this.protocolType, isConnected: this.isConnected }; } /** * 从存储加载配置(优先从 Vuex,其次从 localStorage) */ loadConfig() { let scaleConfig = {}; if (store) { try { const state = store.state.scale; if (state && state.scaleConfig) { scaleConfig = state.scaleConfig; this._log('配置从 Vuex 加载'); } } catch (e) { this._log(`Vuex 加载配置失败: ${e.message}`, 'error'); } } if (!scaleConfig.deviceId) { const storageConfig = uni.getStorageSync('scaleConfig'); if (storageConfig) { scaleConfig = storageConfig; this._log('配置从 localStorage 加载'); } } if (scaleConfig) { this.deviceId = scaleConfig.deviceId || null; this.deviceName = scaleConfig.deviceName || ''; this.serviceId = scaleConfig.serviceId || null; this.notifyCharId = scaleConfig.characteristicId || null; this.writeCharId = scaleConfig.writeCharacteristicId || null; } return this.getConfig(); } /** * 自动连接已配置的设备 */ async autoConnect() { if (this.connecting || this.isConnected) { return this.isConnected; } this.loadConfig(); // 调试日志:显示加载的配置 this._log(`配置信息: deviceId=${this.deviceId}, serviceId=${this.serviceId}, notifyCharId=${this.notifyCharId}, writeCharId=${this.writeCharId}`); if (!this.deviceId || !this.serviceId) { throw new Error('未配置称重设备,请先在设置中配置'); } return this.connect(); } /** * 获取已连接的蓝牙设备列表 */ _getConnectedDevices() { return new Promise((resolve) => { uni.getConnectedBluetoothDevices({ success: (res) => resolve(res.devices || []), fail: () => resolve([]) }); }); } /** * 连接指定设备 */ async connect() { if (this.connecting || this.isConnected) { return this.isConnected; } if (!this.deviceId) { throw new Error('缺少设备ID'); } this.connecting = true; try { await this.init(); // 检查设备是否已经连接(避免重复连接报错) const connectedDevices = await this._getConnectedDevices(); const isAlreadyConnected = connectedDevices.some(d => d.deviceId === this.deviceId); if (isAlreadyConnected) { this._log('设备已连接,跳过创建连接'); } else { this._log(`正在连接设备: ${this.deviceName || this.deviceId}`); await new Promise((resolve, reject) => { uni.createBLEConnection({ deviceId: this.deviceId, timeout: 15000, success: resolve, fail: reject }); }); } try { const mtuRes = await new Promise((res) => { uni.setBLEMTU({ deviceId: this.deviceId, mtu: 512, success: res, fail: () => res({ mtu: 23 }) }); }); this._mtu = (mtuRes.mtu || 512) - 3; this._log(`MTU 协商: 请求 512, 实际=${mtuRes.mtu}, 写入块=${this._mtu}`); } catch (e) { this._log(`MTU 协商异常, 使用默认值 20: ${e.message}`, 'error'); this._mtu = 20; } await this._delay(800); if (this.notifyCharId) { try { await new Promise((resolve, reject) => { uni.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.notifyCharId, state: true, success: resolve, fail: reject }); }); this._log(`通知已启用: ${this.notifyCharId}`); } catch (e) { this._log(`启用通知失败: ${e.message}`, 'error'); } } else { this._log(`未配置通知特征值,无法接收数据`, 'warn'); } this.isConnected = true; this.connecting = false; this._log('连接成功'); this._notifyConnectionChange(true); return true; } catch (error) { this._log(`连接失败: ${error.message}`, 'error'); this.isConnected = false; this.connecting = false; this._notifyError(error); this._notifyConnectionChange(false); throw error; } } /** * 断开连接 */ async disconnect() { if (this.pendingRead) { clearTimeout(this.pendingRead.timer); this.pendingRead.reject(new Error('连接已断开')); this.pendingRead = null; } if (this.deviceId) { try { await new Promise((resolve, reject) => { uni.closeBLEConnection({ deviceId: this.deviceId, success: resolve, fail: reject }); }); } catch (e) { this._log(`断开连接失败: ${e.message}`, 'error'); } } this.isConnected = false; this._log('已断开连接'); this._notifyConnectionChange(false); } /** * 发送命令 */ async sendCommand(command) { if (!this.isConnected) { throw new Error('设备未连接'); } if (!this.writeCharId) { this._log(`未配置写入特征值,无法发送命令`, 'error'); throw new Error('未配置写入特征值'); } let cmdData; if (Array.isArray(command)) { cmdData = new Uint8Array(command); } else if (this.commands[command]) { // 从命令表获取(如 'READ_WEIGHT' → [0x52]) cmdData = new Uint8Array(this.commands[command]); } else { throw new Error('未知的命令: ' + command); } // 显示 HEX 和 ASCII const hexStr = Array.from(cmdData).map(b => b.toString(16).padStart(2, '0')).join(' '); const asciiStr = String.fromCharCode(...cmdData).replace(/[^\x20-\x7E]/g, '?'); this._log(`发送命令: "${asciiStr}" (HEX: ${hexStr})`); try { await this._writeData(cmdData.buffer); return true; } catch (error) { this._log(`命令发送失败: ${error.message}`, 'error'); throw error; } } /** * 写入数据到蓝牙设备(自动适配 MTU 和 writeType) */ async _writeData(buffer, attempt = 0) { const dataLen = buffer.byteLength; const mtu = this._mtu || 20; if (dataLen > mtu) { this._log(`数据 ${dataLen}B > MTU ${mtu}B, 自动分片发送`); return this._writeDataInChunks(buffer, mtu); } const writeTypes = [null, 'writeNoResponse', 'write']; const maxAttempts = 3; if (attempt >= maxAttempts) { this._log(`写入重试 ${maxAttempts} 次仍失败,视为尾包/打印机忙导致`, 'error'); return { success: true, silent: true }; } const writeType = writeTypes[attempt] || null; const options = { deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeCharId, value: buffer }; if (writeType) { options.writeType = writeType; } return new Promise((resolve, reject) => { uni.writeBLECharacteristicValue({ ...options, success: (res) => { this._writeType = writeType; this._log(`写入成功, writeType=${writeType || 'auto'}`); resolve(res); }, fail: (err) => { this._log(`写入失败 attempt=${attempt + 1}, errCode=${err.errCode}, msg=${err.errMsg}`, 'error'); const delay = 100 + attempt * 150; setTimeout(() => { this._writeData(buffer, attempt + 1).then(resolve).catch(reject); }, delay); } }); }); } /** * 分片发送数据 */ async _writeDataInChunks(buffer, mtu = null) { const totalLength = buffer.byteLength; mtu = mtu || this._mtu || 20; const dataView = new Uint8Array(buffer); for (let offset = 0; offset < totalLength; offset += mtu) { const end = Math.min(offset + mtu, totalLength); const chunk = dataView.slice(offset, end); const chunkBuffer = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength); const isLastChunk = end >= totalLength; try { await this._writeChunk(chunkBuffer); if (offset + mtu < totalLength) { await this._delay(50); } } catch (err) { if (isLastChunk) { const retryDelays = [300, 600, 1000]; let delivered = false; for (let attempt = 0; attempt < retryDelays.length; attempt++) { const wait = retryDelays[attempt]; this._log(`尾包发送失败,第 ${attempt + 1}/${retryDelays.length} 次重试,等待 ${wait}ms...`, 'error'); await this._delay(wait); try { await this._writeChunkWithResponse(chunkBuffer); this._log(`尾包重试成功`); delivered = true; break; } catch (retryErr) { this._log(`尾包第 ${attempt + 1} 次重试仍失败: ${retryErr.message}`, 'error'); } } if (delivered) { continue; } this._log(`尾包 3 次重试均失败,数据可能不完整`, 'error'); return; } this._log(`分片发送失败 at offset ${offset}: ${err.message}`, 'error'); throw err; } } } /** * 发送单个数据包(分片用) */ _writeChunk(chunkBuffer, retryWithWrite = false, attempt = 0) { return new Promise((resolve, reject) => { uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeCharId, value: chunkBuffer, writeType: retryWithWrite ? 'write' : 'writeNoResponse', success: resolve, fail: async (err) => { if (!retryWithWrite && err.errCode === 10007) { this._log('writeNoResponse 不支持,降级为 write 模式', 'error'); try { await this._delay(50); await this._writeChunk(chunkBuffer, true, 0); resolve(); } catch (downgradeErr) { reject(downgradeErr); } return; } if (err.errCode === 10007 && attempt < 3) { const wait = 100 * (attempt + 1); this._log(`设备忙(10007),${wait}ms 后第 ${attempt + 1}/3 次重试...`, 'error'); try { await this._delay(wait); await this._writeChunk(chunkBuffer, retryWithWrite, attempt + 1); resolve(); } catch (retryErr) { reject(retryErr); } return; } reject(err); } }); }); } /** * 发送单个数据包 —— 强制使用 'write' 模式(带响应) */ _writeChunkWithResponse(chunkBuffer) { return new Promise((resolve, reject) => { uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeCharId, value: chunkBuffer, writeType: 'write', success: resolve, fail: reject }); }); } /** * 主动读取重量 */ async readWeight(timeout) { if (!this.isConnected) { throw new Error('设备未连接'); } if (this.pendingRead) { throw new Error('已有读取请求等待中,请勿重复发送'); } const actualTimeout = timeout || this.readTimeout; return new Promise(async (resolve, reject) => { const timer = setTimeout(() => { this.pendingRead = null; reject(new Error(`读取重量超时(${actualTimeout}ms),设备未返回数据`)); }, actualTimeout); this.pendingRead = { resolve, reject, timer }; try { await this.sendCommand('READ_WEIGHT'); this._log('已发送读取重量命令,等待设备响应...'); } catch (error) { clearTimeout(timer); this.pendingRead = null; reject(error); } }); } /** * 去皮(归零) */ async tare() { return this.sendCommand('TARE'); } /** * 砝码校准 * @param {number} weight 校准重量(默认1kg) */ async calibrate(weight = 1) { let command; if (weight <= 1) { command = 'CALIBRATE_1KG'; } else if (weight <= 2) { command = 'CALIBRATE_2KG'; } else { command = 'CALIBRATE_5KG'; } return this.sendCommand(command); } /** * 切换单位 * @param {string} unit kg, g, lb */ async changeUnit(unit) { const unitMap = { 'kg': 'UNIT_KG', 'g': 'UNIT_G', 'lb': 'UNIT_LB' }; const command = unitMap[unit]; if (!command) { throw new Error('不支持的单位'); } return this.sendCommand(command); } /** * 获取当前重量 */ getCurrentWeight() { return { ...this.currentWeight }; } /** * 获取实时重量(带稳定性等待) */ async getStableWeight(timeout = 10000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const checkWeight = () => { if (this.currentWeight.stable) { resolve(this.currentWeight); } else if (Date.now() - startTime > timeout) { reject(new Error('等待重量稳定超时')); } else { setTimeout(checkWeight, 100); } }; checkWeight(); }); } /** * 发送自定义命令 */ async sendRawCommand(commandArray) { return this.sendCommand(commandArray); } /** * 注册重量变化回调 */ onWeightChange(callback) { if (!this.weightChangeCallbacks.includes(callback)) { this.weightChangeCallbacks.push(callback); } } /** * 取消重量变化回调 */ offWeightChange(callback) { const index = this.weightChangeCallbacks.indexOf(callback); if (index > -1) { this.weightChangeCallbacks.splice(index, 1); } } /** * 注册连接状态回调 */ onConnectionChange(callback) { if (!this.connectionChangeCallbacks.includes(callback)) { this.connectionChangeCallbacks.push(callback); } } /** * 取消连接状态回调 */ offConnectionChange(callback) { const index = this.connectionChangeCallbacks.indexOf(callback); if (index > -1) { this.connectionChangeCallbacks.splice(index, 1); } } /** * 注册错误回调 */ onError(callback) { if (!this.errorCallbacks.includes(callback)) { this.errorCallbacks.push(callback); } } /** * 取消错误回调 */ offError(callback) { const index = this.errorCallbacks.indexOf(callback); if (index > -1) { this.errorCallbacks.splice(index, 1); } } /** * 注册日志回调 */ onLog(callback) { if (!this.logCallbacks.includes(callback)) { this.logCallbacks.push(callback); } } /** * 取消日志回调 */ offLog(callback) { const index = this.logCallbacks.indexOf(callback); if (index > -1) { this.logCallbacks.splice(index, 1); } } /** * 通知重量变化 */ _notifyWeightChange(weight) { this.weightChangeCallbacks.forEach(cb => { try { cb(weight); } catch (e) { this._log(`重量变化回调执行失败: ${e.message}`, 'error'); } }); } /** * 通知连接状态变化 */ _notifyConnectionChange(connected) { this.connectionChangeCallbacks.forEach(cb => { try { cb(connected); } catch (e) { this._log(`连接状态回调执行失败: ${e.message}`, 'error'); } }); } /** * 通知错误 */ _notifyError(error) { this.errorCallbacks.forEach(cb => { try { cb(error); } catch (e) { this._log(`错误回调执行失败: ${e.message}`, 'error'); } }); } /** * 记录日志 */ _log(message, level = 'info') { console[level === 'error' ? 'error' : 'log'](`[称重服务] ${message}`); this.logCallbacks.forEach(cb => { try { cb(message, level); } catch (e) { } }); } /** * 延迟 */ _delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } export default new BluetoothScaleService();