taro-bluetooth-print 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +128 -14
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -81644
- package/dist/index.umd.js +1 -1
- package/dist/types/adapters/AdapterFactory.d.ts +0 -1
- package/dist/types/adapters/AlipayAdapter.d.ts +0 -1
- package/dist/types/adapters/BaiduAdapter.d.ts +0 -1
- package/dist/types/adapters/BaseAdapter.d.ts +0 -1
- package/dist/types/adapters/ByteDanceAdapter.d.ts +0 -1
- package/dist/types/adapters/TaroAdapter.d.ts +0 -1
- package/dist/types/adapters/WebBluetoothAdapter.d.ts +0 -1
- package/dist/types/config/PrinterConfig.d.ts +0 -1
- package/dist/types/core/BluetoothPrinter.d.ts +0 -1
- package/dist/types/drivers/EscPos.d.ts +0 -1
- package/dist/types/drivers/TsplDriver.d.ts +251 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/plugins/PluginManager.d.ts +87 -0
- package/dist/types/plugins/builtin/LoggingPlugin.d.ts +14 -0
- package/dist/types/plugins/builtin/RetryPlugin.d.ts +18 -0
- package/dist/types/plugins/index.d.ts +7 -0
- package/dist/types/plugins/types.d.ts +97 -0
- package/dist/types/services/CommandBuilder.d.ts +0 -1
- package/dist/types/services/ConnectionManager.d.ts +0 -1
- package/dist/types/services/PrintJobManager.d.ts +0 -1
- package/dist/types/services/interfaces/index.d.ts +0 -1
- package/dist/types/template/TemplateEngine.d.ts +0 -1
- package/package.json +15 -17
- package/src/drivers/TsplDriver.ts +417 -0
- package/src/index.ts +14 -0
- package/src/plugins/PluginManager.ts +193 -0
- package/src/plugins/builtin/LoggingPlugin.ts +97 -0
- package/src/plugins/builtin/RetryPlugin.ts +109 -0
- package/src/plugins/index.ts +10 -0
- package/src/plugins/types.ts +119 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TSPL Driver
|
|
3
|
+
* TSC Printer Language driver for label/barcode printers
|
|
4
|
+
*
|
|
5
|
+
* TSPL is commonly used in thermal transfer label printers (TSC, Zebra, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Logger } from '@/utils/logger';
|
|
9
|
+
import { Encoding } from '@/utils/encoding';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Label size configuration
|
|
13
|
+
*/
|
|
14
|
+
export interface LabelSize {
|
|
15
|
+
/** Label width in mm */
|
|
16
|
+
width: number;
|
|
17
|
+
/** Label height in mm */
|
|
18
|
+
height: number;
|
|
19
|
+
/** Gap between labels in mm (default: 3) */
|
|
20
|
+
gap?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Text position and style
|
|
25
|
+
*/
|
|
26
|
+
export interface TextOptions {
|
|
27
|
+
/** X position in dots */
|
|
28
|
+
x: number;
|
|
29
|
+
/** Y position in dots */
|
|
30
|
+
y: number;
|
|
31
|
+
/** Font size (1-8, default: 2) */
|
|
32
|
+
font?: number;
|
|
33
|
+
/** Rotation (0, 90, 180, 270, default: 0) */
|
|
34
|
+
rotation?: 0 | 90 | 180 | 270;
|
|
35
|
+
/** Horizontal multiplier (1-10, default: 1) */
|
|
36
|
+
xMultiplier?: number;
|
|
37
|
+
/** Vertical multiplier (1-10, default: 1) */
|
|
38
|
+
yMultiplier?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Barcode options
|
|
43
|
+
*/
|
|
44
|
+
export interface BarcodeOptions {
|
|
45
|
+
/** X position in dots */
|
|
46
|
+
x: number;
|
|
47
|
+
/** Y position in dots */
|
|
48
|
+
y: number;
|
|
49
|
+
/** Barcode type */
|
|
50
|
+
type: '128' | '39' | 'EAN13' | 'EAN8' | 'UPCA' | 'QRCODE';
|
|
51
|
+
/** Height in dots (default: 100) */
|
|
52
|
+
height?: number;
|
|
53
|
+
/** Narrow bar width (default: 2) */
|
|
54
|
+
narrow?: number;
|
|
55
|
+
/** Wide bar width (default: 4) */
|
|
56
|
+
wide?: number;
|
|
57
|
+
/** Show human-readable text (default: true) */
|
|
58
|
+
showText?: boolean;
|
|
59
|
+
/** Rotation (0, 90, 180, 270, default: 0) */
|
|
60
|
+
rotation?: 0 | 90 | 180 | 270;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* QR Code options
|
|
65
|
+
*/
|
|
66
|
+
export interface QRCodeOptions {
|
|
67
|
+
/** X position in dots */
|
|
68
|
+
x: number;
|
|
69
|
+
/** Y position in dots */
|
|
70
|
+
y: number;
|
|
71
|
+
/** Error correction level (L, M, Q, H, default: M) */
|
|
72
|
+
eccLevel?: 'L' | 'M' | 'Q' | 'H';
|
|
73
|
+
/** Cell width (1-10, default: 6) */
|
|
74
|
+
cellWidth?: number;
|
|
75
|
+
/** Mode (A=Auto, M=Manual, default: A) */
|
|
76
|
+
mode?: 'A' | 'M';
|
|
77
|
+
/** Rotation (0, 90, 180, 270, default: 0) */
|
|
78
|
+
rotation?: 0 | 90 | 180 | 270;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Box/Rectangle options
|
|
83
|
+
*/
|
|
84
|
+
export interface BoxOptions {
|
|
85
|
+
/** X position in dots */
|
|
86
|
+
x: number;
|
|
87
|
+
/** Y position in dots */
|
|
88
|
+
y: number;
|
|
89
|
+
/** Width in dots */
|
|
90
|
+
width: number;
|
|
91
|
+
/** Height in dots */
|
|
92
|
+
height: number;
|
|
93
|
+
/** Line thickness (default: 2) */
|
|
94
|
+
thickness?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Line options
|
|
99
|
+
*/
|
|
100
|
+
export interface LineOptions {
|
|
101
|
+
/** Start X position in dots */
|
|
102
|
+
x1: number;
|
|
103
|
+
/** Start Y position in dots */
|
|
104
|
+
y1: number;
|
|
105
|
+
/** End X position in dots */
|
|
106
|
+
x2: number;
|
|
107
|
+
/** End Y position in dots */
|
|
108
|
+
y2: number;
|
|
109
|
+
/** Line thickness (default: 2) */
|
|
110
|
+
thickness?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* TSPL Driver for label printers
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const tspl = new TsplDriver();
|
|
119
|
+
*
|
|
120
|
+
* const commands = tspl
|
|
121
|
+
* .size(60, 40)
|
|
122
|
+
* .gap(3)
|
|
123
|
+
* .clear()
|
|
124
|
+
* .text('Product Name', { x: 50, y: 50, font: 3 })
|
|
125
|
+
* .barcode('1234567890', { x: 50, y: 100, type: '128' })
|
|
126
|
+
* .print(1)
|
|
127
|
+
* .getBuffer();
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export class TsplDriver {
|
|
131
|
+
private commands: string[] = [];
|
|
132
|
+
private readonly logger = Logger.scope('TsplDriver');
|
|
133
|
+
private dpi = 203; // Default DPI for most label printers
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Set printer DPI (dots per inch)
|
|
137
|
+
* @param dpi - DPI value (203 or 300)
|
|
138
|
+
*/
|
|
139
|
+
setDPI(dpi: 203 | 300): this {
|
|
140
|
+
this.dpi = dpi;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Convert mm to dots
|
|
146
|
+
*/
|
|
147
|
+
mmToDots(mm: number): number {
|
|
148
|
+
return Math.round((mm * this.dpi) / 25.4);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Convert dots to mm
|
|
153
|
+
*/
|
|
154
|
+
dotsToMm(dots: number): number {
|
|
155
|
+
return (dots * 25.4) / this.dpi;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Set label size
|
|
160
|
+
* @param width - Label width in mm
|
|
161
|
+
* @param height - Label height in mm
|
|
162
|
+
*/
|
|
163
|
+
size(width: number, height: number): this {
|
|
164
|
+
this.commands.push(`SIZE ${width} mm, ${height} mm`);
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Set gap between labels
|
|
170
|
+
* @param gap - Gap size in mm
|
|
171
|
+
* @param offset - Offset in mm (default: 0)
|
|
172
|
+
*/
|
|
173
|
+
gap(gap: number, offset = 0): this {
|
|
174
|
+
this.commands.push(`GAP ${gap} mm, ${offset} mm`);
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Set print speed
|
|
180
|
+
* @param speed - Speed level (1-10)
|
|
181
|
+
*/
|
|
182
|
+
speed(speed: number): this {
|
|
183
|
+
this.commands.push(`SPEED ${Math.min(10, Math.max(1, speed))}`);
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Set print density
|
|
189
|
+
* @param density - Density level (0-15)
|
|
190
|
+
*/
|
|
191
|
+
density(density: number): this {
|
|
192
|
+
this.commands.push(`DENSITY ${Math.min(15, Math.max(0, density))}`);
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Set print direction
|
|
198
|
+
* @param direction - 0=normal, 1=reversed
|
|
199
|
+
*/
|
|
200
|
+
direction(direction: 0 | 1): this {
|
|
201
|
+
this.commands.push(`DIRECTION ${direction}`);
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Clear image buffer
|
|
207
|
+
*/
|
|
208
|
+
clear(): this {
|
|
209
|
+
this.commands.push('CLS');
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Add text to label
|
|
215
|
+
* @param content - Text content
|
|
216
|
+
* @param options - Text options
|
|
217
|
+
*/
|
|
218
|
+
text(content: string, options: TextOptions): this {
|
|
219
|
+
const {
|
|
220
|
+
x,
|
|
221
|
+
y,
|
|
222
|
+
font = 2,
|
|
223
|
+
rotation = 0,
|
|
224
|
+
xMultiplier = 1,
|
|
225
|
+
yMultiplier = 1,
|
|
226
|
+
} = options;
|
|
227
|
+
|
|
228
|
+
// TEXT x, y, "font", rotation, x-mul, y-mul, "content"
|
|
229
|
+
this.commands.push(
|
|
230
|
+
`TEXT ${x},${y},"${font}",${rotation},${xMultiplier},${yMultiplier},"${this.escapeString(content)}"`
|
|
231
|
+
);
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Add barcode to label
|
|
237
|
+
* @param content - Barcode content
|
|
238
|
+
* @param options - Barcode options
|
|
239
|
+
*/
|
|
240
|
+
barcode(content: string, options: BarcodeOptions): this {
|
|
241
|
+
const {
|
|
242
|
+
x,
|
|
243
|
+
y,
|
|
244
|
+
type,
|
|
245
|
+
height = 100,
|
|
246
|
+
narrow = 2,
|
|
247
|
+
wide = 4,
|
|
248
|
+
showText = true,
|
|
249
|
+
rotation = 0,
|
|
250
|
+
} = options;
|
|
251
|
+
|
|
252
|
+
const readable = showText ? 1 : 0;
|
|
253
|
+
|
|
254
|
+
if (type === 'QRCODE') {
|
|
255
|
+
// Use QRCODE command instead
|
|
256
|
+
return this.qrcode(content, { x, y, rotation });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// BARCODE x, y, "type", height, readable, rotation, narrow, wide, "content"
|
|
260
|
+
this.commands.push(
|
|
261
|
+
`BARCODE ${x},${y},"${type}",${height},${readable},${rotation},${narrow},${wide},"${content}"`
|
|
262
|
+
);
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Add QR code to label
|
|
268
|
+
* @param content - QR code content
|
|
269
|
+
* @param options - QR code options
|
|
270
|
+
*/
|
|
271
|
+
qrcode(content: string, options: QRCodeOptions): this {
|
|
272
|
+
const {
|
|
273
|
+
x,
|
|
274
|
+
y,
|
|
275
|
+
eccLevel = 'M',
|
|
276
|
+
cellWidth = 6,
|
|
277
|
+
mode = 'A',
|
|
278
|
+
rotation = 0,
|
|
279
|
+
} = options;
|
|
280
|
+
|
|
281
|
+
// QRCODE x, y, ECC level, cell width, mode, rotation, "content"
|
|
282
|
+
this.commands.push(
|
|
283
|
+
`QRCODE ${x},${y},${eccLevel},${cellWidth},${mode},${rotation},"${this.escapeString(content)}"`
|
|
284
|
+
);
|
|
285
|
+
return this;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Draw a box/rectangle
|
|
290
|
+
* @param options - Box options
|
|
291
|
+
*/
|
|
292
|
+
box(options: BoxOptions): this {
|
|
293
|
+
const { x, y, width, height, thickness = 2 } = options;
|
|
294
|
+
this.commands.push(`BOX ${x},${y},${x + width},${y + height},${thickness}`);
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Draw a line
|
|
300
|
+
* @param options - Line options
|
|
301
|
+
*/
|
|
302
|
+
line(options: LineOptions): this {
|
|
303
|
+
const { x1, y1, x2, y2, thickness = 2 } = options;
|
|
304
|
+
|
|
305
|
+
if (x1 === x2 || y1 === y2) {
|
|
306
|
+
// Horizontal or vertical line - use BAR command
|
|
307
|
+
const width = Math.abs(x2 - x1) || thickness;
|
|
308
|
+
const height = Math.abs(y2 - y1) || thickness;
|
|
309
|
+
this.commands.push(`BAR ${Math.min(x1, x2)},${Math.min(y1, y2)},${width},${height}`);
|
|
310
|
+
} else {
|
|
311
|
+
// Diagonal line - use DIAGONAL command if supported
|
|
312
|
+
this.commands.push(`DIAGONAL ${x1},${y1},${thickness},${Math.sqrt((x2-x1)**2 + (y2-y1)**2)},${Math.atan2(y2-y1, x2-x1) * 180 / Math.PI}`);
|
|
313
|
+
}
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Fill a rectangular area
|
|
319
|
+
* @param x - X position
|
|
320
|
+
* @param y - Y position
|
|
321
|
+
* @param width - Width
|
|
322
|
+
* @param height - Height
|
|
323
|
+
*/
|
|
324
|
+
bar(x: number, y: number, width: number, height: number): this {
|
|
325
|
+
this.commands.push(`BAR ${x},${y},${width},${height}`);
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Reverse a rectangular area (white becomes black and vice versa)
|
|
331
|
+
* @param x - X position
|
|
332
|
+
* @param y - Y position
|
|
333
|
+
* @param width - Width
|
|
334
|
+
* @param height - Height
|
|
335
|
+
*/
|
|
336
|
+
reverse(x: number, y: number, width: number, height: number): this {
|
|
337
|
+
this.commands.push(`REVERSE ${x},${y},${width},${height}`);
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Print the label
|
|
343
|
+
* @param copies - Number of copies (default: 1)
|
|
344
|
+
* @param sets - Number of sets (default: 1)
|
|
345
|
+
*/
|
|
346
|
+
print(copies = 1, sets = 1): this {
|
|
347
|
+
this.commands.push(`PRINT ${copies},${sets}`);
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Feed labels
|
|
353
|
+
* @param count - Number of labels to feed
|
|
354
|
+
*/
|
|
355
|
+
feed(count = 1): this {
|
|
356
|
+
this.commands.push(`FORMFEED`);
|
|
357
|
+
for (let i = 1; i < count; i++) {
|
|
358
|
+
this.commands.push(`FORMFEED`);
|
|
359
|
+
}
|
|
360
|
+
return this;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Cut paper (if cutter available)
|
|
365
|
+
*/
|
|
366
|
+
cut(): this {
|
|
367
|
+
this.commands.push('CUT');
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Beep the buzzer
|
|
373
|
+
*/
|
|
374
|
+
beep(): this {
|
|
375
|
+
this.commands.push('BEEP');
|
|
376
|
+
return this;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Home the print head
|
|
381
|
+
*/
|
|
382
|
+
home(): this {
|
|
383
|
+
this.commands.push('HOME');
|
|
384
|
+
return this;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Escape special characters in string
|
|
389
|
+
*/
|
|
390
|
+
private escapeString(str: string): string {
|
|
391
|
+
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Get all commands as string
|
|
396
|
+
*/
|
|
397
|
+
getCommands(): string {
|
|
398
|
+
return this.commands.join('\r\n') + '\r\n';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get commands as buffer for sending to printer
|
|
403
|
+
*/
|
|
404
|
+
getBuffer(): Uint8Array {
|
|
405
|
+
const commandString = this.getCommands();
|
|
406
|
+
this.logger.debug(`TSPL commands:\n${commandString}`);
|
|
407
|
+
return Encoding.encode(commandString, 'ASCII');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Clear all commands
|
|
412
|
+
*/
|
|
413
|
+
reset(): this {
|
|
414
|
+
this.commands = [];
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,15 @@ export { EventEmitter } from './core/EventEmitter';
|
|
|
12
12
|
|
|
13
13
|
// Drivers
|
|
14
14
|
export { EscPos } from './drivers/EscPos';
|
|
15
|
+
export { TsplDriver } from './drivers/TsplDriver';
|
|
16
|
+
export type {
|
|
17
|
+
LabelSize,
|
|
18
|
+
TextOptions as TsplTextOptions,
|
|
19
|
+
BarcodeOptions as TsplBarcodeOptions,
|
|
20
|
+
QRCodeOptions as TsplQRCodeOptions,
|
|
21
|
+
BoxOptions,
|
|
22
|
+
LineOptions,
|
|
23
|
+
} from './drivers/TsplDriver';
|
|
15
24
|
|
|
16
25
|
// Adapters
|
|
17
26
|
export { TaroAdapter } from './adapters/TaroAdapter';
|
|
@@ -90,5 +99,10 @@ export type {
|
|
|
90
99
|
LoggingConfig,
|
|
91
100
|
} from './config/PrinterConfig';
|
|
92
101
|
|
|
102
|
+
// Plugin System
|
|
103
|
+
export { PluginManager } from './plugins/PluginManager';
|
|
104
|
+
export { createLoggingPlugin, createRetryPlugin } from './plugins';
|
|
105
|
+
export type { Plugin, PluginHooks, PluginOptions, PluginFactory } from './plugins/types';
|
|
106
|
+
|
|
93
107
|
// Types
|
|
94
108
|
export * from './types';
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Manager
|
|
3
|
+
* Manages plugin lifecycle and hook execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Plugin, PluginHooks, PluginOptions } from './types';
|
|
7
|
+
import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
|
|
8
|
+
import { PrinterState } from '@/types';
|
|
9
|
+
import { Logger } from '@/utils/logger';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manages plugins for BluetoothPrinter
|
|
13
|
+
*/
|
|
14
|
+
export class PluginManager {
|
|
15
|
+
private plugins: Map<string, Plugin> = new Map();
|
|
16
|
+
private readonly logger = Logger.scope('PluginManager');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register a plugin
|
|
20
|
+
* @param plugin - Plugin to register
|
|
21
|
+
* @param options - Plugin options
|
|
22
|
+
* @throws {BluetoothPrintError} If plugin with same name already exists
|
|
23
|
+
*/
|
|
24
|
+
async register(plugin: Plugin, options?: PluginOptions): Promise<void> {
|
|
25
|
+
if (this.plugins.has(plugin.name)) {
|
|
26
|
+
throw new BluetoothPrintError(
|
|
27
|
+
ErrorCode.INVALID_CONFIGURATION,
|
|
28
|
+
`Plugin "${plugin.name}" is already registered`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.logger.info(`Registering plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`);
|
|
33
|
+
|
|
34
|
+
if (plugin.init) {
|
|
35
|
+
await plugin.init(options);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.plugins.set(plugin.name, plugin);
|
|
39
|
+
this.logger.debug(`Plugin registered: ${plugin.name}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Unregister a plugin
|
|
44
|
+
* @param name - Plugin name to unregister
|
|
45
|
+
*/
|
|
46
|
+
async unregister(name: string): Promise<void> {
|
|
47
|
+
const plugin = this.plugins.get(name);
|
|
48
|
+
if (!plugin) {
|
|
49
|
+
this.logger.warn(`Plugin not found: ${name}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (plugin.destroy) {
|
|
54
|
+
await plugin.destroy();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.plugins.delete(name);
|
|
58
|
+
this.logger.info(`Plugin unregistered: ${name}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get a registered plugin
|
|
63
|
+
* @param name - Plugin name
|
|
64
|
+
* @returns Plugin instance or undefined
|
|
65
|
+
*/
|
|
66
|
+
get(name: string): Plugin | undefined {
|
|
67
|
+
return this.plugins.get(name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get all registered plugin names
|
|
72
|
+
* @returns Array of plugin names
|
|
73
|
+
*/
|
|
74
|
+
getNames(): string[] {
|
|
75
|
+
return Array.from(this.plugins.keys());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a plugin is registered
|
|
80
|
+
* @param name - Plugin name
|
|
81
|
+
* @returns True if registered
|
|
82
|
+
*/
|
|
83
|
+
has(name: string): boolean {
|
|
84
|
+
return this.plugins.has(name);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Execute a hook across all plugins
|
|
89
|
+
* @param hookName - Name of the hook to execute
|
|
90
|
+
* @param args - Arguments to pass to the hook
|
|
91
|
+
* @returns Result from hooks (last non-void result)
|
|
92
|
+
*/
|
|
93
|
+
async executeHook<K extends keyof PluginHooks>(
|
|
94
|
+
hookName: K,
|
|
95
|
+
...args: Parameters<NonNullable<PluginHooks[K]>>
|
|
96
|
+
): Promise<unknown> {
|
|
97
|
+
let result: unknown = undefined;
|
|
98
|
+
|
|
99
|
+
for (const [name, plugin] of this.plugins) {
|
|
100
|
+
const hook = plugin.hooks[hookName];
|
|
101
|
+
if (hook) {
|
|
102
|
+
try {
|
|
103
|
+
// @ts-expect-error - TypeScript can't infer the correct types here
|
|
104
|
+
const hookResult = await hook(...args);
|
|
105
|
+
if (hookResult !== undefined) {
|
|
106
|
+
result = hookResult;
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.logger.error(`Plugin "${name}" hook "${hookName}" failed:`, error);
|
|
110
|
+
// Continue to next plugin
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Execute beforeConnect hooks
|
|
120
|
+
*/
|
|
121
|
+
async beforeConnect(deviceId: string): Promise<string> {
|
|
122
|
+
const result = await this.executeHook('beforeConnect', deviceId);
|
|
123
|
+
return (result as string) || deviceId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Execute afterConnect hooks
|
|
128
|
+
*/
|
|
129
|
+
async afterConnect(deviceId: string): Promise<void> {
|
|
130
|
+
await this.executeHook('afterConnect', deviceId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Execute beforeDisconnect hooks
|
|
135
|
+
*/
|
|
136
|
+
async beforeDisconnect(deviceId: string): Promise<void> {
|
|
137
|
+
await this.executeHook('beforeDisconnect', deviceId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Execute afterDisconnect hooks
|
|
142
|
+
*/
|
|
143
|
+
async afterDisconnect(deviceId: string): Promise<void> {
|
|
144
|
+
await this.executeHook('afterDisconnect', deviceId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Execute beforePrint hooks
|
|
149
|
+
*/
|
|
150
|
+
async beforePrint(buffer: Uint8Array): Promise<Uint8Array> {
|
|
151
|
+
const result = await this.executeHook('beforePrint', buffer);
|
|
152
|
+
return (result as Uint8Array) || buffer;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Execute afterPrint hooks
|
|
157
|
+
*/
|
|
158
|
+
async afterPrint(bytesSent: number): Promise<void> {
|
|
159
|
+
await this.executeHook('afterPrint', bytesSent);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Execute onError hooks
|
|
164
|
+
* @returns True if error should be suppressed
|
|
165
|
+
*/
|
|
166
|
+
async onError(error: BluetoothPrintError): Promise<boolean> {
|
|
167
|
+
const result = await this.executeHook('onError', error);
|
|
168
|
+
return result === true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Execute onStateChange hooks
|
|
173
|
+
*/
|
|
174
|
+
async onStateChange(state: PrinterState, previousState: PrinterState): Promise<void> {
|
|
175
|
+
await this.executeHook('onStateChange', state, previousState);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Execute onProgress hooks
|
|
180
|
+
*/
|
|
181
|
+
async onProgress(sent: number, total: number): Promise<void> {
|
|
182
|
+
await this.executeHook('onProgress', sent, total);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Clear all plugins
|
|
187
|
+
*/
|
|
188
|
+
async clear(): Promise<void> {
|
|
189
|
+
for (const name of this.plugins.keys()) {
|
|
190
|
+
await this.unregister(name);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|