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.
- package/CHANGELOG.md +38 -0
- package/README.md +128 -22
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -6995
- package/dist/index.umd.js +1 -1
- package/dist/types/adapters/AdapterFactory.d.ts +0 -1
- package/dist/types/adapters/AlipayAdapter.d.ts +6 -34
- package/dist/types/adapters/BaiduAdapter.d.ts +6 -34
- package/dist/types/adapters/BaseAdapter.d.ts +112 -1
- package/dist/types/adapters/ByteDanceAdapter.d.ts +6 -34
- package/dist/types/adapters/TaroAdapter.d.ts +6 -34
- package/dist/types/adapters/WebBluetoothAdapter.d.ts +0 -1
- package/dist/types/config/PrinterConfig.d.ts +0 -1
- package/dist/types/core/BluetoothPrinter.d.ts +0 -1
- package/dist/types/drivers/EscPos.d.ts +0 -1
- package/dist/types/drivers/TsplDriver.d.ts +251 -0
- package/dist/types/encoding/gbk-data.d.ts +12 -0
- package/dist/types/encoding/gbk-table.d.ts +5 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/plugins/PluginManager.d.ts +87 -0
- package/dist/types/plugins/builtin/LoggingPlugin.d.ts +14 -0
- package/dist/types/plugins/builtin/RetryPlugin.d.ts +18 -0
- package/dist/types/plugins/index.d.ts +7 -0
- package/dist/types/plugins/types.d.ts +97 -0
- package/dist/types/services/CommandBuilder.d.ts +6 -1
- package/dist/types/services/ConnectionManager.d.ts +0 -1
- package/dist/types/services/PrintJobManager.d.ts +6 -2
- package/dist/types/services/interfaces/index.d.ts +0 -1
- package/dist/types/template/TemplateEngine.d.ts +0 -1
- package/package.json +16 -18
- package/src/adapters/AlipayAdapter.ts +8 -314
- package/src/adapters/BaiduAdapter.ts +8 -312
- package/src/adapters/BaseAdapter.ts +366 -0
- package/src/adapters/ByteDanceAdapter.ts +8 -316
- package/src/adapters/TaroAdapter.ts +8 -367
- package/src/core/EventEmitter.ts +9 -6
- package/src/drivers/TsplDriver.ts +417 -0
- package/src/encoding/gbk-data.ts +1911 -0
- package/src/encoding/gbk-table.ts +22 -498
- package/src/index.ts +14 -0
- package/src/plugins/PluginManager.ts +193 -0
- package/src/plugins/builtin/LoggingPlugin.ts +97 -0
- package/src/plugins/builtin/RetryPlugin.ts +109 -0
- package/src/plugins/index.ts +10 -0
- package/src/plugins/types.ts +119 -0
- package/src/preview/PreviewRenderer.ts +7 -1
- package/src/queue/PrintQueue.ts +10 -6
- package/src/services/CommandBuilder.ts +30 -0
- 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
|
+
}
|