taro-bluetooth-print 2.1.0 → 2.1.1

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.
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Command Builder Service
3
+ *
4
+ * Builds print commands using the printer driver
5
+ */
6
+
7
+ import { Service } from 'typedi';
8
+ import type { IPrinterDriver, IQrOptions } from '@/types';
9
+ import { ICommandBuilder } from './interfaces';
10
+ import { Logger } from '@/utils/logger';
11
+ import { EscPos } from '@/drivers/EscPos';
12
+
13
+ /**
14
+ * Command Builder implementation
15
+ */
16
+ @Service()
17
+ export class CommandBuilder implements ICommandBuilder {
18
+ private driver: IPrinterDriver;
19
+ private buffer: Uint8Array[] = [];
20
+ private readonly logger = Logger.scope('CommandBuilder');
21
+
22
+ /**
23
+ * Creates a new CommandBuilder instance
24
+ *
25
+ * @param driver - Printer driver instance
26
+ */
27
+ constructor(driver?: IPrinterDriver) {
28
+ this.driver = driver || new EscPos();
29
+
30
+ // Initialize printer with ESC/POS init command
31
+ this.buffer.push(...this.driver.init());
32
+ }
33
+
34
+ /**
35
+ * Adds text to the print queue
36
+ *
37
+ * @param content - Text content
38
+ * @param encoding - Text encoding
39
+ * @returns this - For method chaining
40
+ */
41
+ text(content: string, encoding?: string): this {
42
+ this.logger.debug('Adding text:', content.substring(0, 50));
43
+ this.buffer.push(...this.driver.text(content, encoding));
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Adds line feeds to the print queue
49
+ *
50
+ * @param lines - Number of lines to feed
51
+ * @returns this - For method chaining
52
+ */
53
+ feed(lines = 1): this {
54
+ this.logger.debug('Adding feed:', lines);
55
+ this.buffer.push(...this.driver.feed(lines));
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Adds a paper cut command to the print queue
61
+ *
62
+ * @returns this - For method chaining
63
+ */
64
+ cut(): this {
65
+ this.logger.debug('Adding cut command');
66
+ this.buffer.push(...this.driver.cut());
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Adds an image to the print queue
72
+ *
73
+ * @param data - Image data as Uint8Array
74
+ * @param width - Image width
75
+ * @param height - Image height
76
+ * @returns this - For method chaining
77
+ */
78
+ image(data: Uint8Array, width: number, height: number): this {
79
+ this.logger.debug(`Adding image: ${width}x${height}`);
80
+ this.buffer.push(...this.driver.image(data, width, height));
81
+ return this;
82
+ }
83
+
84
+ /**
85
+ * Adds a QR code to the print queue
86
+ *
87
+ * @param content - QR code content
88
+ * @param options - QR code options
89
+ * @returns this - For method chaining
90
+ */
91
+ qr(content: string, options?: IQrOptions): this {
92
+ this.logger.debug('Adding QR code:', content.substring(0, 50));
93
+ this.buffer.push(...this.driver.qr(content, options));
94
+ return this;
95
+ }
96
+
97
+ /**
98
+ * Clears the print queue
99
+ *
100
+ * @returns this - For method chaining
101
+ */
102
+ clear(): this {
103
+ this.logger.debug('Clearing buffer');
104
+ this.buffer = [];
105
+ // Re-initialize printer
106
+ this.buffer.push(...this.driver.init());
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Gets the current buffer
112
+ *
113
+ * @returns Uint8Array - Current print buffer
114
+ */
115
+ getBuffer(): Uint8Array {
116
+ // Combine all buffers
117
+ const totalLength = this.buffer.reduce((acc, b) => acc + b.length, 0);
118
+ const combined = new Uint8Array(totalLength);
119
+ let offset = 0;
120
+ for (const b of this.buffer) {
121
+ combined.set(b, offset);
122
+ offset += b.length;
123
+ }
124
+ return combined;
125
+ }
126
+
127
+ /**
128
+ * Gets the total number of bytes in the buffer
129
+ *
130
+ * @returns number - Total bytes
131
+ */
132
+ getTotalBytes(): number {
133
+ return this.buffer.reduce((acc, b) => acc + b.length, 0);
134
+ }
135
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Connection Manager Service
3
+ *
4
+ * Manages Bluetooth device connections
5
+ */
6
+
7
+ import { Service } from 'typedi';
8
+ import type { IPrinterAdapter } from '@/types';
9
+ import { PrinterState } from '@/types';
10
+ import { IConnectionManager } from './interfaces';
11
+ import { AdapterFactory } from '@/adapters/AdapterFactory';
12
+ import { Logger } from '@/utils/logger';
13
+ import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
14
+
15
+ /**
16
+ * Connection Manager implementation
17
+ */
18
+ @Service()
19
+ export class ConnectionManager implements IConnectionManager {
20
+ private adapter: IPrinterAdapter;
21
+ private deviceId: string | null = null;
22
+ private state: PrinterState = PrinterState.DISCONNECTED;
23
+ private readonly logger = Logger.scope('ConnectionManager');
24
+
25
+ /**
26
+ * Creates a new ConnectionManager instance
27
+ */
28
+ constructor(adapter?: IPrinterAdapter) {
29
+ this.adapter = adapter || AdapterFactory.create();
30
+
31
+ // Listen to adapter state changes
32
+ this.adapter.onStateChange?.(state => {
33
+ this.state = state;
34
+ this.logger.debug('State changed:', state);
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Connects to a Bluetooth device
40
+ *
41
+ * @param deviceId - Bluetooth device ID
42
+ * @returns Promise<void>
43
+ */
44
+ async connect(deviceId: string): Promise<void> {
45
+ this.logger.info('Connecting to device:', deviceId);
46
+
47
+ try {
48
+ this.deviceId = deviceId;
49
+ await this.adapter.connect(deviceId);
50
+ this.state = PrinterState.CONNECTED;
51
+ this.logger.info('Connected successfully');
52
+ } catch (error) {
53
+ this.deviceId = null;
54
+ this.state = PrinterState.DISCONNECTED;
55
+ const printError =
56
+ error instanceof BluetoothPrintError
57
+ ? error
58
+ : new BluetoothPrintError(
59
+ ErrorCode.CONNECTION_FAILED,
60
+ 'Connection failed',
61
+ error as Error
62
+ );
63
+ this.logger.error('Connection failed:', printError);
64
+ throw printError;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Disconnects from the current device
70
+ *
71
+ * @returns Promise<void>
72
+ */
73
+ async disconnect(): Promise<void> {
74
+ if (!this.deviceId) {
75
+ this.logger.warn('Disconnect called but no device connected');
76
+ return;
77
+ }
78
+
79
+ const deviceId = this.deviceId;
80
+ this.logger.info('Disconnecting from device:', deviceId);
81
+
82
+ try {
83
+ await this.adapter.disconnect(deviceId);
84
+ this.deviceId = null;
85
+ this.state = PrinterState.DISCONNECTED;
86
+ this.logger.info('Disconnected successfully');
87
+ } catch (error) {
88
+ const printError = new BluetoothPrintError(
89
+ ErrorCode.DEVICE_DISCONNECTED,
90
+ 'Disconnect failed',
91
+ error as Error
92
+ );
93
+ this.logger.error('Disconnect failed:', printError);
94
+ throw printError;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Checks if a device is connected
100
+ *
101
+ * @returns boolean - True if connected, false otherwise
102
+ */
103
+ isConnected(): boolean {
104
+ return this.state === PrinterState.CONNECTED;
105
+ }
106
+
107
+ /**
108
+ * Gets the current device ID
109
+ *
110
+ * @returns string | null - Device ID or null if not connected
111
+ */
112
+ getDeviceId(): string | null {
113
+ return this.deviceId;
114
+ }
115
+
116
+ /**
117
+ * Gets the current connection state
118
+ *
119
+ * @returns PrinterState - Current state
120
+ */
121
+ getState(): PrinterState {
122
+ return this.state;
123
+ }
124
+
125
+ /**
126
+ * Gets the printer adapter instance
127
+ *
128
+ * @returns IPrinterAdapter - Printer adapter
129
+ */
130
+ getAdapter(): IPrinterAdapter {
131
+ return this.adapter;
132
+ }
133
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Print Job Manager Service
3
+ *
4
+ * Manages print jobs, including pause/resume/cancel functionality
5
+ */
6
+
7
+ import { Service } from 'typedi';
8
+ import type { IPrinterAdapter } from '@/types';
9
+ import { IAdapterOptions, PrinterState } from '@/types';
10
+ import { IPrintJobManager, IConnectionManager } from './interfaces';
11
+ import { Logger } from '@/utils/logger';
12
+ import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
13
+
14
+ /**
15
+ * Print Job Manager implementation
16
+ */
17
+ @Service()
18
+ export class PrintJobManager implements IPrintJobManager {
19
+ private adapter: IPrinterAdapter;
20
+ private connectionManager: IConnectionManager;
21
+ private jobBuffer: Uint8Array | null = null;
22
+ private jobOffset = 0;
23
+ private _isPaused = false;
24
+ private _isInProgress = false;
25
+ private adapterOptions: IAdapterOptions = {};
26
+ private readonly logger = Logger.scope('PrintJobManager');
27
+
28
+ /**
29
+ * Creates a new PrintJobManager instance
30
+ *
31
+ * @param connectionManagerOrAdapter - Connection manager instance or adapter instance (for backward compatibility)
32
+ * @param adapter - Printer adapter instance (optional, will be taken from connectionManager if not provided)
33
+ */
34
+ constructor(
35
+ connectionManagerOrAdapter?: IConnectionManager | IPrinterAdapter,
36
+ adapter?: IPrinterAdapter
37
+ ) {
38
+ // Handle case where adapter is explicitly provided
39
+ if (adapter) {
40
+ // New API: new PrintJobManager(connectionManager, adapter)
41
+ this.connectionManager = connectionManagerOrAdapter as IConnectionManager;
42
+ this.adapter = adapter;
43
+ }
44
+ // Handle backward compatibility for old API: new PrintJobManager(adapter, driver)
45
+ else if (
46
+ connectionManagerOrAdapter &&
47
+ typeof (connectionManagerOrAdapter as IPrinterAdapter).connect === 'function'
48
+ ) {
49
+ // Old API: new PrintJobManager(adapter)
50
+ this.adapter = connectionManagerOrAdapter as IPrinterAdapter;
51
+ // Create a minimal connection manager for backward compatibility
52
+ this.connectionManager = {
53
+ getDeviceId: () => 'test-device', // Default device ID for backward compatibility
54
+ isConnected: () => true,
55
+ getState: () => PrinterState.CONNECTED, // Default to CONNECTED
56
+ connect: () => Promise.resolve(),
57
+ disconnect: () => Promise.resolve(),
58
+ getAdapter: () => this.adapter,
59
+ };
60
+ }
61
+ // Handle new API: new PrintJobManager(connectionManager)
62
+ else {
63
+ // New API: dependency injection or manual service creation
64
+ this.connectionManager = connectionManagerOrAdapter as IConnectionManager;
65
+ // Try to get adapter from connectionManager
66
+ if (
67
+ this.connectionManager &&
68
+ typeof (
69
+ this.connectionManager as IConnectionManager & { getAdapter?: () => IPrinterAdapter }
70
+ ).getAdapter === 'function'
71
+ ) {
72
+ this.adapter = (
73
+ this.connectionManager as IConnectionManager & { getAdapter: () => IPrinterAdapter }
74
+ ).getAdapter();
75
+ } else {
76
+ throw new Error(
77
+ 'Printer adapter not provided and could not be retrieved from connection manager'
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Starts a print job
85
+ *
86
+ * @param buffer - Print data buffer
87
+ * @returns Promise<void>
88
+ */
89
+ async start(buffer: Uint8Array): Promise<void> {
90
+ if (this._isInProgress) {
91
+ throw new BluetoothPrintError(
92
+ ErrorCode.PRINT_JOB_IN_PROGRESS,
93
+ 'A print job is already in progress. Wait for completion or cancel it.'
94
+ );
95
+ }
96
+
97
+ this.logger.info(`Starting print job: ${buffer.length} bytes`);
98
+
99
+ this.jobBuffer = buffer;
100
+ this.jobOffset = 0;
101
+ this._isPaused = false;
102
+ this._isInProgress = true;
103
+
104
+ try {
105
+ await this.processJob();
106
+
107
+ // Check if the job was paused
108
+ if (this._isPaused) {
109
+ this.logger.info('Print job paused');
110
+ // Don't reset _isInProgress when paused, so resume() and cancel() can still work
111
+ } else {
112
+ // Print job completed successfully
113
+ this.logger.info('Print job completed successfully');
114
+ this._isInProgress = false;
115
+ this.jobBuffer = null;
116
+ this.jobOffset = 0;
117
+ }
118
+ } catch (error) {
119
+ this.logger.error('Print job failed:', error);
120
+ this._isInProgress = false;
121
+ this.jobBuffer = null;
122
+ this.jobOffset = 0;
123
+
124
+ const printError =
125
+ error instanceof BluetoothPrintError
126
+ ? error
127
+ : new BluetoothPrintError(ErrorCode.PRINT_JOB_FAILED, 'Print job failed', error as Error);
128
+ throw printError;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Pauses the current print job
134
+ */
135
+ pause(): void {
136
+ if (!this._isInProgress) {
137
+ this.logger.warn('Pause called but no print job in progress');
138
+ return;
139
+ }
140
+
141
+ this._isPaused = true;
142
+ this.logger.info('Print job paused');
143
+ }
144
+
145
+ /**
146
+ * Resumes a paused print job
147
+ *
148
+ * @returns Promise<void>
149
+ */
150
+ async resume(): Promise<void> {
151
+ if (!this._isInProgress || !this._isPaused) {
152
+ this.logger.warn('Resume called but no paused print job');
153
+ return;
154
+ }
155
+
156
+ this._isPaused = false;
157
+ this.logger.info('Print job resumed');
158
+
159
+ try {
160
+ await this.processJob();
161
+
162
+ if (!this._isPaused) {
163
+ // Print job completed successfully
164
+ this.logger.info('Print job completed successfully');
165
+ this._isInProgress = false;
166
+ this.jobBuffer = null;
167
+ this.jobOffset = 0;
168
+ }
169
+ } catch (error) {
170
+ this.logger.error('Print job failed after resume:', error);
171
+ this._isInProgress = false;
172
+ this.jobBuffer = null;
173
+ this.jobOffset = 0;
174
+
175
+ const printError =
176
+ error instanceof BluetoothPrintError
177
+ ? error
178
+ : new BluetoothPrintError(ErrorCode.PRINT_JOB_FAILED, 'Print job failed', error as Error);
179
+ throw printError;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Cancels the current print job
185
+ */
186
+ cancel(): void {
187
+ if (!this._isInProgress) {
188
+ this.logger.warn('Cancel called but no print job in progress');
189
+ return;
190
+ }
191
+
192
+ this._isPaused = false;
193
+ this._isInProgress = false;
194
+ this.jobBuffer = null;
195
+ this.jobOffset = 0;
196
+ this.logger.info('Print job cancelled');
197
+ }
198
+
199
+ /**
200
+ * Gets the number of bytes remaining to print
201
+ *
202
+ * @returns number - Bytes remaining
203
+ */
204
+ remaining(): number {
205
+ if (this.jobBuffer) {
206
+ return this.jobBuffer.length - this.jobOffset;
207
+ }
208
+ return 0;
209
+ }
210
+
211
+ /**
212
+ * Checks if the print job is paused
213
+ *
214
+ * @returns boolean - True if paused, false otherwise
215
+ */
216
+ isPaused(): boolean {
217
+ return this._isPaused;
218
+ }
219
+
220
+ /**
221
+ * Checks if a print job is in progress
222
+ *
223
+ * @returns boolean - True if in progress, false otherwise
224
+ */
225
+ isInProgress(): boolean {
226
+ return this._isInProgress;
227
+ }
228
+
229
+ /**
230
+ * Sets adapter options for write operations
231
+ *
232
+ * @param options - Adapter options
233
+ */
234
+ setOptions(options: IAdapterOptions): void {
235
+ this.adapterOptions = { ...this.adapterOptions, ...options };
236
+ this.logger.debug('Adapter options updated:', this.adapterOptions);
237
+ }
238
+
239
+ /**
240
+ * Processes the print job in chunks
241
+ * Supports pause/resume functionality
242
+ *
243
+ * @returns Promise<void>
244
+ */
245
+ private async processJob(): Promise<void> {
246
+ if (!this.jobBuffer) {
247
+ return;
248
+ }
249
+
250
+ // Check if adapter has write method
251
+ if (!this.adapter || typeof this.adapter.write !== 'function') {
252
+ throw new BluetoothPrintError(
253
+ ErrorCode.INVALID_CONFIGURATION,
254
+ 'Printer adapter does not support write operation'
255
+ );
256
+ }
257
+
258
+ const printerChunkSize = 512;
259
+ const total = this.jobBuffer.length;
260
+ const jobBuffer = this.jobBuffer; // Cache to avoid closure issues
261
+
262
+ try {
263
+ while (this.jobOffset < jobBuffer.length) {
264
+ if (this._isPaused) {
265
+ this.logger.debug('Job paused at offset:', this.jobOffset);
266
+ return;
267
+ }
268
+
269
+ // Check device connection state
270
+ // Note: We rely on the adapter to handle connection state changes
271
+
272
+ const end = Math.min(this.jobOffset + printerChunkSize, jobBuffer.length);
273
+ const chunk = jobBuffer.slice(this.jobOffset, end);
274
+
275
+ await this.adapter.write(this.getDeviceId(), chunk.buffer, this.adapterOptions);
276
+
277
+ this.jobOffset = end;
278
+
279
+ this.logger.debug(`Processed ${this.jobOffset}/${total} bytes`);
280
+ }
281
+ } catch (error) {
282
+ this.logger.error('Error processing job:', error);
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Gets the current device ID from the connection manager
289
+ *
290
+ * @returns string - Device ID
291
+ * @throws BluetoothPrintError if no device is connected
292
+ */
293
+ private getDeviceId(): string {
294
+ const deviceId = this.connectionManager.getDeviceId();
295
+ if (!deviceId) {
296
+ throw new BluetoothPrintError(ErrorCode.DEVICE_DISCONNECTED, 'Device ID not available');
297
+ }
298
+ return deviceId;
299
+ }
300
+ }