node-thermal-printer-js 1.0.8 ā 1.2.1
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 +561 -6
- package/ble_server.py +345 -0
- package/package.json +7 -1
- package/scripts/install-deps.js +177 -0
package/ble_server.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Persistent BLE Printer Server
|
|
3
|
+
Maintains a long-running connection pool to avoid reconnection overhead.
|
|
4
|
+
Listens for print jobs over a local socket and sends them optimally.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from bleak import BleakClient, BleakScanner
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BLEPrinterServer:
|
|
19
|
+
def __init__(self, device_name: str = "PSF588", device_address: Optional[str] = None,
|
|
20
|
+
char_uuid: str = "49535343-8841-43f4-a8d4-ecbe34729bb3",
|
|
21
|
+
chunk_size: int = 244, delay_ms: int = 0, pair: bool = False,
|
|
22
|
+
connect_timeout: float = 15.0, scan_timeout: float = 10.0,
|
|
23
|
+
port: int = 5555, host: str = "127.0.0.1"):
|
|
24
|
+
self.device_name = device_name
|
|
25
|
+
self.device_address = device_address
|
|
26
|
+
self.char_uuid = char_uuid
|
|
27
|
+
self.chunk_size = max(20, min(chunk_size, 512))
|
|
28
|
+
self.delay_sec = max(0, delay_ms) / 1000.0
|
|
29
|
+
self.pair = pair
|
|
30
|
+
self.connect_timeout = connect_timeout
|
|
31
|
+
self.scan_timeout = scan_timeout
|
|
32
|
+
self.port = port
|
|
33
|
+
self.host = host
|
|
34
|
+
self.client: Optional[BleakClient] = None
|
|
35
|
+
self.response_required = False
|
|
36
|
+
self.is_running = True
|
|
37
|
+
|
|
38
|
+
def _resolve_characteristic_mode(self, services, preferred_uuid: Optional[str]) -> tuple[Optional[str], bool]:
|
|
39
|
+
"""Resolve characteristic UUID and whether response is required."""
|
|
40
|
+
if preferred_uuid:
|
|
41
|
+
preferred_uuid_l = preferred_uuid.lower()
|
|
42
|
+
for service in services:
|
|
43
|
+
for char in service.characteristics:
|
|
44
|
+
if (char.uuid or "").lower() == preferred_uuid_l:
|
|
45
|
+
props = set(char.properties)
|
|
46
|
+
if "write-without-response" in props:
|
|
47
|
+
print(
|
|
48
|
+
f"[SERVER] Using configured characteristic: {char.uuid} (response_required=False)",
|
|
49
|
+
flush=True,
|
|
50
|
+
)
|
|
51
|
+
return char.uuid, False
|
|
52
|
+
if "write" in props:
|
|
53
|
+
print(
|
|
54
|
+
f"[SERVER] Using configured characteristic: {char.uuid} (response_required=True)",
|
|
55
|
+
flush=True,
|
|
56
|
+
)
|
|
57
|
+
return char.uuid, True
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
f"Configured characteristic {preferred_uuid} is not writable"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
raise RuntimeError(
|
|
63
|
+
f"Configured characteristic {preferred_uuid} not found on target device"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return self._pick_characteristic(services)
|
|
67
|
+
|
|
68
|
+
async def find_device(self) -> tuple[Optional[str], Optional[str]]:
|
|
69
|
+
"""Find BLE device by name or use cached address."""
|
|
70
|
+
if self.device_address:
|
|
71
|
+
return self.device_address, None
|
|
72
|
+
|
|
73
|
+
print(f"[SERVER] Scanning for device: {self.device_name}", flush=True)
|
|
74
|
+
devices = await BleakScanner.discover(timeout=self.scan_timeout)
|
|
75
|
+
name_filter = (self.device_name or "").lower()
|
|
76
|
+
|
|
77
|
+
for device in devices:
|
|
78
|
+
name = (device.name or "").lower()
|
|
79
|
+
if name_filter and name_filter in name:
|
|
80
|
+
print(f"[SERVER] Found device: {device.name} ({device.address})", flush=True)
|
|
81
|
+
return device.address, device.name
|
|
82
|
+
|
|
83
|
+
raise RuntimeError(
|
|
84
|
+
f"No BLE device found for name filter '{self.device_name}'. "
|
|
85
|
+
"Try setting --address explicitly."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
async def connect_to_printer(self):
|
|
89
|
+
"""Establish and maintain BLE connection."""
|
|
90
|
+
if self.client and self.client.is_connected:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
address, found_name = await self.find_device()
|
|
94
|
+
self.device_address = address
|
|
95
|
+
|
|
96
|
+
print(f"[SERVER] Connecting to {address}...", flush=True)
|
|
97
|
+
self.client = BleakClient(
|
|
98
|
+
address,
|
|
99
|
+
timeout=self.connect_timeout,
|
|
100
|
+
pair=self.pair,
|
|
101
|
+
winrt={"use_cached_services": False},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
await self.client.connect()
|
|
105
|
+
print(f"[SERVER] Connected to {address}", flush=True)
|
|
106
|
+
|
|
107
|
+
# Get services and pick writable characteristic
|
|
108
|
+
if hasattr(self.client, "get_services"):
|
|
109
|
+
services = await self.client.get_services()
|
|
110
|
+
else:
|
|
111
|
+
services = self.client.services
|
|
112
|
+
|
|
113
|
+
self.char_uuid, self.response_required = self._resolve_characteristic_mode(
|
|
114
|
+
services, self.char_uuid
|
|
115
|
+
)
|
|
116
|
+
if not self.char_uuid:
|
|
117
|
+
raise RuntimeError("No writable BLE characteristic found on target device")
|
|
118
|
+
|
|
119
|
+
print(f"[SERVER] Using characteristic: {self.char_uuid} (response_required={self.response_required})", flush=True)
|
|
120
|
+
|
|
121
|
+
def _pick_characteristic(self, services) -> tuple[Optional[str], bool]:
|
|
122
|
+
"""Prefer write-without-response characteristics."""
|
|
123
|
+
candidates = []
|
|
124
|
+
|
|
125
|
+
for service in services:
|
|
126
|
+
for char in service.characteristics:
|
|
127
|
+
props = set(char.properties)
|
|
128
|
+
if "write-without-response" in props:
|
|
129
|
+
candidates.append((char.uuid, False))
|
|
130
|
+
elif "write" in props:
|
|
131
|
+
candidates.append((char.uuid, True))
|
|
132
|
+
|
|
133
|
+
# Return first write-without-response, then first write
|
|
134
|
+
for uuid, response_required in candidates:
|
|
135
|
+
if not response_required:
|
|
136
|
+
print(f"[SERVER] Selected write-without-response characteristic: {uuid}", flush=True)
|
|
137
|
+
return uuid, response_required
|
|
138
|
+
|
|
139
|
+
for uuid, response_required in candidates:
|
|
140
|
+
print(f"[SERVER] Selected write characteristic (requires response): {uuid}", flush=True)
|
|
141
|
+
return uuid, response_required
|
|
142
|
+
|
|
143
|
+
return None, None
|
|
144
|
+
|
|
145
|
+
async def send_print_data(self, payload: bytes) -> dict:
|
|
146
|
+
"""Send print data to connected printer."""
|
|
147
|
+
try:
|
|
148
|
+
# Ensure connection is alive
|
|
149
|
+
if not self.client or not self.client.is_connected:
|
|
150
|
+
print(f"[SERVER] Device not connected, attempting to connect...", flush=True)
|
|
151
|
+
try:
|
|
152
|
+
await self.connect_to_printer()
|
|
153
|
+
except Exception as conn_err:
|
|
154
|
+
return {"ok": False, "error": f"Failed to connect to printer: {str(conn_err)}"}
|
|
155
|
+
|
|
156
|
+
print(f"[SERVER] Sending {len(payload)} bytes in chunks of {self.chunk_size}...", flush=True)
|
|
157
|
+
bytes_sent = 0
|
|
158
|
+
|
|
159
|
+
for idx in range(0, len(payload), self.chunk_size):
|
|
160
|
+
chunk = payload[idx : idx + self.chunk_size]
|
|
161
|
+
await asyncio.wait_for(
|
|
162
|
+
self.client.write_gatt_char(
|
|
163
|
+
self.char_uuid, chunk, response=self.response_required
|
|
164
|
+
),
|
|
165
|
+
timeout=8.0,
|
|
166
|
+
)
|
|
167
|
+
bytes_sent += len(chunk)
|
|
168
|
+
if self.delay_sec:
|
|
169
|
+
await asyncio.sleep(self.delay_sec)
|
|
170
|
+
|
|
171
|
+
print(f"[SERVER] Print sent successfully ({bytes_sent} bytes)", flush=True)
|
|
172
|
+
return {"ok": True, "bytes_sent": bytes_sent}
|
|
173
|
+
|
|
174
|
+
except Exception as exc:
|
|
175
|
+
print(f"[SERVER] Send failed: {repr(exc)}", file=sys.stderr, flush=True)
|
|
176
|
+
self.client = None # Reset connection on error
|
|
177
|
+
return {"ok": False, "error": str(exc)}
|
|
178
|
+
|
|
179
|
+
async def handle_client(self, reader, writer):
|
|
180
|
+
"""Handle incoming print requests from app.js."""
|
|
181
|
+
addr = writer.get_extra_info("peername")
|
|
182
|
+
print(f"[SERVER] Client connected: {addr}", flush=True)
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# One request per connection to avoid stream deadlocks.
|
|
186
|
+
print("[SERVER] Waiting for request line...", flush=True)
|
|
187
|
+
try:
|
|
188
|
+
line = await asyncio.wait_for(reader.readline(), timeout=20.0)
|
|
189
|
+
except asyncio.TimeoutError:
|
|
190
|
+
print("[SERVER] Request read timed out", flush=True)
|
|
191
|
+
response = {"ok": False, "error": "Timed out waiting for request"}
|
|
192
|
+
writer.write((json.dumps(response) + "\n").encode("utf-8"))
|
|
193
|
+
await writer.drain()
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if not line:
|
|
197
|
+
print("[SERVER] Connection closed before request payload", flush=True)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
print(f"[SERVER] Received raw line bytes: {len(line)}", flush=True)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
request = json.loads(line.decode("utf-8"))
|
|
204
|
+
command = request.get("command")
|
|
205
|
+
|
|
206
|
+
if command == "print":
|
|
207
|
+
print("[SERVER] Handling print command", flush=True)
|
|
208
|
+
payload_b64 = request.get("data_b64")
|
|
209
|
+
if not payload_b64:
|
|
210
|
+
response = {"ok": False, "error": "Missing data_b64"}
|
|
211
|
+
else:
|
|
212
|
+
try:
|
|
213
|
+
payload = base64.b64decode(payload_b64)
|
|
214
|
+
response = await self.send_print_data(payload)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
response = {"ok": False, "error": f"Decode error: {str(e)}"}
|
|
217
|
+
|
|
218
|
+
elif command == "status":
|
|
219
|
+
is_connected = self.client and self.client.is_connected
|
|
220
|
+
response = {
|
|
221
|
+
"ok": True,
|
|
222
|
+
"connected": is_connected,
|
|
223
|
+
"device_address": self.device_address,
|
|
224
|
+
"char_uuid": self.char_uuid,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
elif command == "ping":
|
|
228
|
+
response = {"ok": True, "message": "pong"}
|
|
229
|
+
|
|
230
|
+
else:
|
|
231
|
+
response = {"ok": False, "error": f"Unknown command: {command}"}
|
|
232
|
+
|
|
233
|
+
writer.write((json.dumps(response) + "\n").encode("utf-8"))
|
|
234
|
+
await writer.drain()
|
|
235
|
+
print(f"[SERVER] Response sent: {response}", flush=True)
|
|
236
|
+
|
|
237
|
+
except json.JSONDecodeError as e:
|
|
238
|
+
response = {"ok": False, "error": f"JSON decode error: {str(e)}"}
|
|
239
|
+
writer.write((json.dumps(response) + "\n").encode("utf-8"))
|
|
240
|
+
await writer.drain()
|
|
241
|
+
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
print(f"[SERVER] Client error: {repr(exc)}", file=sys.stderr, flush=True)
|
|
244
|
+
finally:
|
|
245
|
+
writer.close()
|
|
246
|
+
await writer.wait_closed()
|
|
247
|
+
print(f"[SERVER] Client disconnected: {addr}", flush=True)
|
|
248
|
+
|
|
249
|
+
async def start_server(self):
|
|
250
|
+
"""Start the socket server."""
|
|
251
|
+
print(f"[SERVER] Starting BLE server on {self.host}:{self.port}", flush=True)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
# Initial connection to printer (non-blocking, will retry on first request)
|
|
255
|
+
await self.connect_to_printer()
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
print(f"[SERVER] Initial connection failed: {repr(exc)}", file=sys.stderr, flush=True)
|
|
258
|
+
print("[SERVER] Will retry on first client request", flush=True)
|
|
259
|
+
|
|
260
|
+
async def client_handler(reader, writer):
|
|
261
|
+
await self.handle_client(reader, writer)
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
# Enable SO_REUSEADDR to allow reusing the port immediately
|
|
265
|
+
server = await asyncio.start_server(
|
|
266
|
+
client_handler, self.host, self.port,
|
|
267
|
+
reuse_address=True
|
|
268
|
+
)
|
|
269
|
+
except OSError as e:
|
|
270
|
+
if "already in use" in str(e) or "10048" in str(e):
|
|
271
|
+
print(f"[SERVER] Port {self.port} still in TIME_WAIT, retrying in 2 seconds...", file=sys.stderr, flush=True)
|
|
272
|
+
await asyncio.sleep(2)
|
|
273
|
+
server = await asyncio.start_server(
|
|
274
|
+
client_handler, self.host, self.port,
|
|
275
|
+
reuse_address=True
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
raise
|
|
279
|
+
|
|
280
|
+
print(f"[SERVER] Server ready. Listening on {self.host}:{self.port}", flush=True)
|
|
281
|
+
sys.stdout.flush()
|
|
282
|
+
sys.stderr.flush()
|
|
283
|
+
|
|
284
|
+
async with server:
|
|
285
|
+
await server.serve_forever()
|
|
286
|
+
|
|
287
|
+
async def shutdown(self):
|
|
288
|
+
"""Gracefully shutdown."""
|
|
289
|
+
self.is_running = False
|
|
290
|
+
if self.client:
|
|
291
|
+
await self.client.disconnect()
|
|
292
|
+
print("[SERVER] Disconnected from printer", flush=True)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def parse_args():
|
|
296
|
+
parser = argparse.ArgumentParser(description="Persistent BLE Printer Server")
|
|
297
|
+
parser.add_argument("--name", default="PSF588", help="BLE device name filter")
|
|
298
|
+
parser.add_argument("--address", default=None, help="BLE MAC/address (skips scan)")
|
|
299
|
+
parser.add_argument(
|
|
300
|
+
"--char-uuid",
|
|
301
|
+
default="49535343-8841-43f4-a8d4-ecbe34729bb3",
|
|
302
|
+
help="Writable characteristic UUID",
|
|
303
|
+
)
|
|
304
|
+
parser.add_argument("--chunk-size", type=int, default=244, help="Write chunk size (default: 244)")
|
|
305
|
+
parser.add_argument("--delay-ms", type=int, default=0, help="Delay between chunks in ms")
|
|
306
|
+
parser.add_argument("--pair", action="store_true", help="Request OS pairing")
|
|
307
|
+
parser.add_argument("--connect-timeout", type=float, default=15.0, help="Connect timeout seconds")
|
|
308
|
+
parser.add_argument("--scan-timeout", type=float, default=10.0, help="Scan timeout seconds")
|
|
309
|
+
parser.add_argument("--port", type=int, default=5555, help="Server port")
|
|
310
|
+
parser.add_argument("--host", default="127.0.0.1", help="Server host")
|
|
311
|
+
return parser.parse_args()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def main():
|
|
315
|
+
args = parse_args()
|
|
316
|
+
# Set unbuffered/line-buffered output immediately
|
|
317
|
+
import io
|
|
318
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, write_through=True)
|
|
319
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, write_through=True)
|
|
320
|
+
|
|
321
|
+
print("[SERVER] Starting BLE server...", flush=True)
|
|
322
|
+
try:
|
|
323
|
+
server = BLEPrinterServer(
|
|
324
|
+
device_name=args.name,
|
|
325
|
+
device_address=args.address,
|
|
326
|
+
char_uuid=args.char_uuid,
|
|
327
|
+
chunk_size=args.chunk_size,
|
|
328
|
+
delay_ms=args.delay_ms,
|
|
329
|
+
pair=args.pair,
|
|
330
|
+
connect_timeout=args.connect_timeout,
|
|
331
|
+
scan_timeout=args.scan_timeout,
|
|
332
|
+
port=args.port,
|
|
333
|
+
host=args.host,
|
|
334
|
+
)
|
|
335
|
+
asyncio.run(server.start_server())
|
|
336
|
+
except KeyboardInterrupt:
|
|
337
|
+
print("\n[SERVER] Shutting down...", flush=True)
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
print(f"Server error: {repr(exc)}", file=sys.stderr, flush=True)
|
|
340
|
+
print(traceback.format_exc(), file=sys.stderr, flush=True)
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-thermal-printer-js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "ESC/POS printer helper for PSF588 Bluetooth and COM printing.",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"exports": {
|
|
@@ -10,13 +10,19 @@
|
|
|
10
10
|
"app.js",
|
|
11
11
|
"ble_print.py",
|
|
12
12
|
"ble_scan.py",
|
|
13
|
+
"ble_server.py",
|
|
14
|
+
"scripts/install-deps.js",
|
|
13
15
|
"README.md"
|
|
14
16
|
],
|
|
15
17
|
"scripts": {
|
|
18
|
+
"postinstall": "node scripts/install-deps.js",
|
|
16
19
|
"test": "node test-api.js",
|
|
17
20
|
"dev": "node --watch app.js",
|
|
18
21
|
"pack:dry": "npm pack --dry-run"
|
|
19
22
|
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=16.0.0"
|
|
25
|
+
},
|
|
20
26
|
"keywords": [
|
|
21
27
|
"printer",
|
|
22
28
|
"escpos",
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script to verify and auto-install Python dependencies
|
|
4
|
+
* Runs: npm install --> postinstall hook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
const findPythonCmd = () => {
|
|
15
|
+
const localVenvCandidates =
|
|
16
|
+
process.platform === "win32"
|
|
17
|
+
? [
|
|
18
|
+
{
|
|
19
|
+
cmd: path.join(scriptDir, ".venv", "Scripts", "python.exe"),
|
|
20
|
+
args: [],
|
|
21
|
+
},
|
|
22
|
+
{ cmd: path.join(scriptDir, ".venv", "Scripts", "python"), args: [] },
|
|
23
|
+
]
|
|
24
|
+
: [
|
|
25
|
+
{ cmd: path.join(scriptDir, ".venv", "bin", "python"), args: [] },
|
|
26
|
+
{ cmd: path.join(scriptDir, ".venv", "bin", "python3"), args: [] },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const systemCandidates =
|
|
30
|
+
process.platform === "win32"
|
|
31
|
+
? [
|
|
32
|
+
{ cmd: "py", args: ["-3.11"] },
|
|
33
|
+
{ cmd: "py", args: ["-3"] },
|
|
34
|
+
{ cmd: "py", args: [] },
|
|
35
|
+
{ cmd: "python3", args: [] },
|
|
36
|
+
{ cmd: "python", args: [] },
|
|
37
|
+
]
|
|
38
|
+
: [
|
|
39
|
+
{ cmd: "python3", args: [] },
|
|
40
|
+
{ cmd: "python", args: [] },
|
|
41
|
+
{ cmd: "py", args: [] },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const candidates = [
|
|
45
|
+
...localVenvCandidates.filter((candidate) => existsSync(candidate.cmd)),
|
|
46
|
+
...systemCandidates,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
try {
|
|
51
|
+
const result = spawnSync(
|
|
52
|
+
candidate.cmd,
|
|
53
|
+
[...candidate.args, "--version"],
|
|
54
|
+
{
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
stdio: "pipe",
|
|
57
|
+
timeout: 2000,
|
|
58
|
+
shell: false,
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Skip if command failed or timed out
|
|
63
|
+
if (result.status !== 0 || result.error) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const version = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
68
|
+
// Check if Python 3.9+
|
|
69
|
+
const match = version.match(/Python (\d+)\.(\d+)/);
|
|
70
|
+
if (match) {
|
|
71
|
+
const major = parseInt(match[1], 10);
|
|
72
|
+
const minor = parseInt(match[2], 10);
|
|
73
|
+
if (major > 3 || (major === 3 && minor >= 9)) {
|
|
74
|
+
console.log(
|
|
75
|
+
`ā Found ${candidate.cmd} ${candidate.args.join(" ")} (${version})`,
|
|
76
|
+
);
|
|
77
|
+
return candidate;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Continue to next candidate
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const installBleak = (pythonCmd) => {
|
|
89
|
+
console.log(`\nš¦ Installing bleak Python package...`);
|
|
90
|
+
try {
|
|
91
|
+
const result = spawnSync(
|
|
92
|
+
pythonCmd.cmd,
|
|
93
|
+
[...pythonCmd.args, "-m", "pip", "install", "bleak"],
|
|
94
|
+
{
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
stdio: "inherit",
|
|
97
|
+
timeout: 120000,
|
|
98
|
+
shell: false,
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
if (result.status !== 0) {
|
|
102
|
+
throw new Error(`pip exited with code ${result.status ?? "unknown"}`);
|
|
103
|
+
}
|
|
104
|
+
console.log("ā bleak installed successfully");
|
|
105
|
+
return true;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.warn("ā Could not auto-install bleak");
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const main = async () => {
|
|
113
|
+
console.log("\nš§ node-thermal-printer postinstall setup\n");
|
|
114
|
+
|
|
115
|
+
// Step 1: Find Python
|
|
116
|
+
console.log("1ļøā£ Checking Python installation...");
|
|
117
|
+
const pythonCmd = findPythonCmd();
|
|
118
|
+
|
|
119
|
+
if (!pythonCmd) {
|
|
120
|
+
console.error("\nā ERROR: Python 3.9+ not found!");
|
|
121
|
+
console.error(
|
|
122
|
+
"\nPlease install Python from https://www.python.org/downloads/",
|
|
123
|
+
);
|
|
124
|
+
console.error("After installing Python, run: npm install again\n");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(` Using: ${pythonCmd}\n`);
|
|
129
|
+
|
|
130
|
+
// Step 2: Install bleak
|
|
131
|
+
console.log("2ļøā£ Checking bleak dependency...");
|
|
132
|
+
try {
|
|
133
|
+
const importCheck = spawnSync(
|
|
134
|
+
pythonCmd.cmd,
|
|
135
|
+
[...pythonCmd.args, "-c", "import bleak"],
|
|
136
|
+
{
|
|
137
|
+
stdio: "pipe",
|
|
138
|
+
timeout: 2000,
|
|
139
|
+
shell: false,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
if (importCheck.status !== 0) {
|
|
143
|
+
throw new Error("bleak import check failed");
|
|
144
|
+
}
|
|
145
|
+
console.log("ā bleak already installed\n");
|
|
146
|
+
} catch {
|
|
147
|
+
const installed = installBleak(pythonCmd);
|
|
148
|
+
if (!installed) {
|
|
149
|
+
console.error("\nā Manual installation required:");
|
|
150
|
+
console.error(
|
|
151
|
+
` ${pythonCmd.cmd} ${pythonCmd.args.join(" ")} -m pip install bleak\n`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Step 3: Platform-specific notes
|
|
157
|
+
console.log("3ļøā£ Platform-specific requirements:");
|
|
158
|
+
const platform = process.platform;
|
|
159
|
+
if (platform === "linux") {
|
|
160
|
+
console.log(" š Linux detected - BLE requires group permissions:");
|
|
161
|
+
console.log(" sudo usermod -a -G dialout,plugdev $USER");
|
|
162
|
+
console.log(" (Log out and back in for changes to take effect)\n");
|
|
163
|
+
} else if (platform === "darwin") {
|
|
164
|
+
console.log(" š macOS detected - Ensure Bluetooth is enabled\n");
|
|
165
|
+
} else if (platform === "win32") {
|
|
166
|
+
console.log(
|
|
167
|
+
" š Windows detected - Ensure Bluetooth drivers are installed\n",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log("ā
Setup complete! You can now use node-thermal-printer-js\n");
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
main().catch((err) => {
|
|
175
|
+
console.error("Error during setup:", err.message);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|