node-thermal-printer-js 1.0.8 → 1.2.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/README.md +275 -24
- package/app.js +546 -6
- package/ble_server.py +345 -0
- package/package.json +7 -1
- package/scripts/install-deps.js +135 -0
package/app.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import "dotenv/config";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { spawn, spawnSync, execSync } from "node:child_process";
|
|
3
|
+
import { createConnection, createServer } from "node:net";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
|
|
6
8
|
const getEscPosPayload = (data) =>
|
|
@@ -11,6 +13,170 @@ const getEscPosPayload = (data) =>
|
|
|
11
13
|
|
|
12
14
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
const bleScriptPath = path.join(scriptDir, "ble_print.py");
|
|
16
|
+
const bleServerPath = path.join(scriptDir, "ble_server.py");
|
|
17
|
+
|
|
18
|
+
// Global server process reference
|
|
19
|
+
let bleServerProcess = null;
|
|
20
|
+
let bleServerHost = null;
|
|
21
|
+
let bleServerPort = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find an available Python 3.9+ executable on the system.
|
|
25
|
+
* Tries: py, python3, python (platform-aware)
|
|
26
|
+
* @returns {string|null} Command name if found, null otherwise
|
|
27
|
+
*/
|
|
28
|
+
const findPythonCmd = () => {
|
|
29
|
+
const candidates = getPythonLaunchCandidates();
|
|
30
|
+
|
|
31
|
+
for (const cmd of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
const result = spawnSync(cmd, ["--version"], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
timeout: 2000,
|
|
36
|
+
shell: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
40
|
+
const match = output.match(/Python (\d+\.\d+)/);
|
|
41
|
+
if (match && parseFloat(match[1]) >= 3.9) {
|
|
42
|
+
console.log(`[BLE] ✓ Detected Python: ${cmd} (${output})`);
|
|
43
|
+
return cmd;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Continue to next candidate
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getPythonLaunchCandidates = () => {
|
|
53
|
+
const localVenvCandidates = process.platform === "win32"
|
|
54
|
+
? [
|
|
55
|
+
path.join(scriptDir, ".venv", "Scripts", "python.exe"),
|
|
56
|
+
path.join(scriptDir, ".venv", "Scripts", "python"),
|
|
57
|
+
]
|
|
58
|
+
: [
|
|
59
|
+
path.join(scriptDir, ".venv", "bin", "python"),
|
|
60
|
+
path.join(scriptDir, ".venv", "bin", "python3"),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const systemCandidates = process.platform === "win32"
|
|
64
|
+
? ["py", "python3", "python"]
|
|
65
|
+
: ["python3", "python", "py"];
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
...localVenvCandidates.filter((candidate) => existsSync(candidate)),
|
|
69
|
+
...systemCandidates,
|
|
70
|
+
];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Kill any stale BLE server processes still bound to a port.
|
|
75
|
+
* Prevents "Address already in use" errors on reinstall or crash recovery.
|
|
76
|
+
* @param {number} port Port number to clean up
|
|
77
|
+
* @param {number} timeout Timeout in ms for the operation
|
|
78
|
+
*/
|
|
79
|
+
const killStaleProcesses = async (port, timeout = 3000) => {
|
|
80
|
+
const platformCmd = process.platform === "win32"
|
|
81
|
+
? `netstat -ano | findstr :${port}`
|
|
82
|
+
: `lsof -ti:${port}`;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const result = execSync(platformCmd, {
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
stdio: "pipe",
|
|
88
|
+
timeout,
|
|
89
|
+
shell: true,
|
|
90
|
+
}).trim();
|
|
91
|
+
|
|
92
|
+
if (!result) {
|
|
93
|
+
console.log(`[BLE] Port ${port} is clean`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.warn(`[BLE] ⚠ Found stale process on port ${port}, cleaning up...`);
|
|
98
|
+
|
|
99
|
+
if (process.platform === "win32") {
|
|
100
|
+
// Extract PID from netstat output: "TCP 127.0.0.1:5555 0.0.0.0:0 LISTENING 12345"
|
|
101
|
+
const lines = result.split("\n");
|
|
102
|
+
const pids = new Set();
|
|
103
|
+
lines.forEach((line) => {
|
|
104
|
+
const parts = line.trim().split(/\s+/);
|
|
105
|
+
if (parts.length > 0) {
|
|
106
|
+
const pid = parts[parts.length - 1];
|
|
107
|
+
if (/^\d+$/.test(pid)) pids.add(pid);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
for (const pid of pids) {
|
|
112
|
+
try {
|
|
113
|
+
execSync(`taskkill /PID ${pid} /F`, { timeout: 2000, stdio: "pipe" });
|
|
114
|
+
console.log(`[BLE] ✓ Killed PID ${pid}`);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn(`[BLE] Could not kill PID ${pid}:`, e.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// On Unix-like systems, lsof returns just the PID
|
|
121
|
+
const pids = result.split("\n").filter((p) => p);
|
|
122
|
+
for (const pid of pids) {
|
|
123
|
+
try {
|
|
124
|
+
execSync(`kill -9 ${pid}`, { timeout: 2000, stdio: "pipe" });
|
|
125
|
+
console.log(`[BLE] ✓ Killed PID ${pid}`);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.warn(`[BLE] Could not kill PID ${pid}:`, e.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// If netstat/lsof fails, not critical - just log and continue
|
|
133
|
+
console.debug(`[BLE] Could not check for stale processes: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const findBindablePort = (host, preferredPort, strictPreferred = false) =>
|
|
138
|
+
new Promise((resolve, reject) => {
|
|
139
|
+
const probe = createServer();
|
|
140
|
+
|
|
141
|
+
const finishWithEphemeral = () => {
|
|
142
|
+
const fallback = createServer();
|
|
143
|
+
fallback.once("error", reject);
|
|
144
|
+
fallback.listen(0, host, () => {
|
|
145
|
+
const address = fallback.address();
|
|
146
|
+
const port =
|
|
147
|
+
typeof address === "object" && address ? address.port : null;
|
|
148
|
+
fallback.close(() => {
|
|
149
|
+
if (!port) {
|
|
150
|
+
reject(
|
|
151
|
+
new Error("Unable to resolve an ephemeral port for BLE server"),
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
resolve(port);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
probe.once("error", (err) => {
|
|
161
|
+
if (strictPreferred || !preferredPort) {
|
|
162
|
+
reject(err);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
finishWithEphemeral();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
probe.listen(preferredPort || 0, host, () => {
|
|
169
|
+
const address = probe.address();
|
|
170
|
+
const port = typeof address === "object" && address ? address.port : null;
|
|
171
|
+
probe.close(() => {
|
|
172
|
+
if (!port) {
|
|
173
|
+
reject(new Error("Unable to resolve a bindable port for BLE server"));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
resolve(port);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
14
180
|
|
|
15
181
|
const runPythonProcess = ({ cmd, cmdArgs, scriptArgs }) =>
|
|
16
182
|
new Promise((resolve, reject) => {
|
|
@@ -57,6 +223,320 @@ const runPythonProcess = ({ cmd, cmdArgs, scriptArgs }) =>
|
|
|
57
223
|
});
|
|
58
224
|
});
|
|
59
225
|
|
|
226
|
+
const startBleServer = async (options = {}) => {
|
|
227
|
+
if (bleServerProcess) {
|
|
228
|
+
console.log("[BLE] Server already running");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const detectedPythonCmd = findPythonCmd();
|
|
233
|
+
if (!detectedPythonCmd) {
|
|
234
|
+
console.warn(
|
|
235
|
+
"[BLE] Python auto-detection did not find a preferred launcher. Trying fallback candidates, including the project virtualenv if present.",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const host =
|
|
240
|
+
options.host || process.env.PRINTER_BLE_SERVER_HOST || "127.0.0.1";
|
|
241
|
+
const configuredPort = options.port || process.env.PRINTER_BLE_SERVER_PORT;
|
|
242
|
+
const preferredPort = configuredPort ? Number(configuredPort) : 5555;
|
|
243
|
+
const strictPreferred = Boolean(configuredPort);
|
|
244
|
+
|
|
245
|
+
// Clean up any stale processes before trying to bind
|
|
246
|
+
if (!strictPreferred) {
|
|
247
|
+
await killStaleProcesses(preferredPort, 3000).catch((err) => {
|
|
248
|
+
console.debug("[BLE] Stale process cleanup warning:", err.message);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const selectedPort = await findBindablePort(
|
|
253
|
+
host,
|
|
254
|
+
preferredPort,
|
|
255
|
+
strictPreferred,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (selectedPort !== preferredPort && !strictPreferred) {
|
|
259
|
+
console.warn(
|
|
260
|
+
`[BLE] Port ${preferredPort} busy. Switching BLE server to free port ${selectedPort}.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
bleServerHost = host;
|
|
265
|
+
bleServerPort = selectedPort;
|
|
266
|
+
|
|
267
|
+
const args = [
|
|
268
|
+
bleServerPath,
|
|
269
|
+
"--name",
|
|
270
|
+
options.bleName || process.env.PRINTER_BLE_NAME || "PSF588",
|
|
271
|
+
"--port",
|
|
272
|
+
String(selectedPort),
|
|
273
|
+
"--host",
|
|
274
|
+
host,
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
if (options.bleAddress || process.env.PRINTER_BLE_ADDRESS) {
|
|
278
|
+
args.push(
|
|
279
|
+
"--address",
|
|
280
|
+
options.bleAddress || process.env.PRINTER_BLE_ADDRESS,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (options.charUUID || process.env.PRINTER_BLE_CHAR_UUID) {
|
|
285
|
+
args.push(
|
|
286
|
+
"--char-uuid",
|
|
287
|
+
options.charUUID || process.env.PRINTER_BLE_CHAR_UUID,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (options.chunkSize || process.env.PRINTER_BLE_CHUNK_SIZE) {
|
|
292
|
+
args.push(
|
|
293
|
+
"--chunk-size",
|
|
294
|
+
String(options.chunkSize || process.env.PRINTER_BLE_CHUNK_SIZE),
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (
|
|
299
|
+
options.delayMs !== undefined ||
|
|
300
|
+
process.env.PRINTER_BLE_DELAY_MS !== undefined
|
|
301
|
+
) {
|
|
302
|
+
args.push(
|
|
303
|
+
"--delay-ms",
|
|
304
|
+
String(options.delayMs ?? process.env.PRINTER_BLE_DELAY_MS ?? 0),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (options.connectTimeout || process.env.PRINTER_BLE_CONNECT_TIMEOUT) {
|
|
309
|
+
args.push(
|
|
310
|
+
"--connect-timeout",
|
|
311
|
+
String(options.connectTimeout || process.env.PRINTER_BLE_CONNECT_TIMEOUT),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.scanTimeout || process.env.PRINTER_BLE_SCAN_TIMEOUT) {
|
|
316
|
+
args.push(
|
|
317
|
+
"--scan-timeout",
|
|
318
|
+
String(options.scanTimeout || process.env.PRINTER_BLE_SCAN_TIMEOUT),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (options.pair || process.env.PRINTER_BLE_PAIR === "1") {
|
|
323
|
+
args.push("--pair");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const envCmd = options.pythonCmd || process.env.PRINTER_PYTHON_CMD;
|
|
327
|
+
const candidates = envCmd
|
|
328
|
+
? [{ cmd: envCmd, cmdArgs: ["-u"] }] // -u for unbuffered output
|
|
329
|
+
: [
|
|
330
|
+
...(detectedPythonCmd ? [{ cmd: detectedPythonCmd, cmdArgs: ["-u"] }] : []),
|
|
331
|
+
...getPythonLaunchCandidates().map((cmd) => ({ cmd, cmdArgs: ["-u"] })),
|
|
332
|
+
];
|
|
333
|
+
|
|
334
|
+
return new Promise((resolve, reject) => {
|
|
335
|
+
let lastError;
|
|
336
|
+
let serverReady = false;
|
|
337
|
+
|
|
338
|
+
const tryStart = (index) => {
|
|
339
|
+
if (index >= candidates.length) {
|
|
340
|
+
return reject(
|
|
341
|
+
new Error(
|
|
342
|
+
`Failed to start BLE server. Errors: ${lastError?.message}`,
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const candidate = candidates[index];
|
|
348
|
+
console.log(
|
|
349
|
+
`[BLE] Attempting to start server with: ${candidate.cmd} ${candidate.cmdArgs.join(" ")} ...`,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const child = spawn(candidate.cmd, [...candidate.cmdArgs, ...args], {
|
|
353
|
+
stdio: "pipe",
|
|
354
|
+
detached: false,
|
|
355
|
+
shell: false,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
console.log(`[BLE] Process spawned with PID ${child.pid}`);
|
|
359
|
+
|
|
360
|
+
child.on("error", (err) => {
|
|
361
|
+
lastError = new Error(
|
|
362
|
+
`Launch error for ${candidate.cmd}: ${err.message}`,
|
|
363
|
+
);
|
|
364
|
+
console.error(`[BLE] ${lastError.message}`);
|
|
365
|
+
tryStart(index + 1);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
let stdoutBuffer = "";
|
|
369
|
+
let stderrBuffer = "";
|
|
370
|
+
|
|
371
|
+
child.stdout.on("data", (data) => {
|
|
372
|
+
const chunk = data.toString();
|
|
373
|
+
stdoutBuffer += chunk;
|
|
374
|
+
process.stdout.write(`[BLE-OUT] ${chunk}`);
|
|
375
|
+
|
|
376
|
+
if (chunk.includes("Server ready") && chunk.includes("Listening on")) {
|
|
377
|
+
if (!serverReady) {
|
|
378
|
+
serverReady = true;
|
|
379
|
+
bleServerProcess = child;
|
|
380
|
+
console.log(
|
|
381
|
+
`[BLE] ✓ Server ready and listening on ${bleServerHost}:${bleServerPort}!`,
|
|
382
|
+
);
|
|
383
|
+
resolve();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
child.stderr.on("data", (data) => {
|
|
389
|
+
const chunk = data.toString();
|
|
390
|
+
stderrBuffer += chunk;
|
|
391
|
+
process.stderr.write(`[BLE-ERR] ${chunk}`);
|
|
392
|
+
|
|
393
|
+
if (chunk.includes("Server ready") && chunk.includes("Listening on")) {
|
|
394
|
+
if (!serverReady) {
|
|
395
|
+
serverReady = true;
|
|
396
|
+
bleServerProcess = child;
|
|
397
|
+
console.log(
|
|
398
|
+
`[BLE] ✓ Server ready and listening on ${bleServerHost}:${bleServerPort}!`,
|
|
399
|
+
);
|
|
400
|
+
resolve();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
child.on("close", (code) => {
|
|
406
|
+
if (!serverReady) {
|
|
407
|
+
lastError = new Error(
|
|
408
|
+
`Server exited with code ${code}. Stdout: ${stdoutBuffer}. Stderr: ${stderrBuffer}`,
|
|
409
|
+
);
|
|
410
|
+
console.error(`[BLE] ${lastError.message}`);
|
|
411
|
+
tryStart(index + 1);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Hard timeout: if server doesn't report ready in 20 seconds, fail
|
|
416
|
+
const timeoutId = setTimeout(() => {
|
|
417
|
+
if (!serverReady) {
|
|
418
|
+
console.error("[BLE] Server startup timeout (20s) - killing process");
|
|
419
|
+
lastError = new Error(
|
|
420
|
+
"Server startup timeout (no 'Server ready' message received)",
|
|
421
|
+
);
|
|
422
|
+
try {
|
|
423
|
+
child.kill("SIGTERM");
|
|
424
|
+
} catch (e) {
|
|
425
|
+
console.error("[BLE] Error killing process:", e.message);
|
|
426
|
+
}
|
|
427
|
+
setTimeout(() => tryStart(index + 1), 500);
|
|
428
|
+
}
|
|
429
|
+
}, 20000);
|
|
430
|
+
|
|
431
|
+
// Cleanup timeout if server becomes ready
|
|
432
|
+
child.once("close", () => clearTimeout(timeoutId));
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
tryStart(0);
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const stopBleServer = async () => {
|
|
440
|
+
if (bleServerProcess) {
|
|
441
|
+
bleServerProcess.kill();
|
|
442
|
+
bleServerProcess = null;
|
|
443
|
+
bleServerHost = null;
|
|
444
|
+
bleServerPort = null;
|
|
445
|
+
console.log("[BLE] Server stopped");
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const sendToBleServer = (request, options = {}) => {
|
|
450
|
+
const host =
|
|
451
|
+
options.host ||
|
|
452
|
+
bleServerHost ||
|
|
453
|
+
process.env.PRINTER_BLE_SERVER_HOST ||
|
|
454
|
+
"127.0.0.1";
|
|
455
|
+
const port =
|
|
456
|
+
options.port ||
|
|
457
|
+
bleServerPort ||
|
|
458
|
+
process.env.PRINTER_BLE_SERVER_PORT ||
|
|
459
|
+
5555;
|
|
460
|
+
const timeoutMs = Number(
|
|
461
|
+
options.requestTimeoutMs ||
|
|
462
|
+
process.env.PRINTER_BLE_REQUEST_TIMEOUT_MS ||
|
|
463
|
+
40000,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
return new Promise((resolve, reject) => {
|
|
467
|
+
let settled = false;
|
|
468
|
+
let responseBuffer = "";
|
|
469
|
+
|
|
470
|
+
const settleResolve = (value) => {
|
|
471
|
+
if (settled) return;
|
|
472
|
+
settled = true;
|
|
473
|
+
resolve(value);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const settleReject = (error) => {
|
|
477
|
+
if (settled) return;
|
|
478
|
+
settled = true;
|
|
479
|
+
reject(error);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
const socket = createConnection({ host, port }, () => {
|
|
483
|
+
console.log(`[BLE] Socket connected to ${host}:${port}`);
|
|
484
|
+
const message = JSON.stringify(request) + "\n";
|
|
485
|
+
socket.end(message);
|
|
486
|
+
console.log(`[BLE] Sent request: ${JSON.stringify(request)}`);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
socket.on("data", (data) => {
|
|
490
|
+
responseBuffer += data.toString("utf-8");
|
|
491
|
+
|
|
492
|
+
const newlineIndex = responseBuffer.indexOf("\n");
|
|
493
|
+
if (newlineIndex === -1) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const responseLine = responseBuffer.slice(0, newlineIndex).trim();
|
|
498
|
+
if (!responseLine) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const response = JSON.parse(responseLine);
|
|
504
|
+
console.log(`[BLE] Received response: ${JSON.stringify(response)}`);
|
|
505
|
+
socket.end();
|
|
506
|
+
if (response.ok) {
|
|
507
|
+
settleResolve(response);
|
|
508
|
+
} else {
|
|
509
|
+
settleReject(
|
|
510
|
+
new Error(response.error || "Unknown error from server"),
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
socket.end();
|
|
515
|
+
settleReject(
|
|
516
|
+
new Error(`Failed to parse server response: ${err.message}`),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
socket.on("error", (err) => {
|
|
522
|
+
console.error(`[BLE] Socket error: ${err.message}`);
|
|
523
|
+
settleReject(new Error(`Socket connection failed: ${err.message}`));
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
socket.on("close", () => {
|
|
527
|
+
console.log("[BLE] Socket closed");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
socket.setTimeout(timeoutMs, () => {
|
|
531
|
+
console.error("[BLE] Socket timeout waiting for server response");
|
|
532
|
+
socket.destroy();
|
|
533
|
+
settleReject(
|
|
534
|
+
new Error(`BLE server request timeout after ${timeoutMs}ms`),
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
};
|
|
539
|
+
|
|
60
540
|
const printViaComPort = async (data, options = {}) => {
|
|
61
541
|
const portPath = options.portPath || process.env.PRINTER_COM_PORT;
|
|
62
542
|
const baudRate = options.baudRate || 9600;
|
|
@@ -150,10 +630,10 @@ const printViaBleBridge = (data, options = {}) => {
|
|
|
150
630
|
const candidates = envCmd
|
|
151
631
|
? [{ cmd: envCmd, cmdArgs: [] }]
|
|
152
632
|
: [
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
633
|
+
{ cmd: "py", cmdArgs: ["-3.11"] },
|
|
634
|
+
{ cmd: "py", cmdArgs: ["-3"] },
|
|
635
|
+
{ cmd: "python", cmdArgs: [] },
|
|
636
|
+
];
|
|
157
637
|
|
|
158
638
|
return new Promise(async (resolve, reject) => {
|
|
159
639
|
const failures = [];
|
|
@@ -184,15 +664,75 @@ const printViaBleBridge = (data, options = {}) => {
|
|
|
184
664
|
});
|
|
185
665
|
};
|
|
186
666
|
|
|
667
|
+
const printViaBleServer = async (data, options = {}) => {
|
|
668
|
+
const payload = getEscPosPayload(data).toString("base64");
|
|
669
|
+
|
|
670
|
+
// Start server if not already running
|
|
671
|
+
if (!bleServerProcess && options.autoStart !== false) {
|
|
672
|
+
try {
|
|
673
|
+
await startBleServer(options);
|
|
674
|
+
// Give the server a moment to fully stabilize after reporting ready
|
|
675
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
676
|
+
} catch (err) {
|
|
677
|
+
console.warn(
|
|
678
|
+
"[BLE] Failed to auto-start server, falling back to legacy method:",
|
|
679
|
+
err.message,
|
|
680
|
+
);
|
|
681
|
+
return printViaBleBridge(data, options);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Send print request to server
|
|
686
|
+
const request = {
|
|
687
|
+
command: "print",
|
|
688
|
+
data_b64: payload,
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
return await sendToBleServer(request, {
|
|
693
|
+
host: options.host || process.env.PRINTER_BLE_SERVER_HOST,
|
|
694
|
+
port: options.port || process.env.PRINTER_BLE_SERVER_PORT,
|
|
695
|
+
requestTimeoutMs:
|
|
696
|
+
options.requestTimeoutMs || process.env.PRINTER_BLE_REQUEST_TIMEOUT_MS,
|
|
697
|
+
});
|
|
698
|
+
} catch (err) {
|
|
699
|
+
console.warn(
|
|
700
|
+
"[BLE] Server print failed, falling back to legacy method:",
|
|
701
|
+
err.message,
|
|
702
|
+
);
|
|
703
|
+
return printViaBleBridge(data, options);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
187
707
|
// Print to a paired Bluetooth printer exposed as a Windows COM (RFCOMM) port.
|
|
188
708
|
// Preferable on Windows: pair the PSF588 printer in OS Bluetooth settings
|
|
189
709
|
// and note the outgoing COM port (e.g. COM5). Then call `printToPSF588(data, { portPath: 'COM5' })`.
|
|
190
710
|
export const printData = (data, options = {}) => {
|
|
191
|
-
const transport =
|
|
711
|
+
const transport =
|
|
712
|
+
options.transport || process.env.PRINTER_TRANSPORT || "ble-server";
|
|
713
|
+
|
|
714
|
+
if (transport === "ble-server") {
|
|
715
|
+
// New: Use persistent BLE server (faster, recommended)
|
|
716
|
+
return printViaBleServer(data, options);
|
|
717
|
+
}
|
|
192
718
|
|
|
193
719
|
if (transport === "ble") {
|
|
720
|
+
// Legacy: Spawn process for each print (slower)
|
|
194
721
|
return printViaBleBridge(data, options);
|
|
195
722
|
}
|
|
196
723
|
|
|
724
|
+
// Default: COM port
|
|
197
725
|
return printViaComPort(data, options);
|
|
198
726
|
};
|
|
727
|
+
|
|
728
|
+
// Lifecycle management exports
|
|
729
|
+
export const startPrinterServer = (options = {}) => startBleServer(options);
|
|
730
|
+
export const stopPrinterServer = () => stopBleServer();
|
|
731
|
+
export const getPrinterServerStatus = () => {
|
|
732
|
+
return {
|
|
733
|
+
running: !!bleServerProcess,
|
|
734
|
+
processInfo: bleServerProcess,
|
|
735
|
+
host: bleServerHost,
|
|
736
|
+
port: bleServerPort,
|
|
737
|
+
};
|
|
738
|
+
};
|