@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/discovery.js CHANGED
@@ -1,87 +1,142 @@
1
- import { DEVICES, findDevice, getUsbIds, isMassStorageMode } from '@thermal-label/brother-ql-core';
2
- import { SerialTransport, TcpTransport, UsbTransport } from '@thermal-label/transport/node';
3
- import * as usb from 'usb';
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 BROTHER_VID = 0x04f9;
6
- async function readSerialNumber(device, idx) {
7
- return new Promise(resolve => {
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
- async function enumerateUsbDevices() {
14
- const results = [];
15
- for (const device of usb.getDeviceList()) {
16
- const desc = device.deviceDescriptor;
17
- if (desc.idVendor !== BROTHER_VID)
18
- continue;
19
- if (isMassStorageMode(desc.idProduct)) {
20
- // eslint-disable-next-line no-console
21
- console.warn(`[brother-ql] Detected printer in Editor Lite (mass storage) mode (PID 0x${desc.idProduct.toString(16).toUpperCase()}). ` +
22
- 'Hold the Editor Lite button until the LED turns off to switch to printer mode.');
23
- continue;
24
- }
25
- const descriptor = findDevice(desc.idVendor, desc.idProduct);
26
- if (!descriptor)
27
- continue;
28
- let serialNumber;
29
- if (desc.iSerialNumber) {
30
- device.open();
31
- try {
32
- serialNumber = await readSerialNumber(device, desc.iSerialNumber);
33
- }
34
- finally {
35
- device.close();
36
- }
37
- }
38
- results.push({ device, descriptor, serialNumber });
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()` enumerates USB and skips printers in Editor Lite
46
- * mass-storage mode (a warning is logged the user has to switch
47
- * them out of Editor Lite manually). Network printers open via
48
- * `openPrinter({ host, port })`; there is no mDNS implementation so
49
- * `listPrinters()` never surfaces them.
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 found = await enumerateUsbDevices();
55
- return found.map(({ device, descriptor, serialNumber }) => ({
56
- device: descriptor,
57
- ...(serialNumber === undefined ? {} : { serialNumber }),
58
- transport: 'usb',
59
- connectionId: `${String(device.busNumber)}.${String(device.deviceAddress)}`,
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
- if (options.path !== undefined) {
64
- const transport = await SerialTransport.open(options.path, options.baudRate);
65
- // Serial (typically RFCOMM over OS-paired Bluetooth) carries no
66
- // identifying metadata — attach any descriptor that declares the
67
- // `bluetooth-spp` transport. `getStatus()` returns accurate
68
- // detectedMedia regardless of which descriptor we attach, but
69
- // the descriptor's `name` is what surfaces in logs.
70
- const descriptor = Object.values(DEVICES).find(d => d.transports['bluetooth-spp'] !== undefined);
71
- /* v8 ignore next -- the registry carries QL_820NWBc with bluetooth-spp */
72
- if (!descriptor)
73
- throw new Error('No bluetooth-spp-capable Brother QL descriptor found.');
74
- return new BrotherQLPrinter(descriptor, transport, 'serial');
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
- if (options.host !== undefined) {
77
- const transport = await TcpTransport.connect(options.host, options.port);
78
- const descriptor = Object.values(DEVICES).find(d => d.transports.tcp !== undefined);
79
- /* v8 ignore next -- the registry always has TCP-capable entries */
80
- if (!descriptor)
81
- throw new Error('No network-capable Brother QL descriptor found.');
82
- return new BrotherQLPrinter(descriptor, transport, 'tcp');
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
- const found = await enumerateUsbDevices();
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
- throw new Error('No compatible Brother QL printer found.');
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
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"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"}
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
@@ -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;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,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
- * Poll the status endpoint until 32 bytes are available.
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, so retry with a short
47
- * delay up to `STATUS_POLL_ATTEMPTS` times.
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
@@ -1 +1 @@
1
- {"version":3,"file":"printer.d.ts","sourceRoot":"","sources":["../src/printer.ts"],"names":[],"mappings":"AAYA,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;AAoBxC;;;;;;;;;;;;;;;;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,UAAU,CAA8B;IAChD;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;gBAExC,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa;IAMvF,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;YAyCF,YAAY;IAU1B,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAWpF;;;;;;OAMG;IACH,SAAS,IAAI,OAAO,CAAC,eAAe,CAAC;IAkB/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
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 new Promise(r => setTimeout(r, USB_CHUNK_DELAY_MS));
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
- * Poll the status endpoint until 32 bytes are available.
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, so retry with a short
126
- * delay up to `STATUS_POLL_ATTEMPTS` times.
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
- await new Promise(r => setTimeout(r, STATUS_POLL_INTERVAL_MS));
135
- const bytes = await this.transport.read(STATUS_BYTE_COUNT);
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