jc-printer-sdk-ts 0.1.0 → 0.1.2

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 (41) hide show
  1. package/README.md +65 -0
  2. package/dist/browserImages.d.ts +43 -0
  3. package/dist/browserImages.d.ts.map +1 -0
  4. package/dist/browserImages.js +224 -0
  5. package/dist/browserImages.js.map +1 -0
  6. package/dist/client.d.ts +6 -1
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +89 -0
  9. package/dist/client.js.map +1 -1
  10. package/dist/imagePrint.d.ts +69 -0
  11. package/dist/imagePrint.d.ts.map +1 -0
  12. package/dist/imagePrint.js +180 -0
  13. package/dist/imagePrint.js.map +1 -0
  14. package/dist/index.d.ts +4 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/niimbotProtocol.d.ts +38 -0
  19. package/dist/niimbotProtocol.d.ts.map +1 -0
  20. package/dist/niimbotProtocol.js +265 -0
  21. package/dist/niimbotProtocol.js.map +1 -0
  22. package/dist/transport.d.ts +5 -1
  23. package/dist/transport.d.ts.map +1 -1
  24. package/dist/transport.js +58 -0
  25. package/dist/transport.js.map +1 -1
  26. package/dist/types.d.ts +28 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/webBluetoothTransport.d.ts +137 -0
  29. package/dist/webBluetoothTransport.d.ts.map +1 -0
  30. package/dist/webBluetoothTransport.js +870 -0
  31. package/dist/webBluetoothTransport.js.map +1 -0
  32. package/package.json +1 -1
  33. package/src/browserImages.ts +323 -0
  34. package/src/client.ts +121 -0
  35. package/src/imagePrint.ts +306 -0
  36. package/src/index.ts +4 -0
  37. package/src/niimbotProtocol.ts +355 -0
  38. package/src/transport.ts +66 -0
  39. package/src/types.ts +44 -0
  40. package/src/web-bluetooth.d.ts +44 -0
  41. package/src/webBluetoothTransport.ts +1171 -0
