微信小程序蓝牙打印请搜索插件 ESCPOS指令打印 ,先申请,再V我50RMB可永久使用。
代码中用到的中文转码方法见:gbk.js gb2312编码字符转Uint8Array,解决打印机中文乱码问题
命令规则参考 小程序插件文档 Printer类部分
import {isAscii,U2B} from './gbk.js';
const fontSize=12,
/*计算字符串长度(1个中文=2个英文字符)*/
charLen=str=>{
let width=0;
for(let i=0;i<str.length;i++){
width+=isAscii(str.charCodeAt(i))?1:2;
}
return width;
},
ESC_POS={
ALIGN:{
C: [0x1b, 0x61, 0x01], // 居中
L: [0x1b, 0x61, 0x00], // 左对齐
R: [0x1b, 0x61, 0x02], // 右对齐
},
BEEP:[0x1b,0x07], // 蜂鸣器
COLOR:{
BLACK:[0x1b,0x72,0x00],
RED:[0x1b,0x72,0x01]
},
/*文本格式*/
TEXT: {
NORMAL: [0x1b, 0x21, 0x00], // Normal text
D_H: [0x1b, 0x21, 0x10], // Double height text
D_W: [0x1b, 0x21, 0x20], // Double width text
D_W_H: [0x1b, 0x21, 0x30], // Double width & height text
UNDERL_OFF: [0x1b, 0x2d, 0x00], // Underline font OFF
UNDERL_ON: [0x1b, 0x2d, 0x01], // Underline font 1-dot ON
UNDERL_2: [0x1b, 0x2d, 0x02], // Underline font 2-dot ON
BOLD_OFF: [0x1b, 0x45, 0x00], // Bold font OFF
BOLD_ON: [0x1b, 0x45, 0x01], // Bold font ON
ITALIC_OFF: [0x1b, 0x35], // Italic font ON
ITALIC_ON: [0x1b, 0x34], // Italic font ON
FONT_A: [0x1b, 0x4d, 0x00], // Font type A
FONT_B: [0x1b, 0x4d, 0x01], // Font type B
FONT_C: [0x1b, 0x4d, 0x02], // Font type C
},
LINE_SPACING:{
LS_DEFAULT:[0x1b,0x32], //默认行高,30点
LS_SET(size){return [0x1b,0x33,size]} //size点行高
},
CUT: {
FULL: [0x1d, 0x56, 0x00], // 全切
PART: [0x1d, 0x56, 0x01], // 半切
FULL_TO: [0x1d, 0x56, 0x40], // 走纸到切纸位置+n/144英寸并全切
A_TO: [0x1d, 0x56, 0x41], // 走纸到切纸位置+n/144英寸并半切
B_TO: [0x1d, 0x56, 0x42], // 走纸到切纸位置+n/144英寸并半切
},
BARCODE: {
TXT_OFF: [0x1d, 0x48, 0x00], // HRI barcode chars OFF
TXT_ABV: [0x1d, 0x48, 0x01], // HRI barcode chars above
TXT_BLW: [0x1d, 0x48, 0x02], // HRI barcode chars below
TXT_BTH: [0x1d, 0x48, 0x03], // HRI barcode chars both above and below
FONT_A: [0x1d, 0x66, 0x00], // Font type A for HRI barcode chars
FONT_B: [0x1d, 0x66, 0x01], // Font type B for HRI barcode chars
HEIGHT(h){return [0x1d,0x68,h]},// Barcode Height [1-255]
WIDTH(w){return [0x1d,0x77,w]},// Barcode Width [2-6]
HEIGHT_DEFAULT: [0x1d, 0x68, 0x64], // Barcode height default:100
WIDTH_DEFAULT: [0x1d, 0x77, 0x01], // Barcode width default:1
UPC_A: [0x1d, 0x6b, 0x00], // 0x41 11,12 48-57
UPC_E: [0x1d, 0x6b, 0x01], // 0x42 11,12 48-57
EAN13: [0x1d, 0x6b, 0x02], // 0x43 12,13 48-57
EAN8: [0x1d, 0x6b, 0x03], // 0x44 7,8 48-57
CODE39: [0x1d, 0x6b, 0x04], // 0x45 变长 32,36,37,43,45-57,65-90
I25: [0x1d, 0x6b, 0x05], // 0x46 偶数 48-57 (ITF)
CODEBAR: [0x1d, 0x6b, 0x06], // 0x47 变长 36,43,45-58,65-68 (NW7)
CODE93: [0x1d, 0x6b, 0x07], // 0x48 变长 0-127
CODE128: [0x1d, 0x6b, 0x08], // 0x49 变长 0-127
CODE11: [0x1d, 0x6b, 0x09], // 0x4a 变长 48-57
MSI: [0x1d, 0x6b, 0x0a], // 0x4b 变长 48-57
},
QRCODE:{
SIZE(size){return [0x1D,0x28,0x6b,0x03,0x00,0x31,0x43,size]},
CORRECT_L:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x30], // 可覆盖%7,默认
CORRECT_M:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x31], // 可覆盖%15
CORRECT_Q:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x32], // 可覆盖%25
CORRECT_H:[0x1D,0x28,0x6b,0x03,0x00,0x31,0x45,0x33], // 可覆盖%30
},
/**
* [HARDWARE Printer hardware]
* @type {Object}
*/
HARDWARE: {
INIT: [0x1b, 0x40], // Clear data in buffer and reset modes
HW_SELECT: [0x1b, 0x3d, 0x01], // Printer select
HW_RESET: [0x1b, 0x3f, 0x0a, 0x00], // Reset printer hardware
},
/**
* [CASH_DRAWER Cash Drawer]
* @type {Object}
*/
CASH_DRAWER: {
CD_KICK_2: [0x1b, 0x70, 0x00], // Sends a pulse to pin 2 []
CD_KICK_5: [0x1b, 0x70, 0x01], // Sends a pulse to pin 5 []
},
/**
* [MARGINS Margins sizes]
* @type {Object}
*/
MARGINS: {
BOTTOM: [0x1b, 0x4f], // Fix bottom size
LEFT: [0x1b, 0x6c], // Fix left size
RIGHT: [0x1b, 0x51], // Fix right size
},
/**
* [IMAGE_FORMAT Image format]
* @type {Object}
*/
IMAGE_FORMAT: {
S_RASTER_N: [0x1d, 0x76, 0x30, 0x00], // Set raster image normal size
S_RASTER_2W: [0x1d, 0x76, 0x30, 0x01], // Set raster image double width
S_RASTER_2H: [0x1d, 0x76, 0x30, 0x02], // Set raster image double height
S_RASTER_Q: [0x1d, 0x76, 0x30, 0x03], // Set raster image quadruple
},
/**
* [BITMAP_FORMAT description]
* @type {Object}
*/
BITMAP_FORMAT: {BITMAP_S8:[0x1b,0x2a,0x00],BITMAP_D8:[0x1b,0x2a,0x01],BITMAP_S24:[0x1b,0x2a,0x20],BITMAP_D24:[0x1b,0x2a,0x21]},
/**
* [GSV0_FORMAT description]
* @type {Object}
*/
GSV0_FORMAT: {
GSV0_NORMAL: [0x1d, 0x76, 0x30, 0x00],
GSV0_DW: [0x1d, 0x76, 0x30, 0x01],
GSV0_DH: [0x1d, 0x76, 0x30, 0x02],
GSV0_DWDH: [0x1d, 0x76, 0x30, 0x03]
},
};
export default class self {
static PER_MM=8;
static BRANDS=[
{
name:'群索',
init:[31,27,31,128,4,5,6,68],
cut:[29,12],
}
];
#connections=[];
pushConnection(connection,brand=null){
this.#connections.push({connection,brand});
}
updateConnection(connection,brand=null){
const row=this.#connections.find(row=>row.connection===connection);
if(row){
row.brand=brand;
}
}
printByESCPOS(lines,times=1,width=48){
if(!this.#connections.length) return;
let brand=this.#connections[0].brand,
data=this.#buildESCPOS(lines,width,brand);
this.#connections.forEach(row=>{
if(brand!==row.brand){
brand=row.brand;
data=this.#buildESCPOS(lines,width,brand);
}
row.connection.sendData(data,times,'正在发送数据');
});
}
/*构造ESC/POS指令*/
#buildESCPOS(lines,width,brand){
const pixels=width*self.PER_MM,
data=Array.from(ESC_POS.HARDWARE.INIT);
if(!brand) data.push(29,87,pixels%256,parseInt(pixels/256));
data.push(27,74,10);
let fontVolume=parseInt(pixels/fontSize);
lines.forEach(i=>{
if(i.align&&ESC_POS.ALIGN[i.align]) data.push(...ESC_POS.ALIGN[i.align]);
if(i.color&&ESC_POS.COLOR[i.color]) data.push(...ESC_POS.COLOR[i.color]);
if(i.text||i.fill){
if(i.size&&ESC_POS.TEXT[i.size]){
data.push(...ESC_POS.TEXT[i.size]);
if(i.size=='D_W'||i.size=='D_W_H'){
fontVolume=parseInt(pixels/fontSize/2);
}else if(i.size=='NORMAL'||i.size=='D_H'){
fontVolume=parseInt(pixels/fontSize);
}
}
if(i.blod&&ESC_POS.TEXT['BOLD_'+i.blod]) data.push(...ESC_POS.TEXT['BOLD_'+i.blod]);
if(i.font&&ESC_POS.TEXT['FONT_'+i.font]) data.push(...ESC_POS.TEXT['FONT_'+i.font]);
if(i.underl&&ESC_POS.TEXT['UNDERL_'+i.underl]) data.push(...ESC_POS.TEXT['UNDERL_'+i.underl]);
if(i.italic&&ESC_POS.TEXT['ITALIC_'+i.italic]) data.push(...ESC_POS.TEXT['ITALIC_'+i.italic]);
if(i.text){
if(i.r) i.text+=(new Array(fontVolume-(charLen(i.text)+charLen(i.r))%fontVolume).fill(i.fill||' ').join(''))+i.r;
else if(i.fill){
let count=fontVolume-charLen(i.text);
if(count>0) i.text=(new Array(Math.ceil(count/2)).fill(i.fill).join(''))+i.text+(new Array(parseInt(count/2)).fill(i.fill).join(''));
}
data.push(...U2B(i.text,fontVolume));
}else{
if(fontVolume<charLen(i.fill)) i.fill='-';
let str=new Array(parseInt(fontVolume/charLen(i.fill))).fill(i.fill).join('')
if(charLen(str)<fontVolume){
for(let i=charLen(str);i<fontVolume;i++){
if(i%2==0) str+=' ';
else str=' '+str;
}
}
data.push(...U2B(str,fontVolume));
}
}else if(typeof i.line==='number') data.push(...(new Array(i.line).fill(10)));
else if(i.beep){
// data.push(0x1b,0x28,0x41,0x04,0x00,0x30,0x00,0x09,0x02)
}else if(typeof i.barcode==='string'){
let str=i.barcode.replace(/[^\x00-\x7F]/g,'');
if(!str) return true;
data.push(...ESC_POS.BARCODE.HEIGHT(60));
let codeLen=str.length;
if(/[^\x30-\x39]/.test(str)==false){
if(codeLen==7||codeLen==8) data.push(...ESC_POS.BARCODE.EAN8);
else if(codeLen==11) data.push(...ESC_POS.BARCODE.UPC_A);
else if(codeLen==12||codeLen==13) data.push(...ESC_POS.BARCODE.EAN13);
else data.push(...ESC_POS.BARCODE[codeLen%2==0?'I25':'CODE11']);
}else if(/[\x00-\x1F\x21-\x23\x26-\x2A\x2C\x3A-\x40\x5B-\x7F]/.test(str)) data.push(...ESC_POS.BARCODE.CODE93);
else data.push(...ESC_POS.BARCODE[/[\x20\x25\x45-\x5A]/.test(str)?'CODE39':'CODEBAR']);
data.push(...str.split('').map(c=>c.charCodeAt(0)),0x00);
}else if(i.qrcode){
data.push(27,74,10);
let poi,
p=parseInt(i.size)*self.PER_MM,
buffer=U2B(i.qrcode),
len=buffer.length+3;
if(p<80){p=80}else if(p>1000){p=1000}
if(len<29){poi=19}else if(len<54){poi=23}
else if(len<85){poi=27}else{poi=len<119?31:35}
data.push(...ESC_POS.QRCODE.SIZE(p>poi?(p/poi).toFixed():1));
data.push(...ESC_POS.QRCODE.CORRECT_M);
data.push(29,40,107,len%256,parseInt(len/256),49,80,48,...buffer) //可能有126字节限制
data.push(29,40,107,3,0,49,81,48)
data.push(27,74,10)
}else if(i.raster) data.push(29,118,48,0,i.x%256,parseInt(i.x/256),i.y%256,parseInt(i.y/256),...i.raster,27,74,10);
/* // 以光栅格式绘图(Function 112),暂时不可用,可能是设备支持的问题
data.push(29,40,76,4,0,48,1,51,51)
data.push(29,40,76,17,0,48,112,48,1,1,49,56,0,1,0,255,255,255,255,255,255,255)
data.push(29,40,76,2,0,48,2)
//页模式,部分指令目前设备不支持
data.push(27,76) //进入页模式
data.push(27,36,0,0) // X归零
data.push(29,36,0,0) // Y归零,设备不支持Y向位移
data.push(...ESC_POS.FF) //输出缓冲区并回到标准模式 */
if(i.next){
data.push(...(brand?brand.cut:ESC_POS.CUT.FULL));
}else if(i.cut&&ESC_POS.CUT[i.cut]) data.push(...ESC_POS.CUT[i.cut]);
});
return data;
}
}
运行在企业微信、飞书等App内的网页可采用平台提供的JS-SDK封装BLEConnect类,具体参考:【附企业微信bridge封装】基于JS-SDK的低功耗蓝牙连接类BLEConnect
import BLEConnect from '../../devices/BLEConnect';
import BasePrinter from '../../devices/Printer';
class Printer extends BasePrinter {
BLE=null;
setBLE(...args){
if(!this.BLE){
// 第二个参数仅供参考
this.BLE=new BLEConnect(['write'],async ()=>{
const ch=await appEnv.popupMenu(
Printer.BRANDS.map(b=>({label:b.name})),
'打印机是否下列品牌?'
);
if(ch===null){
this.updateConnection(this.BLE);
this.BLE.maxPack=200;
return null;
}else{
this.updateConnection(this.BLE,Printer.BRANDS[ch]);
this.BLE.maxPack=200;
return Printer.BRANDS[ch].init;
}
});
this.pushConnection(this.BLE);
}
return this.BLE.setDevice(...args);
}
}