taro-bluetooth-print 2.7.0 → 2.8.3

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.
@@ -0,0 +1,372 @@
1
+ /**
2
+ * CloudPrintManager - 云打印管理器
3
+ *
4
+ * 支持通过 WebSocket 连接到云端服务器
5
+ * 实现远程下发打印任务
6
+ *
7
+ * 应用场景:
8
+ * - IoT 打印机(WiFi/4G 连接)
9
+ * - 餐饮云厨房
10
+ * - 物流云打印
11
+ */
12
+
13
+ import { EventEmitter } from '@/core/EventEmitter';
14
+ import { Logger } from '@/utils/logger';
15
+
16
+ export interface CloudPrintOptions {
17
+ /** WebSocket 服务器地址 */
18
+ serverUrl: string;
19
+ /** 设备 ID */
20
+ deviceId: string;
21
+ /** API 密钥(可选) */
22
+ apiKey?: string;
23
+ /** 自动重连 */
24
+ reconnect?: boolean;
25
+ /** 重连间隔 (ms) */
26
+ reconnectInterval?: number;
27
+ /** 心跳间隔 (ms) */
28
+ heartbeatInterval?: number;
29
+ /** 连接超时 (ms) */
30
+ connectTimeout?: number;
31
+ }
32
+
33
+ export interface PrintJob {
34
+ /** 任务 ID */
35
+ id: string;
36
+ /** 打印数据 */
37
+ data: Uint8Array | string;
38
+ /** 份数 */
39
+ copies?: number;
40
+ /** 优先级 */
41
+ priority?: number;
42
+ }
43
+
44
+ export interface CloudPrinterStatus {
45
+ /** 打印机状态 */
46
+ status: 'idle' | 'printing' | 'error' | 'offline';
47
+ /** 纸张状态 */
48
+ paper?: 'ok' | 'low' | 'out';
49
+ /** 错误信息 */
50
+ error?: string;
51
+ /** 最后更新时间 */
52
+ timestamp: number;
53
+ }
54
+
55
+ /** 服务器消息类型 */
56
+ interface ServerMessage {
57
+ type: string;
58
+ status?: string;
59
+ paper?: string;
60
+ error?: string;
61
+ success?: boolean;
62
+ jobId?: string;
63
+ deviceId?: string;
64
+ timestamp?: number;
65
+ }
66
+
67
+ export interface CloudPrintEvents {
68
+ /** 连接成功 */
69
+ connect: void;
70
+ /** 断开连接 */
71
+ disconnect: void;
72
+ /** 连接错误 */
73
+ error: Error;
74
+ /** 状态更新 */
75
+ status: CloudPrinterStatus;
76
+ /** 打印完成 */
77
+ 'print-complete': string;
78
+ /** 打印失败 */
79
+ 'print-error': { jobId: string; error: string };
80
+ /** 收到原始消息 */
81
+ message: Record<string, unknown>;
82
+ }
83
+
84
+ export type CloudPrintEvent = keyof CloudPrintEvents;
85
+
86
+ /**
87
+ * 云打印管理器
88
+ *
89
+ * 通过 WebSocket 连接到云端服务器,实现远程打印任务下发
90
+ */
91
+ export class CloudPrintManager extends EventEmitter<CloudPrintEvents> {
92
+ private readonly log = Logger.scope('CloudPrintManager');
93
+ private options: Required<CloudPrintOptions>;
94
+ private ws: WebSocket | null = null;
95
+ private isConnected: boolean = false;
96
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
97
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
98
+ private status: CloudPrinterStatus = { status: 'offline', timestamp: Date.now() };
99
+ private connectResolve: (() => void) | null = null;
100
+ private connectReject: ((err: Error) => void) | null = null;
101
+
102
+ constructor(options: CloudPrintOptions) {
103
+ super();
104
+ this.options = {
105
+ reconnect: true,
106
+ reconnectInterval: 5000,
107
+ heartbeatInterval: 30000,
108
+ connectTimeout: 10000,
109
+ apiKey: undefined,
110
+ ...options,
111
+ } as Required<CloudPrintOptions>;
112
+ }
113
+
114
+ /**
115
+ * 检查是否已连接
116
+ */
117
+ get connected(): boolean {
118
+ return this.isConnected;
119
+ }
120
+
121
+ /**
122
+ * 获取当前打印机状态
123
+ */
124
+ get currentStatus(): CloudPrinterStatus {
125
+ return { ...this.status };
126
+ }
127
+
128
+ /**
129
+ * 连接到云端服务器
130
+ */
131
+ async connect(): Promise<void> {
132
+ if (this.isConnected && this.ws) {
133
+ this.log.debug('Already connected');
134
+ return Promise.resolve();
135
+ }
136
+
137
+ return new Promise((resolve, reject) => {
138
+ this.connectResolve = resolve;
139
+ this.connectReject = reject;
140
+
141
+ const timeout = setTimeout(() => {
142
+ this.log.error('Connection timeout');
143
+ this.ws?.close();
144
+ reject(new Error('Connection timeout'));
145
+ }, this.options.connectTimeout);
146
+
147
+ try {
148
+ // 构建 WebSocket URL
149
+ const url = new URL(this.options.serverUrl);
150
+ url.searchParams.set('deviceId', this.options.deviceId);
151
+ if (this.options.apiKey) {
152
+ url.searchParams.set('apiKey', this.options.apiKey);
153
+ }
154
+
155
+ this.log.debug(`Connecting to ${url.toString()}`);
156
+ this.ws = new WebSocket(url.toString());
157
+
158
+ this.ws.onopen = () => {
159
+ clearTimeout(timeout);
160
+ this.isConnected = true;
161
+ this.log.info('Connected to cloud server');
162
+ this.startHeartbeat();
163
+ this.emit('connect');
164
+ if (this.connectResolve) {
165
+ this.connectResolve();
166
+ this.connectResolve = null;
167
+ this.connectReject = null;
168
+ }
169
+ };
170
+
171
+ this.ws.onclose = () => {
172
+ clearTimeout(timeout);
173
+ this.isConnected = false;
174
+ this.log.warn('Disconnected from cloud server');
175
+ this.stopHeartbeat();
176
+ this.emit('disconnect');
177
+ this.scheduleReconnect();
178
+
179
+ // If we have a pending connect promise, reject it
180
+ if (this.connectReject) {
181
+ this.connectReject(new Error('Connection closed before established'));
182
+ this.connectResolve = null;
183
+ this.connectReject = null;
184
+ }
185
+ };
186
+
187
+ this.ws.onerror = event => {
188
+ clearTimeout(timeout);
189
+ const error = new Error('WebSocket error');
190
+ this.log.error('WebSocket error', event);
191
+ this.emit('error', error);
192
+ if (this.connectReject) {
193
+ this.connectReject(error);
194
+ this.connectResolve = null;
195
+ this.connectReject = null;
196
+ }
197
+ };
198
+
199
+ this.ws.onmessage = (event: MessageEvent) => {
200
+ this.handleMessage(String(event.data));
201
+ };
202
+ } catch (error) {
203
+ clearTimeout(timeout);
204
+ this.log.error('Failed to create WebSocket', error);
205
+ if (this.connectReject) {
206
+ this.connectReject(error as Error);
207
+ this.connectResolve = null;
208
+ this.connectReject = null;
209
+ }
210
+ }
211
+ });
212
+ }
213
+
214
+ /**
215
+ * 断开连接
216
+ */
217
+ disconnect(): void {
218
+ this.log.info('Disconnecting from cloud server');
219
+ this.stopHeartbeat();
220
+ this.cancelReconnect();
221
+ if (this.ws) {
222
+ this.ws.close();
223
+ this.ws = null;
224
+ }
225
+ this.isConnected = false;
226
+ this.status = { status: 'offline', timestamp: Date.now() };
227
+ }
228
+
229
+ /**
230
+ * 发送打印任务
231
+ * @throws Error 如果未连接
232
+ */
233
+ print(job: PrintJob): void {
234
+ if (!this.isConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
235
+ throw new Error('Not connected to server');
236
+ }
237
+
238
+ const message = {
239
+ type: 'print',
240
+ jobId: job.id,
241
+ data: this.arrayBufferToBase64(job.data),
242
+ copies: job.copies || 1,
243
+ priority: job.priority || 0,
244
+ timestamp: Date.now(),
245
+ };
246
+
247
+ this.log.debug(`Sending print job: ${job.id}`);
248
+ this.ws.send(JSON.stringify(message));
249
+ }
250
+
251
+ /**
252
+ * 获取打印机状态
253
+ */
254
+ getStatus(): CloudPrinterStatus {
255
+ if (!this.isConnected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
256
+ return { status: 'offline', timestamp: Date.now() };
257
+ }
258
+
259
+ const message = {
260
+ type: 'status',
261
+ deviceId: this.options.deviceId,
262
+ timestamp: Date.now(),
263
+ };
264
+
265
+ this.ws.send(JSON.stringify(message));
266
+ return { ...this.status };
267
+ }
268
+
269
+ /**
270
+ * 处理接收到的消息
271
+ */
272
+ private handleMessage(data: string): void {
273
+ try {
274
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
+ const message = JSON.parse(data) as ServerMessage;
276
+ this.log.debug('Received message', message.type);
277
+
278
+ switch (message.type) {
279
+ case 'status':
280
+ this.status = {
281
+ status: message.status as CloudPrinterStatus['status'],
282
+ paper: message.paper as CloudPrinterStatus['paper'],
283
+ error: message.error,
284
+ timestamp: Date.now(),
285
+ };
286
+ this.emit('status', this.status);
287
+ break;
288
+
289
+ case 'print-result':
290
+ if (message.success) {
291
+ this.log.info(`Print job completed: ${message.jobId || ''}`);
292
+ this.emit('print-complete', message.jobId || '');
293
+ } else {
294
+ this.log.error(`Print job failed: ${message.jobId || ''}`, message.error);
295
+ this.emit('print-error', { jobId: message.jobId || '', error: message.error || '' });
296
+ }
297
+ break;
298
+
299
+ case 'pong':
300
+ // 心跳响应,无需处理
301
+ break;
302
+
303
+ default:
304
+ this.emit('message', message as unknown as Record<string, unknown>);
305
+ }
306
+ } catch (error) {
307
+ this.log.error('Failed to parse message:', error);
308
+ }
309
+ }
310
+
311
+ /**
312
+ * 启动心跳
313
+ */
314
+ private startHeartbeat(): void {
315
+ this.stopHeartbeat();
316
+ this.heartbeatTimer = setInterval(() => {
317
+ if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) {
318
+ this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
319
+ }
320
+ }, this.options.heartbeatInterval);
321
+ }
322
+
323
+ /**
324
+ * 停止心跳
325
+ */
326
+ private stopHeartbeat(): void {
327
+ if (this.heartbeatTimer) {
328
+ clearInterval(this.heartbeatTimer);
329
+ this.heartbeatTimer = null;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * 安排重连
335
+ */
336
+ private scheduleReconnect(): void {
337
+ if (!this.options.reconnect) return;
338
+
339
+ this.log.debug(`Scheduling reconnect in ${this.options.reconnectInterval}ms`);
340
+ this.reconnectTimer = setTimeout(() => {
341
+ this.log.info('Attempting to reconnect...');
342
+ this.connect().catch(() => {
343
+ // Reconnect failed, will be rescheduled by onclose handler
344
+ });
345
+ }, this.options.reconnectInterval);
346
+ }
347
+
348
+ /**
349
+ * 取消重连
350
+ */
351
+ private cancelReconnect(): void {
352
+ if (this.reconnectTimer) {
353
+ clearTimeout(this.reconnectTimer);
354
+ this.reconnectTimer = null;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * ArrayBuffer 转 Base64
360
+ */
361
+ private arrayBufferToBase64(buffer: Uint8Array | string): string {
362
+ if (typeof buffer === 'string') {
363
+ return btoa(buffer);
364
+ }
365
+ let binary = '';
366
+ const bytes = new Uint8Array(buffer);
367
+ for (let i = 0; i < bytes.byteLength; i++) {
368
+ binary += String.fromCharCode(bytes[i]!);
369
+ }
370
+ return btoa(binary);
371
+ }
372
+ }
@@ -1,3 +1,5 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-floating-promises */
2
+
1
3
  /**
2
4
  * PrintScheduler - Scheduled printing service
3
5
  * Supports one-time scheduling, repeat intervals, and cron expressions
@@ -37,10 +39,10 @@ export interface ScheduleOptions {
37
39
 
38
40
  export interface ScheduleEvents {
39
41
  'will-execute': ScheduledPrint;
40
- 'executed': { job: ScheduledPrint; success: boolean; error?: Error };
42
+ executed: { job: ScheduledPrint; success: boolean; error?: Error };
41
43
  'next-run': { job: ScheduledPrint; runTime: number };
42
- 'completed': ScheduledPrint;
43
- 'failed': { job: ScheduledPrint; error: Error };
44
+ completed: ScheduledPrint;
45
+ failed: { job: ScheduledPrint; error: Error };
44
46
  }
45
47
 
46
48
  /**
@@ -163,7 +165,9 @@ export class PrintScheduler extends EventEmitter<ScheduleEvents> {
163
165
  this.onPrintExecute = executor;
164
166
  }
165
167
 
166
- scheduleOnce(options: Omit<ScheduleOptions, 'cronExpression' | 'repeatInterval'>): ScheduledPrint {
168
+ scheduleOnce(
169
+ options: Omit<ScheduleOptions, 'cronExpression' | 'repeatInterval'>
170
+ ): ScheduledPrint {
167
171
  const { onceAt, ...rest } = options;
168
172
  const executeAt = onceAt instanceof Date ? onceAt.getTime() : onceAt;
169
173
 
@@ -266,7 +270,10 @@ export class PrintScheduler extends EventEmitter<ScheduleEvents> {
266
270
  return true;
267
271
  }
268
272
 
269
- update(jobId: string, updates: Partial<Pick<ScheduledPrint, 'name' | 'templateData' | 'printerId'>>): boolean {
273
+ update(
274
+ jobId: string,
275
+ updates: Partial<Pick<ScheduledPrint, 'name' | 'templateData' | 'printerId'>>
276
+ ): boolean {
270
277
  const job = this.jobs.get(jobId);
271
278
  if (!job) return false;
272
279
 
@@ -332,9 +339,12 @@ export class PrintScheduler extends EventEmitter<ScheduleEvents> {
332
339
  return;
333
340
  }
334
341
 
335
- this.timer = setTimeout(() => {
336
- this.executeJob(nextJob);
337
- }, Math.min(delay, 2147483647));
342
+ this.timer = setTimeout(
343
+ () => {
344
+ this.executeJob(nextJob);
345
+ },
346
+ Math.min(delay, 2147483647)
347
+ );
338
348
  }
339
349
 
340
350
  private async executeJob(job: ScheduledPrint): Promise<void> {