@@ -0,0 +1,1171 @@
1
+ import type {
2
+ BitmapLike,
3
+ BundleLike,
4
+ IAtBitmapDocument,
5
+ PaperInfo,
6
+ PrintCallback,
7
+ PrintDocument,
8
+ PrinterDevice,
9
+ PrinterInfo,
10
+ PrinterReadiness,
11
+ PrinterReadinessWarningCode,
12
+ PrinterSdkCallback,
13
+ PrinterState,
14
+ PrinterStatusSnapshot,
15
+ ScanCallback,
16
+ } from "./types.js";
17
+ import { JCPrintApi } from "./client.js";
18
+ import type { PrinterTransport } from "./transport.js";
19
+ import {
20
+ createConnectFrame,
21
+ createFrame,
22
+ createRowFrames,
23
+ fallbackProfile,
24
+ identifyProfile,
25
+ parseFrame,
26
+ parseModelId,
27
+ parseProtocolVersion,
28
+ rasterizeDocumentPage,
29
+ type NiimbotPrinterProfile,
30
+ } from "./niimbotProtocol.js";
31
+
32
+ export interface WebBluetoothPrinterTransportOptions {
33
+ serviceUuid?: string;
34
+ characteristicUuid?: string;
35
+ deviceNameFilter?: string;
36
+ scanMode?: "all" | "name";
37
+ optionalServices?: string[];
38
+ chunkSize?: number;
39
+ preflightStatus?: boolean;
40
+ statusQueryTimeoutMs?: number;
41
+ }
42
+
43
+ interface ResolvedWebBluetoothPrinterTransportOptions {
44
+ serviceUuid: string;
45
+ characteristicUuid: string;
46
+ deviceNameFilter: string;
47
+ scanMode: "all" | "name";
48
+ optionalServices: string[];
49
+ chunkSize: number;
50
+ preflightStatus: boolean;
51
+ statusQueryTimeoutMs: number;
52
+ }
53
+
54
+ export const NIIMBOT_WEB_BLUETOOTH_SERVICE_UUID = "e7810a71-73ae-499d-8c15-faa9aef0c3f2";
55
+ export const NIIMBOT_WEB_BLUETOOTH_CHARACTERISTIC_UUID = "bef8d6c9-9c21-4c9e-b632-bd58c1009f9f";
56
+
57
+ export const DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS: ResolvedWebBluetoothPrinterTransportOptions = {
58
+ serviceUuid: NIIMBOT_WEB_BLUETOOTH_SERVICE_UUID,
59
+ characteristicUuid: NIIMBOT_WEB_BLUETOOTH_CHARACTERISTIC_UUID,
60
+ deviceNameFilter: "",
61
+ scanMode: "all",
62
+ optionalServices: [],
63
+ chunkSize: 180,
64
+ preflightStatus: true,
65
+ statusQueryTimeoutMs: 700,
66
+ };
67
+
68
+ interface PrintJobContext {
69
+ density: number;
70
+ paperType: number;
71
+ printMode: number;
72
+ callback?: PrintCallback;
73
+ }
74
+
75
+ type PayloadEnvelope = {
76
+ kind: "jc-printer-sdk-ts/web-bluetooth";
77
+ sentAt: string;
78
+ job: {
79
+ density: number;
80
+ paperType: number;
81
+ printMode: number;
82
+ };
83
+ document?: PrintDocument;
84
+ jsonPages?: string[];
85
+ infoPages?: string[];
86
+ profile?: Record<string, unknown>;
87
+ };
88
+
89
+ interface ParsedResponse {
90
+ cmd: number;
91
+ data: number[];
92
+ }
93
+
94
+ interface PendingResponse {
95
+ expected: Set<number>;
96
+ resolve(response: ParsedResponse | null): void;
97
+ reject(error: Error): void;
98
+ timeoutId: number;
99
+ optional: boolean;
100
+ }
101
+
102
+ const DEFAULT_GATT_BUSY_RETRY_COUNT = 8;
103
+ const DEFAULT_GATT_BUSY_RETRY_DELAY_MS = 25;
104
+ const HEARTBEAT_ADVANCED_1 = 0x01;
105
+ const HEARTBEAT_ADVANCED_2 = 0x04;
106
+ const HEARTBEAT_RESPONSE_COMMANDS = [0xd9, 0xdd, 0xde, 0xdf];
107
+ const INVERTED_LID_MODEL_IDS = new Set([512, 514, 513, 2304, 1792, 3584, 5120, 2560, 3840, 4352, 272, 273, 274]);
108
+
109
+ function clone<T>(value: T): T {
110
+ if (typeof globalThis.structuredClone === "function") {
111
+ return globalThis.structuredClone(value);
112
+ }
113
+ return JSON.parse(JSON.stringify(value)) as T;
114
+ }
115
+
116
+ function normalizeUuid(uuid: string): string {
117
+ return uuid.trim().toLowerCase();
118
+ }
119
+
120
+ function uniqueStrings(values: Array<string | undefined>): string[] {
121
+ return Array.from(new Set(values.map((value) => value?.trim()).filter((value): value is string => !!value)));
122
+ }
123
+
124
+ function isGattOperationBusyError(error: unknown): boolean {
125
+ if (!(error instanceof Error)) {
126
+ return false;
127
+ }
128
+ return /GATT operation already in progress/i.test(error.message);
129
+ }
130
+
131
+ function resolveOptions(
132
+ options: Partial<WebBluetoothPrinterTransportOptions> = {},
133
+ ): ResolvedWebBluetoothPrinterTransportOptions {
134
+ const chunkSize = Number.isFinite(options.chunkSize)
135
+ ? Math.trunc(Number(options.chunkSize))
136
+ : DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.chunkSize;
137
+
138
+ return {
139
+ serviceUuid: options.serviceUuid ?? DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.serviceUuid,
140
+ characteristicUuid: options.characteristicUuid ?? DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.characteristicUuid,
141
+ deviceNameFilter: options.deviceNameFilter?.trim() ?? DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.deviceNameFilter,
142
+ scanMode: options.scanMode ?? DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.scanMode,
143
+ optionalServices: [...(options.optionalServices ?? DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.optionalServices)],
144
+ chunkSize: Math.max(20, chunkSize),
145
+ preflightStatus: options.preflightStatus ?? DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.preflightStatus,
146
+ statusQueryTimeoutMs: Math.max(
147
+ 100,
148
+ Math.trunc(Number(options.statusQueryTimeoutMs ?? DEFAULT_WEB_BLUETOOTH_PRINTER_TRANSPORT_OPTIONS.statusQueryTimeoutMs)),
149
+ ),
150
+ };
151
+ }
152
+
153
+ function signedByte(value: number): number {
154
+ const byte = value & 0xff;
155
+ return byte > 0x7f ? byte - 0x100 : byte;
156
+ }
157
+
158
+ function signedWord(high: number, low: number): number {
159
+ const value = ((high & 0xff) << 8) | (low & 0xff);
160
+ return value > 0x7fff ? value - 0x10000 : value;
161
+ }
162
+
163
+ function defaultPrinterInfo(): PrinterInfo {
164
+ return {
165
+ deviceType: 2,
166
+ deviceName: "",
167
+ deviceVersion: "",
168
+ softwareVersion: "web-bluetooth",
169
+ deviceAddress: "",
170
+ deviceAddrType: 2,
171
+ deviceDPI: 0,
172
+ deviceWidth: 0,
173
+ manufacturer: "",
174
+ seriesName: "",
175
+ devIntName: "",
176
+ peripheralFlags: 0,
177
+ hardwareFlags: 0,
178
+ softwareFlags: 0,
179
+ };
180
+ }
181
+
182
+ function defaultPaperInfo(): PaperInfo {
183
+ return {
184
+ state: 0,
185
+ gapHeightPixel: 0,
186
+ totalHeightPixel: 0,
187
+ paperType: 0,
188
+ gapHeight: 0,
189
+ totalHeight: 0,
190
+ paperWidthPixel: 0,
191
+ paperWidth: 0,
192
+ direction: 0,
193
+ tailLengthPixel: 0,
194
+ tailLength: 0,
195
+ };
196
+ }
197
+
198
+ export class WebBluetoothPrinterTransport implements PrinterTransport {
199
+ private options: ResolvedWebBluetoothPrinterTransportOptions;
200
+ private sdkCallback: PrinterSdkCallback | undefined;
201
+ private device: BluetoothDevice | undefined;
202
+ private server: BluetoothRemoteGATTServer | undefined;
203
+ private characteristic: BluetoothRemoteGATTCharacteristic | undefined;
204
+ private printerName = "";
205
+ private printerInfo: PrinterInfo = defaultPrinterInfo();
206
+ private paperInfo: PaperInfo = defaultPaperInfo();
207
+ private printerState: PrinterState = "Disconnected";
208
+ private lastAddress = "";
209
+ private currentJob: PrintJobContext | undefined;
210
+ private lastPayload: PayloadEnvelope | undefined;
211
+ private reconnecting = false;
212
+ private connectedProfile: NiimbotPrinterProfile | null = null;
213
+ private b1HandshakeDone = false;
214
+ private responseQueue: ParsedResponse[] = [];
215
+ private pendingResponse: PendingResponse | undefined;
216
+ private gattOperationQueue: Promise<void> = Promise.resolve();
217
+
218
+ constructor(options: Partial<WebBluetoothPrinterTransportOptions> = {}) {
219
+ this.options = resolveOptions(options);
220
+ }
221
+
222
+ configure(options: Partial<WebBluetoothPrinterTransportOptions>): void {
223
+ this.options = resolveOptions({
224
+ ...this.options,
225
+ ...options,
226
+ });
227
+ }
228
+
229
+ getLastPayload(): PayloadEnvelope | undefined {
230
+ return this.lastPayload ? clone(this.lastPayload) : undefined;
231
+ }
232
+
233
+ setSdkCallback(callback?: PrinterSdkCallback): void {
234
+ this.sdkCallback = callback;
235
+ if (callback && this.printerState !== "Disconnected" && this.lastAddress) {
236
+ callback.onConnectSuccess(this.lastAddress, 20);
237
+ }
238
+ }
239
+
240
+ async initSdk(): Promise<boolean> {
241
+ return true;
242
+ }
243
+
244
+ async init(): Promise<boolean> {
245
+ return true;
246
+ }
247
+
248
+ async connectBluetoothPrinter(address: string): Promise<number> {
249
+ if (!navigator.bluetooth) {
250
+ throw new Error("Web Bluetooth is not available in this browser.");
251
+ }
252
+
253
+ const deviceFilter = this.options.deviceNameFilter?.trim() || address.trim();
254
+ const serviceUuid = normalizeUuid(this.options.serviceUuid);
255
+ const characteristicUuid = normalizeUuid(this.options.characteristicUuid);
256
+ if (!serviceUuid || !characteristicUuid) {
257
+ throw new Error("Service UUID and characteristic UUID are required for Web Bluetooth printing.");
258
+ }
259
+ const optionalServices = uniqueStrings([serviceUuid, ...(this.options.optionalServices ?? [])]);
260
+ const useNameFilter = this.options.scanMode === "name" && deviceFilter;
261
+ const requestOptions: BluetoothRequestDeviceOptions = useNameFilter
262
+ ? {
263
+ filters: [{ namePrefix: deviceFilter }],
264
+ optionalServices,
265
+ }
266
+ : {
267
+ acceptAllDevices: true,
268
+ optionalServices,
269
+ };
270
+
271
+ const device = await navigator.bluetooth.requestDevice(requestOptions);
272
+ const server = await this.runGattOperation(() => device.gatt!.connect());
273
+ if (!server) {
274
+ throw new Error("Failed to open the selected Bluetooth device.");
275
+ }
276
+
277
+ const service = await this.runGattOperation(() => server.getPrimaryService(serviceUuid));
278
+ const characteristic = await this.runGattOperation(() => service.getCharacteristic(characteristicUuid));
279
+
280
+ this.device = device;
281
+ this.server = server;
282
+ this.characteristic = characteristic;
283
+ this.lastAddress = device.name || device.id || deviceFilter || serviceUuid;
284
+ this.printerName = this.lastAddress;
285
+ this.printerState = "Connected";
286
+ this.printerInfo = {
287
+ ...defaultPrinterInfo(),
288
+ deviceName: this.printerName,
289
+ deviceAddress: this.lastAddress,
290
+ softwareVersion: "web-bluetooth",
291
+ deviceAddrType: 2,
292
+ };
293
+ this.paperInfo = defaultPaperInfo();
294
+
295
+ device.addEventListener("gattserverdisconnected", this.handleDisconnect);
296
+ characteristic.addEventListener("characteristicvaluechanged", this.handleCharacteristicChanged);
297
+ await this.runGattOperation(() => characteristic.startNotifications());
298
+ await this.handshakeAndIdentify();
299
+ this.sdkCallback?.onConnectSuccess(this.lastAddress, 20);
300
+ return 0;
301
+ }
302
+
303
+ async openPrinterByAddress(address: string): Promise<number> {
304
+ return this.connectBluetoothPrinter(address);
305
+ }
306
+
307
+ async openPrinterSync(address: string): Promise<number> {
308
+ return this.connectBluetoothPrinter(address);
309
+ }
310
+
311
+ async connectWifiPrinter(): Promise<number> {
312
+ throw new Error("Wi-Fi printing is not implemented in the browser transport.");
313
+ }
314
+
315
+ async configurationWifi(): Promise<number> {
316
+ return 0;
317
+ }
318
+
319
+ async getConfigurationWifiName(): Promise<string> {
320
+ return "";
321
+ }
322
+
323
+ async scanWifiPrinter(callback: ScanCallback): Promise<void> {
324
+ callback.onFinish();
325
+ }
326
+
327
+ async setPrinterAutoShutdownTime(): Promise<number> {
328
+ return 0;
329
+ }
330
+
331
+ async setTotalPrintQuantity(): Promise<void> {
332
+ return;
333
+ }
334
+
335
+ async setTotalQuantityOfPrints(): Promise<void> {
336
+ return;
337
+ }
338
+
339
+ async startPrintJob(
340
+ density: number,
341
+ paperType: number,
342
+ printMode: number,
343
+ callback: PrintCallback,
344
+ ): Promise<void> {
345
+ if (this.currentJob && (this.printerState === "Printing" || this.printerState === "Working")) {
346
+ throw new Error("A print job is already in progress on this Web Bluetooth printer.");
347
+ }
348
+
349
+ this.currentJob = { density, paperType, printMode, callback };
350
+ this.printerState = "Working";
351
+
352
+ try {
353
+ if (this.options.preflightStatus) {
354
+ const readiness = await this.evaluatePrintReadiness(this.options.statusQueryTimeoutMs, true);
355
+ if (!readiness.canPrint) {
356
+ throw new Error(`Printer is not ready to print: ${readiness.reason}`);
357
+ }
358
+ }
359
+
360
+ this.printerState = "Printing";
361
+ callback.onProgress(0, 0, {
362
+ phase: "start",
363
+ density,
364
+ paperType,
365
+ printMode,
366
+ });
367
+ } catch (error) {
368
+ this.currentJob = undefined;
369
+ this.printerState = this.server?.connected ? "Connected" : "Disconnected";
370
+ throw error;
371
+ }
372
+ }
373
+
374
+ async commitData(jsonPages: string[], infoPages: string[]): Promise<void> {
375
+ await this.writePayload({
376
+ kind: "jc-printer-sdk-ts/web-bluetooth",
377
+ sentAt: new Date().toISOString(),
378
+ job: {
379
+ density: this.currentJob?.density ?? 0,
380
+ paperType: this.currentJob?.paperType ?? 0,
381
+ printMode: this.currentJob?.printMode ?? 0,
382
+ },
383
+ jsonPages: clone(jsonPages),
384
+ infoPages: clone(infoPages),
385
+ });
386
+ }
387
+
388
+ async commitDocument(document: PrintDocument, infoPages: string[]): Promise<void> {
389
+ const profile = await this.resolveProfile(document);
390
+ this.lastPayload = {
391
+ kind: "jc-printer-sdk-ts/web-bluetooth",
392
+ sentAt: new Date().toISOString(),
393
+ job: {
394
+ density: document.job.density ?? this.currentJob?.density ?? 0,
395
+ paperType: document.job.paperType ?? this.currentJob?.paperType ?? 0,
396
+ printMode: document.job.printMode ?? this.currentJob?.printMode ?? 0,
397
+ },
398
+ document: clone(document),
399
+ infoPages: clone(infoPages),
400
+ profile: { ...profile },
401
+ };
402
+ await this.printNiimbotDocument(document, profile);
403
+ this.currentJob?.callback?.onProgress(document.pages.length, document.job.totalQuantity ?? 1, {
404
+ pages: document.pages.length,
405
+ profile: profile.label,
406
+ task: profile.task,
407
+ dpi: profile.dpi,
408
+ });
409
+ this.currentJob?.callback?.onBufferFree(document.pages.length, profile.printheadWidthPx);
410
+ }
411
+
412
+ async endJob(): Promise<boolean> {
413
+ return this.endPrintJob();
414
+ }
415
+
416
+ async endPrintJob(): Promise<boolean> {
417
+ this.printerState = this.server?.connected ? "Connected" : "Disconnected";
418
+ this.currentJob = undefined;
419
+ return true;
420
+ }
421
+
422
+ async cancelJob(): Promise<boolean> {
423
+ this.currentJob?.callback?.onCancelJob(true);
424
+ this.currentJob = undefined;
425
+ this.printerState = this.server?.connected ? "Connected" : "Disconnected";
426
+ return true;
427
+ }
428
+
429
+ async cancel(): Promise<void> {
430
+ await this.cancelJob();
431
+ }
432
+
433
+ async reopenPrinter(): Promise<boolean> {
434
+ if (this.reconnecting || !this.device?.gatt || this.server?.connected) {
435
+ return !!this.server?.connected;
436
+ }
437
+
438
+ this.reconnecting = true;
439
+ try {
440
+ const server = await this.runGattOperation(() => this.device!.gatt!.connect());
441
+ this.server = server;
442
+ const service = await this.runGattOperation(() => server.getPrimaryService(normalizeUuid(this.options.serviceUuid)));
443
+ const characteristic = await this.runGattOperation(() =>
444
+ service.getCharacteristic(normalizeUuid(this.options.characteristicUuid)),
445
+ );
446
+ this.characteristic = characteristic;
447
+ characteristic.addEventListener("characteristicvaluechanged", this.handleCharacteristicChanged);
448
+ await this.runGattOperation(() => characteristic.startNotifications());
449
+ await this.handshakeAndIdentify();
450
+ this.printerState = "Connected";
451
+ this.sdkCallback?.onConnectSuccess(this.lastAddress, 20);
452
+ return true;
453
+ } finally {
454
+ this.reconnecting = false;
455
+ }
456
+ }
457
+
458
+ async reopenPrinterSync(): Promise<boolean> {
459
+ return this.reopenPrinter();
460
+ }
461
+
462
+ async close(): Promise<void> {
463
+ this.currentJob?.callback?.onCancelJob(true);
464
+ this.currentJob = undefined;
465
+ this.printerState = "Disconnected";
466
+ if (this.device) {
467
+ this.device.removeEventListener("gattserverdisconnected", this.handleDisconnect);
468
+ }
469
+ if (this.characteristic) {
470
+ this.characteristic.removeEventListener("characteristicvaluechanged", this.handleCharacteristicChanged);
471
+ try {
472
+ await this.runGattOperation(() => this.characteristic!.stopNotifications());
473
+ } catch {
474
+ // Some browsers reject when the device is already gone.
475
+ }
476
+ }
477
+ if (this.server?.connected) {
478
+ this.server.disconnect();
479
+ }
480
+ this.server = undefined;
481
+ this.characteristic = undefined;
482
+ this.printerName = "";
483
+ this.lastAddress = "";
484
+ this.sdkCallback?.onDisConnect();
485
+ }
486
+
487
+ async quit(): Promise<void> {
488
+ await this.close();
489
+ }
490
+
491
+ async printBitmap(bitmap: BitmapLike, _bundle: BundleLike = {}): Promise<boolean> {
492
+ await this.writeJson({
493
+ kind: "jc-printer-sdk-ts/web-bluetooth-bitmap",
494
+ sentAt: new Date().toISOString(),
495
+ bitmap,
496
+ });
497
+ return true;
498
+ }
499
+
500
+ async printATBitmap(bitmap: IAtBitmapDocument, _bundle: BundleLike = {}): Promise<boolean> {
501
+ await this.writeJson({
502
+ kind: "jc-printer-sdk-ts/web-bluetooth-at-bitmap",
503
+ sentAt: new Date().toISOString(),
504
+ bitmap,
505
+ });
506
+ return true;
507
+ }
508
+
509
+ async getPaperInfo(): Promise<PaperInfo> {
510
+ return clone(this.paperInfo);
511
+ }
512
+
513
+ async isConnection(): Promise<number> {
514
+ return this.server?.connected ? 0 : -1;
515
+ }
516
+
517
+ async getPrintScaleMultiplier(): Promise<number> {
518
+ return 1;
519
+ }
520
+
521
+ async getPrinterName(): Promise<string> {
522
+ return this.printerName;
523
+ }
524
+
525
+ async getPrinterInfo(): Promise<PrinterInfo> {
526
+ return clone(this.printerInfo);
527
+ }
528
+
529
+ async queryPrinterStatus(timeoutMs = this.options.statusQueryTimeoutMs): Promise<PrinterStatusSnapshot> {
530
+ if (!this.isDeviceConnected()) {
531
+ return this.createLocalStatusSnapshot(false);
532
+ }
533
+
534
+ if (this.currentJob && (this.printerState === "Printing" || this.printerState === "Working")) {
535
+ return this.createLocalStatusSnapshot(true);
536
+ }
537
+
538
+ return this.queryPhysicalPrinterStatus(timeoutMs);
539
+ }
540
+
541
+ async canStartPrintJob(timeoutMs = this.options.statusQueryTimeoutMs): Promise<PrinterReadiness> {
542
+ return this.evaluatePrintReadiness(timeoutMs, false);
543
+ }
544
+
545
+ async getPrinterState(): Promise<PrinterState> {
546
+ return this.printerState;
547
+ }
548
+
549
+ async waitPrinterState(state: PrinterState, timeoutMs: number): Promise<boolean> {
550
+ const startedAt = Date.now();
551
+ while (Date.now() - startedAt < timeoutMs) {
552
+ if (this.printerState === state) {
553
+ return true;
554
+ }
555
+ await new Promise((resolve) => setTimeout(resolve, 100));
556
+ }
557
+ return false;
558
+ }
559
+
560
+ async measureFontHeight(
561
+ text: string,
562
+ _x: number,
563
+ _y: number,
564
+ _width: number,
565
+ _height: number,
566
+ _rotation: number,
567
+ fontSize: number,
568
+ ): Promise<number> {
569
+ const lines = text.split(/\r?\n/).length;
570
+ return Math.max(fontSize, Math.ceil(fontSize * 1.2 * lines));
571
+ }
572
+
573
+ async generatePreviewImage(json: string, info: string): Promise<BitmapLike> {
574
+ return {
575
+ width: 0,
576
+ height: 0,
577
+ description: "web-bluetooth-preview",
578
+ meta: { json, info },
579
+ };
580
+ }
581
+
582
+ async generatePrintPreviewImage(
583
+ json: string,
584
+ widthMm: number,
585
+ heightMm: number,
586
+ orientation: number,
587
+ ): Promise<BitmapLike> {
588
+ return {
589
+ width: widthMm,
590
+ height: heightMm,
591
+ description: "web-bluetooth-print-preview",
592
+ meta: { json, orientation },
593
+ };
594
+ }
595
+
596
+ setPrinterState(state: PrinterState): void {
597
+ this.printerState = state;
598
+ }
599
+
600
+ setPrinterInfo(info: PrinterInfo): void {
601
+ this.printerInfo = clone(info);
602
+ }
603
+
604
+ setPaperInfo(info: PaperInfo): void {
605
+ this.paperInfo = clone(info);
606
+ }
607
+
608
+ setScanResults(_results: PrinterDevice[]): void {
609
+ return;
610
+ }
611
+
612
+ emitPrintProgress(pageIndex: number, quantityIndex: number, meta?: Record<string, unknown>): void {
613
+ this.currentJob?.callback?.onProgress(pageIndex, quantityIndex, meta ?? {});
614
+ }
615
+
616
+ emitPrintError(errorCode: number, printState?: number): void {
617
+ this.currentJob?.callback?.onError(errorCode, printState);
618
+ }
619
+
620
+ emitBufferFree(pageIndex: number, bufferSize: number): void {
621
+ this.currentJob?.callback?.onBufferFree(pageIndex, bufferSize);
622
+ }
623
+
624
+ emitCancel(success: boolean): void {
625
+ this.currentJob?.callback?.onCancelJob(success);
626
+ }
627
+
628
+ private isDeviceConnected(): boolean {
629
+ return !!this.server?.connected && !!this.characteristic;
630
+ }
631
+
632
+ private createLocalStatusSnapshot(connected = this.isDeviceConnected()): PrinterStatusSnapshot {
633
+ return {
634
+ connected,
635
+ state: this.printerState,
636
+ statusAvailable: false,
637
+ };
638
+ }
639
+
640
+ private async evaluatePrintReadiness(
641
+ timeoutMs: number,
642
+ ignoreCurrentJob: boolean,
643
+ ): Promise<PrinterReadiness> {
644
+ if (!this.isDeviceConnected()) {
645
+ return this.createReadiness("not_connected", "Printer is not connected.", false, this.createLocalStatusSnapshot(false));
646
+ }
647
+
648
+ if (!ignoreCurrentJob && this.currentJob && this.printerState === "Printing") {
649
+ return this.createReadiness(
650
+ "sdk_printing",
651
+ "A print job is already in progress on this Web Bluetooth printer.",
652
+ false,
653
+ this.createLocalStatusSnapshot(true),
654
+ );
655
+ }
656
+
657
+ if (!ignoreCurrentJob && this.currentJob && this.printerState === "Working") {
658
+ return this.createReadiness(
659
+ "printer_working",
660
+ "Printer is preparing another job.",
661
+ false,
662
+ this.createLocalStatusSnapshot(true),
663
+ );
664
+ }
665
+
666
+ const status = await this.queryPhysicalPrinterStatus(timeoutMs);
667
+ const warnings = this.collectReadinessWarnings(status);
668
+
669
+ if (!status.statusAvailable) {
670
+ return this.createReadiness(
671
+ "status_unavailable",
672
+ "Printer is connected, but live hardware status did not respond before the timeout.",
673
+ true,
674
+ status,
675
+ warnings,
676
+ );
677
+ }
678
+
679
+ if (status.lidClosed === false) {
680
+ return this.createReadiness("cover_open", "Printer cover is open.", false, status, warnings);
681
+ }
682
+
683
+ if (status.paperInserted === false) {
684
+ return this.createReadiness("paper_missing", "Printer has no paper inserted.", false, status, warnings);
685
+ }
686
+
687
+ if (status.voltageState !== undefined && status.voltageState !== 0) {
688
+ return this.createReadiness("voltage_error", "Printer voltage state is abnormal.", false, status, warnings);
689
+ }
690
+
691
+ return this.createReadiness("ready", "Printer is ready to receive a print job.", true, status, warnings);
692
+ }
693
+
694
+ private createReadiness(
695
+ code: PrinterReadiness["code"],
696
+ reason: string,
697
+ canPrint: boolean,
698
+ status: PrinterStatusSnapshot,
699
+ warnings: PrinterReadinessWarningCode[] = [],
700
+ ): PrinterReadiness {
701
+ return {
702
+ canPrint,
703
+ code,
704
+ reason,
705
+ warnings,
706
+ state: status.state,
707
+ connected: status.connected,
708
+ status,
709
+ };
710
+ }
711
+
712
+ private collectReadinessWarnings(status: PrinterStatusSnapshot): PrinterReadinessWarningCode[] {
713
+ const warnings: PrinterReadinessWarningCode[] = [];
714
+ if (status.paperRfidSuccess === false) {
715
+ warnings.push("paper_rfid_failed");
716
+ }
717
+ if (status.ribbonInserted === false) {
718
+ warnings.push("ribbon_missing");
719
+ }
720
+ if (status.ribbonRfidSuccess === false) {
721
+ warnings.push("ribbon_rfid_failed");
722
+ }
723
+ if (status.lightingErrorCode !== undefined && status.lightingErrorCode !== 0) {
724
+ warnings.push("lighting_error");
725
+ }
726
+ return warnings;
727
+ }
728
+
729
+ private async queryPhysicalPrinterStatus(timeoutMs: number): Promise<PrinterStatusSnapshot> {
730
+ if (!this.isDeviceConnected()) {
731
+ return this.createLocalStatusSnapshot(false);
732
+ }
733
+
734
+ const heartbeatType = this.connectedProfile?.protocolVersion !== undefined && this.connectedProfile.protocolVersion < 3
735
+ ? HEARTBEAT_ADVANCED_1
736
+ : HEARTBEAT_ADVANCED_2;
737
+ let response = await this.sendCommand(0xdc, [heartbeatType], HEARTBEAT_RESPONSE_COMMANDS, timeoutMs, true);
738
+ if (!response && heartbeatType !== HEARTBEAT_ADVANCED_1) {
739
+ response = await this.sendCommand(0xdc, [HEARTBEAT_ADVANCED_1], HEARTBEAT_RESPONSE_COMMANDS, timeoutMs, true);
740
+ }
741
+
742
+ if (!response) {
743
+ return this.createLocalStatusSnapshot(true);
744
+ }
745
+
746
+ return this.parseHeartbeatStatus(response);
747
+ }
748
+
749
+ private parseHeartbeatStatus(response: ParsedResponse): PrinterStatusSnapshot {
750
+ const status: PrinterStatusSnapshot = {
751
+ connected: this.isDeviceConnected(),
752
+ state: this.printerState,
753
+ statusAvailable: false,
754
+ responseCommand: response.cmd,
755
+ rawData: [...response.data],
756
+ };
757
+
758
+ if (response.cmd === 0xd9) {
759
+ this.applyAdvanced2Heartbeat(status, response.data);
760
+ } else if (response.cmd === 0xdd) {
761
+ this.applyAdvanced1Heartbeat(status, response.data);
762
+ }
763
+
764
+ return status;
765
+ }
766
+
767
+ private applyAdvanced2Heartbeat(status: PrinterStatusSnapshot, data: number[]): void {
768
+ if (data.length < 9) {
769
+ return;
770
+ }
771
+
772
+ status.chargeLevel = signedByte(data[2]);
773
+ status.temperature = signedByte(data[3]);
774
+ status.lidClosed = signedByte(data[4]) === 0;
775
+ status.paperInserted = signedByte(data[5]) === 0;
776
+ status.paperRfidSuccess = signedByte(data[6]) !== 0;
777
+ status.ribbonRfidSuccess = signedByte(data[7]) !== 0;
778
+ status.ribbonInserted = signedByte(data[8]) === 0;
779
+ if (data.length >= 11) {
780
+ status.wifiRssi = signedWord(data[9], data[10]);
781
+ }
782
+ if (data.length >= 13) {
783
+ status.lightingErrorCode = signedByte(data[12]);
784
+ }
785
+ if (data.length >= 14) {
786
+ status.voltageState = signedByte(data[13]);
787
+ }
788
+ status.statusAvailable = true;
789
+ }
790
+
791
+ private applyAdvanced1Heartbeat(status: PrinterStatusSnapshot, data: number[]): void {
792
+ if (data.length === 10) {
793
+ status.lidClosed = signedByte(data[8]) === 0;
794
+ status.chargeLevel = signedByte(data[9]);
795
+ } else if (data.length === 13) {
796
+ status.lidClosed = signedByte(data[9]) === 0;
797
+ status.chargeLevel = signedByte(data[10]);
798
+ status.paperInserted = signedByte(data[11]) === 0;
799
+ status.paperRfidSuccess = signedByte(data[12]) !== 0;
800
+ } else if (data.length === 19) {
801
+ status.lidClosed = signedByte(data[15]) === 0;
802
+ status.chargeLevel = signedByte(data[16]);
803
+ status.paperInserted = signedByte(data[17]) === 0;
804
+ status.paperRfidSuccess = signedByte(data[18]) !== 0;
805
+ } else if (data.length === 20) {
806
+ status.paperInserted = signedByte(data[18]) === 0;
807
+ status.paperRfidSuccess = signedByte(data[19]) !== 0;
808
+ } else {
809
+ return;
810
+ }
811
+
812
+ if (status.lidClosed !== undefined && INVERTED_LID_MODEL_IDS.has(this.printerInfo.deviceType)) {
813
+ status.lidClosed = !status.lidClosed;
814
+ }
815
+ status.statusAvailable = true;
816
+ }
817
+
818
+ private handleDisconnect = (): void => {
819
+ this.printerState = "Disconnected";
820
+ this.server = undefined;
821
+ this.characteristic = undefined;
822
+ this.sdkCallback?.onDisConnect();
823
+ };
824
+
825
+ private handleCharacteristicChanged = (event: Event): void => {
826
+ const characteristic = event.target as BluetoothRemoteGATTCharacteristic | null;
827
+ const value = characteristic?.value;
828
+ if (!value) {
829
+ return;
830
+ }
831
+
832
+ const frame = parseFrame(value);
833
+ if (!frame) {
834
+ return;
835
+ }
836
+
837
+ const response = { cmd: frame.cmd, data: frame.data };
838
+ const pending = this.pendingResponse;
839
+ if (pending && pending.expected.has(response.cmd)) {
840
+ clearTimeout(pending.timeoutId);
841
+ this.pendingResponse = undefined;
842
+ pending.resolve(response);
843
+ return;
844
+ }
845
+
846
+ this.responseQueue.push(response);
847
+ };
848
+
849
+ private async writePayload(payload: PayloadEnvelope): Promise<void> {
850
+ this.lastPayload = clone(payload);
851
+ await this.writeJson(payload);
852
+ this.currentJob?.callback?.onProgress(1, 1, {
853
+ bytes: JSON.stringify(payload).length,
854
+ });
855
+ this.currentJob?.callback?.onBufferFree(1, JSON.stringify(payload).length);
856
+ }
857
+
858
+ private async writeJson(payload: unknown): Promise<void> {
859
+ const characteristic = this.characteristic;
860
+ if (!characteristic) {
861
+ throw new Error("No writable Bluetooth characteristic is connected.");
862
+ }
863
+
864
+ const text = JSON.stringify(payload);
865
+ const encoded = new TextEncoder().encode(text);
866
+ const chunkSize = Math.max(20, this.options.chunkSize ?? 180);
867
+ const canWriteWithoutResponse = typeof characteristic.writeValueWithoutResponse === "function";
868
+
869
+ for (let offset = 0; offset < encoded.length; offset += chunkSize) {
870
+ const chunk = encoded.slice(offset, offset + chunkSize);
871
+ await this.writeBytes(chunk, 0, characteristic, canWriteWithoutResponse);
872
+ }
873
+ }
874
+
875
+ private async handshakeAndIdentify(): Promise<void> {
876
+ this.responseQueue = [];
877
+ this.connectedProfile = null;
878
+ this.b1HandshakeDone = false;
879
+
880
+ await this.writeBytes(createConnectFrame());
881
+ await this.waitForCommand([0xc2], 1500, true);
882
+
883
+ const status = await this.sendCommand(0xa5, [0x01], [0xb5], 1500, true);
884
+ const protocolVersion = status ? parseProtocolVersion(status.data) : undefined;
885
+ const model = await this.sendCommand(0x40, [0x08], [0x48], 1500, true);
886
+ const modelId = model ? parseModelId(model.data) : undefined;
887
+ this.connectedProfile = identifyProfile(modelId, protocolVersion);
888
+
889
+ if (this.connectedProfile?.task === "b1") {
890
+ await this.finishB1Handshake();
891
+ }
892
+
893
+ const profile = this.connectedProfile;
894
+ if (profile) {
895
+ this.printerInfo = {
896
+ ...this.printerInfo,
897
+ deviceType: profile.modelId ?? 0,
898
+ deviceDPI: profile.dpi,
899
+ deviceWidth: profile.printheadWidthPx,
900
+ seriesName: profile.label,
901
+ hardwareFlags: profile.modelId ?? 0,
902
+ softwareFlags: profile.protocolVersion ?? 0,
903
+ };
904
+ }
905
+ }
906
+
907
+ private async finishB1Handshake(): Promise<void> {
908
+ if (this.b1HandshakeDone) {
909
+ return;
910
+ }
911
+
912
+ for (const subCode of [0x0b, 0x0d, 0x0a, 0x07, 0x03, 0x0c, 0x09]) {
913
+ await this.sendCommand(0x40, [subCode], [0x40 + subCode], 1500, true);
914
+ }
915
+ await this.sendCommand(0xdc, [0x04], [0xd9], 1500, true);
916
+ this.b1HandshakeDone = true;
917
+ }
918
+
919
+ private async resolveProfile(document: PrintDocument): Promise<NiimbotPrinterProfile> {
920
+ const requestedTask = document.job.meta?.printTask;
921
+ const task = requestedTask === "b1" || requestedTask === "v4" ? requestedTask : "auto";
922
+ const requestedDpi = Number(document.job.meta?.dpi ?? 0);
923
+
924
+ if (task === "auto" && this.connectedProfile) {
925
+ return clone(this.connectedProfile);
926
+ }
927
+
928
+ const fallback = fallbackProfile(task, requestedDpi);
929
+ if (fallback.task === "b1") {
930
+ await this.finishB1Handshake();
931
+ }
932
+ return fallback;
933
+ }
934
+
935
+ private async printNiimbotDocument(document: PrintDocument, profile: NiimbotPrinterProfile): Promise<void> {
936
+ if (!document.pages.length) {
937
+ throw new Error("The print document has no pages.");
938
+ }
939
+
940
+ const copies = Math.max(1, Math.trunc(Number(document.job.totalQuantity ?? 1)));
941
+ const physicalPages = document.pages.length * copies;
942
+ const density = this.normalizeDensity(Number(document.job.density ?? 3), profile.task);
943
+ const speed = this.normalizeSpeed(Number(document.job.meta?.speed ?? 2));
944
+
945
+ this.currentJob?.callback?.onProgress(0, 0, {
946
+ phase: "protocol-start",
947
+ task: profile.task,
948
+ profile: profile.label,
949
+ dpi: profile.dpi,
950
+ physicalPages,
951
+ });
952
+
953
+ await this.sendCommand(0x21, [density], [0x31], 2000);
954
+ await this.sendCommand(0x23, [Number(document.job.paperType ?? 1) & 0xff], [0x33], 2000);
955
+
956
+ const startPayload =
957
+ profile.task === "v4"
958
+ ? [physicalPages >> 8, physicalPages & 0xff, 0, 0, 0, 0, 0, speed, 0]
959
+ : [physicalPages >> 8, physicalPages & 0xff, 0, 0, 0, 0, 0];
960
+ await this.sendCommand(0x01, startPayload, [0x02], 3000);
961
+ if (profile.task === "v4") {
962
+ await this.sendCommand(0xa3, [0x01], undefined, 0, true);
963
+ await delay(30);
964
+ }
965
+
966
+ let printedPage = 0;
967
+ for (let copyIndex = 0; copyIndex < copies; copyIndex += 1) {
968
+ for (let pageIndex = 0; pageIndex < document.pages.length; pageIndex += 1) {
969
+ const page = document.pages[pageIndex];
970
+ const raster = await rasterizeDocumentPage(page, document, profile);
971
+
972
+ if (profile.task === "b1") {
973
+ await this.sendCommand(0x03, [0x01], [0x04], 3000);
974
+ } else {
975
+ await this.sendCommand(0xa3, [0x01], undefined, 0, true);
976
+ await delay(30);
977
+ }
978
+
979
+ const sizePayload =
980
+ profile.task === "v4"
981
+ ? [
982
+ raster.heightPx >> 8,
983
+ raster.heightPx & 0xff,
984
+ raster.widthPx >> 8,
985
+ raster.widthPx & 0xff,
986
+ 0x00,
987
+ 0x01,
988
+ 0x00,
989
+ 0x00,
990
+ 0x00,
991
+ 0x00,
992
+ 0x00,
993
+ 0x00,
994
+ 0x00,
995
+ ]
996
+ : [
997
+ raster.heightPx >> 8,
998
+ raster.heightPx & 0xff,
999
+ raster.widthPx >> 8,
1000
+ raster.widthPx & 0xff,
1001
+ 0x00,
1002
+ 0x01,
1003
+ ];
1004
+ await this.sendCommand(0x13, sizePayload, [0x14], 3000);
1005
+
1006
+ await this.writeRows(createRowFrames(raster), profile.paceMs);
1007
+ await this.sendCommand(0xe3, [0x01], [0xe4], 8000);
1008
+ printedPage += 1;
1009
+ this.currentJob?.callback?.onProgress(pageIndex + 1, copyIndex + 1, {
1010
+ phase: "page-sent",
1011
+ page: printedPage,
1012
+ total: physicalPages,
1013
+ widthPx: raster.widthPx,
1014
+ heightPx: raster.heightPx,
1015
+ stride: raster.stride,
1016
+ });
1017
+ }
1018
+ }
1019
+
1020
+ await this.waitUntilPrinted(physicalPages);
1021
+ await this.sendCommand(0xf3, [0x01], [0xf4], 8000);
1022
+ if (profile.task === "v4") {
1023
+ await this.sendCommand(0xdc, [0x01], undefined, 0, true);
1024
+ }
1025
+ }
1026
+
1027
+ private async waitUntilPrinted(pageNumber: number): Promise<void> {
1028
+ const startedAt = Date.now();
1029
+ this.responseQueue = this.responseQueue.filter((response) => response.cmd !== 0xb3);
1030
+
1031
+ while (Date.now() - startedAt < 25000) {
1032
+ const response = await this.sendCommand(0xa3, [0x01], [0xb3], 1200, true);
1033
+ if (response?.data && response.data.length >= 2) {
1034
+ const page = (response.data[0] << 8) | response.data[1];
1035
+ if (page >= pageNumber) {
1036
+ return;
1037
+ }
1038
+ }
1039
+ await delay(400);
1040
+ }
1041
+ }
1042
+
1043
+ private async writeRows(frames: Uint8Array[], paceMs: number): Promise<void> {
1044
+ for (const frame of frames) {
1045
+ await this.writeBytes(frame, paceMs);
1046
+ }
1047
+ }
1048
+
1049
+ private async sendCommand(
1050
+ cmd: number,
1051
+ data: number[] | Uint8Array,
1052
+ expected?: number[],
1053
+ timeoutMs = 2000,
1054
+ optional = false,
1055
+ ): Promise<ParsedResponse | null> {
1056
+ await this.writeBytes(createFrame(cmd, data));
1057
+ return expected?.length ? this.waitForCommand(expected, timeoutMs, optional) : null;
1058
+ }
1059
+
1060
+ private async waitForCommand(
1061
+ expected: number[],
1062
+ timeoutMs: number,
1063
+ optional: boolean,
1064
+ ): Promise<ParsedResponse | null> {
1065
+ const expectedSet = new Set(expected.map((cmd) => cmd & 0xff));
1066
+ const queuedIndex = this.responseQueue.findIndex((response) => expectedSet.has(response.cmd));
1067
+ if (queuedIndex >= 0) {
1068
+ const [queued] = this.responseQueue.splice(queuedIndex, 1);
1069
+ return queued;
1070
+ }
1071
+
1072
+ return new Promise((resolve, reject) => {
1073
+ this.pendingResponse = {
1074
+ expected: expectedSet,
1075
+ resolve,
1076
+ reject,
1077
+ optional,
1078
+ timeoutId: window.setTimeout(() => {
1079
+ this.pendingResponse = undefined;
1080
+ if (optional) {
1081
+ resolve(null);
1082
+ } else {
1083
+ reject(new Error(`Timed out waiting for printer response 0x${expected[0].toString(16)}.`));
1084
+ }
1085
+ }, timeoutMs),
1086
+ };
1087
+ });
1088
+ }
1089
+
1090
+ private async writeBytes(
1091
+ bytes: Uint8Array,
1092
+ paceMs = 0,
1093
+ characteristic = this.characteristic,
1094
+ canWriteWithoutResponse = !!characteristic && typeof characteristic.writeValueWithoutResponse === "function",
1095
+ ): Promise<void> {
1096
+ if (!characteristic) {
1097
+ throw new Error("No writable Bluetooth characteristic is connected.");
1098
+ }
1099
+
1100
+ const payload = new ArrayBuffer(bytes.byteLength);
1101
+ new Uint8Array(payload).set(bytes);
1102
+
1103
+ await this.runGattOperation(async () => {
1104
+ if (canWriteWithoutResponse) {
1105
+ await characteristic.writeValueWithoutResponse(payload);
1106
+ } else {
1107
+ await characteristic.writeValue(payload);
1108
+ }
1109
+ });
1110
+
1111
+ if (paceMs > 0) {
1112
+ await delay(paceMs);
1113
+ }
1114
+ }
1115
+
1116
+ private async runGattOperation<T>(operation: () => Promise<T>): Promise<T> {
1117
+ const run = async (): Promise<T> => {
1118
+ let lastError: unknown;
1119
+
1120
+ for (let attempt = 0; attempt <= DEFAULT_GATT_BUSY_RETRY_COUNT; attempt += 1) {
1121
+ try {
1122
+ return await operation();
1123
+ } catch (error) {
1124
+ lastError = error;
1125
+ if (!isGattOperationBusyError(error) || attempt >= DEFAULT_GATT_BUSY_RETRY_COUNT) {
1126
+ throw error;
1127
+ }
1128
+
1129
+ await delay(DEFAULT_GATT_BUSY_RETRY_DELAY_MS * (attempt + 1));
1130
+ }
1131
+ }
1132
+
1133
+ throw lastError;
1134
+ };
1135
+
1136
+ const queued = this.gattOperationQueue.then(run, run);
1137
+ this.gattOperationQueue = queued.then(
1138
+ () => undefined,
1139
+ () => undefined,
1140
+ );
1141
+ return queued;
1142
+ }
1143
+
1144
+ private normalizeDensity(value: number, task: "v4" | "b1"): number {
1145
+ if (task === "b1") {
1146
+ return Math.max(1, Math.min(5, Math.round(value)));
1147
+ }
1148
+ return Math.max(1, Math.min(14, Math.round(value)));
1149
+ }
1150
+
1151
+ private normalizeSpeed(value: number): number {
1152
+ return Math.max(0, Math.min(4, Math.round(value)));
1153
+ }
1154
+ }
1155
+
1156
+ function delay(ms: number): Promise<void> {
1157
+ return new Promise((resolve) => window.setTimeout(resolve, ms));
1158
+ }
1159
+
1160
+ export function createWebBluetoothPrinterTransport(
1161
+ options: Partial<WebBluetoothPrinterTransportOptions> = {},
1162
+ ): WebBluetoothPrinterTransport {
1163
+ return new WebBluetoothPrinterTransport(options);
1164
+ }
1165
+
1166
+ export function createWebBluetoothPrinterClient(
1167
+ options: Partial<WebBluetoothPrinterTransportOptions> = {},
1168
+ sdkCallback?: PrinterSdkCallback,
1169
+ ): JCPrintApi {
1170
+ return new JCPrintApi(createWebBluetoothPrinterTransport(options), sdkCallback);
1171
+ }