高级用法
本文档涵盖自定义驱动开发、自定义适配器开发、插件编写指南和性能优化建议。
自定义驱动开发
如果需要支持非标准打印机或新的指令集,可以通过实现 IPrinterDriver 接口来创建自定义驱动。
驱动接口定义
typescript
import { IPrinterDriver, IQrOptions } from 'taro-bluetooth-print';
interface IPrinterDriver {
init(): Uint8Array[];
text(content: string, encoding?: string): Uint8Array[];
image(data: Uint8Array, width: number, height: number): Uint8Array[];
qr(content: string, options?: IQrOptions): Uint8Array[];
cut(): Uint8Array[];
feed(lines?: number): Uint8Array[];
// 可选方法
barcode?(content: string, format: string, options?: object): Uint8Array[];
getBuffer?(): Uint8Array;
reset?(): void;
}实现示例:自定义 EPL 驱动
EPL(Eltron Programming Language)常见于老式桌面标签打印机:
typescript
import { IPrinterDriver } from 'taro-bluetooth-print';
class EplDriver implements IPrinterDriver {
private buffer: Uint8Array[] = [];
private width = 608; // 默认 4" @ 152 DPI
private height = 304; // 默认 2"
init(): Uint8Array[] {
this.buffer = [];
this.buffer.push(new Uint8Array([0x1B, 0x40])); // ESC @ — 初始化
return this.buffer;
}
text(content: string, encoding = 'UTF-8'): Uint8Array[] {
const encoded = new TextEncoder().encode(content);
// EPL 文本命令: "A 10 0 0 1 1 0 N ..." 格式
const cmd = `A 10 0 0 1 1 0 N "${content}"\n`;
this.buffer.push(new TextEncoder().encode(cmd));
return this.buffer;
}
image(data: Uint8Array, width: number, height: number): Uint8Array[] {
const monoData = this.toMonoBitmap(data, width, height);
const gwCmd = `GW 0,0,${width},${height}\n`;
this.buffer.push(new TextEncoder().encode(gwCmd));
this.buffer.push(monoData);
return this.buffer;
}
qr(content: string): Uint8Array[] {
return this.buffer;
}
feed(lines = 1): Uint8Array[] {
const cmd = `P${lines}\n`;
this.buffer.push(new TextEncoder().encode(cmd));
return this.buffer;
}
cut(): Uint8Array[] {
this.buffer.push(new TextEncoder().encode('G\n')); // G = cut
return this.buffer;
}
getBuffer(): Uint8Array {
const totalLen = this.buffer.reduce((sum, arr) => sum + arr.length, 0);
const result = new Uint8Array(totalLen);
let offset = 0;
for (const arr of this.buffer) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
private toMonoBitmap(data: Uint8Array, width: number, height: number): Uint8Array {
const bitPerLine = Math.ceil(width / 8);
const linePadding = (8 - (bitPerLine % 8)) % 8;
const bytesPerLine = bitPerLine + linePadding;
const result = new Uint8Array(bytesPerLine * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIdx = (y * width + x) * 4;
const r = data[pixelIdx];
const g = data[pixelIdx + 1];
const b = data[pixelIdx + 2];
const gray = (r * 0.299 + g * 0.587 + b * 0.114);
const bit = gray < 128 ? 1 : 0;
const byteIdx = y * bytesPerLine + Math.floor(x / 8);
const bitIdx = 7 - (x % 8);
result[byteIdx] |= (bit << bitIdx);
}
}
return result;
}
}
// 使用自定义驱动
const printer = new BluetoothPrinter(undefined, new EplDriver());
await printer.connect(deviceId);
await printer.print();扩展现有驱动
更常见的做法是扩展现有的 EscPos 类来添加特殊指令:
typescript
import { EscPos } from 'taro-bluetooth-print';
class ExtendedEscPos extends EscPos {
// 添加开钱箱指令
openCashDrawer(): this {
const cmd = new Uint8Array([0x1B, 0x70, 0x00, 0x19, 0xFA]);
this.addCommand(cmd);
return this;
}
// 添加自定义浓度设置
setDensity(level: number): this {
const n1 = Math.min(Math.max(level, 0), 15);
const cmd = new Uint8Array([0x1B, 0x37, n1, 0, 0]);
this.addCommand(cmd);
return this;
}
// 添加鸣叫指令
beep(count = 1, duration = 100): this {
const cmd = new Uint8Array([0x1B, 0x42, count, duration]);
this.addCommand(cmd);
return this;
}
}自定义适配器开发
适配器负责与平台蓝牙 API 交互。以下是实现一个自定义 BLE 适配器的基本模式:
适配器接口
typescript
interface IPrinterAdapter {
connect(deviceId: string): Promise<void>;
disconnect(deviceId: string): Promise<void>;
write(deviceId: string, buffer: ArrayBuffer, options?: IAdapterOptions): Promise<void>;
startDiscovery?(): Promise<void>;
stopDiscovery?(): Promise<void>;
onStateChange?(callback: (state: PrinterState) => void): void;
}实现示例:Flutter 蓝牙适配器(伪代码)
typescript
class FlutterBleAdapter implements IPrinterAdapter {
private deviceId: string | null = null;
async connect(deviceId: string): Promise<void> {
const result = await FlutterChannel.invokeMethod('BLE.connect', { deviceId });
if (!result.success) {
throw new Error(`连接失败: ${result.error}`);
}
this.deviceId = deviceId;
}
async disconnect(deviceId: string): Promise<void> {
await FlutterChannel.invokeMethod('BLE.disconnect', { deviceId });
this.deviceId = null;
}
async write(deviceId: string, buffer: ArrayBuffer, options?: IAdapterOptions): Promise<void> {
const chunkSize = options?.chunkSize ?? 20;
const delay = options?.delay ?? 20;
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length; i += chunkSize) {
const chunk = bytes.slice(i, Math.min(i + chunkSize, bytes.length));
await FlutterChannel.invokeMethod('BLE.write', { deviceId, data: Array.from(chunk) });
if (i + chunkSize < bytes.length) {
await sleep(delay);
}
}
}
}BaseAdapter 基础类
内置的 BaseAdapter 提供了蓝牙操作的通用逻辑骨架:
typescript
import { BaseAdapter } from 'taro-bluetooth-print';
class MyCustomAdapter extends BaseAdapter {
protected async discoverServicesImpl(deviceId: string): Promise<void> {
// 覆盖服务发现逻辑
}
protected async discoverCharacteristicsImpl(
deviceId: string,
serviceId: string
): Promise<void> {
// 覆盖特征发现逻辑
}
protected async writeImpl(
deviceId: string,
serviceId: string,
characteristicId: string,
data: ArrayBuffer
): Promise<void> {
// 覆盖写入逻辑
}
}插件开发指南
完整插件示例
以下是一个完整的分析插件,统计打印数据并上报:
typescript
import { Plugin, PluginHooks } from 'taro-bluetooth-print';
export interface AnalyticsPluginOptions {
endpoint: string;
sampleRate?: number;
}
export function createAnalyticsPlugin(options: AnalyticsPluginOptions): Plugin {
let sessionId: string;
return {
name: 'analytics',
onInit: () => {
sessionId = crypto.randomUUID();
console.log('[Analytics] Session started:', sessionId);
},
hooks: {
beforePrint: (buffer, context) => {
if (Math.random() > (options.sampleRate ?? 1)) {
return buffer;
}
const payload = {
sessionId,
timestamp: Date.now(),
bytes: buffer.byteLength,
driver: context?.driver ?? 'unknown',
deviceId: context?.deviceId ?? 'unknown',
};
fetch(options.endpoint, {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
}).catch(() => {}); // 静默失败
return buffer;
},
afterPrint: (result) => {
console.log('[Analytics] Print complete:', {
bytes: result.bytesSent,
duration: result.duration,
success: result.success,
});
},
onError: (error) => {
console.error('[Analytics] Print error:', {
code: error.code,
message: error.message,
});
},
},
onDestroy: () => {
console.log('[Analytics] Session ended:', sessionId);
},
};
}性能优化建议
1. 调整分片参数
typescript
// 环境好时推荐配置(蓝牙 5.0+ 设备)
printer.setOptions({
chunkSize: 200, // 蓝牙 4.2+ 可用更大值
delay: 5,
retries: 2,
});
// 环境差时使用保守配置
printer.setOptions({
chunkSize: 10,
delay: 50,
retries: 5,
});2. 减少不必要的样式切换
typescript
// ❌ 低效 — 频繁切换样式
printer.text('标题1', 'GBK').setBold(true).text('内容1').setBold(false)
.text('标题2', 'GBK').setBold(true).text('内容2').setBold(false);
// ✅ 高效 — 批量相同样式
printer.setBold(true);
printer.text('标题1', 'GBK').text('标题2', 'GBK');
printer.setBold(false);
printer.text('内容1', 'GBK').text('内容2', 'GBK');3. 合理使用打印队列
typescript
// ❌ 低效 — 多次小数据打印
for (const item of items) {
await printer.text(item).print(); // 每次打印都有连接开销
}
// ✅ 高效 — 批量构建,一次打印
const queue = new PrintQueue();
for (const item of items) {
queue.add(buildPrintData(item));
}
await queue.processAll(printer);4. 使用模板引擎缓存
typescript
// ❌ 低效 — 每次渲染模板
for (const order of orders) {
const commands = engine.renderReceipt(order);
await printer.print(commands);
}
// ✅ 高效 — 预编译模板
const template = engine.compile('receipt');
for (const order of orders) {
const commands = template(order);
await printer.print(commands);
}5. 及时断开连接
typescript
try {
await printer.connect(deviceId);
await printer.print();
} finally {
await printer.disconnect(); // 立即释放资源
}6. 生产环境关闭调试日志
typescript
import { Logger, LogLevel } from 'taro-bluetooth-print';
// 开发环境
Logger.setLevel(LogLevel.DEBUG);
// 生产环境
Logger.setLevel(LogLevel.ERROR);错误处理最佳实践
typescript
import { BluetoothPrintError, ErrorCode } from 'taro-bluetooth-print';
async function robustPrint(printer: BluetoothPrinter, data: Uint8Array) {
try {
await printer.print(data);
} catch (error) {
if (error instanceof BluetoothPrintError) {
switch (error.code) {
case ErrorCode.CONNECTION_FAILED:
await printer.connect(deviceId);
return robustPrint(printer, data);
case ErrorCode.WRITE_FAILED:
printer.setOptions({ retries: 5 });
return robustPrint(printer, data);
case ErrorCode.DEVICE_DISCONNECTED:
await printer.connect(deviceId);
return robustPrint(printer, data);
default:
throw error;
}
}
throw error;
}
}