taro-bluetooth-print 2.2.0 → 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 (49) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +128 -22
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.es.js +1 -6995
  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 +6 -34
  8. package/dist/types/adapters/BaiduAdapter.d.ts +6 -34
  9. package/dist/types/adapters/BaseAdapter.d.ts +112 -1
  10. package/dist/types/adapters/ByteDanceAdapter.d.ts +6 -34
  11. package/dist/types/adapters/TaroAdapter.d.ts +6 -34
  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/encoding/gbk-data.d.ts +12 -0
  18. package/dist/types/encoding/gbk-table.d.ts +5 -1
  19. package/dist/types/index.d.ts +5 -0
  20. package/dist/types/plugins/PluginManager.d.ts +87 -0
  21. package/dist/types/plugins/builtin/LoggingPlugin.d.ts +14 -0
  22. package/dist/types/plugins/builtin/RetryPlugin.d.ts +18 -0
  23. package/dist/types/plugins/index.d.ts +7 -0
  24. package/dist/types/plugins/types.d.ts +97 -0
  25. package/dist/types/services/CommandBuilder.d.ts +6 -1
  26. package/dist/types/services/ConnectionManager.d.ts +0 -1
  27. package/dist/types/services/PrintJobManager.d.ts +6 -2
  28. package/dist/types/services/interfaces/index.d.ts +0 -1
  29. package/dist/types/template/TemplateEngine.d.ts +0 -1
  30. package/package.json +16 -18
  31. package/src/adapters/AlipayAdapter.ts +8 -314
  32. package/src/adapters/BaiduAdapter.ts +8 -312
  33. package/src/adapters/BaseAdapter.ts +366 -0
  34. package/src/adapters/ByteDanceAdapter.ts +8 -316
  35. package/src/adapters/TaroAdapter.ts +8 -367
  36. package/src/core/EventEmitter.ts +9 -6
  37. package/src/drivers/TsplDriver.ts +417 -0
  38. package/src/encoding/gbk-data.ts +1911 -0
  39. package/src/encoding/gbk-table.ts +22 -498
  40. package/src/index.ts +14 -0
  41. package/src/plugins/PluginManager.ts +193 -0
  42. package/src/plugins/builtin/LoggingPlugin.ts +97 -0
  43. package/src/plugins/builtin/RetryPlugin.ts +109 -0
  44. package/src/plugins/index.ts +10 -0
  45. package/src/plugins/types.ts +119 -0
  46. package/src/preview/PreviewRenderer.ts +7 -1
  47. package/src/queue/PrintQueue.ts +10 -6
  48. package/src/services/CommandBuilder.ts +30 -0
  49. package/src/services/PrintJobManager.ts +51 -35
@@ -15,6 +15,52 @@ export interface ServiceInfo {
15
15
  characteristicId: string;
16
16
  }
17
17
 
18
+ /**
19
+ * BLE characteristic properties (common across all mini-program platforms)
20
+ */
21
+ export interface BLECharacteristicProperties {
22
+ write?: boolean;
23
+ writeWithoutResponse?: boolean;
24
+ read?: boolean;
25
+ notify?: boolean;
26
+ indicate?: boolean;
27
+ }
28
+
29
+ /**
30
+ * BLE characteristic (common across all mini-program platforms)
31
+ */
32
+ export interface BLECharacteristic {
33
+ uuid: string;
34
+ properties: BLECharacteristicProperties;
35
+ }
36
+
37
+ /**
38
+ * Unified mini-program BLE API interface.
39
+ * All mini-program platforms (Taro/WeChat, Alipay, Baidu, ByteDance)
40
+ * share the same API shape, only the global object differs.
41
+ */
42
+ export interface MiniProgramBLEApi {
43
+ createBLEConnection(options: { deviceId: string }): Promise<void>;
44
+ closeBLEConnection(options: { deviceId: string }): Promise<void>;
45
+ getBLEConnectionState(options: { deviceId: string }): Promise<{ connected: boolean }>;
46
+ writeBLECharacteristicValue(options: {
47
+ deviceId: string;
48
+ serviceId: string;
49
+ characteristicId: string;
50
+ value: ArrayBuffer;
51
+ }): Promise<void>;
52
+ getBLEDeviceServices(options: {
53
+ deviceId: string;
54
+ }): Promise<{ services: Array<{ uuid: string }> }>;
55
+ getBLEDeviceCharacteristics(options: {
56
+ deviceId: string;
57
+ serviceId: string;
58
+ }): Promise<{ characteristics: BLECharacteristic[] }>;
59
+ onBLEConnectionStateChange(
60
+ callback: (res: { deviceId: string; connected: boolean }) => void
61
+ ): void;
62
+ }
63
+
18
64
  /**
19
65
  * Base adapter class that provides common functionality for all platform-specific adapters
20
66
  */
