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.
Files changed (35) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +128 -14
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.es.js +1 -81644
  5. package/dist/index.umd.js +1 -1
  6. package/dist/types/adapters/AdapterFactory.d.ts +0 -1
  7. package/dist/types/adapters/AlipayAdapter.d.ts +0 -1
  8. package/dist/types/adapters/BaiduAdapter.d.ts +0 -1
  9. package/dist/types/adapters/BaseAdapter.d.ts +0 -1
  10. package/dist/types/adapters/ByteDanceAdapter.d.ts +0 -1
  11. package/dist/types/adapters/TaroAdapter.d.ts +0 -1
  12. package/dist/types/adapters/WebBluetoothAdapter.d.ts +0 -1
  13. package/dist/types/config/PrinterConfig.d.ts +0 -1
  14. package/dist/types/core/BluetoothPrinter.d.ts +0 -1
  15. package/dist/types/drivers/EscPos.d.ts +0 -1
  16. package/dist/types/drivers/TsplDriver.d.ts +251 -0
  17. package/dist/types/index.d.ts +5 -0
  18. package/dist/types/plugins/PluginManager.d.ts +87 -0
  19. package/dist/types/plugins/builtin/LoggingPlugin.d.ts +14 -0
  20. package/dist/types/plugins/builtin/RetryPlugin.d.ts +18 -0
  21. package/dist/types/plugins/index.d.ts +7 -0
  22. package/dist/types/plugins/types.d.ts +97 -0
  23. package/dist/types/services/CommandBuilder.d.ts +0 -1
  24. package/dist/types/services/ConnectionManager.d.ts +0 -1
  25. package/dist/types/services/PrintJobManager.d.ts +0 -1
  26. package/dist/types/services/interfaces/index.d.ts +0 -1
  27. package/dist/types/template/TemplateEngine.d.ts +0 -1
  28. package/package.json +15 -17
  29. package/src/drivers/TsplDriver.ts +417 -0
  30. package/src/index.ts +14 -0
  31. package/src/plugins/PluginManager.ts +193 -0
  32. package/src/plugins/builtin/LoggingPlugin.ts +97 -0
  33. package/src/plugins/builtin/RetryPlugin.ts +109 -0
  34. package/src/plugins/index.ts +10 -0
  35. 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
+ }