@thermal-label/brother-ql-core 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/data/devices.json +4 -0
- package/dist/__tests__/devices.test.js +12 -20
- package/dist/__tests__/devices.test.js.map +1 -1
- package/dist/__tests__/network-status.test.d.ts +2 -0
- package/dist/__tests__/network-status.test.d.ts.map +1 -0
- package/dist/__tests__/network-status.test.js +132 -0
- package/dist/__tests__/network-status.test.js.map +1 -0
- package/dist/__tests__/protocol.test.js +103 -0
- package/dist/__tests__/protocol.test.js.map +1 -1
- package/dist/__tests__/status.test.js +13 -1
- package/dist/__tests__/status.test.js.map +1 -1
- package/dist/devices.d.ts +1 -1
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.generated.d.ts +1 -0
- package/dist/devices.generated.d.ts.map +1 -1
- package/dist/devices.generated.js +4 -0
- package/dist/devices.generated.js.map +1 -1
- package/dist/devices.js +0 -12
- package/dist/devices.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/network-status.d.ts +45 -0
- package/dist/network-status.d.ts.map +1 -0
- package/dist/network-status.js +140 -0
- package/dist/network-status.js.map +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +34 -6
- package/dist/protocol.js.map +1 -1
- package/dist/status.d.ts +5 -3
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +21 -6
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/devices.test.ts +13 -23
- package/src/__tests__/network-status.test.ts +165 -0
- package/src/__tests__/protocol.test.ts +129 -2
- package/src/__tests__/status.test.ts +15 -1
- package/src/devices.generated.ts +4 -0
- package/src/devices.ts +0 -16
- package/src/index.ts +3 -1
- package/src/network-status.ts +173 -0
- package/src/protocol.ts +41 -7
- package/src/status.ts +26 -6
- package/src/types.ts +5 -1
package/dist/types.d.ts
CHANGED
|
@@ -98,7 +98,11 @@ export interface BrotherQLMedia extends MediaDescriptor {
|
|
|
98
98
|
printableDots?: number;
|
|
99
99
|
leftMarginPins?: number;
|
|
100
100
|
rightMarginPins?: number;
|
|
101
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Die-cut page length in dots at 300 dpi: the reference's "print area
|
|
103
|
+
* length" (§2.3.2(b) column 4). The encoder sends exactly this many
|
|
104
|
+
* rows for die-cut media; see DECISIONS D20.
|
|
105
|
+
*/
|
|
102
106
|
dieCutMaskedAreaDots?: number;
|
|
103
107
|
}
|
|
104
108
|
/**
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,uBAAuB,EACvB,YAAY,EACZ,aAAa,EACd,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AACjD,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC;AACnC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACxE,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,UAAU,EAAE,UAAU,CAAC;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,IAAI,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;IAC1D,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,uBAAuB,EACvB,YAAY,EACZ,aAAa,EACd,MAAM,0BAA0B,CAAC;AAElC,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AACjD,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,IAAI,CAAC;AACnC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE/C;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,KAAK,GAAG,UAAU,GAAG,UAAU,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACxE,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,UAAU,EAAE,UAAU,CAAC;IACvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,IAAI,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;IAC1D,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,aAAa,CAAC;AAE5C,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,EAAE,cAAc,CAAC;IACtB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,YAAY;IACzD,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC;IACrC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thermal-label/brother-ql-core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Protocol encoding, device registry, and media registry for Brother QL label printers",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brother",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@mbtech-nl/bitmap": "^1.3.0",
|
|
59
|
-
"@thermal-label/contracts": "^0.6.
|
|
59
|
+
"@thermal-label/contracts": "^0.6.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@mbtech-nl/tsconfig": "^1.1.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { DEVICES, findDevice
|
|
2
|
+
import { DEVICES, findDevice } from '../devices.js';
|
|
3
3
|
|
|
4
4
|
describe('findDevice', () => {
|
|
5
5
|
it('returns correct entry for QL-820NWBc (PID shared with QL-820NWB)', () => {
|
|
@@ -33,27 +33,6 @@ describe('findDevice', () => {
|
|
|
33
33
|
});
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
describe('isMassStorageMode', () => {
|
|
37
|
-
it('returns true for 0x20a9 (QL-1100 mass storage)', () => {
|
|
38
|
-
expect(isMassStorageMode(0x20a9)).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns true for 0x20aa (QL-1110NWB mass storage)', () => {
|
|
42
|
-
expect(isMassStorageMode(0x20aa)).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('returns true for 0x20ac (QL-1115NWB mass storage)', () => {
|
|
46
|
-
expect(isMassStorageMode(0x20ac)).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('returns false for all printer-class PIDs', () => {
|
|
50
|
-
for (const dev of Object.values(DEVICES)) {
|
|
51
|
-
const ids = getUsbIds(dev);
|
|
52
|
-
if (ids) expect(isMassStorageMode(ids.pid)).toBe(false);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
36
|
describe('Device registry invariants', () => {
|
|
58
37
|
it('every two-color device has a 720-dot engine', () => {
|
|
59
38
|
for (const dev of Object.values(DEVICES)) {
|
|
@@ -77,6 +56,18 @@ describe('Device registry invariants', () => {
|
|
|
77
56
|
}
|
|
78
57
|
});
|
|
79
58
|
|
|
59
|
+
it('modelNames, where set, includes the marketing name and only distinct wire names', () => {
|
|
60
|
+
for (const dev of Object.values(DEVICES)) {
|
|
61
|
+
if (dev.modelNames === undefined) continue;
|
|
62
|
+
expect(dev.modelNames).toContain(dev.name);
|
|
63
|
+
expect(new Set(dev.modelNames).size).toBe(dev.modelNames.length);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('QL-820NWBc reports itself as QL-820NWB on the wire', () => {
|
|
68
|
+
expect(DEVICES.QL_820NWBc.modelNames).toEqual(['QL-820NWB', 'QL-820NWBc']);
|
|
69
|
+
});
|
|
70
|
+
|
|
80
71
|
it('every device declares a USB transport with hex-string vid+pid', () => {
|
|
81
72
|
for (const dev of Object.values(DEVICES)) {
|
|
82
73
|
expect(dev.transports.usb).toBeDefined();
|
|
@@ -164,7 +155,6 @@ describe('PT-* device entries', () => {
|
|
|
164
155
|
const dev = DEVICES.PT_P750W;
|
|
165
156
|
expect(dev.transports.usb?.pid).toBe('0x2062');
|
|
166
157
|
expect(dev.capabilities?.massStoragePid).toBe('0x2065');
|
|
167
|
-
expect(isMassStorageMode(0x2065)).toBe(true);
|
|
168
158
|
});
|
|
169
159
|
|
|
170
160
|
it('PT-P910BT declares bluetooth-spp transport', () => {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseMediaName, statusFromPrinterMib, type PrinterMibStatus } from '../network-status.js';
|
|
3
|
+
|
|
4
|
+
/** Bench values, QL-820NWBc 2026-09-15 (plan 17 Context). */
|
|
5
|
+
const DK_11201_NAME = '29mm x 90mm / 1.1" x 3.5"';
|
|
6
|
+
const DK_22205_NAME = '62mm / 2.4"';
|
|
7
|
+
|
|
8
|
+
function mib(overrides: Partial<PrinterMibStatus> = {}): PrinterMibStatus {
|
|
9
|
+
return {
|
|
10
|
+
printerStatus: 3,
|
|
11
|
+
errorState: new Uint8Array([0x00]),
|
|
12
|
+
mediaName: DK_22205_NAME,
|
|
13
|
+
feedDir: -1,
|
|
14
|
+
xFeedDir: -1,
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Octets with the given RFC 2790 bit numbers set (bit 0 = MSB of octet 0). */
|
|
20
|
+
function errorBits(...bits: number[]): Uint8Array {
|
|
21
|
+
const octets = new Uint8Array(2);
|
|
22
|
+
for (const bit of bits) octets[bit >> 3] = (octets[bit >> 3] ?? 0) | (0x80 >> (bit & 7));
|
|
23
|
+
return octets;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('parseMediaName', () => {
|
|
27
|
+
it.each([
|
|
28
|
+
[DK_11201_NAME, { widthMm: 29, heightMm: 90 }],
|
|
29
|
+
[DK_22205_NAME, { widthMm: 62 }],
|
|
30
|
+
['17mm x 54mm / 0.66" x 2.1"', { widthMm: 17, heightMm: 54 }],
|
|
31
|
+
['62mm', { widthMm: 62 }],
|
|
32
|
+
['24mm Dia / 0.9" Dia', { widthMm: 24, heightMm: 24 }],
|
|
33
|
+
['29 mm × 90 mm', { widthMm: 29, heightMm: 90 }],
|
|
34
|
+
])('%s', (name, expected) => {
|
|
35
|
+
expect(parseMediaName(name)).toEqual(expected);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it.each(['', 'garbage', '2.4"', 'mm', '62 / 2.4"', 'Roll 62'])('rejects %j', name => {
|
|
39
|
+
expect(parseMediaName(name)).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('statusFromPrinterMib', () => {
|
|
44
|
+
it('idle, no errors, 62 mm continuous → ready with DK-22205 and the two-colour warning', () => {
|
|
45
|
+
const status = statusFromPrinterMib(mib());
|
|
46
|
+
expect(status.ready).toBe(true);
|
|
47
|
+
expect(status.errors).toEqual([]);
|
|
48
|
+
expect(status.mediaLoaded).toBe(true);
|
|
49
|
+
expect(status.detectedMedia?.id).toBe(259);
|
|
50
|
+
expect(status.rawBytes).toHaveLength(0);
|
|
51
|
+
expect(status.details).toContainEqual({
|
|
52
|
+
label: 'Two-colour',
|
|
53
|
+
value: 'not detectable over network',
|
|
54
|
+
severity: 'warn',
|
|
55
|
+
});
|
|
56
|
+
expect(status.details?.find(d => d.label === 'Printer state')?.value).toBe('idle');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('29×90 die-cut → DK-11201, no two-colour row', () => {
|
|
60
|
+
const status = statusFromPrinterMib(mib({ mediaName: DK_11201_NAME }));
|
|
61
|
+
expect(status.detectedMedia?.id).toBe(271);
|
|
62
|
+
expect(status.details?.some(d => d.label === 'Two-colour')).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('printing counts as ready; warmup, other and unknown do not', () => {
|
|
66
|
+
expect(statusFromPrinterMib(mib({ printerStatus: 4 })).ready).toBe(true);
|
|
67
|
+
expect(statusFromPrinterMib(mib({ printerStatus: 5 })).ready).toBe(false);
|
|
68
|
+
expect(statusFromPrinterMib(mib({ printerStatus: 1 })).ready).toBe(false);
|
|
69
|
+
expect(statusFromPrinterMib(mib({ printerStatus: 99 })).ready).toBe(false);
|
|
70
|
+
expect(
|
|
71
|
+
statusFromPrinterMib(mib({ printerStatus: 99 })).details?.find(
|
|
72
|
+
d => d.label === 'Printer state',
|
|
73
|
+
)?.value,
|
|
74
|
+
).toBe('unknown');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
[1, 'no_media', 'No media'],
|
|
79
|
+
[4, 'cover_open', 'Cover open'],
|
|
80
|
+
[5, 'cutter_jam', 'Jammed'],
|
|
81
|
+
[6, 'not_ready', 'Offline'],
|
|
82
|
+
[7, 'system_error', 'Service requested'],
|
|
83
|
+
[13, 'no_media', 'Input tray empty'],
|
|
84
|
+
[8, 'system_error', 'inputTrayMissing'],
|
|
85
|
+
[14, 'system_error', 'overduePreventMaint'],
|
|
86
|
+
])('error-state bit %i → %s', (bit, code, message) => {
|
|
87
|
+
const status = statusFromPrinterMib(mib({ errorState: errorBits(bit) }));
|
|
88
|
+
expect(status.errors).toEqual([{ code, message }]);
|
|
89
|
+
expect(status.ready).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('lowPaper is a warning row, not an error', () => {
|
|
93
|
+
const status = statusFromPrinterMib(mib({ errorState: errorBits(0) }));
|
|
94
|
+
expect(status.errors).toEqual([]);
|
|
95
|
+
expect(status.ready).toBe(true);
|
|
96
|
+
expect(status.details).toContainEqual({ label: 'Media', value: 'Media low', severity: 'warn' });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('reads error state from 0, 1 and 2 octets', () => {
|
|
100
|
+
expect(statusFromPrinterMib(mib({ errorState: new Uint8Array(0) })).errors).toEqual([]);
|
|
101
|
+
expect(statusFromPrinterMib(mib({ errorState: new Uint8Array([0x08]) })).errors).toEqual([
|
|
102
|
+
{ code: 'cover_open', message: 'Cover open' },
|
|
103
|
+
]);
|
|
104
|
+
expect(statusFromPrinterMib(mib({ errorState: new Uint8Array([0x00, 0x04]) })).errors).toEqual([
|
|
105
|
+
{ code: 'no_media', message: 'Input tray empty' },
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('the name wins over Dim OIDs (bench: unit 4 claimed, 1/100 mm sent)', () => {
|
|
110
|
+
const status = statusFromPrinterMib(
|
|
111
|
+
mib({ mediaName: DK_11201_NAME, xFeedDir: 2900, feedDir: 9000, dimUnit: 4 }),
|
|
112
|
+
);
|
|
113
|
+
expect(status.detectedMedia?.id).toBe(271);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('falls back to Dim OIDs in ten-thousandths of an inch (unit 3)', () => {
|
|
117
|
+
// 29 mm = 11417, 90 mm = 35433 ten-thousandths of an inch.
|
|
118
|
+
const status = statusFromPrinterMib(
|
|
119
|
+
mib({ mediaName: 'nonsense', xFeedDir: 11417, feedDir: 35433, dimUnit: 3 }),
|
|
120
|
+
);
|
|
121
|
+
expect(status.detectedMedia?.id).toBe(271);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('falls back to Dim OIDs in micrometres (unit 4)', () => {
|
|
125
|
+
const status = statusFromPrinterMib(
|
|
126
|
+
mib({ mediaName: 'nonsense', xFeedDir: 62000, feedDir: -1, dimUnit: 4 }),
|
|
127
|
+
);
|
|
128
|
+
expect(status.detectedMedia?.id).toBe(259);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('no media when the name is unparseable and the Dim OIDs are negative or unitless', () => {
|
|
132
|
+
expect(
|
|
133
|
+
statusFromPrinterMib(mib({ mediaName: 'nonsense', xFeedDir: -1, feedDir: -1, dimUnit: 3 }))
|
|
134
|
+
.detectedMedia,
|
|
135
|
+
).toBeUndefined();
|
|
136
|
+
expect(
|
|
137
|
+
statusFromPrinterMib(mib({ mediaName: 'nonsense', xFeedDir: 11417, feedDir: 35433 }))
|
|
138
|
+
.detectedMedia,
|
|
139
|
+
).toBeUndefined();
|
|
140
|
+
// RFC 3805 only defines 3 and 4; anything else is not converted.
|
|
141
|
+
expect(
|
|
142
|
+
statusFromPrinterMib(
|
|
143
|
+
mib({ mediaName: 'nonsense', xFeedDir: 11417, feedDir: 35433, dimUnit: 1 }),
|
|
144
|
+
).detectedMedia,
|
|
145
|
+
).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('unrecognised media name → no media, warn row with the raw string', () => {
|
|
149
|
+
const status = statusFromPrinterMib(mib({ mediaName: 'Roll 62' }));
|
|
150
|
+
expect(status.mediaLoaded).toBe(false);
|
|
151
|
+
expect(status.detectedMedia).toBeUndefined();
|
|
152
|
+
expect(status.details).toContainEqual({
|
|
153
|
+
label: 'Media',
|
|
154
|
+
value: 'not recognised: "Roll 62"',
|
|
155
|
+
severity: 'warn',
|
|
156
|
+
});
|
|
157
|
+
expect(status.ready).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('a recognised size with no registry entry → warn row, no media', () => {
|
|
161
|
+
const status = statusFromPrinterMib(mib({ mediaName: '99mm x 99mm / x' }));
|
|
162
|
+
expect(status.detectedMedia).toBeUndefined();
|
|
163
|
+
expect(status.details?.some(d => d.value.startsWith('not recognised'))).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createBitmap } from '@mbtech-nl/bitmap';
|
|
2
|
+
import { createBitmap, type LabelBitmap } from '@mbtech-nl/bitmap';
|
|
3
3
|
import {
|
|
4
4
|
buildInvalidate,
|
|
5
5
|
buildInitialize,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
encodeJobForEngine,
|
|
17
17
|
type EncoderEngine,
|
|
18
18
|
} from '../protocol.js';
|
|
19
|
-
import type { PageData } from '../types.js';
|
|
19
|
+
import type { BrotherQLMedia, PageData } from '../types.js';
|
|
20
20
|
import { MEDIA } from '../media.js';
|
|
21
21
|
|
|
22
22
|
describe('buildInvalidate', () => {
|
|
@@ -494,3 +494,130 @@ describe('encodeJob legacy entry point — back compat', () => {
|
|
|
494
494
|
expect(out[204]).toBe(0x1b);
|
|
495
495
|
});
|
|
496
496
|
});
|
|
497
|
+
|
|
498
|
+
describe('die-cut page length (reference §2.3.2(b), §2.3.3, §2.3.4)', () => {
|
|
499
|
+
const DK_29X90 = MEDIA[271]!; // print area length 991 dots
|
|
500
|
+
const DK_62MM = MEDIA[259]!;
|
|
501
|
+
const QL_ENGINE: EncoderEngine = {
|
|
502
|
+
protocol: 'ql-raster',
|
|
503
|
+
headDots: 720,
|
|
504
|
+
capabilities: { autocut: true, mediaDetection: true },
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/** Bitmap with every pixel of one row set, so the row is recognisable on the wire. */
|
|
508
|
+
function bitmapWithBlackRow(widthPx: number, heightPx: number, row: number): LabelBitmap {
|
|
509
|
+
const bitmap = createBitmap(widthPx, heightPx);
|
|
510
|
+
const bytesPerRow = Math.ceil(widthPx / 8);
|
|
511
|
+
bitmap.data.fill(0xff, row * bytesPerRow, (row + 1) * bytesPerRow);
|
|
512
|
+
return bitmap;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function findCmd(buf: Uint8Array, third: number): number {
|
|
516
|
+
for (let i = 0; i < buf.length - 4; i++) {
|
|
517
|
+
if (buf[i] === 0x1b && buf[i + 1] === 0x69 && buf[i + 2] === third) return i;
|
|
518
|
+
}
|
|
519
|
+
return -1;
|
|
520
|
+
}
|
|
521
|
+
const rasterNumber = (buf: Uint8Array): number => {
|
|
522
|
+
const i = findCmd(buf, 0x7a);
|
|
523
|
+
return (buf[i + 7] ?? 0) | ((buf[i + 8] ?? 0) << 8);
|
|
524
|
+
};
|
|
525
|
+
const marginDots = (buf: Uint8Array): number => {
|
|
526
|
+
const i = findCmd(buf, 0x64);
|
|
527
|
+
return (buf[i + 3] ?? 0) | ((buf[i + 4] ?? 0) << 8);
|
|
528
|
+
};
|
|
529
|
+
/** Raster rows as [opcode, colour, payload] triples, in wire order. */
|
|
530
|
+
function rasterRows(buf: Uint8Array): { op: number; color: number; payload: Uint8Array }[] {
|
|
531
|
+
const rows = [];
|
|
532
|
+
// Rows start after ESC i d (5 bytes) and run until the print command.
|
|
533
|
+
let i = findCmd(buf, 0x64) + 5;
|
|
534
|
+
while (i < buf.length && (buf[i] === 0x67 || buf[i] === 0x77)) {
|
|
535
|
+
const len = buf[i + 2] ?? 0;
|
|
536
|
+
rows.push({ op: buf[i]!, color: buf[i + 1]!, payload: buf.subarray(i + 3, i + 3 + len) });
|
|
537
|
+
i += 3 + len;
|
|
538
|
+
}
|
|
539
|
+
return rows;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
it('sends the print-area length as the raster number and margin 0 on DK-11201', () => {
|
|
543
|
+
const bitmap = createBitmap(306, 608);
|
|
544
|
+
const out = encodeJobForEngine([{ bitmap, media: DK_29X90 }], {}, QL_ENGINE);
|
|
545
|
+
expect(rasterNumber(out)).toBe(991);
|
|
546
|
+
expect(marginDots(out)).toBe(0);
|
|
547
|
+
expect(rasterRows(out)).toHaveLength(991);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('ignores marginDots on die-cut media', () => {
|
|
551
|
+
const bitmap = createBitmap(306, 10);
|
|
552
|
+
const out = encodeJobForEngine(
|
|
553
|
+
[{ bitmap, media: DK_29X90, options: { marginDots: 100 } }],
|
|
554
|
+
{},
|
|
555
|
+
QL_ENGINE,
|
|
556
|
+
);
|
|
557
|
+
expect(marginDots(out)).toBe(0);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('centres the bitmap in the page', () => {
|
|
561
|
+
const bitmap = bitmapWithBlackRow(306, 3, 1);
|
|
562
|
+
const out = encodeJobForEngine([{ bitmap, media: DK_29X90 }], {}, QL_ENGINE);
|
|
563
|
+
const rows = rasterRows(out);
|
|
564
|
+
const black = rows.map((r, i) => (r.payload.some(b => b !== 0) ? i : -1)).filter(i => i >= 0);
|
|
565
|
+
// padTop = floor((991 - 3) / 2) = 494; the black row is bitmap row 1.
|
|
566
|
+
expect(black).toEqual([495]);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('doubles the page length when QL high-res rows are supplied', () => {
|
|
570
|
+
const bitmap = createBitmap(306, 100);
|
|
571
|
+
const out = encodeJob([{ bitmap, media: DK_29X90, options: { highResolution: true } }]);
|
|
572
|
+
expect(rasterNumber(out)).toBe(1982);
|
|
573
|
+
expect(rasterRows(out)).toHaveLength(1982);
|
|
574
|
+
expect(marginDots(out)).toBe(0);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('rejects a bitmap taller than the page, naming both numbers', () => {
|
|
578
|
+
const bitmap = createBitmap(306, 1200);
|
|
579
|
+
expect(() => encodeJobForEngine([{ bitmap, media: DK_29X90 }], {}, QL_ENGINE)).toThrow(
|
|
580
|
+
/1200 rows.*exactly 991/,
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('pads both planes alike on two-colour die-cut', () => {
|
|
585
|
+
const bitmap = bitmapWithBlackRow(306, 3, 1);
|
|
586
|
+
const redBitmap = bitmapWithBlackRow(306, 3, 1);
|
|
587
|
+
const out = encodeJobForEngine([{ bitmap, redBitmap, media: DK_29X90 }], {}, QL_ENGINE);
|
|
588
|
+
const rows = rasterRows(out);
|
|
589
|
+
expect(rows).toHaveLength(991 * 2);
|
|
590
|
+
const inked = rows
|
|
591
|
+
.map((r, i) => (r.payload.some(b => b !== 0) ? { i, color: r.color } : undefined))
|
|
592
|
+
.filter(r => r !== undefined);
|
|
593
|
+
expect(inked).toEqual([
|
|
594
|
+
{ i: 495 * 2, color: 0x01 },
|
|
595
|
+
{ i: 495 * 2 + 1, color: 0x02 },
|
|
596
|
+
]);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('throws when a die-cut entry lacks dieCutMaskedAreaDots', () => {
|
|
600
|
+
const media: BrotherQLMedia = { ...DK_29X90 };
|
|
601
|
+
delete media.dieCutMaskedAreaDots;
|
|
602
|
+
const bitmap = createBitmap(306, 10);
|
|
603
|
+
expect(() => encodeJobForEngine([{ bitmap, media }], {}, QL_ENGINE)).toThrow(
|
|
604
|
+
/dieCutMaskedAreaDots/,
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('leaves the continuous path byte-identical', async () => {
|
|
609
|
+
const { createHash } = await import('node:crypto');
|
|
610
|
+
const bitmap = bitmapWithBlackRow(696, 40, 7);
|
|
611
|
+
const out = encodeJobForEngine(
|
|
612
|
+
[{ bitmap, media: DK_62MM, options: { marginDots: 50 } }],
|
|
613
|
+
{ copies: 2 },
|
|
614
|
+
QL_ENGINE,
|
|
615
|
+
);
|
|
616
|
+
expect(rasterNumber(out)).toBe(40);
|
|
617
|
+
expect(marginDots(out)).toBe(50);
|
|
618
|
+
// Digest recorded from the encoder before die-cut padding existed.
|
|
619
|
+
expect(createHash('sha256').update(out).digest('hex')).toBe(
|
|
620
|
+
'ebc2b5f467faca133f0f72c6f3123671d4e4dc130f2dd8cad8d9a69fa50e95fc',
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
@@ -18,7 +18,8 @@ function makeStatusBytes(
|
|
|
18
18
|
bytes[0] = 0x80;
|
|
19
19
|
bytes[1] = 0x20;
|
|
20
20
|
bytes[2] = 0x42;
|
|
21
|
-
bytes[3] =
|
|
21
|
+
bytes[3] = 0x34;
|
|
22
|
+
bytes[4] = 0x41;
|
|
22
23
|
bytes[8] = overrides?.errInfo1 ?? 0;
|
|
23
24
|
bytes[9] = overrides?.errInfo2 ?? 0;
|
|
24
25
|
bytes[10] = overrides?.mediaWidthMm ?? 62;
|
|
@@ -150,6 +151,18 @@ describe('parseStatus', () => {
|
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
describe('parseStatus — details[]', () => {
|
|
154
|
+
it('model-code row renders bytes 3/4 as ASCII (4A = QL-820NWB capture)', () => {
|
|
155
|
+
const status = parseStatus(CAPTURE_DK_22205);
|
|
156
|
+
expect(status.details?.[0]).toEqual({ label: 'Model code', value: '4A' });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('model-code row falls back to hex for unprintable bytes', () => {
|
|
160
|
+
const bytes = makeStatusBytes();
|
|
161
|
+
bytes[3] = 0x00;
|
|
162
|
+
bytes[4] = 0x41;
|
|
163
|
+
expect(parseStatus(bytes).details?.[0]?.value).toBe('0x00 0x41');
|
|
164
|
+
});
|
|
165
|
+
|
|
153
166
|
it('always emits a Print phase row', () => {
|
|
154
167
|
const status = parseStatus(makeStatusBytes());
|
|
155
168
|
expect(status.details).toBeDefined();
|
|
@@ -196,6 +209,7 @@ describe('parseStatus — details[]', () => {
|
|
|
196
209
|
it('details carries both phase and cooling rows while printing + cooling', () => {
|
|
197
210
|
const status = parseStatus(makeStatusBytes({ phaseType: 0x01, notification: 0x03 }));
|
|
198
211
|
expect(status.details).toEqual([
|
|
212
|
+
{ label: 'Model code', value: '4A' },
|
|
199
213
|
{ label: 'Print phase', value: 'printing' },
|
|
200
214
|
{ label: 'Head cooling', value: 'cooling started', severity: 'warn' },
|
|
201
215
|
]);
|
package/src/devices.generated.ts
CHANGED
package/src/devices.ts
CHANGED
|
@@ -6,18 +6,6 @@ import { DEVICES, REGISTRY } from './devices.generated.js';
|
|
|
6
6
|
// labelwriter-aligned `REGISTRY` symbol.
|
|
7
7
|
export const DEVICE_REGISTRY = REGISTRY;
|
|
8
8
|
|
|
9
|
-
// Mass-storage PIDs aligned to Linux usb-ids (gowdy.us / linux-usb.org).
|
|
10
|
-
// Each large-format printer with Editor Lite enumerates as USB Mass
|
|
11
|
-
// Storage Class on this alternate PID until the user holds the Editor
|
|
12
|
-
// Lite button to switch back to printer-class. Aggregated from each
|
|
13
|
-
// device entry's `capabilities.massStoragePid`.
|
|
14
|
-
const MASS_STORAGE_PIDS = new Set<number>(
|
|
15
|
-
Object.values(DEVICES).flatMap(d => {
|
|
16
|
-
const pid = d.capabilities?.massStoragePid;
|
|
17
|
-
return typeof pid === 'string' ? [parseInt(pid, 16)] : [];
|
|
18
|
-
}),
|
|
19
|
-
);
|
|
20
|
-
|
|
21
9
|
export { DEVICES };
|
|
22
10
|
|
|
23
11
|
/**
|
|
@@ -39,7 +27,3 @@ export function findDevice(vid: number, pid: number): BrotherQLDevice | undefine
|
|
|
39
27
|
return ids?.vid === vid && ids.pid === pid;
|
|
40
28
|
});
|
|
41
29
|
}
|
|
42
|
-
|
|
43
|
-
export function isMassStorageMode(pid: number): boolean {
|
|
44
|
-
return MASS_STORAGE_PIDS.has(pid);
|
|
45
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ export {
|
|
|
33
33
|
UnsupportedOperationError,
|
|
34
34
|
} from '@thermal-label/contracts';
|
|
35
35
|
|
|
36
|
-
export { DEVICE_REGISTRY, DEVICES, findDevice, getUsbIds
|
|
36
|
+
export { DEVICE_REGISTRY, DEVICES, findDevice, getUsbIds } from './devices.js';
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Protocols this core's encoder produces correct wire bytes for.
|
|
@@ -63,6 +63,8 @@ export {
|
|
|
63
63
|
} from './protocol.js';
|
|
64
64
|
export type { EncoderEngine, RasterProtocolConfig } from './protocol.js';
|
|
65
65
|
export { parseStatus, STATUS_REQUEST } from './status.js';
|
|
66
|
+
export { hasTwoColourSibling, parseMediaName, statusFromPrinterMib } from './network-status.js';
|
|
67
|
+
export type { PrinterMibStatus } from './network-status.js';
|
|
66
68
|
export { createPreviewOffline } from './preview.js';
|
|
67
69
|
|
|
68
70
|
export type {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { PrintEngine, PrinterError, StatusDetail } from '@thermal-label/contracts';
|
|
2
|
+
import type { BrotherQLMedia, BrotherQLStatus } from './types.js';
|
|
3
|
+
import { findMediaByDimensions, MEDIA } from './media.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Values read from the standard Printer-MIB / Host-Resources-MIB
|
|
7
|
+
* (RFC 3805 / RFC 2790) of a network print server. Port 9100 carries
|
|
8
|
+
* no status, so this is the only status source on TCP; see plan 17.
|
|
9
|
+
*/
|
|
10
|
+
export interface PrinterMibStatus {
|
|
11
|
+
/** hrPrinterStatus: 1 other, 2 unknown, 3 idle, 4 printing, 5 warmup. */
|
|
12
|
+
printerStatus: number;
|
|
13
|
+
/** hrPrinterDetectedErrorState: 0–2 octets, bit 0 = MSB of octet 0. */
|
|
14
|
+
errorState: Uint8Array;
|
|
15
|
+
/** prtInputMediaName, e.g. `29mm x 90mm / 1.1" x 3.5"` or `62mm / 2.4"`. */
|
|
16
|
+
mediaName: string;
|
|
17
|
+
/** prtInputMediaDimFeedDir (length) / XFeedDir (width) in `dimUnit`; negative = unknown. */
|
|
18
|
+
feedDir?: number;
|
|
19
|
+
xFeedDir?: number;
|
|
20
|
+
/** prtInputDimUnit: 3 = ten-thousandths of an inch, 4 = micrometres. */
|
|
21
|
+
dimUnit?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PRINTER_STATE: Record<number, string> = {
|
|
25
|
+
1: 'other',
|
|
26
|
+
2: 'unknown',
|
|
27
|
+
3: 'idle',
|
|
28
|
+
4: 'printing',
|
|
29
|
+
5: 'warmup',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** RFC 2790 hrPrinterDetectedErrorState bit names, index = bit. */
|
|
33
|
+
const ERROR_STATE_BITS = [
|
|
34
|
+
'lowPaper',
|
|
35
|
+
'noPaper',
|
|
36
|
+
'lowToner',
|
|
37
|
+
'noToner',
|
|
38
|
+
'doorOpen',
|
|
39
|
+
'jammed',
|
|
40
|
+
'offline',
|
|
41
|
+
'serviceRequested',
|
|
42
|
+
'inputTrayMissing',
|
|
43
|
+
'outputTrayMissing',
|
|
44
|
+
'markerSupplyMissing',
|
|
45
|
+
'outputNearFull',
|
|
46
|
+
'outputFull',
|
|
47
|
+
'inputTrayEmpty',
|
|
48
|
+
'overduePreventMaint',
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/** Bit → contracts error code + message; unlisted set bits become `system_error`. */
|
|
52
|
+
const ERROR_STATE_CODES: Record<number, { code: string; message: string }> = {
|
|
53
|
+
1: { code: 'no_media', message: 'No media' },
|
|
54
|
+
4: { code: 'cover_open', message: 'Cover open' },
|
|
55
|
+
5: { code: 'cutter_jam', message: 'Jammed' },
|
|
56
|
+
6: { code: 'not_ready', message: 'Offline' },
|
|
57
|
+
7: { code: 'system_error', message: 'Service requested' },
|
|
58
|
+
13: { code: 'no_media', message: 'Input tray empty' },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Warning-only bits: reported as a details row, never an error. */
|
|
62
|
+
const ERROR_STATE_WARNINGS: Record<number, string> = {
|
|
63
|
+
0: 'Media low',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function errorStateBit(errorState: Uint8Array, bit: number): boolean {
|
|
67
|
+
const octet = errorState[bit >> 3];
|
|
68
|
+
return octet !== undefined && (octet & (0x80 >> (bit & 7))) !== 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse the metric half of a Brother `prtInputMediaName`.
|
|
73
|
+
* `"29mm x 90mm / …"` → 29×90 die-cut, `"62mm / …"` → 62 continuous.
|
|
74
|
+
* The `Dia` form is the reference's naming for round labels and is
|
|
75
|
+
* unverified on the wire. Anything else → `undefined`.
|
|
76
|
+
*/
|
|
77
|
+
export function parseMediaName(name: string): { widthMm: number; heightMm?: number } | undefined {
|
|
78
|
+
const metric = name.split('/')[0] ?? '';
|
|
79
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*mm(?:\s*(?:[x×]\s*(\d+(?:\.\d+)?)\s*mm|(dia)))?\s*$/i.exec(
|
|
80
|
+
metric,
|
|
81
|
+
);
|
|
82
|
+
if (!m?.[1]) return undefined;
|
|
83
|
+
const widthMm = Number(m[1]);
|
|
84
|
+
if (m[3]) return { widthMm, heightMm: widthMm };
|
|
85
|
+
return m[2] === undefined ? { widthMm } : { widthMm, heightMm: Number(m[2]) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function dimToMm(value: number, unit: number): number | undefined {
|
|
89
|
+
if (unit === 3) return Math.round(value * 0.00254);
|
|
90
|
+
if (unit === 4) return Math.round(value / 1000);
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The name string wins; the Dim OIDs are the fallback. Bench
|
|
96
|
+
* (QL-820NWB, DK-11201): `prtInputDimUnit` = 4 (micrometres) with
|
|
97
|
+
* `2900 × 9000`, which is 1/100 mm, so trusting the unit yields a
|
|
98
|
+
* 3 × 9 mm label. The name is the only value measured to be right.
|
|
99
|
+
*/
|
|
100
|
+
function resolveDimensions(
|
|
101
|
+
input: PrinterMibStatus,
|
|
102
|
+
): { widthMm: number; heightMm: number } | undefined {
|
|
103
|
+
const parsed = parseMediaName(input.mediaName);
|
|
104
|
+
if (parsed) return { widthMm: parsed.widthMm, heightMm: parsed.heightMm ?? 0 };
|
|
105
|
+
const { xFeedDir, feedDir, dimUnit } = input;
|
|
106
|
+
if (xFeedDir === undefined || xFeedDir < 0 || dimUnit === undefined) return undefined;
|
|
107
|
+
const widthMm = dimToMm(xFeedDir, dimUnit);
|
|
108
|
+
if (widthMm === undefined) return undefined;
|
|
109
|
+
const heightMm = feedDir !== undefined && feedDir >= 0 ? dimToMm(feedDir, dimUnit) : 0;
|
|
110
|
+
return { widthMm, heightMm: heightMm ?? 0 };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether a roll of this width and type also exists as a two-colour
|
|
115
|
+
* variant (62 mm continuous: DK-22205 vs DK-22251). Those are
|
|
116
|
+
* indistinguishable over the network, so callers warn.
|
|
117
|
+
*/
|
|
118
|
+
export function hasTwoColourSibling(media: BrotherQLMedia): boolean {
|
|
119
|
+
return Object.values(MEDIA).some(
|
|
120
|
+
m => m.palette !== undefined && m.type === media.type && m.widthMm === media.widthMm,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Map Printer-MIB values onto the driver's status. `ready` requires
|
|
126
|
+
* an idle or printing state and no error bits. Two-colour rolls are
|
|
127
|
+
* invisible on every network surface (DK-22251 reads as 62 mm
|
|
128
|
+
* continuous), so the single-colour sibling is resolved and a `warn`
|
|
129
|
+
* row says so.
|
|
130
|
+
*/
|
|
131
|
+
export function statusFromPrinterMib(
|
|
132
|
+
input: PrinterMibStatus,
|
|
133
|
+
engine?: Pick<PrintEngine, 'headDots' | 'mediaCompatibility'>,
|
|
134
|
+
): BrotherQLStatus {
|
|
135
|
+
const errors: PrinterError[] = [];
|
|
136
|
+
const details: StatusDetail[] = [
|
|
137
|
+
{ label: 'Printer state', value: PRINTER_STATE[input.printerStatus] ?? 'unknown' },
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
for (const [bit, name] of ERROR_STATE_BITS.entries()) {
|
|
141
|
+
if (!errorStateBit(input.errorState, bit)) continue;
|
|
142
|
+
const warning = ERROR_STATE_WARNINGS[bit];
|
|
143
|
+
if (warning !== undefined) {
|
|
144
|
+
details.push({ label: 'Media', value: warning, severity: 'warn' });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
errors.push(ERROR_STATE_CODES[bit] ?? { code: 'system_error', message: name });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const dims = resolveDimensions(input);
|
|
151
|
+
const detected = dims
|
|
152
|
+
? findMediaByDimensions(dims.widthMm, dims.heightMm, false, engine)
|
|
153
|
+
: undefined;
|
|
154
|
+
if (detected === undefined) {
|
|
155
|
+
details.push({
|
|
156
|
+
label: 'Media',
|
|
157
|
+
value: `not recognised: ${JSON.stringify(input.mediaName)}`,
|
|
158
|
+
severity: 'warn',
|
|
159
|
+
});
|
|
160
|
+
} else if (hasTwoColourSibling(detected)) {
|
|
161
|
+
details.push({ label: 'Two-colour', value: 'not detectable over network', severity: 'warn' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const state = input.printerStatus;
|
|
165
|
+
return {
|
|
166
|
+
ready: (state === 3 || state === 4) && errors.length === 0,
|
|
167
|
+
mediaLoaded: detected !== undefined,
|
|
168
|
+
...(detected === undefined ? {} : { detectedMedia: detected }),
|
|
169
|
+
errors,
|
|
170
|
+
details,
|
|
171
|
+
rawBytes: new Uint8Array(0),
|
|
172
|
+
};
|
|
173
|
+
}
|