Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 179 additions & 6 deletions pages/index/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ Page({
data: {
devices: [],
connected: false,
chs: []
chs: [],
printing: false,
progress: 0,
totalPackages: 0,
sentPackages: 0,
statusMessage: ''
},
onUnload() {
this.closeBluetoothAdapter()
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
})
17 changes: 17 additions & 0 deletions pages/index/index.wxml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,24 @@
<button wx:if="{{canWrite}}" type="primary" bindtap="writeBLECharacteristicValue" style="margin-bottom: 10px;">
写数据
</button>
<button wx:if="{{canWrite}}" type="primary" bindtap="printQRCodeTest" style="margin-bottom: 10px;">
打印二维码
</button>
<button wx:if="{{canWrite}}" type="primary" bindtap="chooseImageAndPrint" style="margin-bottom: 10px;">
打印图片
</button>
<button bindtap="closeBLEConnection">断开连接</button>
</view>
</view>

<!-- 打印状态显示区域 -->
<view class="print-status" wx:if="{{printing || statusMessage}}">
<view class="status-message">{{statusMessage}}</view>
<view class="progress-bar" wx:if="{{printing}}">
<view class="progress-fill" style="width: {{progress}}%"></view>
</view>
</view>

<!-- 隐藏的Canvas用于图片转换 -->
<canvas canvas-id="printCanvas" style="width: 384px; height: 0px; position: absolute; left: -9999px;"></canvas>
</view>
31 changes: 31 additions & 0 deletions pages/index/index.wxss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
79 changes: 79 additions & 0 deletions printer/printerjobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down