node-thermal-printer-js 1.0.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 +108 -0
- package/app.js +197 -0
- package/ble_print.py +147 -0
- package/ble_scan.py +72 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# node-thermal-printer-js
|
|
2
|
+
|
|
3
|
+
Public npm package for sending ESC/POS print jobs to a PSF588 printer over BLE or classic Bluetooth COM port.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install node-thermal-printer-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { printData } from "node-thermal-printer-js";
|
|
15
|
+
|
|
16
|
+
await printData("Hello from npm!");
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Local Development
|
|
20
|
+
|
|
21
|
+
This project can print ESC/POS data to PSF588 using either:
|
|
22
|
+
- BLE via Python bridge (`ble_print.py` + `bleak`)
|
|
23
|
+
- Classic Bluetooth Serial (COM port) via `serialport`
|
|
24
|
+
|
|
25
|
+
## Install Dependencies
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install
|
|
29
|
+
py -3.11 -m pip install -r requirements.txt
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Test Print
|
|
33
|
+
|
|
34
|
+
### BLE (recommended when no COM port exists)
|
|
35
|
+
|
|
36
|
+
Default PSF588 setup (auto-detects printer and uses correct characteristic):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node test-print.js ble
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or with explicit order ID:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node test-print.js ble ORDER123
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### BLE with custom device/UUID
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node test-print.js ble ORDER123 PSF588 "AA:BB:CC:DD:EE:FF" "49535343-8841-43f4-a8d4-ecbe34729bb3"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Args order for BLE:
|
|
55
|
+
1. `ble`
|
|
56
|
+
2. `orderId` (optional)
|
|
57
|
+
3. `bleName` (optional, default: `PSF588`)
|
|
58
|
+
4. `bleAddress` (optional, auto-detected if not provided)
|
|
59
|
+
5. `charUUID` (optional, default: `49535343-8841-43f4-a8d4-ecbe34729bb3` for PSF588)
|
|
60
|
+
|
|
61
|
+
### Scan for BLE devices
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
node test-print.js scan
|
|
65
|
+
node test-print.js scan PSF588
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Output shows all available characteristics and their write permissions.
|
|
69
|
+
|
|
70
|
+
### COM mode
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
node test-print.js com ORDER123 COM5 9600
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Environment Variables
|
|
77
|
+
|
|
78
|
+
- `PRINTER_TRANSPORT=ble|com` (default: `ble`)
|
|
79
|
+
- `PRINTER_COM_PORT=COM5` (default: `COM5` for COM mode)
|
|
80
|
+
- `PRINTER_BLE_NAME=PSF588` (default: `PSF588`)
|
|
81
|
+
- `PRINTER_BLE_ADDRESS=AA:BB:CC:DD:EE:FF` (optional, auto-detected)
|
|
82
|
+
- `PRINTER_BLE_CHAR_UUID=49535343-8841-43f4-a8d4-ecbe34729bb3` (default for PSF588)
|
|
83
|
+
- `PRINTER_BLE_CONNECT_TIMEOUT=15` (seconds)
|
|
84
|
+
- `PRINTER_BLE_SCAN_TIMEOUT=10` (seconds)
|
|
85
|
+
- `PRINTER_BLE_PAIR=1` (set to enable OS pairing before connect)
|
|
86
|
+
- `PRINTER_PYTHON_CMD=py` (Python launcher)
|
|
87
|
+
|
|
88
|
+
## Publish Checklist
|
|
89
|
+
|
|
90
|
+
Before publishing, make sure the package name is unique on npm and then run:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm pack --dry-run
|
|
94
|
+
npm publish --access public
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## PSF588 Printer Info
|
|
98
|
+
|
|
99
|
+
- **Characteristic for printing:** `49535343-8841-43f4-a8d4-ecbe34729bb3`
|
|
100
|
+
- **Properties:** write-without-response, write
|
|
101
|
+
- **Data format:** ESC/POS (raw binary)
|
|
102
|
+
|
|
103
|
+
## Troubleshooting
|
|
104
|
+
|
|
105
|
+
- **"Access Denied" on write:** Characteristic doesn't support writes. Use `node test-print.js scan PSF588` to find a writable one.
|
|
106
|
+
- **Connection timeout:** Keep printer in pairing mode, disconnect from other devices, ensure Bluetooth is enabled on Windows.
|
|
107
|
+
- **Python not found:** Install Python 3.11+ and ensure it's on PATH, or set `PRINTER_PYTHON_CMD=py -3.11`.
|
|
108
|
+
|
package/app.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const getEscPosPayload = (data) =>
|
|
6
|
+
Buffer.concat([
|
|
7
|
+
Buffer.from([0x1b, 0x40, 0x0a]),
|
|
8
|
+
Buffer.from(`${data}\n\n\n\n\n`, "utf-8"),
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const bleScriptPath = path.join(scriptDir, "ble_print.py");
|
|
13
|
+
|
|
14
|
+
const runPythonProcess = ({ cmd, cmdArgs, scriptArgs }) =>
|
|
15
|
+
new Promise((resolve, reject) => {
|
|
16
|
+
const child = spawn(cmd, [...cmdArgs, ...scriptArgs], {
|
|
17
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
let stdout = "";
|
|
21
|
+
let stderr = "";
|
|
22
|
+
|
|
23
|
+
child.stdout.on("data", (chunk) => {
|
|
24
|
+
stdout += chunk.toString();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
child.stderr.on("data", (chunk) => {
|
|
28
|
+
stderr += chunk.toString();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on("error", (err) => {
|
|
32
|
+
reject(new Error(`Launch error for ${cmd}: ${err.message}`));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on("close", (code) => {
|
|
36
|
+
if (code === 0) {
|
|
37
|
+
resolve({
|
|
38
|
+
ok: true,
|
|
39
|
+
stdout: stdout.trim(),
|
|
40
|
+
stderr: stderr.trim(),
|
|
41
|
+
code,
|
|
42
|
+
cmd,
|
|
43
|
+
cmdArgs,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resolve({
|
|
49
|
+
ok: false,
|
|
50
|
+
stdout: stdout.trim(),
|
|
51
|
+
stderr: stderr.trim(),
|
|
52
|
+
code,
|
|
53
|
+
cmd,
|
|
54
|
+
cmdArgs,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const printViaComPort = async (order, options = {}) => {
|
|
60
|
+
const portPath = options.portPath || process.env.PRINTER_COM_PORT || "COM5";
|
|
61
|
+
const baudRate = options.baudRate || 9600;
|
|
62
|
+
|
|
63
|
+
// Dynamically import `serialport` only when COM transport is requested.
|
|
64
|
+
let SerialPortModule;
|
|
65
|
+
try {
|
|
66
|
+
SerialPortModule = await import("serialport");
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return Promise.reject(
|
|
69
|
+
new Error(`serialport module not available: ${e.message}`),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const SerialPortCtor =
|
|
74
|
+
SerialPortModule.default || SerialPortModule.SerialPort || SerialPortModule;
|
|
75
|
+
|
|
76
|
+
const port = new SerialPortCtor({
|
|
77
|
+
path: portPath,
|
|
78
|
+
baudRate,
|
|
79
|
+
autoOpen: false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
port.open((err) => {
|
|
84
|
+
if (err)
|
|
85
|
+
return reject(new Error(`Failed to open ${portPath}: ${err.message}`));
|
|
86
|
+
|
|
87
|
+
const payload = getEscPosPayload(order);
|
|
88
|
+
|
|
89
|
+
port.write(payload, (writeErr) => {
|
|
90
|
+
if (writeErr) {
|
|
91
|
+
port.close(() => reject(writeErr));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
port.drain((drainErr) => {
|
|
96
|
+
port.close(() => {
|
|
97
|
+
if (drainErr) return reject(drainErr);
|
|
98
|
+
resolve();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const printViaBleBridge = (order, options = {}) => {
|
|
107
|
+
const payload = getEscPosPayload(order).toString("base64");
|
|
108
|
+
const args = [
|
|
109
|
+
bleScriptPath,
|
|
110
|
+
"--data-b64",
|
|
111
|
+
payload,
|
|
112
|
+
"--name",
|
|
113
|
+
options.bleName || process.env.PRINTER_BLE_NAME || "PSF588",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
if (options.bleAddress || process.env.PRINTER_BLE_ADDRESS) {
|
|
117
|
+
args.push(
|
|
118
|
+
"--address",
|
|
119
|
+
options.bleAddress || process.env.PRINTER_BLE_ADDRESS,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.charUUID || process.env.PRINTER_BLE_CHAR_UUID) {
|
|
124
|
+
args.push(
|
|
125
|
+
"--char-uuid",
|
|
126
|
+
options.charUUID || process.env.PRINTER_BLE_CHAR_UUID,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.connectTimeout || process.env.PRINTER_BLE_CONNECT_TIMEOUT) {
|
|
131
|
+
args.push(
|
|
132
|
+
"--connect-timeout",
|
|
133
|
+
String(options.connectTimeout || process.env.PRINTER_BLE_CONNECT_TIMEOUT),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (options.scanTimeout || process.env.PRINTER_BLE_SCAN_TIMEOUT) {
|
|
138
|
+
args.push(
|
|
139
|
+
"--scan-timeout",
|
|
140
|
+
String(options.scanTimeout || process.env.PRINTER_BLE_SCAN_TIMEOUT),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options.pair || process.env.PRINTER_BLE_PAIR === "1") {
|
|
145
|
+
args.push("--pair");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const envCmd = options.pythonCmd || process.env.PRINTER_PYTHON_CMD;
|
|
149
|
+
const candidates = envCmd
|
|
150
|
+
? [{ cmd: envCmd, cmdArgs: [] }]
|
|
151
|
+
: [
|
|
152
|
+
{ cmd: "py", cmdArgs: ["-3.11"] },
|
|
153
|
+
{ cmd: "py", cmdArgs: ["-3"] },
|
|
154
|
+
{ cmd: "python", cmdArgs: [] },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
return new Promise(async (resolve, reject) => {
|
|
158
|
+
const failures = [];
|
|
159
|
+
|
|
160
|
+
for (const candidate of candidates) {
|
|
161
|
+
// Try multiple launchers because Windows py default can point to a missing runtime.
|
|
162
|
+
const result = await runPythonProcess({
|
|
163
|
+
cmd: candidate.cmd,
|
|
164
|
+
cmdArgs: candidate.cmdArgs,
|
|
165
|
+
scriptArgs: args,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (result.ok) {
|
|
169
|
+
resolve(result.stdout);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
failures.push(
|
|
174
|
+
`${result.cmd} ${result.cmdArgs.join(" ")} -> code ${result.code}: ${result.stderr || result.stdout || "no output"}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
reject(
|
|
179
|
+
new Error(
|
|
180
|
+
`BLE bridge failed for all Python launchers. ${failures.join(" | ")}`,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Print to a paired Bluetooth printer exposed as a Windows COM (RFCOMM) port.
|
|
187
|
+
// Preferable on Windows: pair the PSF588 printer in OS Bluetooth settings
|
|
188
|
+
// and note the outgoing COM port (e.g. COM5). Then call `printToPSF588(order, { portPath: 'COM5' })`.
|
|
189
|
+
export const printData = (order, options = {}) => {
|
|
190
|
+
const transport = options.transport || process.env.PRINTER_TRANSPORT || "ble";
|
|
191
|
+
|
|
192
|
+
if (transport === "ble") {
|
|
193
|
+
return printViaBleBridge(order, options);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return printViaComPort(order, options);
|
|
197
|
+
};
|
package/ble_print.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import base64
|
|
4
|
+
import sys
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
from bleak import BleakClient, BleakScanner
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_args():
|
|
11
|
+
parser = argparse.ArgumentParser(description="Send ESC/POS bytes to a BLE printer")
|
|
12
|
+
parser.add_argument("--name", default="PSF588", help="BLE device name contains filter")
|
|
13
|
+
parser.add_argument("--address", default=None, help="BLE MAC/address to connect directly")
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--char-uuid",
|
|
16
|
+
default="49535343-8841-43f4-a8d4-ecbe34729bb3",
|
|
17
|
+
help="Writable characteristic UUID (default works for PSF588)",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("--data-b64", required=True, help="Base64 payload bytes")
|
|
20
|
+
parser.add_argument("--scan-timeout", type=float, default=10.0, help="Scan timeout seconds")
|
|
21
|
+
parser.add_argument("--connect-timeout", type=float, default=15.0, help="Connect timeout seconds")
|
|
22
|
+
parser.add_argument("--chunk-size", type=int, default=180, help="Write chunk size")
|
|
23
|
+
parser.add_argument("--delay-ms", type=int, default=20, help="Delay between chunks in ms")
|
|
24
|
+
parser.add_argument("--pair", action="store_true", help="Request OS pairing before connect")
|
|
25
|
+
parser.add_argument("--connect-retries", type=int, default=2, help="Connect retry attempts")
|
|
26
|
+
return parser.parse_args()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def find_device(name_filter: str, timeout: float):
|
|
30
|
+
devices = await BleakScanner.discover(timeout=timeout)
|
|
31
|
+
name_filter = (name_filter or "").lower()
|
|
32
|
+
|
|
33
|
+
for device in devices:
|
|
34
|
+
name = (device.name or "").lower()
|
|
35
|
+
if name_filter and name_filter in name:
|
|
36
|
+
return device, device.address, device.name
|
|
37
|
+
|
|
38
|
+
return None, None, None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def pick_writable_characteristic(services):
|
|
42
|
+
# Prefer write-without-response characteristics (more reliable for bulk data)
|
|
43
|
+
candidates = []
|
|
44
|
+
|
|
45
|
+
for service in services:
|
|
46
|
+
for char in service.characteristics:
|
|
47
|
+
props = set(char.properties)
|
|
48
|
+
if "write-without-response" in props:
|
|
49
|
+
candidates.append((char.uuid, False))
|
|
50
|
+
elif "write" in props:
|
|
51
|
+
candidates.append((char.uuid, True))
|
|
52
|
+
|
|
53
|
+
# Return first write-without-response, then first write
|
|
54
|
+
for uuid, response_required in candidates:
|
|
55
|
+
if not response_required:
|
|
56
|
+
print(f"[DEBUG] Selected write-without-response characteristic: {uuid}")
|
|
57
|
+
return uuid, response_required
|
|
58
|
+
|
|
59
|
+
for uuid, response_required in candidates:
|
|
60
|
+
print(f"[DEBUG] Selected write characteristic (requires response): {uuid}")
|
|
61
|
+
return uuid, response_required
|
|
62
|
+
|
|
63
|
+
print("[DEBUG] No writable characteristics found")
|
|
64
|
+
return None, None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def run_print(args):
|
|
68
|
+
payload = base64.b64decode(args.data_b64)
|
|
69
|
+
|
|
70
|
+
address = args.address
|
|
71
|
+
device = None
|
|
72
|
+
device_name = args.name
|
|
73
|
+
|
|
74
|
+
if not address:
|
|
75
|
+
device, address, found_name = await find_device(device_name, args.scan_timeout)
|
|
76
|
+
if not address:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
f"No BLE device found for name filter '{device_name}'. "
|
|
79
|
+
"Try setting --address explicitly."
|
|
80
|
+
)
|
|
81
|
+
print(f"Found device: {found_name} ({address})")
|
|
82
|
+
|
|
83
|
+
target = device if device is not None else address
|
|
84
|
+
|
|
85
|
+
last_error = None
|
|
86
|
+
for attempt in range(1, max(1, args.connect_retries) + 1):
|
|
87
|
+
try:
|
|
88
|
+
async with BleakClient(
|
|
89
|
+
target,
|
|
90
|
+
timeout=args.connect_timeout,
|
|
91
|
+
pair=args.pair,
|
|
92
|
+
winrt={"use_cached_services": False},
|
|
93
|
+
) as client:
|
|
94
|
+
if not client.is_connected:
|
|
95
|
+
raise RuntimeError(f"Failed to connect to {address}")
|
|
96
|
+
|
|
97
|
+
if hasattr(client, "get_services"):
|
|
98
|
+
services = await client.get_services()
|
|
99
|
+
else:
|
|
100
|
+
services = client.services
|
|
101
|
+
|
|
102
|
+
char_uuid = args.char_uuid
|
|
103
|
+
response_required = False
|
|
104
|
+
|
|
105
|
+
if not char_uuid:
|
|
106
|
+
# Re-scan services fresh and pick writable characteristic
|
|
107
|
+
char_uuid, response_required = pick_writable_characteristic(services)
|
|
108
|
+
if not char_uuid:
|
|
109
|
+
raise RuntimeError("No writable BLE characteristic found on target device")
|
|
110
|
+
|
|
111
|
+
print(f"Using characteristic: {char_uuid} (response_required={response_required})")
|
|
112
|
+
|
|
113
|
+
chunk_size = max(20, min(args.chunk_size, 512))
|
|
114
|
+
delay_sec = max(0, args.delay_ms) / 1000.0
|
|
115
|
+
|
|
116
|
+
bytes_sent = 0
|
|
117
|
+
for idx in range(0, len(payload), chunk_size):
|
|
118
|
+
chunk = payload[idx : idx + chunk_size]
|
|
119
|
+
print(f"[DEBUG] Writing {len(chunk)} bytes to {char_uuid}...")
|
|
120
|
+
await client.write_gatt_char(char_uuid, chunk, response=response_required)
|
|
121
|
+
bytes_sent += len(chunk)
|
|
122
|
+
if delay_sec:
|
|
123
|
+
await asyncio.sleep(delay_sec)
|
|
124
|
+
|
|
125
|
+
print(f"BLE print sent successfully ({bytes_sent} bytes)")
|
|
126
|
+
return
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
last_error = exc
|
|
129
|
+
print(f"Connect/write attempt {attempt} failed: {repr(exc)}", file=sys.stderr)
|
|
130
|
+
if attempt < max(1, args.connect_retries):
|
|
131
|
+
await asyncio.sleep(1)
|
|
132
|
+
raise last_error if last_error else RuntimeError("BLE print failed without a specific error")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
args = parse_args()
|
|
137
|
+
try:
|
|
138
|
+
asyncio.run(run_print(args))
|
|
139
|
+
except Exception as exc:
|
|
140
|
+
detail = str(exc) or repr(exc)
|
|
141
|
+
print(f"BLE print failed: {detail}", file=sys.stderr)
|
|
142
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|
package/ble_scan.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from bleak import BleakClient, BleakScanner
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_args():
|
|
9
|
+
parser = argparse.ArgumentParser(description="Scan BLE devices and list writable characteristics")
|
|
10
|
+
parser.add_argument("--name-filter", default="", help="Device name contains filter")
|
|
11
|
+
parser.add_argument("--timeout", type=float, default=10.0, help="Scan timeout seconds")
|
|
12
|
+
parser.add_argument("--connect-timeout", type=float, default=15.0, help="Connect timeout seconds")
|
|
13
|
+
return parser.parse_args()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def run_scan(args):
|
|
17
|
+
print("Scanning for BLE devices...")
|
|
18
|
+
devices = await BleakScanner.discover(timeout=args.timeout)
|
|
19
|
+
|
|
20
|
+
if not devices:
|
|
21
|
+
print("No BLE devices found.")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
filtered = [d for d in devices if not args.name_filter or (args.name_filter.lower() in (d.name or "").lower())]
|
|
25
|
+
|
|
26
|
+
if not filtered:
|
|
27
|
+
print(f"No devices matching filter '{args.name_filter}'")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
print(f"\nFound {len(filtered)} device(s):\n")
|
|
31
|
+
|
|
32
|
+
for device in filtered:
|
|
33
|
+
print(f"Device: {device.name} ({device.address})")
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
async with BleakClient(device, timeout=args.connect_timeout) as client:
|
|
37
|
+
if hasattr(client, "get_services"):
|
|
38
|
+
services = await client.get_services()
|
|
39
|
+
else:
|
|
40
|
+
services = client.services
|
|
41
|
+
|
|
42
|
+
if not services:
|
|
43
|
+
print(" No services found")
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
for service in services:
|
|
47
|
+
print(f" Service: {service.uuid}")
|
|
48
|
+
|
|
49
|
+
for char in service.characteristics:
|
|
50
|
+
props = ", ".join(char.properties)
|
|
51
|
+
writable = "write" in char.properties or "write-without-response" in char.properties
|
|
52
|
+
marker = " [WRITABLE]" if writable else ""
|
|
53
|
+
print(f" Characteristic: {char.uuid}{marker}")
|
|
54
|
+
print(f" Properties: {props}")
|
|
55
|
+
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
print(f" Error connecting: {exc}")
|
|
58
|
+
|
|
59
|
+
print()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
args = parse_args()
|
|
64
|
+
try:
|
|
65
|
+
asyncio.run(run_scan(args))
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
print(f"Scan failed: {exc}", file=sys.stderr)
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-thermal-printer-js",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESC/POS printer helper for PSF588 Bluetooth and COM printing.",
|
|
5
|
+
"main": "app.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./app.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"app.js",
|
|
11
|
+
"ble_print.py",
|
|
12
|
+
"ble_scan.py",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node test-api.js",
|
|
17
|
+
"dev": "node --watch app.js",
|
|
18
|
+
"pack:dry": "npm pack --dry-run"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"printer",
|
|
22
|
+
"escpos",
|
|
23
|
+
"bluetooth",
|
|
24
|
+
"serialport",
|
|
25
|
+
"psf588"
|
|
26
|
+
],
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"escpos": "^3.0.0-alpha.6",
|
|
35
|
+
"escpos-serialport": "^3.0.0-alpha.4",
|
|
36
|
+
"serialport": "^13.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|