/** * 蓝牙称重设备服务模块 * 功能: * - 自动连接已配置的称重设备 * - 主动读取重量:发送 'R' 命令,设备返回一次重量数据(readWeight方法) * - 实时监听并解析重量数据(支持ASCII/二进制协议) * - 发送控制命令(去皮、校准、单位切换等) * - 连接状态管理与事件通知 * * 使用方法: * import scaleService from '@/utils/BluetoothScaleService.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); */ import bluetoothManager from './BluetoothManager.js'; class BluetoothScaleService { constructor() { this.isConnected = false; this.deviceId = null; this.deviceName = ''; this.serviceId = null; this.notifyCharId = null; this.writeCharId = null; // 主动读取等待状态(发送读取命令后等待设备返回数据) this.pendingRead = null; // { resolve, reject, timer } this.readTimeout = 5000; // 读取超时时间(ms) // 回调函数 this.callbacks = { onWeightChange: null, onConnectionChange: null, onError: null, onLog: null }; // 当前重量数据 this.currentWeight = { value: 0, unit: 'kg', stable: false, raw: null, timestamp: null }; // 数据协议类型 this.protocolType = 'auto'; // auto, ascii, binary // 预设的称重设备协议 this.protocols = { // 通用ASCII协议(直接解析数字字符串) 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等常见电子秤) A12: { // 协议格式: 0x05 + 整数部分 + 小数部分 + 单位 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 = { READ_WEIGHT: [0x52], // 'R' - 读取重量命令(ASCII 'R') TARE: [0x1B, 0x54], // 去皮命令 CALIBRATE_1KG: [0x1B, 0x43, 0x01], // 1kg校准 CALIBRATE_2KG: [0x1B, 0x43, 0x02], // 2kg校准 CALIBRATE_5KG: [0x1B, 0x43, 0x03], // 5kg校准 UNIT_KG: [0x1B, 0x55, 0x01], // 切换kg UNIT_G: [0x1B, 0x55, 0x02], // 切换g UNIT_LB: [0x1B, 0x55, 0x03], // 切换lb ZERO: [0x1B, 0x5A], // 零位校准 RESET: [0x1B, 0x52] // 复位 }; // 初始化监听器 this._initListeners(); } /** * 初始化蓝牙事件监听 */ _initListeners() { // 连接状态变化 bluetoothManager.setConnectionChangeCallback((deviceId, connected) => { if (deviceId === this.deviceId) { this.isConnected = connected; if (!connected) { this._log('设备连接断开'); } if (this.callbacks.onConnectionChange) { this.callbacks.onConnectionChange(connected); } } }); // 数据接收 bluetoothManager.setDataReceivedCallback((deviceId, value) => { if (deviceId === this.deviceId) { this._handleDataReceived(value); } }); } /** * 处理接收到的数据 */ _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}`); // 解析自定义协议 const weight = this._parseCustom(buffer); pending.resolve({ raw: rawHex, rawAscii: rawAscii, ...weight }); 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 ? '(稳定)' : '(变化中)'}`); if (this.callbacks.onWeightChange) { this.callbacks.onWeightChange(this.currentWeight); } } } catch (error) { this._log(`数据解析错误: ${error.message}`, 'error'); } } /** * 自动检测并解析数据格式 */ _autoParse(buffer) { // 先尝试ASCII解析 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(); // 匹配常见的重量格式 // 例如: "1.23 kg", "2.500kg", "+5.00 KG", "-0.50 kg" 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 }; } // 匹配带状态位的格式 // 例如: "ST,1.23kg" 或 "US,2.50kg" 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' }; } 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) { // 继续尝试下一个协议 } } // 默认二进制解析(A12格式) return this.protocols.A12.parser(buffer); } /** * 自定义协议解析(主动读取模式使用) * * 【重要】此方法用于 readWeight() 主动读取模式,当发送 'R' 命令后 * 设备返回数据时会调用此方法解析。 * * 当前实现:先尝试自动检测(ASCII/二进制),如果都匹配不上则返回原始数据 * 如果你的设备有特殊协议格式,请取消下方注释并修改解析逻辑 * * @param {ArrayBuffer} buffer 设备返回的原始数据 * @returns {{ value: number, unit: string, stable: boolean, rawHex: string, rawAscii: string }} */ _parseCustom(buffer) { const data = new Uint8Array(buffer); const hexStr = this._bufferToHex(buffer); const asciiStr = this._bufferToString(buffer); // ============================================================ // 【方式一】先尝试通用自动解析(支持常见ASCII和二进制协议) // 大部分蓝牙秤可以直接用这个,不需要改下面的代码 // ============================================================ 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() }; } // ============================================================ // 【方式二】如果你的设备有特殊协议格式,在这里添加解析逻辑 // 取消注释并根据实际数据格式修改 // ============================================================ // --- 示例1: 十六进制帧格式 --- // 假设设备返回 6 字节: [帧头0xAA][命令0x52][数据高位][数据低位][小数位][校验] // 例: AA 52 00 7B 02 D1 → 重量 = 0x007B / 10^2 = 1.23 kg // // if (data.length >= 6 && data[0] === 0xAA && data[1] === 0x52) { // const weightRaw = (data[2] << 8) | data[3]; // const decimal = data[4]; // 小数位数 // const value = weightRaw / Math.pow(10, decimal); // this._log(`[自定义协议] 十六进制解析成功: ${value}`); // return { value, unit: 'kg', stable: true, rawHex: hexStr, rawAscii: asciiStr.trim() }; // } // --- 示例2: ASCII带前缀格式 --- // 假设设备返回: "W:1.23kg\r\n" 或 "+00123g\r\n" // // const match = asciiStr.trim().match(/W:([+-]?\d+\.?\d*)\s*(kg|g|lb|oz)?/i); // if (match) { // this._log(`[自定义协议] ASCII解析成功: ${match[1]}`); // return { value: parseFloat(match[1]), unit: (match[2] || 'kg').toLowerCase(), stable: true, rawHex: hexStr, rawAscii: asciiStr.trim() }; // } // ============================================================ // 以上都没有匹配到 → 返回原始数据(value 为 null) // 页面拿到后可以显示原始数据方便调试 // ============================================================ 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 }; } /** * 从存储加载配置 */ loadConfig() { const scaleConfig = uni.getStorageSync('scaleConfig'); 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; this._log('配置已加载'); } return this.getConfig(); } /** * 自动连接已配置的设备 */ async autoConnect() { this.loadConfig(); if (!this.deviceId || !this.serviceId) { throw new Error('未配置称重设备,请先在设置中配置'); } return this.connect(); } /** * 连接指定设备 */ async connect() { if (!this.deviceId) { throw new Error('缺少设备ID'); } try { this._log('正在初始化蓝牙...'); await bluetoothManager.init(); this._log(`正在连接设备: ${this.deviceName || this.deviceId}`); const result = await bluetoothManager.connectKnownDevice( this.deviceId, this.serviceId, this.writeCharId, this.notifyCharId ); // 更新配置 this.serviceId = result.serviceId; this.notifyCharId = result.notifyCharId; this.writeCharId = result.writeCharId; this.isConnected = true; this._log('连接成功'); // 通知连接状态 if (this.callbacks.onConnectionChange) { this.callbacks.onConnectionChange(true); } return true; } catch (error) { this._log(`连接失败: ${error.message}`, 'error'); this.isConnected = false; if (this.callbacks.onError) { this.callbacks.onError(error); } throw error; } } /** * 断开连接 */ async disconnect() { if (this.deviceId) { await bluetoothManager.disconnectDevice(this.deviceId); } this.isConnected = false; this._log('已断开连接'); if (this.callbacks.onConnectionChange) { this.callbacks.onConnectionChange(false); } } /** * 发送命令 */ async sendCommand(command) { if (!this.isConnected) { throw new Error('设备未连接'); } let cmdData; if (Array.isArray(command)) { cmdData = new Uint8Array(command); } else if (this.commands[command]) { cmdData = new Uint8Array(this.commands[command]); } else { throw new Error('未知的命令'); } this._log(`发送命令: ${Array.from(cmdData).map(b => b.toString(16).padStart(2, '0')).join(' ')}`); try { await bluetoothManager.writeData(this.deviceId, cmdData.buffer); return true; } catch (error) { this._log(`命令发送失败: ${error.message}`, 'error'); throw error; } } /** * 主动读取重量 * 向设备发送 'R' 命令,等待设备返回重量数据 * 一次发送一次返回,不会持续读取 * * @param {number} [timeout=5000] 等待设备响应超时时间(ms) * @returns {Promise<{raw: string, rawAscii: string, value: number|null, unit: string, stable: boolean}>} * * 返回值说明: * - raw: 设备返回的十六进制字符串,例如 "aa 52 00 7b 02 d1" * - rawAscii: 设备返回的ASCII字符串,例如 "1.23kg" * - value: 解析后的重量数值(需在 _parseCustom 中实现解析逻辑,默认为 null) * - unit: 单位(默认 'kg') * - stable: 是否稳定(默认 false) * * 使用示例: * const result = await scaleService.readWeight(); * console.log(result.raw); // "aa 52 00 7b 02 d1" * console.log(result.rawAscii); // "1.23kg" * console.log(result.value); // null(需实现 _parseCustom 后才有值) */ 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 { // 发送读取命令 'R' 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) { this.callbacks.onWeightChange = callback; } /** * 取消重量变化回调 */ offWeightChange() { this.callbacks.onWeightChange = null; } /** * 注册连接状态回调 */ onConnectionChange(callback) { this.callbacks.onConnectionChange = callback; } /** * 取消连接状态回调 */ offConnectionChange() { this.callbacks.onConnectionChange = null; } /** * 注册错误回调 */ onError(callback) { this.callbacks.onError = callback; } /** * 取消错误回调 */ offError() { this.callbacks.onError = null; } /** * 记录日志 */ _log(message, level = 'info') { console[level === 'error' ? 'error' : 'log'](`[称重服务] ${message}`); if (this.callbacks.onLog) { this.callbacks.onLog(message, level); } } } export default new BluetoothScaleService();