diff --git a/pages/index/index.js b/pages/index/index.js index 57ab3b8..3d3ef60 100644 --- a/pages/index/index.js +++ b/pages/index/index.js @@ -36,7 +36,12 @@ Page({ data: { devices: [], connected: false, - chs: [] + chs: [], + printing: false, + progress: 0, + totalPackages: 0, + sentPackages: 0, + statusMessage: '' }, onUnload() { this.closeBluetoothAdapter() @@ -259,16 +264,81 @@ Page({ let buffer = printerJobs.buffer(); console.log('ArrayBuffer', 'length: ' + buffer.byteLength, ' hex: ' + ab2hex(buffer)); - // 1.并行调用多次会存在写失败的可能性 - // 2.建议每次写入不超过20字节 - // 分包处理,延时调用 + this._sendBuffer(buffer); + }, + + // 发送Buffer数据并显示进度 + _sendBuffer(buffer) { const maxChunk = 20; const delay = 20; - for (let i = 0, j = 0, length = buffer.byteLength; i < length; i += maxChunk, j++) { + const length = buffer.byteLength; + const totalPackages = Math.ceil(length / maxChunk); + + console.log('=== 分包发送开始 ==='); + console.log('总长度:', length, '字节'); + console.log('分包大小:', maxChunk, '字节'); + console.log('总包数:', totalPackages); + + this.setData({ + printing: true, + totalPackages: totalPackages, + sentPackages: 0, + progress: 0, + statusMessage: '正在传输[0/' + totalPackages + ']' + }); + + for (let i = 0, j = 0; i < length; i += maxChunk, j++) { let subPackage = buffer.slice(i, i + maxChunk <= length ? (i + maxChunk) : length); - setTimeout(this._writeBLECharacteristicValue, j * delay, subPackage); + + // 打印分包信息 + const subPackageArray = Array.from(new Uint8Array(subPackage)); + console.log(`分包 ${j + 1}/${totalPackages} (${subPackageArray.length}字节):`, + subPackageArray.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + + setTimeout(this._writeBLECharacteristicValueWithProgress, j * delay, subPackage, j + 1, totalPackages); } }, + + // 带进度的蓝牙写入 + _writeBLECharacteristicValueWithProgress(buffer, packageNumber, totalPackages) { + wx.writeBLECharacteristicValue({ + deviceId: this._deviceId, + serviceId: this._serviceId, + characteristicId: this._characteristicId, + value: buffer, + success: (res) => { + console.log(`分包 ${packageNumber}/${totalPackages} 发送成功`); + this.setData({ + sentPackages: packageNumber, + progress: Math.round((packageNumber / totalPackages) * 100), + statusMessage: '正在传输[' + packageNumber + '/' + totalPackages + ']' + }); + + if (packageNumber === totalPackages) { + console.log('=== 分包发送完成 ==='); + setTimeout(() => { + this.setData({ + printing: false, + statusMessage: '打印指令发送成功' + }); + // 3秒后清除状态信息 + setTimeout(() => { + this.setData({ + statusMessage: '' + }); + }, 3000); + }, 500); + } + }, + fail: (res) => { + console.log(`分包 ${packageNumber}/${totalPackages} 发送失败:`, res); + this.setData({ + printing: false, + statusMessage: '打印失败' + }); + } + }); + }, _writeBLECharacteristicValue(buffer) { wx.writeBLECharacteristicValue({ deviceId: this._deviceId, @@ -328,5 +398,108 @@ Page({ } } }) + }, + + // 选择图片并打印 + chooseImageAndPrint() { + wx.chooseImage({ + count: 1, + sizeType: ['original', 'compressed'], + sourceType: ['album'], + success: (res) => { + const tempFilePath = res.tempFilePaths[0]; + this._convertImageToBitmap(tempFilePath); + }, + fail: (res) => { + console.log('chooseImage fail', res); + wx.showToast({ + title: '选择图片失败', + icon: 'none' + }); + } + }); + }, + + // 将图片转换为位图 + _convertImageToBitmap(imagePath) { + wx.getImageInfo({ + src: imagePath, + success: (res) => { + const canvas = wx.createCanvasContext('printCanvas'); + const canvasWidth = 384; // 58mm纸宽对应的像素宽度 + const scale = canvasWidth / res.width; + const canvasHeight = Math.round(res.height * scale); + + canvas.drawImage(imagePath, 0, 0, canvasWidth, canvasHeight); + canvas.draw(false, () => { + wx.canvasGetImageData({ + canvasId: 'printCanvas', + x: 0, + y: 0, + width: canvasWidth, + height: canvasHeight, + success: (imageData) => { + this._convertToBitmap(imageData, canvasWidth, canvasHeight); + }, + fail: (res) => { + console.log('canvasGetImageData fail', res); + wx.showToast({ + title: '图片转换失败', + icon: 'none' + }); + } + }); + }); + }, + fail: (res) => { + console.log('getImageInfo fail', res); + wx.showToast({ + title: '获取图片信息失败', + icon: 'none' + }); + } + }); + }, + + // 将图片数据转换为打印机可识别的位图 + _convertToBitmap(imageData, width, height) { + const pixels = imageData.data; + const bitmap = new Uint8Array(Math.ceil(width * height / 8)); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const pixelIndex = (y * width + x) * 4; + const r = pixels[pixelIndex]; + const g = pixels[pixelIndex + 1]; + const b = pixels[pixelIndex + 2]; + const a = pixels[pixelIndex + 3]; + + // 转换为黑白(阈值128) + const gray = (r * 0.299 + g * 0.587 + b * 0.114) * (a / 255); + const isBlack = gray < 128; + + if (isBlack) { + const byteIndex = Math.floor((y * width + x) / 8); + const bitIndex = 7 - ((y * width + x) % 8); + bitmap[byteIndex] |= (1 << bitIndex); + } + } + } + + // 创建打印任务 + const printerJobs = new PrinterJobs(); + printerJobs.setAlign('ct').printImage(bitmap.buffer, width, height).println(); + + const buffer = printerJobs.buffer(); + this._sendBuffer(buffer); + }, + + // 打印二维码测试 + printQRCodeTest() { + const printerJobs = new PrinterJobs(); + printerJobs.setAlign('ct').printQRCode('https://www.example.com', 6).println(); + + const buffer = printerJobs.buffer(); + this._sendBuffer(buffer); } }) \ No newline at end of file diff --git a/pages/index/index.wxml b/pages/index/index.wxml index 4ebed7a..7fb426c 100644 --- a/pages/index/index.wxml +++ b/pages/index/index.wxml @@ -42,7 +42,24 @@ + + + + + + {{statusMessage}} + + + + + + + \ No newline at end of file diff --git a/pages/index/index.wxss b/pages/index/index.wxss index 7369601..a9b6c43 100644 --- a/pages/index/index.wxss +++ b/pages/index/index.wxss @@ -52,4 +52,35 @@ min-height: 2.58823529em; line-height: 2.58823529em; padding: 10rpx; +} + +.print-status { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 20rpx; + text-align: center; +} + +.status-message { + font-size: 28rpx; + margin-bottom: 10rpx; +} + +.progress-bar { + width: 100%; + height: 10rpx; + background: rgba(255, 255, 255, 0.3); + border-radius: 5rpx; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: #007aff; + border-radius: 5rpx; + transition: width 0.3s ease; } \ No newline at end of file diff --git a/printer/printerjobs.js b/printer/printerjobs.js index fda31bb..5fb354d 100644 --- a/printer/printerjobs.js +++ b/printer/printerjobs.js @@ -152,6 +152,85 @@ printerJobs.prototype.clear = function () { return this; }; +/** + * 打印二维码 - 实现ESC/POS二维码打印指令 + * 参考ESC/POS指令集:https://reference.epson-biz.com/modules/ref_escpos/index.php?content_id=143 + * @param {string} content 二维码内容(支持数字、字母、符号) + * @param {number} size 二维码模块大小(1-16),默认值为6 + * @returns {printerJobs} 返回当前实例,支持链式调用 + */ +printerJobs.prototype.printQRCode = function (content, size = 6) { + // 二维码大小限制在1-16之间,超出范围自动修正 + size = Math.max(1, Math.min(16, size)); + + console.log('=== 二维码打印指令开始 ==='); + console.log('内容:', content); + console.log('模块大小:', size); + + // 1. 设置二维码模块大小 (GS ( k 0x03 0x00 0x31 0x43 n) + // n: 模块大小,范围1-16,默认6 + const setModuleSize = [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x43, size]; + console.log('设置模块大小指令:', setModuleSize.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + this._enqueue(setModuleSize); + + // 2. 设置二维码纠错级别 (GS ( k 0x03 0x00 0x31 0x45 n) + // n: 纠错级别,'0'=7%, '1'=15%, '2'=25%, '3'=30%,默认'0' + const setErrorCorrection = [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x45, 0x30]; + console.log('设置纠错级别指令:', setErrorCorrection.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + this._enqueue(setErrorCorrection); + + // 3. 存储二维码数据 (GS ( k pL pH 0x31 0x50 0x30 d1...dn) + // pL pH: 数据长度(包括3个控制字节) + // d1...dn: 二维码内容 + let dataLength = content.length + 3; // 3个控制字节 + let pL = dataLength % 256; // 低字节 + let pH = Math.floor(dataLength / 256); // 高字节 + + const storeQRData = [0x1d, 0x28, 0x6b, pL, pH, 0x31, 0x50, 0x30]; + console.log('存储二维码数据指令:', storeQRData.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + this._enqueue(storeQRData); + + // 4. 添加二维码内容 + let uint8Array = this._encoder.encode(content); + let encoded = Array.from(uint8Array); + console.log('二维码内容编码:', encoded.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + this._enqueue(encoded); + + // 5. 打印二维码 (GS ( k 0x03 0x00 0x31 0x51 0x30) + const printQR = [0x1d, 0x28, 0x6b, 0x03, 0x00, 0x31, 0x51, 0x30]; + console.log('打印二维码指令:', printQR.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')); + this._enqueue(printQR); + + console.log('=== 二维码打印指令结束 ==='); + + return this; +}; + +/** + * 打印位图 + * @param {ArrayBuffer} imageData 位图数据 + * @param {number} width 位图宽度 + * @param {number} height 位图高度 + */ +printerJobs.prototype.printImage = function (imageData, width, height) { + // 计算位图数据长度 + let dataLength = width * height; + let pL = dataLength % 256; + let pH = Math.floor(dataLength / 256); + + // 发送GS v 0指令 + this._enqueue([0x1d, 0x76, 0x30, 0x00]); + this._enqueue([width % 256, Math.floor(width / 256)]); + this._enqueue([height % 256, Math.floor(height / 256)]); + + // 添加位图数据 + let uint8Array = new Uint8Array(imageData); + let encoded = Array.from(uint8Array); + this._enqueue(encoded); + + return this; +}; + /** * 返回ArrayBuffer */