/** * 蓝牙打印服务类 * 功能: * - 统一管理蓝牙打印机连接 * - 支持 CPCL 和 TSPL 两种指令格式 * - 自动与 Vuex store 同步配置 * - 提供指令生成模板 * * 使用方法: * import printerService from '@/utils/BluetoothPrinterService.js'; * * // 初始化并连接 * await printerService.init(); * await printerService.autoConnect(); * * // 打印操作 * await printerService.printText('Hello World'); * await printerService.printWasteLabel(wasteData); */ import * as iconv from 'iconv-lite'; // 获取 Vuex store 实例(延迟加载避免循环依赖) let store = null; const getStore = () => { if (!store) { store = require('@/store').default; } return store; }; class BluetoothPrinterService { constructor() { // 蓝牙状态 this.adapterOpen = false; this.connected = false; this.connecting = false; this.deviceId = ''; this.deviceName = ''; this.serviceId = ''; this.writeCharId = ''; // 打印配置 this.commandType = 'CPCL'; // 'CPCL' | 'TSPL' this.paperWidth = 100; this.paperHeight = 100; // 回调函数 this.connectionChangeCallbacks = []; // 连接状态监听器 this._connectionListener = null; } // ==================== 初始化 ==================== /** * 初始化蓝牙适配器 * @returns {Promise} */ async init() { if (this.adapterOpen) return; return new Promise((resolve, reject) => { uni.openBluetoothAdapter({ success: () => { this.adapterOpen = true; this._registerConnectionListener(); resolve(); }, fail: (err) => { console.error('蓝牙初始化失败', err); uni.showToast({ title: '蓝牙初始化失败,请检查蓝牙是否开启', icon: 'none' }); reject(err); } }); }); } /** * 注册连接状态变化监听 */ _registerConnectionListener() { if (this._connectionListener) return; this._connectionListener = (res) => { if (!res.connected && this.deviceId === res.deviceId) { // 只更新连接状态,保留配置信息以便下次自动连接 this.connected = false; this._notifyConnectionChange(false); // 注意:不再调用 _updateStoreConnection,不再清空 store 中的配置 } }; uni.onBLEConnectionStateChange(this._connectionListener); } /** * 销毁,清理资源 */ destroy() { if (this._connectionListener) { // uni.offBLEConnectionStateChange 无法精确移除单个监听 // 业务层需要在页面卸载时主动调用 disconnect } this.disconnect(); } // ==================== 连接管理 ==================== /** * 从 store 加载配置 */ _loadConfig() { try { const state = getStore().state; if (state.print) { const config = state.print.printerConfig; if (config) { this.deviceId = config.deviceId || ''; this.deviceName = config.deviceName || ''; this.serviceId = config.serviceId || ''; this.writeCharId = config.writeCharId || ''; this.paperWidth = config.paperWidth || 100; this.paperHeight = config.paperHeight || 100; return config; } } } catch (e) { console.warn('加载打印配置失败', e); } return null; } /** * 保存配置到 store */ _saveConfig(config) { try { getStore().dispatch('print/savePrinterConfig', config); } catch (e) { console.warn('保存打印配置失败', e); } } /** * 更新 store 连接状态 */ _updateStoreConnection(connected, device = {}) { try { if (connected) { getStore().dispatch('print/connectPrinter', { deviceId: this.deviceId, name: this.deviceName, ...device }); } else { getStore().dispatch('print/disconnectPrinter'); } } catch (e) { console.warn('更新连接状态失败', e); } } /** * 自动连接已保存的打印机 * @returns {Promise} */ async autoConnect() { if (this.connecting || this.connected) { return this.connected; } const config = this._loadConfig(); if (!config || !config.deviceId) { console.warn('无可用打印机配置'); return false; } this.connecting = true; this._notifyConnectionChange(false, 'connecting'); try { await this.init(); // 传递已保存的 serviceId 和 writeCharId await this._connect(config.deviceId, config.serviceId, config.writeCharId); this.connected = true; this.connecting = false; this._notifyConnectionChange(true); uni.showToast({ title: '打印机连接成功', icon: 'success' }); return true; } catch (err) { this.connected = false; this.connecting = false; this._notifyConnectionChange(false, 'failed'); console.error('自动连接失败', err); uni.showToast({ title: '打印机连接失败', icon: 'none' }); return false; } } /** * 连接指定设备 * @param {string} deviceId - 设备ID * @param {string} savedServiceId - 已保存的服务ID(可选) * @param {string} savedWriteCharId - 已保存的特征值ID(可选) * @returns {Promise} */ async _connect(deviceId, savedServiceId = '', savedWriteCharId = '') { return new Promise(async (resolve, reject) => { // 首先尝试使用已保存的服务配置 if (savedServiceId && savedWriteCharId) { try { console.log('尝试使用已保存的服务配置:', { savedServiceId, savedWriteCharId }); // 先建立连接 await new Promise((res, rej) => { uni.createBLEConnection({ deviceId, timeout: 15000, success: res, fail: rej }); }); // 等待服务发现 await this._delay(800); // 验证保存的服务是否存在 const { services } = await this._getServices(deviceId); const targetService = services.find(s => s.uuid === savedServiceId); if (!targetService) { console.warn('保存的服务不存在,尝试自动发现'); await this._disconnectDevice(deviceId); throw new Error('SAVED_SERVICE_NOT_FOUND'); } // 验证特征值是否存在且可写 const { characteristics } = await this._getCharacteristics(deviceId, savedServiceId); const targetChar = characteristics.find(c => c.uuid === savedWriteCharId && (c.properties.write || c.properties.writeNoResponse) ); if (!targetChar) { console.warn('保存的特征值不可用,尝试自动发现'); await this._disconnectDevice(deviceId); throw new Error('SAVED_CHAR_NOT_FOUND'); } // 使用保存的配置成功 console.log('使用已保存配置连接成功'); this.deviceId = deviceId; this.serviceId = savedServiceId; this.writeCharId = savedWriteCharId; // 更新 store this._saveConfig({ deviceId, serviceId: savedServiceId, writeCharId: savedWriteCharId }); resolve(); return; } catch (err) { // 如果不是"保存的配置无效"错误,则直接拒绝 if (err.message !== 'SAVED_SERVICE_NOT_FOUND' && err.message !== 'SAVED_CHAR_NOT_FOUND') { await this._disconnectDevice(deviceId); reject(err); return; } // 否则继续自动发现 console.log('回退到自动发现服务...'); } } // 自动发现服务逻辑(保底方案) uni.createBLEConnection({ deviceId, timeout: 15000, success: async () => { // 等待服务发现 await this._delay(800); // 获取服务列表 const { services } = await this._getServices(deviceId); if (!services || services.length === 0) { reject(new Error('未找到蓝牙服务')); return; } // 查找第一个有可写特征值的服务 let foundService = null; let writeChar = null; for (const service of services) { try { const { characteristics } = await this._getCharacteristics(deviceId, service.uuid); const writableChar = characteristics.find(c => c.properties.write || c.properties.writeNoResponse ); if (writableChar) { foundService = service; writeChar = writableChar; break; } } catch (e) { // 跳过无效服务 continue; } } if (!writeChar) { reject(new Error('未找到可写入的特征值')); return; } this.deviceId = deviceId; this.serviceId = foundService.uuid; this.writeCharId = writeChar.uuid; // 保存配置 this._saveConfig({ deviceId, serviceId: foundService.uuid, writeCharId: writeChar.uuid }); resolve(); }, fail: reject }); }); } /** * 断开设备连接(内部方法) */ async _disconnectDevice(deviceId) { try { await new Promise((resolve, reject) => { uni.closeBLEConnection({ deviceId, success: resolve, fail: reject }); }); } catch (e) { console.warn('断开设备失败', e); } } /** * 断开连接 * 注意:只做物理断开,保留配置信息以便下次自动连接 */ async disconnect() { if (!this.deviceId) return; try { await new Promise((resolve, reject) => { uni.closeBLEConnection({ deviceId: this.deviceId, success: resolve, fail: reject }); }); } catch (e) { console.warn('断开连接失败', e); } // 只更新连接状态,保留 deviceId、deviceName、serviceId、writeCharId 等配置 this.connected = false; this._notifyConnectionChange(false); // 注意:不再清空配置,不再调用 _updateStoreConnection } /** * 检查是否已连接 */ isConnected() { return this.connected; } // ==================== 打印操作 ==================== /** * 打印文本内容 * @param {string} content - 打印内容(支持 CPCL/TSPL 指令) * @returns {Promise} */ async printText(content) { if (!this.connected) { uni.showToast({ title: '打印机未连接,请先连接打印机', icon: 'none' }); throw new Error('打印机未连接'); } const buffer = this._stringToBuffer(content); await this._writeData(buffer); return true; } /** * 打印废物标签 * @param {Object} data - 废物数据 * @returns {Promise} */ async printWasteLabel(data) { const command = this.generateWasteLabel(data); return this.printText(command); } /** * 打印二维码 * @param {string} content - 二维码内容 * @returns {Promise} */ async printQRCode(content) { const command = this.generateQRCodeTemplate(content); return this.printText(command); } /** * 打印条码 * @param {string} content - 条码内容 * @param {string} type - 条码类型,默认 128 * @returns {Promise} */ async printBarcode(content, type = '128') { const command = this.generateBarcodeTemplate(content, type); return this.printText(command); } // ==================== 指令生成 ==================== /** * 获取纸张尺寸命令 */ getSizeCommand() { if (this.commandType === 'CPCL') { return `SIZE ${this.paperWidth}mm ${this.paperHeight}mm`; } else { return `SIZE ${this.paperWidth} mm ${this.paperHeight} mm`; } } /** * 生成废物标签指令 * @param {Object} data - 废物数据 */ generateWasteLabel(data) { const sizeCmd = this.getSizeCommand(); const gapCmd = this.commandType === 'CPCL' ? 'GAP 2mm' : 'GAP 2 mm'; if (this.commandType === 'CPCL') { return this._generateCPCLWasteLabel(data, sizeCmd, gapCmd); } else { return this._generateTSPLWasteLabel(data, sizeCmd, gapCmd); } } /** * 生成 CPCL 废物标签 */ _generateCPCLWasteLabel(data, sizeCmd, gapCmd) { const fieldOrder = [ 'mingcheng', 'leibie', 'daima', 'xingtai', 'zhuyaochengfen', 'youhaichengfen', 'zhuyi', 'shibiema', 'danwei', 'lianxi', 'riqi', 'zhongliang', 'beizhu', 'biaoshi' ]; const fieldLabels = { mingcheng: '名称', leibie: '类别', daima: '代码', xingtai: '形态', zhuyaochengfen: '主要成分', youhaichengfen: '有害成分', zhuyi: '注意', shibiema: '识别码', danwei: '单位', lianxi: '联系', riqi: '日期', zhongliang: '重量', beizhu: '备注', biaoshi: '●' }; const conf = [ { x: 192, y: 100 }, { x: 192, y: 150 }, { x: 192, y: 200 }, { x: 480, y: 200 }, { x: 192, y: 250 }, { x: 192, y: 350 }, { x: 192, y: 448 }, { x: 216, y: 536 }, { x: 256, y: 584 }, { x: 288, y: 632 }, { x: 192, y: 680 }, { x: 480, y: 680 }, { x: 150, y: 728 }, { x: 630, y: 540 } ]; let commands = []; commands.push('! 0 200 200 800 1'); commands.push(sizeCmd); fieldOrder.forEach((field, index) => { if (data[field]) { const { x, y } = conf[index]; const label = fieldLabels[field] || field; commands.push(`T 1 0 ${x} ${y} ${data[field]}`); } }); if (data.qrcode) { commands.push(`B QR 624 584 M 2 U 4`); commands.push(`L0,${data.qrcode}`); commands.push('ENDQR'); } commands.push('FORM'); commands.push('PRINT'); return commands.join('\n'); } /** * 生成 TSPL 废物标签 */ _generateTSPLWasteLabel(data, sizeCmd, gapCmd) { const fieldOrder = [ 'mingcheng', 'leibie', 'daima', 'xingtai', 'zhuyaochengfen', 'youhaichengfen', 'zhuyi', 'shibiema', 'danwei', 'lianxi', 'riqi', 'zhongliang', 'beizhu' ]; const fieldLabels = { mingcheng: '名称', leibie: '类别', daima: '代码', xingtai: '形态', zhuyaochengfen: '主要成分', youhaichengfen: '有害成分', zhuyi: '注意', shibiema: '识别码', danwei: '单位', lianxi: '联系', riqi: '日期', zhongliang: '重量', beizhu: '备注' }; let commands = []; commands.push(sizeCmd); commands.push(gapCmd); commands.push('CLS'); fieldOrder.forEach((field, index) => { if (data[field]) { const y = 20 + (index * 25); const label = fieldLabels[field] || field; const content = `${label}: ${data[field]}`; commands.push(`TEXT 20 ${y} "4" 0 1 1 "${content}"`); } }); if (data.qrcode) { commands.push(`QRCODE 150 400 L 5 0 A 0 "${data.qrcode}"`); } commands.push('PRINT'); return commands.join('\n'); } /** * 生成二维码模板 * @param {string} content - 二维码内容 */ generateQRCodeTemplate(content) { const sizeCmd = this.getSizeCommand(); const gapCmd = this.commandType === 'CPCL' ? 'GAP 2mm' : 'GAP 2 mm'; if (this.commandType === 'CPCL') { return `! 0 200 200 240 1 ${sizeCmd} ${gapCmd} CENTER QR 300 10 M 2 A 0 MA,${content} ENDQR PRINT`; } else { return `${sizeCmd} ${gapCmd} CLS QRCODE 50 50 L 5 0 A 0 "${content}" PRINT`; } } /** * 生成条码模板 * @param {string} content - 条码内容 * @param {string} type - 条码类型 */ generateBarcodeTemplate(content, type = '128') { const sizeCmd = this.getSizeCommand(); const gapCmd = this.commandType === 'CPCL' ? 'GAP 2mm' : 'GAP 2 mm'; if (this.commandType === 'CPCL') { return `! 0 200 200 240 1 ${sizeCmd} ${gapCmd} CENTER BARCODE 128 1 1 60 1 0 10 40 ${content} PRINT`; } else { return `${sizeCmd} ${gapCmd} CLS BARCODE 50 50 "${type}" 50 1 0 2 2 "${content}" PRINT`; } } // ==================== 配置管理 ==================== /** * 设置指令类型 * @param {'CPCL' | 'TSPL'} type */ setCommandType(type) { this.commandType = type; this._saveConfig({ commandType: type }); } /** * 设置纸张尺寸 * @param {number} width - 宽度(mm) * @param {number} height - 高度(mm) */ setPaperSize(width, height) { this.paperWidth = width; this.paperHeight = height; this._saveConfig({ paperWidth: width, paperHeight: height }); } /** * 获取当前配置 */ getConfig() { return { connected: this.connected, deviceId: this.deviceId, deviceName: this.deviceName, serviceId: this.serviceId, writeCharId: this.writeCharId, commandType: this.commandType, paperWidth: this.paperWidth, paperHeight: this.paperHeight }; } // ==================== 回调管理 ==================== /** * 注册连接状态变化回调 * @param {Function} callback - (connected: boolean, status?: string) => void */ onConnectionChange(callback) { this.connectionChangeCallbacks.push(callback); } /** * 移除连接状态变化回调 */ offConnectionChange(callback) { const index = this.connectionChangeCallbacks.indexOf(callback); if (index > -1) { this.connectionChangeCallbacks.splice(index, 1); } } /** * 通知连接状态变化 */ _notifyConnectionChange(connected, status) { this.connectionChangeCallbacks.forEach(cb => { try { cb(connected, status); } catch (e) { console.warn('连接状态回调执行失败', e); } }); } // ==================== 工具方法 ==================== /** * 字符串转 ArrayBuffer (GBK编码) */ _stringToBuffer(str) { try { const uint8Array = iconv.encode(str, 'gbk'); return uint8Array.buffer; } catch (e) { console.warn('GBK编码失败,使用回退方案', e); const buffer = new ArrayBuffer(str.length); const dataView = new DataView(buffer); for (let i = 0; i < str.length; i++) { dataView.setUint8(i, str.charCodeAt(i) & 0xFF); } return buffer; } } /** * 写入数据到蓝牙设备 */ async _writeData(buffer) { return new Promise((resolve, reject) => { uni.writeBLECharacteristicValue({ deviceId: this.deviceId, serviceId: this.serviceId, characteristicId: this.writeCharId, value: buffer, writeType: 'writeNoResponse', success: resolve, fail: reject }); }); } /** * 延迟 */ _delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 获取设备服务列表 */ _getServices(deviceId) { return new Promise((resolve, reject) => { uni.getBLEDeviceServices({ deviceId, success: resolve, fail: reject }); }); } /** * 获取服务特征值列表 */ _getCharacteristics(deviceId, serviceId) { return new Promise((resolve, reject) => { uni.getBLEDeviceCharacteristics({ deviceId, serviceId, success: resolve, fail: reject }); }); } } // 导出单例 export default new BluetoothPrinterService();