taro-bluetooth-print 2.4.0 → 2.5.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 +41 -0
- package/README.md +10 -2
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/types/adapters/QQAdapter.d.ts +22 -0
- package/dist/types/adapters/ReactNativeAdapter.d.ts +111 -0
- package/dist/types/adapters/index.d.ts +13 -0
- package/dist/types/device/MultiPrinterManager.d.ts +1 -1
- package/dist/types/drivers/StarPrinter.d.ts +243 -0
- package/dist/types/drivers/index.d.ts +1 -0
- package/dist/types/encoding/EncodingService.d.ts +41 -2
- package/dist/types/encoding/index.d.ts +2 -1
- package/dist/types/encoding/korean-japanese.d.ts +127 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/services/BatchPrintManager.d.ts +98 -5
- package/dist/types/services/PrintStatistics.d.ts +189 -0
- package/dist/types/services/ScheduledRetryManager.d.ts +213 -0
- package/dist/types/services/index.d.ts +5 -3
- package/dist/types/utils/image.d.ts +40 -119
- package/dist/types/utils/platform.d.ts +2 -0
- package/package.json +1 -1
- package/src/adapters/AdapterFactory.ts +5 -0
- package/src/adapters/QQAdapter.ts +36 -0
- package/src/adapters/ReactNativeAdapter.ts +517 -0
- package/src/adapters/index.ts +14 -0
- package/src/config/PrinterConfigManager.ts +10 -6
- package/src/device/MultiPrinterManager.ts +10 -10
- package/src/drivers/StarPrinter.ts +555 -0
- package/src/drivers/index.ts +10 -0
- package/src/encoding/EncodingService.ts +261 -4
- package/src/encoding/index.ts +17 -1
- package/src/encoding/korean-japanese.ts +289 -0
- package/src/index.ts +1 -5
- package/src/services/BatchPrintManager.ts +312 -42
- package/src/services/PrintHistory.ts +13 -11
- package/src/services/PrintJobManager.ts +3 -3
- package/src/services/PrintStatistics.ts +504 -0
- package/src/services/PrinterStatus.ts +4 -10
- package/src/services/ScheduledRetryManager.ts +564 -0
- package/src/services/index.ts +38 -3
- package/src/utils/image.ts +476 -342
- package/src/utils/platform.ts +20 -34
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Bluetooth Adapter
|
|
3
|
+
* Implements the IPrinterAdapter interface for React Native using react-native-ble-plx
|
|
4
|
+
*
|
|
5
|
+
* React Native does not have a native Web BLE API, so this adapter uses
|
|
6
|
+
* the react-native-ble-plx library for BLE operations on iOS and Android.
|
|
7
|
+
*/
|
|
8
|
+
|
|
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;
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
BaseAdapter,
|
|
15
|
+
} from './BaseAdapter';
|
|
16
|
+
import { IPrinterAdapter, IAdapterOptions, PrinterState } from '@/types';
|
|
17
|
+
import { Logger } from '@/utils/logger';
|
|
18
|
+
import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* BLE characteristic info with full UUIDs
|
|
22
|
+
*/
|
|
23
|
+
interface RNCharacteristic {
|
|
24
|
+
uuid: string;
|
|
25
|
+
isWritableWithResponse: boolean;
|
|
26
|
+
isWritableWithoutResponse: boolean;
|
|
27
|
+
isReadable: boolean;
|
|
28
|
+
isNotifiable: boolean;
|
|
29
|
+
isIndicatable: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* RNService info with characteristic list
|
|
34
|
+
*/
|
|
35
|
+
interface RNService {
|
|
36
|
+
uuid: string;
|
|
37
|
+
characteristics: RNCharacteristic[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* React Native BLE Manager interface
|
|
42
|
+
* Compatible with react-native-ble-plx API
|
|
43
|
+
*/
|
|
44
|
+
interface BLEManager {
|
|
45
|
+
startDeviceScan(
|
|
46
|
+
serviceUUIDs: string[] | null,
|
|
47
|
+
options: Record<string, unknown> | null,
|
|
48
|
+
onDeviceScanned: (error: unknown, device: unknown) => void
|
|
49
|
+
): void;
|
|
50
|
+
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>;
|
|
62
|
+
writeCharacteristicWithResponseForDevice(
|
|
63
|
+
deviceIdentifier: string,
|
|
64
|
+
serviceUUID: string,
|
|
65
|
+
characteristicUUID: string,
|
|
66
|
+
value: string,
|
|
67
|
+
transactionId?: string
|
|
68
|
+
): Promise<unknown>;
|
|
69
|
+
writeCharacteristicWithoutResponseForDevice(
|
|
70
|
+
deviceIdentifier: string,
|
|
71
|
+
serviceUUID: string,
|
|
72
|
+
characteristicUUID: string,
|
|
73
|
+
value: string,
|
|
74
|
+
transactionId?: string
|
|
75
|
+
): Promise<unknown>;
|
|
76
|
+
readCharacteristicForDevice(
|
|
77
|
+
deviceIdentifier: string,
|
|
78
|
+
serviceUUID: string,
|
|
79
|
+
characteristicUUID: string,
|
|
80
|
+
transactionId?: string
|
|
81
|
+
): Promise<unknown>;
|
|
82
|
+
monitorCharacteristicForDevice(
|
|
83
|
+
deviceIdentifier: string,
|
|
84
|
+
serviceUUID: string,
|
|
85
|
+
characteristicUUID: string,
|
|
86
|
+
onUpdate: (error: unknown, characteristic: unknown) => void,
|
|
87
|
+
transactionId?: string
|
|
88
|
+
): { remove: () => void };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* BLE Device interface from react-native-ble-plx
|
|
93
|
+
*/
|
|
94
|
+
interface RNDevice {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string | null;
|
|
97
|
+
isConnected: boolean;
|
|
98
|
+
rssi: number;
|
|
99
|
+
mtu: number;
|
|
100
|
+
requestConnectionPriority(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* React Native Bluetooth Low Energy adapter
|
|
105
|
+
*
|
|
106
|
+
* Uses react-native-ble-plx for BLE operations on iOS and Android.
|
|
107
|
+
* This adapter does NOT extend MiniProgramAdapter because React Native
|
|
108
|
+
* has a fundamentally different BLE API compared to mini-program platforms.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* import BleManager from 'react-native-ble-plx';
|
|
113
|
+
* import { ReactNativeAdapter } from 'taro-bluetooth-print';
|
|
114
|
+
*
|
|
115
|
+
* BleManager.start({ showAlert: false });
|
|
116
|
+
*
|
|
117
|
+
* const adapter = new ReactNativeAdapter({ bleManager: BleManager });
|
|
118
|
+
* await adapter.connect('device-uuid-123');
|
|
119
|
+
* await adapter.write('device-uuid-123', buffer);
|
|
120
|
+
* await adapter.disconnect('device-uuid-123');
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export class ReactNativeAdapter extends BaseAdapter implements IPrinterAdapter {
|
|
124
|
+
private bleManager: BLEManager;
|
|
125
|
+
private deviceCache: Map<string, RNDevice> = new Map();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Creates a new ReactNativeAdapter instance
|
|
129
|
+
*
|
|
130
|
+
* @param options - Configuration options
|
|
131
|
+
* @param options.bleManager - BLE Manager instance (e.g., from react-native-ble-plx)
|
|
132
|
+
* @throws {Error} If bleManager is not provided or not supported
|
|
133
|
+
*/
|
|
134
|
+
constructor(options: { bleManager: BLEManager }) {
|
|
135
|
+
super();
|
|
136
|
+
|
|
137
|
+
if (!options?.bleManager) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'ReactNativeAdapter requires a bleManager instance (e.g., react-native-ble-plx). ' +
|
|
140
|
+
'Please pass { bleManager: yourBleManager } in the constructor.'
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.bleManager = options.bleManager;
|
|
145
|
+
|
|
146
|
+
// Validate platform
|
|
147
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
148
|
+
Logger.scope('ReactNativeAdapter').warn(
|
|
149
|
+
`Running on unsupported platform: ${Platform.OS}. BLE may not work correctly.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Connect to a Bluetooth device and discover services
|
|
156
|
+
*
|
|
157
|
+
* @param deviceId - Unique identifier (UUID) of the device to connect to
|
|
158
|
+
* @throws {BluetoothPrintError} When connection fails or device not found
|
|
159
|
+
*/
|
|
160
|
+
async connect(deviceId: string): Promise<void> {
|
|
161
|
+
this.validateDeviceId(deviceId);
|
|
162
|
+
|
|
163
|
+
if (this.isDeviceConnected(deviceId)) {
|
|
164
|
+
Logger.scope('ReactNativeAdapter').warn('Device already connected:', deviceId);
|
|
165
|
+
this.updateState(PrinterState.CONNECTED);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.updateState(PrinterState.CONNECTING);
|
|
170
|
+
Logger.scope('ReactNativeAdapter').debug('Connecting to device:', deviceId);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// Add connection timeout
|
|
174
|
+
const timeoutMs = 15000;
|
|
175
|
+
const connectionPromise = this.performConnect(deviceId);
|
|
176
|
+
|
|
177
|
+
let timeoutHandle: NodeJS.Timeout | null = null;
|
|
178
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
179
|
+
timeoutHandle = setTimeout(() => {
|
|
180
|
+
reject(new Error('Connection timeout'));
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const device = await Promise.race([connectionPromise, timeoutPromise]);
|
|
185
|
+
|
|
186
|
+
if (timeoutHandle) {
|
|
187
|
+
clearTimeout(timeoutHandle);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.deviceCache.set(deviceId, device as RNDevice);
|
|
191
|
+
await this.discoverServices(deviceId, device as RNDevice);
|
|
192
|
+
|
|
193
|
+
this.updateState(PrinterState.CONNECTED);
|
|
194
|
+
Logger.scope('ReactNativeAdapter').info('Device connected successfully');
|
|
195
|
+
} catch (error) {
|
|
196
|
+
this.updateState(PrinterState.DISCONNECTED);
|
|
197
|
+
this.cleanupDevice(deviceId);
|
|
198
|
+
|
|
199
|
+
const errorMsg = (error as Error).message || '';
|
|
200
|
+
if (errorMsg.includes('timeout')) {
|
|
201
|
+
throw new BluetoothPrintError(
|
|
202
|
+
ErrorCode.CONNECTION_TIMEOUT,
|
|
203
|
+
`Connection to device ${deviceId} timed out`,
|
|
204
|
+
error as Error
|
|
205
|
+
);
|
|
206
|
+
} else if (errorMsg.includes('not found') || errorMsg.includes('not exist')) {
|
|
207
|
+
throw new BluetoothPrintError(
|
|
208
|
+
ErrorCode.DEVICE_NOT_FOUND,
|
|
209
|
+
`Device ${deviceId} not found`,
|
|
210
|
+
error as Error
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new BluetoothPrintError(
|
|
215
|
+
ErrorCode.CONNECTION_FAILED,
|
|
216
|
+
`Failed to connect to device ${deviceId}`,
|
|
217
|
+
error as Error
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Perform the actual BLE connection
|
|
224
|
+
*/
|
|
225
|
+
private async performConnect(deviceId: string): Promise<unknown> {
|
|
226
|
+
const device = await (this.bleManager.connectToDevice(deviceId, {
|
|
227
|
+
timeout: 10000,
|
|
228
|
+
}) as Promise<RNDevice>);
|
|
229
|
+
|
|
230
|
+
// Request connection priority for better throughput
|
|
231
|
+
try {
|
|
232
|
+
await device.requestConnectionPriority();
|
|
233
|
+
} catch {
|
|
234
|
+
// Ignore priority request errors
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return device;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Disconnect from a Bluetooth device
|
|
242
|
+
*
|
|
243
|
+
* @param deviceId - Unique identifier of the device to disconnect from
|
|
244
|
+
*/
|
|
245
|
+
async disconnect(deviceId: string): Promise<void> {
|
|
246
|
+
this.validateDeviceId(deviceId);
|
|
247
|
+
this.updateState(PrinterState.DISCONNECTING);
|
|
248
|
+
Logger.scope('ReactNativeAdapter').debug('Disconnecting from device:', deviceId);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await (this.bleManager.disconnectFromDevice(deviceId, true) as Promise<void>);
|
|
252
|
+
this.cleanupDevice(deviceId);
|
|
253
|
+
this.deviceCache.delete(deviceId);
|
|
254
|
+
this.updateState(PrinterState.DISCONNECTED);
|
|
255
|
+
Logger.scope('ReactNativeAdapter').info('Device disconnected successfully');
|
|
256
|
+
} catch (error) {
|
|
257
|
+
Logger.scope('ReactNativeAdapter').warn('Disconnect error (ignored):', error);
|
|
258
|
+
this.cleanupDevice(deviceId);
|
|
259
|
+
this.deviceCache.delete(deviceId);
|
|
260
|
+
this.updateState(PrinterState.DISCONNECTED);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Write data to the Bluetooth device in chunks
|
|
266
|
+
*
|
|
267
|
+
* Features:
|
|
268
|
+
* - Automatic chunk size adjustment
|
|
269
|
+
* - Dynamic delay for congestion control
|
|
270
|
+
* - Retry with exponential backoff
|
|
271
|
+
* - Write timeout per chunk
|
|
272
|
+
*
|
|
273
|
+
* @param deviceId - Unique identifier of the connected device
|
|
274
|
+
* @param buffer - Data to write as ArrayBuffer
|
|
275
|
+
* @param options - Optional write settings (chunkSize, delay, retries)
|
|
276
|
+
* @throws {BluetoothPrintError} When write fails after all retries
|
|
277
|
+
*/
|
|
278
|
+
async write(deviceId: string, buffer: ArrayBuffer, options?: IAdapterOptions): Promise<void> {
|
|
279
|
+
this.validateDeviceId(deviceId);
|
|
280
|
+
this.validateBuffer(buffer);
|
|
281
|
+
const serviceInfo = this.getServiceInfo(deviceId);
|
|
282
|
+
const validatedOptions = this.validateOptions(options);
|
|
283
|
+
|
|
284
|
+
const device = this.deviceCache.get(deviceId);
|
|
285
|
+
if (!device || !device.isConnected) {
|
|
286
|
+
this.cleanupDevice(deviceId);
|
|
287
|
+
throw new BluetoothPrintError(
|
|
288
|
+
ErrorCode.DEVICE_DISCONNECTED,
|
|
289
|
+
`Device ${deviceId} is not connected`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let { chunkSize } = validatedOptions;
|
|
294
|
+
const { delay, retries } = validatedOptions;
|
|
295
|
+
const data = new Uint8Array(buffer);
|
|
296
|
+
const totalChunks = Math.ceil(data.length / chunkSize);
|
|
297
|
+
|
|
298
|
+
Logger.scope('ReactNativeAdapter').debug(
|
|
299
|
+
`Writing ${data.length} bytes in ${totalChunks} chunks`
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (data.length === 0) {
|
|
303
|
+
Logger.scope('ReactNativeAdapter').warn('No data to write');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Adaptive transmission parameters
|
|
308
|
+
let successCount = 0;
|
|
309
|
+
let consecutiveFailures = 0;
|
|
310
|
+
const minChunkSize = 10;
|
|
311
|
+
const maxChunkSize = Math.min(512, device.mtu - 5); // Respect MTU if available
|
|
312
|
+
let baseDelay = delay;
|
|
313
|
+
const maxDelay = 200;
|
|
314
|
+
|
|
315
|
+
for (let i = 0; i < data.length; i += chunkSize) {
|
|
316
|
+
const chunk = data.slice(i, i + chunkSize);
|
|
317
|
+
const chunkNum = Math.floor(i / chunkSize) + 1;
|
|
318
|
+
let attempt = 0;
|
|
319
|
+
let writeSuccess = false;
|
|
320
|
+
|
|
321
|
+
while (attempt <= retries) {
|
|
322
|
+
try {
|
|
323
|
+
// Convert Uint8Array to base64 string for react-native-ble-plx
|
|
324
|
+
const base64Value = this.arrayBufferToBase64(chunk.buffer);
|
|
325
|
+
|
|
326
|
+
// Timeout per write
|
|
327
|
+
const timeoutMs = Math.max(2000, Math.min(10000, 1000 + chunk.length * 10));
|
|
328
|
+
let timeoutHandle: NodeJS.Timeout | null = null;
|
|
329
|
+
|
|
330
|
+
const writePromise = (async () => {
|
|
331
|
+
try {
|
|
332
|
+
await (this.bleManager.writeCharacteristicWithResponseForDevice(
|
|
333
|
+
deviceId,
|
|
334
|
+
serviceInfo.serviceId,
|
|
335
|
+
serviceInfo.characteristicId,
|
|
336
|
+
base64Value
|
|
337
|
+
) as Promise<void>);
|
|
338
|
+
} catch {
|
|
339
|
+
// Fallback to without-response if with-response fails
|
|
340
|
+
await (this.bleManager.writeCharacteristicWithoutResponseForDevice(
|
|
341
|
+
deviceId,
|
|
342
|
+
serviceInfo.serviceId,
|
|
343
|
+
serviceInfo.characteristicId,
|
|
344
|
+
base64Value
|
|
345
|
+
) as Promise<void>);
|
|
346
|
+
}
|
|
347
|
+
})();
|
|
348
|
+
|
|
349
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
350
|
+
timeoutHandle = setTimeout(() => {
|
|
351
|
+
reject(new Error(`Write timeout after ${timeoutMs}ms`));
|
|
352
|
+
}, timeoutMs);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
await Promise.race([writePromise, timeoutPromise]);
|
|
356
|
+
|
|
357
|
+
if (timeoutHandle) {
|
|
358
|
+
clearTimeout(timeoutHandle);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
Logger.scope('ReactNativeAdapter').debug(
|
|
362
|
+
`Chunk ${chunkNum}/${totalChunks} written successfully`
|
|
363
|
+
);
|
|
364
|
+
writeSuccess = true;
|
|
365
|
+
break;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
attempt++;
|
|
368
|
+
if (attempt > retries) {
|
|
369
|
+
Logger.scope('ReactNativeAdapter').error(
|
|
370
|
+
`Chunk ${chunkNum} failed after ${retries} retries`
|
|
371
|
+
);
|
|
372
|
+
throw new BluetoothPrintError(
|
|
373
|
+
ErrorCode.WRITE_FAILED,
|
|
374
|
+
`Failed to write chunk ${chunkNum}/${totalChunks}`,
|
|
375
|
+
error as Error
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
Logger.scope('ReactNativeAdapter').warn(
|
|
379
|
+
`Chunk ${chunkNum} write failed, retry ${attempt}/${retries}`
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const retryDelay = baseDelay * Math.pow(2, attempt - 1);
|
|
383
|
+
await new Promise(r => setTimeout(r, Math.min(retryDelay, maxDelay)));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Adaptive chunk size and delay adjustment
|
|
388
|
+
if (writeSuccess) {
|
|
389
|
+
successCount++;
|
|
390
|
+
consecutiveFailures = 0;
|
|
391
|
+
|
|
392
|
+
if (successCount % 3 === 0 && chunkSize < maxChunkSize) {
|
|
393
|
+
chunkSize = Math.min(maxChunkSize, chunkSize + 5);
|
|
394
|
+
baseDelay = Math.max(baseDelay / 1.2, validatedOptions.delay);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
consecutiveFailures++;
|
|
398
|
+
successCount = Math.max(0, successCount - 1);
|
|
399
|
+
|
|
400
|
+
if (consecutiveFailures >= 2 && chunkSize > minChunkSize) {
|
|
401
|
+
chunkSize = Math.max(minChunkSize, chunkSize - 5);
|
|
402
|
+
baseDelay = Math.min(baseDelay * 1.5, maxDelay);
|
|
403
|
+
consecutiveFailures = 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Inter-chunk delay
|
|
408
|
+
if (i + chunkSize < data.length) {
|
|
409
|
+
await new Promise(r => setTimeout(r, baseDelay));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
Logger.scope('ReactNativeAdapter').info(`Successfully wrote ${data.length} bytes`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Start discovering nearby Bluetooth devices
|
|
418
|
+
*
|
|
419
|
+
* Note: This is optional in IPrinterAdapter. In React Native BLE,
|
|
420
|
+
* device discovery is typically done via scan events.
|
|
421
|
+
*/
|
|
422
|
+
startDiscovery?(): Promise<void> {
|
|
423
|
+
return new Promise((resolve, reject) => {
|
|
424
|
+
try {
|
|
425
|
+
this.bleManager.startDeviceScan(
|
|
426
|
+
null,
|
|
427
|
+
{ allowDuplicates: false },
|
|
428
|
+
(error: unknown) => {
|
|
429
|
+
if (error) {
|
|
430
|
+
reject(error);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
);
|
|
434
|
+
resolve();
|
|
435
|
+
} catch (error) {
|
|
436
|
+
reject(error);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Stop discovering nearby Bluetooth devices
|
|
443
|
+
*/
|
|
444
|
+
stopDiscovery?(): Promise<void> {
|
|
445
|
+
return new Promise((resolve) => {
|
|
446
|
+
this.bleManager.stopDeviceScan();
|
|
447
|
+
resolve();
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Discover services and characteristics for a connected device
|
|
453
|
+
*
|
|
454
|
+
* @param deviceId - Device identifier
|
|
455
|
+
* @param device - Connected device object
|
|
456
|
+
*/
|
|
457
|
+
private async discoverServices(deviceId: string, _device: RNDevice): Promise<void> {
|
|
458
|
+
Logger.scope('ReactNativeAdapter').debug('Discovering services for device:', deviceId);
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const servicesResult = await (this.bleManager.discoverAllServicesAndCharacteristicsForDevice(
|
|
462
|
+
deviceId
|
|
463
|
+
) as Promise<{ services: RNService[] }>);
|
|
464
|
+
|
|
465
|
+
const services = servicesResult.services || [];
|
|
466
|
+
|
|
467
|
+
for (const service of services) {
|
|
468
|
+
const writeChar = service.characteristics.find(
|
|
469
|
+
(c: RNCharacteristic) =>
|
|
470
|
+
c.isWritableWithResponse || c.isWritableWithoutResponse
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if (writeChar) {
|
|
474
|
+
this.serviceCache.set(deviceId, {
|
|
475
|
+
serviceId: service.uuid,
|
|
476
|
+
characteristicId: writeChar.uuid,
|
|
477
|
+
});
|
|
478
|
+
Logger.scope('ReactNativeAdapter').info('Found writeable characteristic:', {
|
|
479
|
+
service: service.uuid,
|
|
480
|
+
characteristic: writeChar.uuid,
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
throw new BluetoothPrintError(
|
|
487
|
+
ErrorCode.CHARACTERISTIC_NOT_FOUND,
|
|
488
|
+
'No writeable characteristic found. Make sure the device is a supported printer.'
|
|
489
|
+
);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (error instanceof BluetoothPrintError) {
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
throw new BluetoothPrintError(
|
|
495
|
+
ErrorCode.SERVICE_DISCOVERY_FAILED,
|
|
496
|
+
'Failed to discover device services',
|
|
497
|
+
error as Error
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Convert ArrayBuffer to base64 string for react-native-ble-plx
|
|
504
|
+
*
|
|
505
|
+
* @param buffer - ArrayBuffer to convert
|
|
506
|
+
* @returns Base64 encoded string
|
|
507
|
+
*/
|
|
508
|
+
private arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
509
|
+
const bytes = new Uint8Array(buffer);
|
|
510
|
+
let binary = '';
|
|
511
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
512
|
+
binary += String.fromCharCode(bytes[i] ?? 0);
|
|
513
|
+
}
|
|
514
|
+
// Use built-in btoa (available in React Native)
|
|
515
|
+
return globalThis.btoa(binary);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapters barrel export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { BaseAdapter, MiniProgramAdapter } from './BaseAdapter';
|
|
6
|
+
export type { MiniProgramBLEApi, ServiceInfo, BLECharacteristic, BLECharacteristicProperties } from './BaseAdapter';
|
|
7
|
+
export { TaroAdapter } from './TaroAdapter';
|
|
8
|
+
export { AlipayAdapter } from './AlipayAdapter';
|
|
9
|
+
export { BaiduAdapter } from './BaiduAdapter';
|
|
10
|
+
export { ByteDanceAdapter } from './ByteDanceAdapter';
|
|
11
|
+
export { QQAdapter } from './QQAdapter';
|
|
12
|
+
export { ReactNativeAdapter } from './ReactNativeAdapter';
|
|
13
|
+
export { WebBluetoothAdapter } from './WebBluetoothAdapter';
|
|
14
|
+
export { AdapterFactory } from './AdapterFactory';
|
|
@@ -425,12 +425,16 @@ export class PrinterConfigManager {
|
|
|
425
425
|
* Export all configuration as JSON
|
|
426
426
|
*/
|
|
427
427
|
export(): string {
|
|
428
|
-
return JSON.stringify(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
428
|
+
return JSON.stringify(
|
|
429
|
+
{
|
|
430
|
+
printers: Array.from(this.printers.values()),
|
|
431
|
+
globalConfig: this.globalConfig,
|
|
432
|
+
lastUsedPrinterId: this.lastUsedPrinterId,
|
|
433
|
+
exportedAt: Date.now(),
|
|
434
|
+
},
|
|
435
|
+
null,
|
|
436
|
+
2
|
|
437
|
+
);
|
|
434
438
|
}
|
|
435
439
|
|
|
436
440
|
/**
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { Logger } from '@/utils/logger';
|
|
27
27
|
import { BluetoothPrintError, ErrorCode } from '@/errors/BluetoothError';
|
|
28
28
|
import { BluetoothPrinter } from '@/core/BluetoothPrinter';
|
|
29
|
+
import { PrinterState } from '@/types';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Printer connection info
|
|
@@ -137,11 +138,12 @@ export class MultiPrinterManager {
|
|
|
137
138
|
* Emit an event
|
|
138
139
|
*/
|
|
139
140
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
140
142
|
private emit<K extends keyof MultiPrinterManagerEvents>(event: K, data: any): void {
|
|
141
143
|
this.listeners[event].forEach(handler => {
|
|
142
144
|
try {
|
|
143
|
-
// eslint-disable-next-line @typescript-eslint/no-
|
|
144
|
-
(handler as any)(data);
|
|
145
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
146
|
+
(handler as (data: any) => void)(data);
|
|
145
147
|
} catch (error) {
|
|
146
148
|
this.logger.error(`Error in event handler for "${event}":`, error);
|
|
147
149
|
}
|
|
@@ -200,7 +202,7 @@ export class MultiPrinterManager {
|
|
|
200
202
|
const printer = new BluetoothPrinter();
|
|
201
203
|
|
|
202
204
|
// Set up error handler
|
|
203
|
-
printer.on('error',
|
|
205
|
+
printer.on('error', error => {
|
|
204
206
|
this.emit('printer-error', { printerId, error });
|
|
205
207
|
});
|
|
206
208
|
|
|
@@ -323,13 +325,10 @@ export class MultiPrinterManager {
|
|
|
323
325
|
/**
|
|
324
326
|
* Print to a specific printer
|
|
325
327
|
*/
|
|
326
|
-
|
|
328
|
+
print(printerId: string, data: Uint8Array): void {
|
|
327
329
|
const connection = this.printers.get(printerId);
|
|
328
330
|
if (!connection) {
|
|
329
|
-
throw new BluetoothPrintError(
|
|
330
|
-
ErrorCode.DEVICE_NOT_FOUND,
|
|
331
|
-
`Printer not found: ${printerId}`
|
|
332
|
-
);
|
|
331
|
+
throw new BluetoothPrintError(ErrorCode.DEVICE_NOT_FOUND, `Printer not found: ${printerId}`);
|
|
333
332
|
}
|
|
334
333
|
|
|
335
334
|
connection.lastActivity = Date.now();
|
|
@@ -358,6 +357,7 @@ export class MultiPrinterManager {
|
|
|
358
357
|
}
|
|
359
358
|
|
|
360
359
|
const printPromises = Array.from(this.printers.entries()).map(
|
|
360
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
361
361
|
async ([printerId, connection]) => {
|
|
362
362
|
try {
|
|
363
363
|
// Update activity
|
|
@@ -401,7 +401,7 @@ export class MultiPrinterManager {
|
|
|
401
401
|
*/
|
|
402
402
|
getIdlePrinters(): PrinterConnection[] {
|
|
403
403
|
return Array.from(this.printers.values())
|
|
404
|
-
.filter(c => c.printer.state ===
|
|
404
|
+
.filter(c => c.printer.state === PrinterState.CONNECTED)
|
|
405
405
|
.sort((a, b) => (a.lastActivity ?? 0) - (b.lastActivity ?? 0));
|
|
406
406
|
}
|
|
407
407
|
|
|
@@ -420,7 +420,7 @@ export class MultiPrinterManager {
|
|
|
420
420
|
};
|
|
421
421
|
|
|
422
422
|
for (const connection of this.printers.values()) {
|
|
423
|
-
if (connection.printer.state ===
|
|
423
|
+
if (connection.printer.state === PrinterState.CONNECTED) {
|
|
424
424
|
stats.connected++;
|
|
425
425
|
}
|
|
426
426
|
|