taro-bluetooth-print 2.5.0 → 2.6.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.
@@ -7,12 +7,13 @@
7
7
  */
8
8
 
9
9
  // Platform type - React Native availability is checked at runtime via Platform.OS
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- const Platform = (globalThis as any).Platform;
10
+ interface PlatformInterface {
11
+ OS: string;
12
+ select<T>(options: { ios?: T; android?: T; default?: T }): T;
13
+ }
14
+ const Platform = (globalThis as { Platform?: PlatformInterface }).Platform;
12
15
 
13
- import {
14
- BaseAdapter,
15
- } from './BaseAdapter';
16
+ import { BaseAdapter } from './BaseAdapter';
16
17
  import { IPrinterAdapter, IAdapterOptions, PrinterState } from '@/types';
17
18
  import { Logger } from '@/utils/logger';
18
19
  import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
@@ -48,17 +49,9 @@ interface BLEManager {
48
49
  onDeviceScanned: (error: unknown, device: unknown) => void
49
50
  ): void;
50
51
  stopDeviceScan(): void;
51
- connectToDevice(
52
- deviceIdentifier: string,
53
- options: Record<string, unknown>
54
- ): Promise<unknown>;
55
- disconnectFromDevice(
56
- deviceIdentifier: string,
57
- force?: boolean
58
- ): Promise<unknown>;
59
- discoverAllServicesAndCharacteristicsForDevice(
60
- deviceIdentifier: string
61
- ): Promise<unknown>;
52
+ connectToDevice(deviceIdentifier: string, options: Record<string, unknown>): Promise<unknown>;
53
+ disconnectFromDevice(deviceIdentifier: string, force?: boolean): Promise<unknown>;
54
+ discoverAllServicesAndCharacteristicsForDevice(deviceIdentifier: string): Promise<unknown>;
62
55
  writeCharacteristicWithResponseForDevice(
63
56
  deviceIdentifier: string,
64
57
  serviceUUID: string,
@@ -137,14 +130,14 @@ export class ReactNativeAdapter extends BaseAdapter implements IPrinterAdapter {
137
130
  if (!options?.bleManager) {
138
131
  throw new Error(
139
132
  'ReactNativeAdapter requires a bleManager instance (e.g., react-native-ble-plx). ' +
140
- 'Please pass { bleManager: yourBleManager } in the constructor.'
133
+ 'Please pass { bleManager: yourBleManager } in the constructor.'
141
134
  );
142
135
  }
143
136
 
144
137
  this.bleManager = options.bleManager;
145
138
 
146
139
  // Validate platform
147
- if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
140
+ if (Platform && Platform.OS !== 'ios' && Platform.OS !== 'android') {
148
141
  Logger.scope('ReactNativeAdapter').warn(
149
142
  `Running on unsupported platform: ${Platform.OS}. BLE may not work correctly.`
150
143
  );
@@ -422,18 +415,15 @@ export class ReactNativeAdapter extends BaseAdapter implements IPrinterAdapter {
422
415
  startDiscovery?(): Promise<void> {
423
416
  return new Promise((resolve, reject) => {
424
417
  try {
425
- this.bleManager.startDeviceScan(
426
- null,
427
- { allowDuplicates: false },
428
- (error: unknown) => {
429
- if (error) {
430
- reject(error);
431
- }
418
+ this.bleManager.startDeviceScan(null, { allowDuplicates: false }, (error: unknown) => {
419
+ if (error) {
420
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
421
+ reject(error instanceof Error ? error : new Error(String(error)));
432
422
  }
433
- );
423
+ });
434
424
  resolve();
435
425
  } catch (error) {
436
- reject(error);
426
+ reject(error instanceof Error ? error : new Error(String(error)));
437
427
  }
438
428
  });
439
429
  }
@@ -442,7 +432,7 @@ export class ReactNativeAdapter extends BaseAdapter implements IPrinterAdapter {
442
432
  * Stop discovering nearby Bluetooth devices
443
433
  */
444
434
  stopDiscovery?(): Promise<void> {
445
- return new Promise((resolve) => {
435
+ return new Promise(resolve => {
446
436
  this.bleManager.stopDeviceScan();
447
437
  resolve();
448
438
  });
@@ -466,8 +456,7 @@ export class ReactNativeAdapter extends BaseAdapter implements IPrinterAdapter {
466
456
 
467
457
  for (const service of services) {
468
458
  const writeChar = service.characteristics.find(
469
- (c: RNCharacteristic) =>
470
- c.isWritableWithResponse || c.isWritableWithoutResponse
459
+ (c: RNCharacteristic) => c.isWritableWithResponse || c.isWritableWithoutResponse
471
460
  );
472
461
 
473
462
  if (writeChar) {
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Web Bluetooth Adapter
3
3
  * Implements the IPrinterAdapter interface for Web Bluetooth API (H5 environment)
4
+ *
5
+ * Provides enhanced device filtering, RSSI signal strength monitoring,
6
+ * and improved disconnection handling.
4
7
  */
5
8
 
6
9
  import { IAdapterOptions, PrinterState } from '@/types';
@@ -14,6 +17,12 @@ interface WebBluetoothDeviceInfo {
14
17
  device: BluetoothDevice;
15
18
  server: BluetoothRemoteGATTServer;
16
19
  characteristic: BluetoothRemoteGATTCharacteristic;
20
+ /** RSSI value at time of connection (if available) */
21
+ rssiAtConnection?: number;
22
+ /** Device discovered timestamp */
23
+ discoveredAt?: number;
24
+ /** Device name for reference */
25
+ name?: string;
17
26
  }
18
27
 
19
28
  /**
@@ -28,6 +37,49 @@ export interface WebBluetoothRequestOptions {
28
37
  acceptAllDevices?: boolean;
29
38
  /** Optional services to access */
30
39
  optionalServices?: string[];
40
+ /** Minimum RSSI signal strength (dBm) to accept device */
41
+ minRSSI?: number;
42
+ /** Filter by device name (exact or partial match) */
43
+ name?: string;
44
+ /** Filter by manufacturer data patterns */
45
+ manufacturerDataFilter?: Array<{
46
+ companyIdentifier: number;
47
+ dataPrefix?: Uint8Array;
48
+ }>;
49
+ }
50
+
51
+ /**
52
+ * Discovered device information
53
+ */
54
+ export interface DiscoveredDevice {
55
+ /** Device instance */
56
+ device: BluetoothDevice;
57
+ /** Device name */
58
+ name: string;
59
+ /** Device ID */
60
+ deviceId: string;
61
+ /** RSSI signal strength (dBm) */
62
+ rssi?: number;
63
+ /** Timestamp when device was discovered */
64
+ discoveredAt: number;
65
+ /** Manufacturer data if available */
66
+ manufacturerData?: Map<number, Uint8Array>;
67
+ }
68
+
69
+ /**
70
+ * Device filter options for scanning
71
+ */
72
+ export interface DeviceFilterOptions {
73
+ /** Minimum RSSI threshold (dBm) */
74
+ minRSSI?: number;
75
+ /** Maximum RSSI threshold (dBm) */
76
+ maxRSSI?: number;
77
+ /** Name prefix filter */
78
+ namePrefix?: string;
79
+ /** Name exact match filter */
80
+ name?: string;
81
+ /** Service UUIDs to filter */
82
+ serviceUUIDs?: string[];
31
83
  }
32
84
 
33
85
  /**
@@ -35,8 +87,8 @@ export interface WebBluetoothRequestOptions {
35
87
  */
36
88
  const PRINTER_SERVICE_UUIDS = [
37
89
  '000018f0-0000-1000-8000-00805f9b34fb', // Common printer service
38
- '49535343-fe7d-4ae5-8fa9-9fafd205e455', // Nordic UART Service
39
- 'e7810a71-73ae-499d-8c15-faa9aef0c3f2', // Serial Port Profile
90
+ '49535343-fe7d-4ae5-8fa9-9fafd205e455', // Serial Port Profile
91
+ 'e7810a71-73ae-499d-8c15-faa9aef0c3f2', // Nordic UART Service
40
92
  ];
41
93
 
42
94
  /**
@@ -55,6 +107,8 @@ const PRINTER_SERVICE_UUIDS = [
55
107
  */
56
108
  export class WebBluetoothAdapter extends BaseAdapter {
57
109
  private devices: Map<string, WebBluetoothDeviceInfo> = new Map();
110
+ private discoveredDevices: Map<string, DiscoveredDevice> = new Map();
111
+ private connectionCleanupTimeout: ReturnType<typeof setTimeout> | null = null;
58
112
 
59
113
  /**
60
114
  * Check if Web Bluetooth API is supported in the current browser
@@ -125,9 +179,14 @@ export class WebBluetoothAdapter extends BaseAdapter {
125
179
 
126
180
  // Check if already connected
127
181
  if (this.devices.has(deviceId)) {
128
- this.logger.warn('Device already connected:', deviceId);
129
- this.updateState(PrinterState.CONNECTED);
130
- return;
182
+ const existingInfo = this.devices.get(deviceId);
183
+ if (existingInfo?.server.connected) {
184
+ this.logger.warn('Device already connected:', deviceId);
185
+ this.updateState(PrinterState.CONNECTED);
186
+ return;
187
+ }
188
+ // If not connected but still in map, clean up first
189
+ this.cleanupDeviceInfo(deviceId);
131
190
  }
132
191
 
133
192
  this.updateState(PrinterState.CONNECTING);
@@ -148,8 +207,29 @@ export class WebBluetoothAdapter extends BaseAdapter {
148
207
  // Discover services and find writeable characteristic
149
208
  const characteristic = await this.discoverWriteableCharacteristic(server);
150
209
 
210
+ // Get RSSI if available (may not be available on all devices)
211
+ let rssi: number | undefined;
212
+ try {
213
+ if ('readRemoteRssi' in characteristic.service.device) {
214
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
215
+ rssi = await (characteristic.service.device as any).readRemoteRssi();
216
+ }
217
+ } catch {
218
+ this.logger.debug('RSSI reading not supported on this device');
219
+ }
220
+
151
221
  // Store device info
152
- this.devices.set(deviceId, { device, server, characteristic });
222
+ const deviceInfo: WebBluetoothDeviceInfo = {
223
+ device,
224
+ server,
225
+ characteristic,
226
+ discoveredAt: Date.now(),
227
+ };
228
+ if (rssi !== undefined) {
229
+ deviceInfo.rssiAtConnection = rssi;
230
+ }
231
+ this.devices.set(deviceId, deviceInfo);
232
+
153
233
  this.serviceCache.set(deviceId, {
154
234
  serviceId: characteristic.service?.uuid || '',
155
235
  characteristicId: characteristic.uuid,
@@ -189,28 +269,52 @@ export class WebBluetoothAdapter extends BaseAdapter {
189
269
 
190
270
  /**
191
271
  * Disconnect from a Bluetooth device
272
+ * Enhanced to properly clean up all resources and event listeners
192
273
  *
193
274
  * @param deviceId - Bluetooth device ID
275
+ * @param force - If true, force disconnection even if device not found in cache
194
276
  */
195
- disconnect(deviceId: string): Promise<void> {
277
+ disconnect(deviceId: string, force = false): void {
196
278
  this.validateDeviceId(deviceId);
279
+
280
+ const deviceInfo = this.devices.get(deviceId);
281
+
282
+ // If device not found and not forcing, just return
283
+ if (!deviceInfo && !force) {
284
+ this.logger.debug('Device not found in cache, nothing to disconnect');
285
+ return;
286
+ }
287
+
197
288
  this.updateState(PrinterState.DISCONNECTING);
198
289
  this.logger.debug('Disconnecting from device:', deviceId);
199
290
 
200
291
  try {
201
- const deviceInfo = this.devices.get(deviceId);
292
+ // Cancel any pending connection cleanup
293
+ if (this.connectionCleanupTimeout) {
294
+ clearTimeout(this.connectionCleanupTimeout);
295
+ this.connectionCleanupTimeout = null;
296
+ }
297
+
298
+ // Disconnect from GATT server
202
299
  if (deviceInfo?.server?.connected) {
203
300
  deviceInfo.server.disconnect();
301
+ this.logger.debug('GATT server disconnected');
302
+ }
303
+
304
+ // Remove thegattserverdisconnected event listener to prevent double-handling
305
+ if (deviceInfo?.device) {
306
+ deviceInfo.device.removeEventListener('gattserverdisconnected', () => {
307
+ this.handleDisconnection(deviceId);
308
+ });
204
309
  }
205
310
  } catch (error) {
206
- this.logger.warn('Disconnect error (ignored):', error);
311
+ this.logger.warn('Disconnect error:', error);
207
312
  } finally {
313
+ // Always cleanup resources
208
314
  this.cleanupDeviceInfo(deviceId);
209
315
  this.updateState(PrinterState.DISCONNECTED);
210
316
  this.logger.info('Device disconnected successfully');
211
317
  }
212
-
213
- return Promise.resolve();
214
318
  }
215
319
 
216
320
  /**
@@ -290,10 +394,126 @@ export class WebBluetoothAdapter extends BaseAdapter {
290
394
  this.logger.info(`Successfully wrote ${data.length} bytes`);
291
395
  }
292
396
 
397
+ /**
398
+ * Get device ID from a BluetoothDevice instance
399
+ * Handles different browser implementations
400
+ *
401
+ * @param device - BluetoothDevice instance
402
+ * @returns Device ID string
403
+ */
404
+ getDeviceId(device: BluetoothDevice): string {
405
+ if (!device) {
406
+ throw new BluetoothPrintError(ErrorCode.DEVICE_NOT_FOUND, 'Device is required');
407
+ }
408
+
409
+ // Use device.id if available, otherwise fall back to generated ID
410
+ const deviceId = device.id || this.generateFallbackDeviceId(device);
411
+
412
+ return deviceId;
413
+ }
414
+
415
+ /**
416
+ * Get device information including RSSI
417
+ *
418
+ * @param deviceId - Bluetooth device ID
419
+ * @returns Device info object with RSSI and metadata
420
+ */
421
+ getDeviceInfo(
422
+ deviceId: string
423
+ ): { deviceId: string; name: string; rssi?: number; connected: boolean } | null {
424
+ const deviceInfo = this.devices.get(deviceId);
425
+
426
+ if (!deviceInfo) {
427
+ return null;
428
+ }
429
+
430
+ const result: { deviceId: string; name: string; rssi?: number; connected: boolean } = {
431
+ deviceId,
432
+ name: deviceInfo.device.name || 'Unknown Device',
433
+ connected: deviceInfo.server.connected,
434
+ };
435
+ if (deviceInfo.rssiAtConnection !== undefined) {
436
+ result.rssi = deviceInfo.rssiAtConnection;
437
+ }
438
+ return result;
439
+ }
440
+
441
+ /**
442
+ * Filter discovered devices by criteria
443
+ *
444
+ * @param devices - Array of discovered devices
445
+ * @param filter - Filter criteria
446
+ * @returns Filtered array of devices
447
+ */
448
+ filterDevices(devices: DiscoveredDevice[], filter: DeviceFilterOptions): DiscoveredDevice[] {
449
+ return devices.filter(device => {
450
+ // RSSI filtering
451
+ if (
452
+ filter.minRSSI !== undefined &&
453
+ (device.rssi === undefined || device.rssi < filter.minRSSI)
454
+ ) {
455
+ return false;
456
+ }
457
+ if (
458
+ filter.maxRSSI !== undefined &&
459
+ (device.rssi === undefined || device.rssi > filter.maxRSSI)
460
+ ) {
461
+ return false;
462
+ }
463
+
464
+ // Name prefix filtering
465
+ if (
466
+ filter.namePrefix &&
467
+ !device.name.toLowerCase().startsWith(filter.namePrefix.toLowerCase())
468
+ ) {
469
+ return false;
470
+ }
471
+
472
+ // Name exact/partial matching
473
+ if (filter.name) {
474
+ const searchName = filter.name.toLowerCase();
475
+ if (!device.name.toLowerCase().includes(searchName)) {
476
+ return false;
477
+ }
478
+ }
479
+
480
+ // Service UUID filtering
481
+ if (filter.serviceUUIDs && filter.serviceUUIDs.length > 0) {
482
+ const deviceWithUuids = device.device as BluetoothDevice & { uuids?: string[] };
483
+ const deviceServices = Array.from(deviceWithUuids.uuids || []);
484
+ const hasMatchingService = filter.serviceUUIDs.some(uuid =>
485
+ deviceServices.some(deviceUuid => deviceUuid.toLowerCase() === uuid.toLowerCase())
486
+ );
487
+ if (!hasMatchingService) {
488
+ return false;
489
+ }
490
+ }
491
+
492
+ return true;
493
+ });
494
+ }
495
+
496
+ /**
497
+ * Sort devices by signal strength (RSSI)
498
+ *
499
+ * @param devices - Array of discovered devices
500
+ * @param ascending - Sort in ascending order (weakest first), default false (strongest first)
501
+ * @returns Sorted array
502
+ */
503
+ sortByRSSI(devices: DiscoveredDevice[], ascending = false): DiscoveredDevice[] {
504
+ return [...devices].sort((a, b) => {
505
+ const rssiA = a.rssi ?? -Infinity;
506
+ const rssiB = b.rssi ?? -Infinity;
507
+ return ascending ? rssiA - rssiB : rssiB - rssiA;
508
+ });
509
+ }
510
+
293
511
  /**
294
512
  * Build request options for navigator.bluetooth.requestDevice
295
513
  */
296
514
  private buildRequestOptions(options?: WebBluetoothRequestOptions): RequestDeviceOptions {
515
+ const filters: BluetoothLEScanFilter[] = [];
516
+
297
517
  if (options?.acceptAllDevices) {
298
518
  return {
299
519
  acceptAllDevices: true,
@@ -301,8 +521,7 @@ export class WebBluetoothAdapter extends BaseAdapter {
301
521
  };
302
522
  }
303
523
 
304
- const filters: BluetoothLEScanFilter[] = [];
305
-
524
+ // Build filters
306
525
  if (options?.serviceUUIDs?.length) {
307
526
  filters.push({ services: options.serviceUUIDs });
308
527
  }
@@ -311,11 +530,30 @@ export class WebBluetoothAdapter extends BaseAdapter {
311
530
  filters.push({ namePrefix: options.namePrefix });
312
531
  }
313
532
 
533
+ if (options?.name) {
534
+ filters.push({ name: options.name });
535
+ }
536
+
537
+ // Manufacturer data filter (if provided)
538
+ // Note: This is a newer API, may not be supported in all browsers
539
+ if (options?.manufacturerDataFilter?.length) {
540
+ for (const mf of options.manufacturerDataFilter) {
541
+ const filter: BluetoothLEScanFilter = {
542
+ manufacturerData: [
543
+ {
544
+ companyIdentifier: mf.companyIdentifier,
545
+ },
546
+ ],
547
+ };
548
+ filters.push(filter);
549
+ }
550
+ }
551
+
314
552
  // Default: filter by common printer services
315
553
  if (filters.length === 0) {
316
554
  return {
317
555
  acceptAllDevices: true,
318
- optionalServices: PRINTER_SERVICE_UUIDS,
556
+ optionalServices: options?.optionalServices || PRINTER_SERVICE_UUIDS,
319
557
  };
320
558
  }
321
559
 
@@ -335,6 +573,12 @@ export class WebBluetoothAdapter extends BaseAdapter {
335
573
  return existingInfo.device;
336
574
  }
337
575
 
576
+ // Try to find device from discovered devices
577
+ const discoveredDevice = this.discoveredDevices.get(deviceId);
578
+ if (discoveredDevice?.device) {
579
+ return discoveredDevice.device;
580
+ }
581
+
338
582
  // Request a new device
339
583
  return this.requestDevice();
340
584
  }
@@ -414,6 +658,13 @@ export class WebBluetoothAdapter extends BaseAdapter {
414
658
  */
415
659
  private handleDisconnection(deviceId: string): void {
416
660
  this.logger.warn('Device disconnected unexpectedly:', deviceId);
661
+
662
+ // Clear any pending cleanup
663
+ if (this.connectionCleanupTimeout) {
664
+ clearTimeout(this.connectionCleanupTimeout);
665
+ this.connectionCleanupTimeout = null;
666
+ }
667
+
417
668
  this.cleanupDeviceInfo(deviceId);
418
669
  this.updateState(PrinterState.DISCONNECTED);
419
670
  }
@@ -425,4 +676,14 @@ export class WebBluetoothAdapter extends BaseAdapter {
425
676
  this.devices.delete(deviceId);
426
677
  this.cleanupDevice(deviceId);
427
678
  }
679
+
680
+ /**
681
+ * Generate a fallback device ID when device.id is not available
682
+ * Uses device name + first seen timestamp as identifier
683
+ */
684
+ private generateFallbackDeviceId(device: BluetoothDevice): string {
685
+ const name = device.name || 'unknown';
686
+ const timestamp = Date.now().toString(36);
687
+ return `fallback_${name}_${timestamp}`;
688
+ }
428
689
  }
@@ -3,7 +3,12 @@
3
3
  */
4
4
 
5
5
  export { BaseAdapter, MiniProgramAdapter } from './BaseAdapter';
6
- export type { MiniProgramBLEApi, ServiceInfo, BLECharacteristic, BLECharacteristicProperties } from './BaseAdapter';
6
+ export type {
7
+ MiniProgramBLEApi,
8
+ ServiceInfo,
9
+ BLECharacteristic,
10
+ BLECharacteristicProperties,
11
+ } from './BaseAdapter';
7
12
  export { TaroAdapter } from './TaroAdapter';
8
13
  export { AlipayAdapter } from './AlipayAdapter';
9
14
  export { BaiduAdapter } from './BaiduAdapter';