@@ -140,3 +186,323 @@ export abstract class BaseAdapter implements IPrinterAdapter {
140
186
  this.serviceCache.delete(deviceId);
141
187
  }
142
188
  }
189
+
190
+ /**
191
+ * Base adapter for mini-program platforms (Taro/WeChat, Alipay, Baidu, ByteDance).
192
+ *
193
+ * Implements connect/disconnect/write/discoverServices once with adaptive transmission.
194
+ * Subclasses only need to implement `getApi()` to return the platform-specific BLE API object.
195
+ */
196
+ export abstract class MiniProgramAdapter extends BaseAdapter {
197
+ /**
198
+ * Returns the platform-specific BLE API object.
199
+ * Subclasses must implement this to return the appropriate global (Taro, my, swan, tt).
200
+ */
201
+ protected abstract getApi(): MiniProgramBLEApi;
202
+
203
+ /**
204
+ * Connects to a Bluetooth device and discovers services
205
+ *
206
+ * @param deviceId - Bluetooth device ID
207
+ * @throws {BluetoothPrintError} When connection fails
208
+ */
209
+ async connect(deviceId: string): Promise<void> {
210
+ this.validateDeviceId(deviceId);
211
+
212
+ // 检查是否已连接
213
+ if (this.isDeviceConnected(deviceId)) {
214
+ this.logger.warn('Device already connected:', deviceId);
215
+ this.updateState(PrinterState.CONNECTED);
216
+ return;
217
+ }
218
+
219
+ this.updateState(PrinterState.CONNECTING);
220
+ this.logger.debug('Connecting to device:', deviceId);
221
+
222
+ try {
223
+ // 添加连接超时处理
224
+ let timeoutId: NodeJS.Timeout | undefined;
225
+ const connectionPromise = this.getApi().createBLEConnection({ deviceId });
226
+ const timeoutPromise = new Promise((_, reject) => {
227
+ timeoutId = setTimeout(() => {
228
+ reject(new Error('Connection timeout after 10 seconds'));
229
+ }, 10000);
230
+ });
231
+
232
+ await Promise.race([connectionPromise, timeoutPromise]);
233
+ if (timeoutId) {
234
+ clearTimeout(timeoutId);
235
+ }
236
+ this.logger.info('BLE connection established');
237
+
238
+ // Discover and cache services
239
+ await this.discoverServices(deviceId);
240
+
241
+ this.updateState(PrinterState.CONNECTED);
242
+ this.logger.info('Device connected successfully');
243
+
244
+ // Listen for connection state changes
245
+ this.getApi().onBLEConnectionStateChange(
246
+ (res: { deviceId: string; connected: boolean }) => {
247
+ if (res.deviceId === deviceId && !res.connected) {
248
+ this.logger.warn('Device disconnected unexpectedly');
249
+ this.updateState(PrinterState.DISCONNECTED);
250
+ this.cleanupDevice(deviceId);
251
+ }
252
+ }
253
+ );
254
+ } catch (error) {
255
+ this.updateState(PrinterState.DISCONNECTED);
256
+ this.logger.error('Connection failed:', error);
257
+
258
+ const errorMessage = (error as Error).message || '';
259
+ if (errorMessage.includes('timeout')) {
260
+ throw new BluetoothPrintError(
261
+ ErrorCode.CONNECTION_TIMEOUT,
262
+ `Connection to device ${deviceId} timed out`,
263
+ error as Error
264
+ );
265
+ } else if (errorMessage.includes('not found')) {
266
+ throw new BluetoothPrintError(
267
+ ErrorCode.DEVICE_NOT_FOUND,
268
+ `Device ${deviceId} not found`,
269
+ error as Error
270
+ );
271
+ }
272
+
273
+ throw new BluetoothPrintError(
274
+ ErrorCode.CONNECTION_FAILED,
275
+ `Failed to connect to device ${deviceId}`,
276
+ error as Error
277
+ );
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Disconnects from a Bluetooth device
283
+ *
284
+ * @param deviceId - Bluetooth device ID
285
+ */
286
+ async disconnect(deviceId: string): Promise<void> {
287
+ this.validateDeviceId(deviceId);
288
+ this.updateState(PrinterState.DISCONNECTING);
289
+ this.logger.debug('Disconnecting from device:', deviceId);
290
+
291
+ try {
292
+ await this.getApi().closeBLEConnection({ deviceId });
293
+ this.cleanupDevice(deviceId);
294
+ this.updateState(PrinterState.DISCONNECTED);
295
+ this.logger.info('Device disconnected successfully');
296
+ } catch (error) {
297
+ this.logger.warn('Disconnect error (ignored):', error);
298
+ this.cleanupDevice(deviceId);
299
+ this.updateState(PrinterState.DISCONNECTED);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Writes data to the Bluetooth device in chunks with adaptive transmission.
305
+ *
306
+ * Features:
307
+ * - Automatic chunk size adjustment based on success/failure rate
308
+ * - Dynamic delay adjustment for congestion control
309
+ * - Periodic connection state checks
310
+ * - Exponential backoff for retries
311
+ * - Write timeout per chunk
312
+ *
313
+ * @param deviceId - Bluetooth device ID
314
+ * @param buffer - Data to write as ArrayBuffer
315
+ * @param options - Write options (chunk size, delay, retries)
316
+ * @throws {BluetoothPrintError} When write fails after retries
317
+ */
318
+ async write(deviceId: string, buffer: ArrayBuffer, options?: IAdapterOptions): Promise<void> {
319
+ this.validateDeviceId(deviceId);
320
+ this.validateBuffer(buffer);
321
+ const serviceInfo = this.getServiceInfo(deviceId);
322
+ const validatedOptions = this.validateOptions(options);
323
+
324
+ // 验证设备是否仍处于连接状态
325
+ await this.checkConnectionState(deviceId);
326
+
327
+ let { chunkSize } = validatedOptions;
328
+ const { delay, retries } = validatedOptions;
329
+ const data = new Uint8Array(buffer);
330
+ let totalChunks = Math.ceil(data.length / chunkSize);
331
+
332
+ this.logger.debug(`Writing ${data.length} bytes in ${totalChunks} chunks`);
333
+
334
+ if (data.length === 0) {
335
+ this.logger.warn('No data to write');
336
+ return;
337
+ }
338
+
339
+ // 自适应传输参数
340
+ let successCount = 0;
341
+ let failureCount = 0;
342
+ let consecutiveFailures = 0;
343
+ const minChunkSize = 10;
344
+ const maxChunkSize = 256;
345
+ let baseDelay = delay;
346
+ const maxDelay = 200;
347
+ const connectionCheckInterval = 5;
348
+
349
+ for (let i = 0; i < data.length; i += chunkSize) {
350
+ // 定期检查连接状态
351
+ if (i > 0 && Math.floor(i / chunkSize) % connectionCheckInterval === 0) {
352
+ await this.checkConnectionState(deviceId);
353
+ }
354
+
355
+ const chunk = data.slice(i, i + chunkSize);
356
+ const chunkNum = Math.floor(i / chunkSize) + 1;
357
+ let attempt = 0;
358
+ let writeSuccess = false;
359
+
360
+ while (attempt <= retries) {
361
+ try {
362
+ const timeoutMs = Math.max(1000, Math.min(10000, 1000 + chunk.length * 5));
363
+
364
+ const writePromise = this.getApi().writeBLECharacteristicValue({
365
+ deviceId,
366
+ serviceId: serviceInfo.serviceId,
367
+ characteristicId: serviceInfo.characteristicId,
368
+ value: chunk.buffer,
369
+ });
370
+
371
+ const timeoutPromise = new Promise((_, reject) => {
372
+ setTimeout(() => {
373
+ reject(new Error(`Write timeout after ${timeoutMs} milliseconds`));
374
+ }, timeoutMs);
375
+ });
376
+
377
+ await Promise.race([writePromise, timeoutPromise]);
378
+
379
+ this.logger.debug(`Chunk ${chunkNum}/${totalChunks} written successfully`);
380
+ writeSuccess = true;
381
+ break;
382
+ } catch (error) {
383
+ attempt++;
384
+ if (attempt > retries) {
385
+ this.logger.error(`Chunk ${chunkNum} failed after ${retries} retries`);
386
+ throw new BluetoothPrintError(
387
+ ErrorCode.WRITE_FAILED,
388
+ `Failed to write chunk ${chunkNum}/${totalChunks}`,
389
+ error as Error
390
+ );
391
+ }
392
+ this.logger.warn(`Chunk ${chunkNum} write failed, retry ${attempt}/${retries}`);
393
+
394
+ const retryDelay = baseDelay * Math.pow(2, attempt - 1);
395
+ await new Promise(r => setTimeout(r, Math.min(retryDelay, maxDelay)));
396
+ }
397
+ }
398
+
399
+ // 动态调整块大小和延迟
400
+ if (writeSuccess) {
401
+ successCount++;
402
+ consecutiveFailures = 0;
403
+ failureCount = Math.max(0, failureCount - 1);
404
+
405
+ if (successCount % 3 === 0 && chunkSize < maxChunkSize) {
406
+ chunkSize = Math.min(maxChunkSize, chunkSize + 5);
407
+ baseDelay = Math.max(baseDelay / 1.2, validatedOptions.delay);
408
+ totalChunks = Math.ceil((data.length - i - chunkSize) / chunkSize) + chunkNum;
409
+ this.logger.debug(`Increased chunk size to ${chunkSize}, delay to ${baseDelay}`);
410
+ }
411
+ } else {
412
+ failureCount++;
413
+ consecutiveFailures++;
414
+ successCount = Math.max(0, successCount - 1);
415
+
416
+ if (consecutiveFailures >= 2 && chunkSize > minChunkSize) {
417
+ chunkSize = Math.max(minChunkSize, chunkSize - 5);
418
+ baseDelay = Math.min(baseDelay * 1.5, maxDelay);
419
+ totalChunks = Math.ceil((data.length - i - chunkSize) / chunkSize) + chunkNum;
420
+ this.logger.debug(`Decreased chunk size to ${chunkSize}, delay to ${baseDelay}`);
421
+ consecutiveFailures = 0;
422
+ }
423
+ }
424
+
425
+ // Small delay to prevent congestion
426
+ if (i + chunkSize < data.length) {
427
+ await new Promise(r => setTimeout(r, baseDelay));
428
+ }
429
+ }
430
+
431
+ this.logger.info(`Successfully wrote ${data.length} bytes`);
432
+ }
433
+
434
+ /**
435
+ * Check connection state, throw if disconnected
436
+ */
437
+ private async checkConnectionState(deviceId: string): Promise<void> {
438
+ try {
439
+ const state = await this.getApi().getBLEConnectionState({ deviceId });
440
+ if (!state.connected) {
441
+ this.cleanupDevice(deviceId);
442
+ throw new BluetoothPrintError(ErrorCode.DEVICE_DISCONNECTED, 'Device disconnected');
443
+ }
444
+ } catch (error) {
445
+ if (error instanceof BluetoothPrintError) {
446
+ throw error;
447
+ }
448
+ this.cleanupDevice(deviceId);
449
+ throw new BluetoothPrintError(
450
+ ErrorCode.DEVICE_DISCONNECTED,
451
+ 'Device disconnected',
452
+ error as Error
453
+ );
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Discovers services and characteristics for a device.
459
+ * Caches the writeable characteristic for future writes.
460
+ *
461
+ * @param deviceId - Bluetooth device ID
462
+ * @throws {BluetoothPrintError} When no writeable characteristic is found
463
+ */
464
+ private async discoverServices(deviceId: string): Promise<void> {
465
+ this.logger.debug('Discovering services for device:', deviceId);
466
+
467
+ try {
468
+ const services = await this.getApi().getBLEDeviceServices({ deviceId });
469
+
470
+ for (const service of services.services) {
471
+ const chars = await this.getApi().getBLEDeviceCharacteristics({
472
+ deviceId,
473
+ serviceId: service.uuid,
474
+ });
475
+
476
+ const writeChar = chars.characteristics.find(
477
+ (c: BLECharacteristic) => c.properties.write || c.properties.writeWithoutResponse
478
+ );
479
+
480
+ if (writeChar) {
481
+ this.serviceCache.set(deviceId, {
482
+ serviceId: service.uuid,
483
+ characteristicId: writeChar.uuid,
484
+ });
485
+ this.logger.info('Found writeable characteristic:', {
486
+ service: service.uuid,
487
+ characteristic: writeChar.uuid,
488
+ });
489
+ return;
490
+ }
491
+ }
492
+
493
+ throw new BluetoothPrintError(
494
+ ErrorCode.CHARACTERISTIC_NOT_FOUND,
495
+ 'No writeable characteristic found. Make sure the device is a supported printer.'
496
+ );
497
+ } catch (error) {
498
+ if (error instanceof BluetoothPrintError) {
499
+ throw error;
500
+ }
501
+ throw new BluetoothPrintError(
502
+ ErrorCode.SERVICE_DISCOVERY_FAILED,
503
+ 'Failed to discover device services',
504
+ error as Error
505
+ );
506
+ }
507
+ }
508
+ }