@thermal-label/brother-ql-node 0.6.1 → 0.6.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.
- package/dist/__tests__/discovery.test.js +305 -72
- package/dist/__tests__/discovery.test.js.map +1 -1
- package/dist/__tests__/printer-network.test.d.ts +2 -0
- package/dist/__tests__/printer-network.test.d.ts.map +1 -0
- package/dist/__tests__/printer-network.test.js +242 -0
- package/dist/__tests__/printer-network.test.js.map +1 -0
- package/dist/__tests__/printer.test.js +80 -10
- package/dist/__tests__/printer.test.js.map +1 -1
- package/dist/discovery.d.ts +36 -16
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +178 -72
- package/dist/discovery.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/printer.d.ts +28 -5
- package/dist/printer.d.ts.map +1 -1
- package/dist/printer.js +143 -10
- package/dist/printer.js.map +1 -1
- package/package.json +4 -5
package/dist/discovery.js
CHANGED
|
@@ -1,87 +1,142 @@
|
|
|
1
|
-
import { DEVICES,
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { DEVICES, getUsbIds, MEDIA } from '@thermal-label/brother-ql-core';
|
|
2
|
+
import { DeviceIdentificationRequiredError, DeviceNotFoundError } from '@thermal-label/contracts';
|
|
3
|
+
import { enumerateNetworkDevices, enumerateUsbDevices, identifyNetworkDevice, PRINTER_MIB, SerialTransport, snmpGet, TcpTransport, UsbTransport, } from '@thermal-label/transport/node';
|
|
4
4
|
import { BrotherQLPrinter } from './printer.js';
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
return
|
|
8
|
-
device.getStringDescriptor(idx, (err, value) => {
|
|
9
|
-
resolve(err ? undefined : value);
|
|
10
|
-
});
|
|
11
|
-
});
|
|
5
|
+
const REGISTRY = Object.values(DEVICES);
|
|
6
|
+
function tcpCandidates() {
|
|
7
|
+
return REGISTRY.filter(d => d.transports.tcp !== undefined);
|
|
12
8
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return results;
|
|
9
|
+
function serialCandidates() {
|
|
10
|
+
return REGISTRY.filter(d => d.transports.serial !== undefined || d.transports['bluetooth-spp'] !== undefined);
|
|
11
|
+
}
|
|
12
|
+
function keyList(candidates) {
|
|
13
|
+
return candidates
|
|
14
|
+
.map(d => d.key)
|
|
15
|
+
.sort()
|
|
16
|
+
.join(', ');
|
|
17
|
+
}
|
|
18
|
+
function snmpOptions(community) {
|
|
19
|
+
return community === undefined ? {} : { community };
|
|
20
|
+
}
|
|
21
|
+
function errorMessage(err) {
|
|
22
|
+
return err instanceof Error ? err.message : String(err);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `DeviceIdentificationRequiredError` with a message that says why the
|
|
26
|
+
* printer could not be identified, instead of the contract's generic
|
|
27
|
+
* one; `candidates` and `continueWith` keep the contract shape.
|
|
28
|
+
*/
|
|
29
|
+
function identificationRequired(candidates, reason, open) {
|
|
30
|
+
const err = new DeviceIdentificationRequiredError(candidates, async (deviceKey) => {
|
|
31
|
+
const printer = await open(deviceKey);
|
|
32
|
+
return { [printer.device.engines[0]?.role ?? 'primary']: printer };
|
|
33
|
+
});
|
|
34
|
+
err.message = `${reason}. Pass deviceKey, one of: ${keyList(candidates)}.`;
|
|
35
|
+
return err;
|
|
41
36
|
}
|
|
42
37
|
/**
|
|
43
38
|
* `PrinterDiscovery` implementation for Brother QL printers.
|
|
44
39
|
*
|
|
45
|
-
* `listPrinters()`
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
40
|
+
* `listPrinters()` is the USB enumeration plus one SNMP broadcast on the
|
|
41
|
+
* local subnets. Network printers open by `host`: the model comes from
|
|
42
|
+
* SNMP (`hrDeviceDescr`) because port 9100 carries no model or status
|
|
43
|
+
* signal, and `deviceKey` overrides that. A printer in Editor Lite
|
|
44
|
+
* (mass-storage) mode exposes a PID outside the registry, so it is
|
|
45
|
+
* simply absent from the USB list.
|
|
50
46
|
*/
|
|
51
47
|
export class BrotherQLDiscovery {
|
|
52
48
|
family = 'brother-ql';
|
|
49
|
+
network;
|
|
50
|
+
community;
|
|
51
|
+
constructor(options = {}) {
|
|
52
|
+
this.network = options.network ?? true;
|
|
53
|
+
this.community = options.community;
|
|
54
|
+
}
|
|
53
55
|
async listPrinters() {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const [usb, network] = await Promise.allSettled([
|
|
57
|
+
enumerateUsbDevices(REGISTRY),
|
|
58
|
+
this.network ? enumerateNetworkDevices(REGISTRY, snmpOptions(this.community)) : [],
|
|
59
|
+
]);
|
|
60
|
+
// Either half may be unavailable (no `usb` addon installed, no
|
|
61
|
+
// network); the other still lists. Both failing is a real error.
|
|
62
|
+
if (usb.status === 'rejected' && network.status === 'rejected') {
|
|
63
|
+
throw usb.reason instanceof Error ? usb.reason : new Error(String(usb.reason));
|
|
64
|
+
}
|
|
65
|
+
const printers = [];
|
|
66
|
+
if (usb.status === 'fulfilled') {
|
|
67
|
+
for (const { descriptor, serialNumber, connectionId } of usb.value) {
|
|
68
|
+
printers.push({
|
|
69
|
+
device: descriptor,
|
|
70
|
+
...(serialNumber === undefined ? {} : { serialNumber }),
|
|
71
|
+
transport: 'usb',
|
|
72
|
+
connectionId,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (network.status === 'fulfilled') {
|
|
77
|
+
for (const found of network.value)
|
|
78
|
+
printers.push(networkPrinter(found));
|
|
79
|
+
}
|
|
80
|
+
return printers;
|
|
81
|
+
}
|
|
82
|
+
listMedia() {
|
|
83
|
+
return Object.values(MEDIA);
|
|
61
84
|
}
|
|
62
85
|
async openPrinter(options = {}) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- alias kept for one release
|
|
87
|
+
const serialPath = options.serialPath ?? options.path;
|
|
88
|
+
if (serialPath !== undefined)
|
|
89
|
+
return this.openSerial(serialPath, options);
|
|
90
|
+
if (options.host !== undefined)
|
|
91
|
+
return this.openTcp(options.host, options);
|
|
92
|
+
return this.openUsb(options);
|
|
93
|
+
}
|
|
94
|
+
async openSerial(serialPath, options) {
|
|
95
|
+
// Serial carries no identifying metadata and the registry has more
|
|
96
|
+
// than one serial-capable entry, so the caller names the model.
|
|
97
|
+
const candidates = serialCandidates();
|
|
98
|
+
if (options.deviceKey === undefined) {
|
|
99
|
+
throw identificationRequired(candidates, `Serial port ${serialPath} carries no model signal`, deviceKey => this.openSerial(serialPath, { ...options, deviceKey }));
|
|
75
100
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
101
|
+
const descriptor = descriptorForKey(options.deviceKey, candidates, 'serial');
|
|
102
|
+
const transport = await SerialTransport.open(serialPath, options.baudRate);
|
|
103
|
+
return new BrotherQLPrinter(descriptor, transport, 'serial');
|
|
104
|
+
}
|
|
105
|
+
async openTcp(host, options) {
|
|
106
|
+
// Resolve the descriptor before connecting: a declined open must
|
|
107
|
+
// leave no session on a print server that serves one 9100 client.
|
|
108
|
+
const descriptor = options.deviceKey === undefined
|
|
109
|
+
? await this.identifyTcp(host, options)
|
|
110
|
+
: descriptorForKey(options.deviceKey, tcpCandidates(), 'tcp');
|
|
111
|
+
const transport = await TcpTransport.connect(host, options.port);
|
|
112
|
+
const community = options.snmpCommunity ?? this.community;
|
|
113
|
+
return new BrotherQLPrinter(descriptor, transport, 'tcp', {
|
|
114
|
+
host,
|
|
115
|
+
...(community === undefined ? {} : { community }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async identifyTcp(host, options) {
|
|
119
|
+
const snmp = snmpOptions(options.snmpCommunity ?? this.community);
|
|
120
|
+
let reason;
|
|
121
|
+
try {
|
|
122
|
+
const found = await identifyNetworkDevice(host, REGISTRY, snmp);
|
|
123
|
+
if (found)
|
|
124
|
+
return found.descriptor;
|
|
125
|
+
reason = `${host} reports ${await reportedModel(host, snmp)}, which is not in the brother-ql registry`;
|
|
83
126
|
}
|
|
84
|
-
|
|
127
|
+
catch (err) {
|
|
128
|
+
// "no SNMP answer" / "status is unavailable" are matched by
|
|
129
|
+
// thermal-label-cli to add `--media` to its hint; keep them.
|
|
130
|
+
reason = `No SNMP answer from ${host} (${errorMessage(err)}); the model cannot be identified and status is unavailable, so pass media too`;
|
|
131
|
+
}
|
|
132
|
+
throw identificationRequired(tcpCandidates(), reason, deviceKey => this.openTcp(host, { ...options, deviceKey }));
|
|
133
|
+
}
|
|
134
|
+
async openUsb(options) {
|
|
135
|
+
let usbError;
|
|
136
|
+
const found = await enumerateUsbDevices(REGISTRY).catch((err) => {
|
|
137
|
+
usbError = err;
|
|
138
|
+
return [];
|
|
139
|
+
});
|
|
85
140
|
const match = found.find(entry => {
|
|
86
141
|
const ids = getUsbIds(entry.descriptor);
|
|
87
142
|
if (options.vid !== undefined && ids?.vid !== options.vid)
|
|
@@ -92,8 +147,22 @@ export class BrotherQLDiscovery {
|
|
|
92
147
|
return false;
|
|
93
148
|
return true;
|
|
94
149
|
});
|
|
95
|
-
if (!match)
|
|
96
|
-
|
|
150
|
+
if (!match) {
|
|
151
|
+
// A serial number can also belong to a network printer.
|
|
152
|
+
if (options.serialNumber !== undefined && this.network) {
|
|
153
|
+
const remote = await this.findNetworkBySerial(options.serialNumber);
|
|
154
|
+
if (remote) {
|
|
155
|
+
return this.openTcp(remote.host, {
|
|
156
|
+
...options,
|
|
157
|
+
port: remote.port,
|
|
158
|
+
deviceKey: remote.descriptor.key,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (usbError instanceof Error)
|
|
163
|
+
throw usbError;
|
|
164
|
+
throw new DeviceNotFoundError();
|
|
165
|
+
}
|
|
97
166
|
const ids = getUsbIds(match.descriptor);
|
|
98
167
|
/* v8 ignore next -- USB-discovered devices always have USB transport */
|
|
99
168
|
if (!ids)
|
|
@@ -101,6 +170,43 @@ export class BrotherQLDiscovery {
|
|
|
101
170
|
const transport = await UsbTransport.open(ids.vid, ids.pid);
|
|
102
171
|
return new BrotherQLPrinter(match.descriptor, transport, 'usb');
|
|
103
172
|
}
|
|
173
|
+
async findNetworkBySerial(serialNumber) {
|
|
174
|
+
const found = await enumerateNetworkDevices(REGISTRY, snmpOptions(this.community)).catch(() => []);
|
|
175
|
+
return found.find(d => d.serialNumber === serialNumber);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function descriptorForKey(deviceKey, candidates, kind) {
|
|
179
|
+
const descriptor = DEVICES[deviceKey];
|
|
180
|
+
if (!descriptor) {
|
|
181
|
+
throw new Error(`Unknown deviceKey "${deviceKey}". ${kind === 'tcp' ? 'TCP' : 'Serial'}-capable Brother QL keys: ${keyList(candidates)}.`);
|
|
182
|
+
}
|
|
183
|
+
if (!candidates.includes(descriptor)) {
|
|
184
|
+
throw new Error(`Device ${descriptor.key} has no ${kind} transport — it cannot be opened over ${kind === 'tcp' ? '`host`' : '`serialPath`'}. ${kind === 'tcp' ? 'TCP' : 'Serial'}-capable keys: ${keyList(candidates)}.`);
|
|
185
|
+
}
|
|
186
|
+
return descriptor;
|
|
187
|
+
}
|
|
188
|
+
/** One extra unicast, only on the failure path, so the message can name the model. */
|
|
189
|
+
async function reportedModel(host, snmp) {
|
|
190
|
+
try {
|
|
191
|
+
const answers = await snmpGet(host, [PRINTER_MIB.hrDeviceDescr, PRINTER_MIB.sysDescr], snmp);
|
|
192
|
+
const value = answers[PRINTER_MIB.hrDeviceDescr] ?? answers[PRINTER_MIB.sysDescr];
|
|
193
|
+
if (value?.type === 'string' && value.value.length > 0)
|
|
194
|
+
return JSON.stringify(value.value);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* fall through */
|
|
198
|
+
}
|
|
199
|
+
return 'a model';
|
|
200
|
+
}
|
|
201
|
+
function networkPrinter(found) {
|
|
202
|
+
return {
|
|
203
|
+
device: found.descriptor,
|
|
204
|
+
...(found.serialNumber === undefined ? {} : { serialNumber: found.serialNumber }),
|
|
205
|
+
transport: 'tcp',
|
|
206
|
+
connectionId: found.connectionId,
|
|
207
|
+
host: found.host,
|
|
208
|
+
port: found.port,
|
|
209
|
+
};
|
|
104
210
|
}
|
|
105
211
|
/**
|
|
106
212
|
* Named export discovered by the unified `thermal-label-cli` — the CLI
|
package/dist/discovery.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { BrotherQLDiscovery, discovery } from './discovery.js';
|
|
2
|
-
export type { BrotherQLOpenOptions } from './discovery.js';
|
|
2
|
+
export type { BrotherQLDiscoveryOptions, BrotherQLOpenOptions } from './discovery.js';
|
|
3
3
|
export { BrotherQLPrinter } from './printer.js';
|
|
4
|
+
export type { BrotherQLNetworkOptions } from './printer.js';
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC/D,YAAY,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/printer.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { BrotherQLDevice, BrotherQLPrintOptions, BrotherQLStatus, MediaDescriptor, PreviewOptions, PreviewResult, PrinterAdapter, RawImageData, Transport, TransportType } from '@thermal-label/brother-ql-core';
|
|
2
|
+
/**
|
|
3
|
+
* Where the SNMP side channel of a `'tcp'` printer lives. Status and
|
|
4
|
+
* print confirmation go there; the 9100 socket only ever receives.
|
|
5
|
+
*/
|
|
6
|
+
export interface BrotherQLNetworkOptions {
|
|
7
|
+
host: string;
|
|
8
|
+
/** SNMP community. Default `'public'`. */
|
|
9
|
+
community?: string;
|
|
10
|
+
}
|
|
2
11
|
/**
|
|
3
12
|
* Node.js driver for Brother QL label printers.
|
|
4
13
|
*
|
|
@@ -21,6 +30,7 @@ export declare class BrotherQLPrinter implements PrinterAdapter {
|
|
|
21
30
|
readonly device: BrotherQLDevice;
|
|
22
31
|
readonly transportType: TransportType;
|
|
23
32
|
private readonly transport;
|
|
33
|
+
private readonly network;
|
|
24
34
|
private lastStatus;
|
|
25
35
|
/**
|
|
26
36
|
* Serialises every bulk-OUT operation (print + getStatus) so a
|
|
@@ -33,20 +43,33 @@ export declare class BrotherQLPrinter implements PrinterAdapter {
|
|
|
33
43
|
* all four drivers identical. See `@thermal-label/contracts`.
|
|
34
44
|
*/
|
|
35
45
|
private readonly serializer;
|
|
36
|
-
constructor(device: BrotherQLDevice, transport: Transport, transportType: TransportType);
|
|
46
|
+
constructor(device: BrotherQLDevice, transport: Transport, transportType: TransportType, network?: BrotherQLNetworkOptions);
|
|
37
47
|
get model(): string;
|
|
38
48
|
get connected(): boolean;
|
|
39
49
|
print(image: RawImageData, media?: MediaDescriptor, options?: BrotherQLPrintOptions): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Send the job and wait for the page counter to move. Reads the
|
|
52
|
+
* counter first: when SNMP is unreachable nothing is sent, and the
|
|
53
|
+
* caller gets told to pass `confirm: false` rather than a job that
|
|
54
|
+
* silently went nowhere.
|
|
55
|
+
*/
|
|
56
|
+
private writeConfirmed;
|
|
57
|
+
private readCounter;
|
|
58
|
+
private requireNetwork;
|
|
40
59
|
private writeChunked;
|
|
41
60
|
createPreview(image: RawImageData, options?: PreviewOptions): Promise<PreviewResult>;
|
|
42
61
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
62
|
+
* USB / serial: poll the status endpoint until 32 bytes are available.
|
|
45
63
|
* The USB `transferAsync()` call resolves immediately with 0 bytes if
|
|
46
|
-
* the printer hasn't queued a response yet
|
|
47
|
-
*
|
|
64
|
+
* the printer hasn't queued a response yet; a transport that blocks
|
|
65
|
+
* instead is bounded by the read timeout. `STATUS_POLL_ATTEMPTS`
|
|
66
|
+
* rounds of `STATUS_POLL_INTERVAL_MS` either way.
|
|
67
|
+
*
|
|
68
|
+
* TCP: port 9100 never answers, so the status comes from the
|
|
69
|
+
* printer's SNMP agent and the socket is not touched.
|
|
48
70
|
*/
|
|
49
71
|
getStatus(): Promise<BrotherQLStatus>;
|
|
72
|
+
private getNetworkStatus;
|
|
50
73
|
close(): Promise<void>;
|
|
51
74
|
}
|
|
52
75
|
//# sourceMappingURL=printer.d.ts.map
|
package/dist/printer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"printer.d.ts","sourceRoot":"","sources":["../src/printer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"printer.d.ts","sourceRoot":"","sources":["../src/printer.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,eAAe,EAEf,qBAAqB,EACrB,eAAe,EAEf,eAAe,EAEf,cAAc,EACd,aAAa,EACb,cAAc,EACd,YAAY,EACZ,SAAS,EACT,aAAa,EACd,MAAM,gCAAgC,CAAC;AA6BxC;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAmCD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,gBAAiB,YAAW,cAAc;IACrD,QAAQ,CAAC,MAAM,eAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IAEtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,UAAU,CAA8B;IAChD;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;gBAGlD,MAAM,EAAE,eAAe,EACvB,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,aAAa,EAC5B,OAAO,CAAC,EAAE,uBAAuB;IAQnC,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,SAAS,IAAI,OAAO,CAEvB;IAEK,KAAK,CACT,KAAK,EAAE,YAAY,EACnB,KAAK,CAAC,EAAE,eAAe,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC;IA+ChB;;;;;OAKG;YACW,cAAc;YAyCd,WAAW;IASzB,OAAO,CAAC,cAAc;YAOR,YAAY;IAU1B,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAWpF;;;;;;;;;OASG;IACH,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC;YA0BvB,gBAAgB;IA6BxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
package/dist/printer.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
import { DEFAULT_MEDIA, ROTATE_DIRECTION, STATUS_REQUEST, createPreviewOffline, encodeJobForEngine, flipHorizontal, parseStatus, pickRotation, renderImage, renderMultiPlaneImage, } from '@thermal-label/brother-ql-core';
|
|
2
|
-
import { MediaNotSpecifiedError, WriteSerializer } from '@thermal-label/contracts';
|
|
1
|
+
import { DEFAULT_MEDIA, ROTATE_DIRECTION, STATUS_REQUEST, createPreviewOffline, encodeJobForEngine, flipHorizontal, hasTwoColourSibling, parseStatus, pickRotation, renderImage, renderMultiPlaneImage, statusFromPrinterMib, } from '@thermal-label/brother-ql-core';
|
|
2
|
+
import { MediaNotSpecifiedError, TransportTimeoutError, WriteSerializer, } from '@thermal-label/contracts';
|
|
3
|
+
import { PRINTER_MIB, snmpGet } from '@thermal-label/transport/node';
|
|
3
4
|
const STATUS_BYTE_COUNT = 32;
|
|
4
5
|
const STATUS_POLL_INTERVAL_MS = 150;
|
|
5
6
|
const STATUS_POLL_ATTEMPTS = 10;
|
|
7
|
+
// TCP print confirmation (plan 17 D5): port 9100 never answers, so the
|
|
8
|
+
// only proof a job ran is `prtMarkerLifeCount` moving. A label takes
|
|
9
|
+
// ~2 s; keep waiting while the agent says "printing".
|
|
10
|
+
const CONFIRM_POLL_INTERVAL_MS = 500;
|
|
11
|
+
const CONFIRM_IDLE_BUDGET_MS = 10_000;
|
|
12
|
+
const CONFIRM_MAX_WAIT_MS = 60_000;
|
|
13
|
+
const HR_PRINTER_STATUS_PRINTING = 4;
|
|
14
|
+
const STATUS_OIDS = [
|
|
15
|
+
PRINTER_MIB.hrPrinterStatus,
|
|
16
|
+
PRINTER_MIB.hrPrinterDetectedErrorState,
|
|
17
|
+
PRINTER_MIB.prtInputMediaName,
|
|
18
|
+
PRINTER_MIB.prtInputDimUnit,
|
|
19
|
+
PRINTER_MIB.prtInputMediaDimFeedDir,
|
|
20
|
+
PRINTER_MIB.prtInputMediaDimXFeedDir,
|
|
21
|
+
];
|
|
22
|
+
function integerValue(value) {
|
|
23
|
+
return value?.type === 'integer' ? value.value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function stringValue(value) {
|
|
26
|
+
return value?.type === 'string' ? value.value : undefined;
|
|
27
|
+
}
|
|
28
|
+
function rawValue(value) {
|
|
29
|
+
return value?.type === 'string' || value?.type === 'octets' ? value.raw : undefined;
|
|
30
|
+
}
|
|
31
|
+
function errorMessage(err) {
|
|
32
|
+
return err instanceof Error ? err.message : String(err);
|
|
33
|
+
}
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise(r => setTimeout(r, ms));
|
|
36
|
+
}
|
|
6
37
|
// Empirical: a single libusb bulk transfer of an entire raster job (~50 kB
|
|
7
38
|
// uncompressed two-colour at 280 rows) reliably hangs the QL-820NWBc
|
|
8
39
|
// firmware mid-print. Chunking the OUT pipe to ~1 kB with a 20 ms gap
|
|
@@ -37,6 +68,7 @@ export class BrotherQLPrinter {
|
|
|
37
68
|
device;
|
|
38
69
|
transportType;
|
|
39
70
|
transport;
|
|
71
|
+
network;
|
|
40
72
|
lastStatus;
|
|
41
73
|
/**
|
|
42
74
|
* Serialises every bulk-OUT operation (print + getStatus) so a
|
|
@@ -49,10 +81,11 @@ export class BrotherQLPrinter {
|
|
|
49
81
|
* all four drivers identical. See `@thermal-label/contracts`.
|
|
50
82
|
*/
|
|
51
83
|
serializer = new WriteSerializer();
|
|
52
|
-
constructor(device, transport, transportType) {
|
|
84
|
+
constructor(device, transport, transportType, network) {
|
|
53
85
|
this.device = device;
|
|
54
86
|
this.transport = transport;
|
|
55
87
|
this.transportType = transportType;
|
|
88
|
+
this.network = network;
|
|
56
89
|
}
|
|
57
90
|
get model() {
|
|
58
91
|
return this.device.name;
|
|
@@ -95,14 +128,70 @@ export class BrotherQLPrinter {
|
|
|
95
128
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- every brother-ql device has at least one engine (data invariant)
|
|
96
129
|
const engine = this.device.engines[0];
|
|
97
130
|
const bytes = encodeJobForEngine([page], {}, engine, this.device.name);
|
|
131
|
+
// `confirm` (contracts PrintOptions): on TCP it means "wait for
|
|
132
|
+
// prtMarkerLifeCount to move"; `false` sends blind.
|
|
133
|
+
if (this.transportType === 'tcp' && options?.confirm !== false) {
|
|
134
|
+
await this.serializer.run(() => this.writeConfirmed(bytes, resolvedMedia));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
98
137
|
await this.serializer.run(() => this.writeChunked(bytes));
|
|
99
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Send the job and wait for the page counter to move. Reads the
|
|
141
|
+
* counter first: when SNMP is unreachable nothing is sent, and the
|
|
142
|
+
* caller gets told to pass `confirm: false` rather than a job that
|
|
143
|
+
* silently went nowhere.
|
|
144
|
+
*/
|
|
145
|
+
async writeConfirmed(bytes, media) {
|
|
146
|
+
const net = this.requireNetwork();
|
|
147
|
+
let before;
|
|
148
|
+
try {
|
|
149
|
+
before = await this.readCounter(net);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
throw new Error(`Cannot confirm prints on ${net.host}: SNMP prtMarkerLifeCount unreadable (${errorMessage(err)}). Pass confirm: false to send the job blind.`, { cause: err });
|
|
153
|
+
}
|
|
154
|
+
await this.writeChunked(bytes);
|
|
155
|
+
const started = Date.now();
|
|
156
|
+
for (;;) {
|
|
157
|
+
await sleep(CONFIRM_POLL_INTERVAL_MS);
|
|
158
|
+
let answers;
|
|
159
|
+
try {
|
|
160
|
+
answers = await snmpGet(net.host, [PRINTER_MIB.prtMarkerLifeCount, PRINTER_MIB.hrPrinterStatus], { community: net.community ?? 'public' });
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
throw new Error(`Job sent to ${net.host} but could not be confirmed: SNMP stopped answering (${errorMessage(err)}).`, { cause: err });
|
|
164
|
+
}
|
|
165
|
+
const count = integerValue(answers[PRINTER_MIB.prtMarkerLifeCount]);
|
|
166
|
+
if (count !== undefined && count !== before)
|
|
167
|
+
return;
|
|
168
|
+
const elapsed = Date.now() - started;
|
|
169
|
+
const printing = integerValue(answers[PRINTER_MIB.hrPrinterStatus]) === HR_PRINTER_STATUS_PRINTING;
|
|
170
|
+
if (elapsed >= CONFIRM_MAX_WAIT_MS || (elapsed >= CONFIRM_IDLE_BUDGET_MS && !printing)) {
|
|
171
|
+
throw new Error(notPrintedMessage(net.host, media, elapsed));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async readCounter(net) {
|
|
176
|
+
const answers = await snmpGet(net.host, [PRINTER_MIB.prtMarkerLifeCount], {
|
|
177
|
+
community: net.community ?? 'public',
|
|
178
|
+
});
|
|
179
|
+
const count = integerValue(answers[PRINTER_MIB.prtMarkerLifeCount]);
|
|
180
|
+
if (count === undefined)
|
|
181
|
+
throw new Error('agent has no prtMarkerLifeCount');
|
|
182
|
+
return count;
|
|
183
|
+
}
|
|
184
|
+
requireNetwork() {
|
|
185
|
+
if (this.network)
|
|
186
|
+
return this.network;
|
|
187
|
+
throw new Error('This TCP printer was constructed without its host; SNMP status and print confirmation need it.');
|
|
188
|
+
}
|
|
100
189
|
async writeChunked(bytes) {
|
|
101
190
|
for (let off = 0; off < bytes.length; off += USB_CHUNK_SIZE) {
|
|
102
191
|
const end = Math.min(off + USB_CHUNK_SIZE, bytes.length);
|
|
103
192
|
await this.transport.write(bytes.subarray(off, end));
|
|
104
193
|
if (end < bytes.length) {
|
|
105
|
-
await
|
|
194
|
+
await sleep(USB_CHUNK_DELAY_MS);
|
|
106
195
|
}
|
|
107
196
|
}
|
|
108
197
|
}
|
|
@@ -119,31 +208,75 @@ export class BrotherQLPrinter {
|
|
|
119
208
|
});
|
|
120
209
|
}
|
|
121
210
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
211
|
+
* USB / serial: poll the status endpoint until 32 bytes are available.
|
|
124
212
|
* The USB `transferAsync()` call resolves immediately with 0 bytes if
|
|
125
|
-
* the printer hasn't queued a response yet
|
|
126
|
-
*
|
|
213
|
+
* the printer hasn't queued a response yet; a transport that blocks
|
|
214
|
+
* instead is bounded by the read timeout. `STATUS_POLL_ATTEMPTS`
|
|
215
|
+
* rounds of `STATUS_POLL_INTERVAL_MS` either way.
|
|
216
|
+
*
|
|
217
|
+
* TCP: port 9100 never answers, so the status comes from the
|
|
218
|
+
* printer's SNMP agent and the socket is not touched.
|
|
127
219
|
*/
|
|
128
220
|
getStatus() {
|
|
221
|
+
if (this.transportType === 'tcp')
|
|
222
|
+
return this.getNetworkStatus();
|
|
129
223
|
// Serialised against `print()` so the status request + poll-read
|
|
130
224
|
// round-trip can't interleave into an in-flight raster stream.
|
|
131
225
|
return this.serializer.run(async () => {
|
|
132
226
|
await this.transport.write(STATUS_REQUEST);
|
|
133
227
|
for (let attempt = 0; attempt < STATUS_POLL_ATTEMPTS; attempt++) {
|
|
134
|
-
|
|
135
|
-
const bytes = await this.transport
|
|
228
|
+
const started = Date.now();
|
|
229
|
+
const bytes = await this.transport
|
|
230
|
+
.read(STATUS_BYTE_COUNT, STATUS_POLL_INTERVAL_MS)
|
|
231
|
+
.catch((err) => {
|
|
232
|
+
if (err instanceof TransportTimeoutError)
|
|
233
|
+
return new Uint8Array(0);
|
|
234
|
+
throw err;
|
|
235
|
+
});
|
|
136
236
|
if (bytes.length >= STATUS_BYTE_COUNT) {
|
|
137
237
|
const status = parseStatus(bytes, this.device.engines[0]);
|
|
138
238
|
this.lastStatus = status;
|
|
139
239
|
return status;
|
|
140
240
|
}
|
|
241
|
+
const remaining = STATUS_POLL_INTERVAL_MS - (Date.now() - started);
|
|
242
|
+
if (remaining > 0)
|
|
243
|
+
await sleep(remaining);
|
|
141
244
|
}
|
|
142
245
|
throw new Error('Printer did not respond to status request within 1.5s');
|
|
143
246
|
});
|
|
144
247
|
}
|
|
248
|
+
async getNetworkStatus() {
|
|
249
|
+
const net = this.requireNetwork();
|
|
250
|
+
let answers;
|
|
251
|
+
try {
|
|
252
|
+
answers = await snmpGet(net.host, STATUS_OIDS, { community: net.community ?? 'public' });
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
throw new Error(`Could not read status from ${net.host} over SNMP (${errorMessage(err)}); port 9100 carries no status. Pass media explicitly.`, { cause: err });
|
|
256
|
+
}
|
|
257
|
+
const feedDir = integerValue(answers[PRINTER_MIB.prtInputMediaDimFeedDir]);
|
|
258
|
+
const xFeedDir = integerValue(answers[PRINTER_MIB.prtInputMediaDimXFeedDir]);
|
|
259
|
+
const dimUnit = integerValue(answers[PRINTER_MIB.prtInputDimUnit]);
|
|
260
|
+
const status = statusFromPrinterMib({
|
|
261
|
+
printerStatus: integerValue(answers[PRINTER_MIB.hrPrinterStatus]) ?? 2,
|
|
262
|
+
errorState: rawValue(answers[PRINTER_MIB.hrPrinterDetectedErrorState]) ?? new Uint8Array(0),
|
|
263
|
+
mediaName: stringValue(answers[PRINTER_MIB.prtInputMediaName]) ?? '',
|
|
264
|
+
...(feedDir === undefined ? {} : { feedDir }),
|
|
265
|
+
...(xFeedDir === undefined ? {} : { xFeedDir }),
|
|
266
|
+
...(dimUnit === undefined ? {} : { dimUnit }),
|
|
267
|
+
}, this.device.engines[0]);
|
|
268
|
+
this.lastStatus = status;
|
|
269
|
+
return status;
|
|
270
|
+
}
|
|
145
271
|
async close() {
|
|
146
272
|
await this.transport.close();
|
|
147
273
|
}
|
|
148
274
|
}
|
|
275
|
+
function notPrintedMessage(host, media, elapsedMs) {
|
|
276
|
+
const seconds = (elapsedMs / 1000).toFixed(0);
|
|
277
|
+
const hint = hasTwoColourSibling(media)
|
|
278
|
+
? ` ${String(media.widthMm)} mm rolls come in a two-colour variant that is invisible over the network and rejects single-colour jobs; on such a roll pass its media (DK-22251 = id 251).`
|
|
279
|
+
: '';
|
|
280
|
+
return `Job sent to ${host} but the page counter did not move in ${seconds} s: the printer did not print it.${hint}`;
|
|
281
|
+
}
|
|
149
282
|
//# sourceMappingURL=printer.js.map
|