@thermal-label/brother-ql-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/__tests__/devices.test.d.ts +2 -0
- package/dist/__tests__/devices.test.d.ts.map +1 -0
- package/dist/__tests__/devices.test.js +63 -0
- package/dist/__tests__/devices.test.js.map +1 -0
- package/dist/__tests__/media.test.d.ts +2 -0
- package/dist/__tests__/media.test.d.ts.map +1 -0
- package/dist/__tests__/media.test.js +62 -0
- package/dist/__tests__/media.test.js.map +1 -0
- package/dist/__tests__/protocol.test.d.ts +2 -0
- package/dist/__tests__/protocol.test.d.ts.map +1 -0
- package/dist/__tests__/protocol.test.js +202 -0
- package/dist/__tests__/protocol.test.js.map +1 -0
- package/dist/__tests__/status.test.d.ts +2 -0
- package/dist/__tests__/status.test.d.ts.map +1 -0
- package/dist/__tests__/status.test.js +74 -0
- package/dist/__tests__/status.test.js.map +1 -0
- package/dist/devices.d.ts +255 -0
- package/dist/devices.d.ts.map +1 -0
- package/dist/devices.js +260 -0
- package/dist/devices.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/media.d.ts +5 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +247 -0
- package/dist/media.js.map +1 -0
- package/dist/protocol.d.ts +17 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +173 -0
- package/dist/protocol.js.map +1 -0
- package/dist/status.d.ts +4 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +61 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +71 -0
- package/src/__tests__/devices.test.ts +73 -0
- package/src/__tests__/media.test.ts +70 -0
- package/src/__tests__/protocol.test.ts +241 -0
- package/src/__tests__/status.test.ts +95 -0
- package/src/devices.ts +263 -0
- package/src/index.ts +22 -0
- package/src/media.ts +250 -0
- package/src/protocol.ts +213 -0
- package/src/status.ts +67 -0
- package/src/types.ts +77 -0
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thermal-label/brother-ql-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Protocol encoding, device registry, and media registry for Brother QL label printers",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"brother",
|
|
7
|
+
"brother-ql",
|
|
8
|
+
"label-printer",
|
|
9
|
+
"thermal-label",
|
|
10
|
+
"usb",
|
|
11
|
+
"protocol"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"author": "Mannes Brak",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://thermal-label.github.io/brother-ql/",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/thermal-label/brother-ql.git",
|
|
20
|
+
"directory": "packages/core"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/thermal-label/brother-ql/issues"
|
|
24
|
+
},
|
|
25
|
+
"funding": [
|
|
26
|
+
{
|
|
27
|
+
"type": "github",
|
|
28
|
+
"url": "https://github.com/sponsors/mannes"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "ko-fi",
|
|
32
|
+
"url": "https://ko-fi.com/mannes"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=24.0.0"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"sideEffects": false,
|
|
47
|
+
"main": "./dist/index.js",
|
|
48
|
+
"types": "./src/index.ts",
|
|
49
|
+
"exports": {
|
|
50
|
+
".": {
|
|
51
|
+
"import": "./dist/index.js",
|
|
52
|
+
"types": "./src/index.ts"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@mbtech-nl/bitmap": "^0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@mbtech-nl/tsconfig": "^1.0.0",
|
|
60
|
+
"@types/node": "^22.0.0",
|
|
61
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
62
|
+
"typescript": "~5.5.0",
|
|
63
|
+
"vitest": "^2.0.0"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsc -p tsconfig.json",
|
|
67
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
68
|
+
"test": "vitest run",
|
|
69
|
+
"test:coverage": "vitest run --coverage"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DEVICES, findDevice, isMassStorageMode } from '../devices.js';
|
|
3
|
+
|
|
4
|
+
describe('findDevice', () => {
|
|
5
|
+
it('returns correct descriptor for QL-820NWB', () => {
|
|
6
|
+
const dev = findDevice(0x04f9, 0x20a7);
|
|
7
|
+
expect(dev).toBeDefined();
|
|
8
|
+
expect(dev!.name).toBe('QL-820NWB');
|
|
9
|
+
expect(dev!.twoColor).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns correct descriptor for QL-820NWBc', () => {
|
|
13
|
+
const dev = findDevice(0x04f9, 0x209d);
|
|
14
|
+
expect(dev).toBeDefined();
|
|
15
|
+
expect(dev!.name).toBe('QL-820NWBc');
|
|
16
|
+
expect(dev!.twoColor).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns correct descriptor for QL-500', () => {
|
|
20
|
+
const dev = findDevice(0x04f9, 0x2013);
|
|
21
|
+
expect(dev).toBeDefined();
|
|
22
|
+
expect(dev!.name).toBe('QL-500');
|
|
23
|
+
expect(dev!.autocut).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns undefined for unknown PID', () => {
|
|
27
|
+
expect(findDevice(0x04f9, 0x9999)).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns undefined for unknown VID', () => {
|
|
31
|
+
expect(findDevice(0x1234, 0x20a7)).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('isMassStorageMode', () => {
|
|
36
|
+
it('returns true for 0x20AA (QL-1100 mass storage)', () => {
|
|
37
|
+
expect(isMassStorageMode(0x20aa)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns true for 0x20AB (QL-1110NWB mass storage)', () => {
|
|
41
|
+
expect(isMassStorageMode(0x20ab)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns false for all printer-class PIDs', () => {
|
|
45
|
+
for (const dev of Object.values(DEVICES)) {
|
|
46
|
+
expect(isMassStorageMode(dev.pid)).toBe(false);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Device registry invariants', () => {
|
|
52
|
+
it('every two-color device has bytesPerRow 90', () => {
|
|
53
|
+
for (const dev of Object.values(DEVICES)) {
|
|
54
|
+
if (dev.twoColor) {
|
|
55
|
+
expect(dev.bytesPerRow).toBe(90);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('every device with headPins 1296 has bytesPerRow 162', () => {
|
|
61
|
+
for (const dev of Object.values(DEVICES)) {
|
|
62
|
+
if (dev.headPins === 1296) {
|
|
63
|
+
expect(dev.bytesPerRow).toBe(162);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('every device has bluetooth: false (BT out of scope)', () => {
|
|
69
|
+
for (const dev of Object.values(DEVICES)) {
|
|
70
|
+
expect(dev.bluetooth).toBe(false);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { MEDIA, findMedia, findMediaByWidth } from '../media.js';
|
|
3
|
+
|
|
4
|
+
describe('findMedia', () => {
|
|
5
|
+
it('returns correct descriptor for 62mm continuous (ID 259)', () => {
|
|
6
|
+
const m = findMedia(259);
|
|
7
|
+
expect(m).toBeDefined();
|
|
8
|
+
expect(m!.widthMm).toBe(62);
|
|
9
|
+
expect(m!.type).toBe('continuous');
|
|
10
|
+
expect(m!.lengthMm).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns correct descriptor for 62x29mm die-cut (ID 274)', () => {
|
|
14
|
+
const m = findMedia(274);
|
|
15
|
+
expect(m).toBeDefined();
|
|
16
|
+
expect(m!.type).toBe('die-cut');
|
|
17
|
+
expect(m!.lengthMm).toBeGreaterThan(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns undefined for unknown ID', () => {
|
|
21
|
+
expect(findMedia(9999)).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('findMediaByWidth', () => {
|
|
26
|
+
it('returns all 62mm continuous options', () => {
|
|
27
|
+
const results = findMediaByWidth(62, 'continuous');
|
|
28
|
+
expect(results.length).toBeGreaterThan(0);
|
|
29
|
+
for (const m of results) {
|
|
30
|
+
expect(m.widthMm).toBe(62);
|
|
31
|
+
expect(m.type).toBe('continuous');
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns die-cut for 62mm die-cut', () => {
|
|
36
|
+
const results = findMediaByWidth(62, 'die-cut');
|
|
37
|
+
expect(results.length).toBeGreaterThan(0);
|
|
38
|
+
for (const m of results) {
|
|
39
|
+
expect(m.type).toBe('die-cut');
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns empty array for unknown width', () => {
|
|
44
|
+
expect(findMediaByWidth(999, 'continuous')).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Media registry invariants', () => {
|
|
49
|
+
it('die-cut media has non-zero lengthMm', () => {
|
|
50
|
+
for (const m of Object.values(MEDIA)) {
|
|
51
|
+
if (m.type === 'die-cut') {
|
|
52
|
+
expect(m.lengthMm).toBeGreaterThan(0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('continuous media has lengthMm === 0', () => {
|
|
58
|
+
for (const m of Object.values(MEDIA)) {
|
|
59
|
+
if (m.type === 'continuous') {
|
|
60
|
+
expect(m.lengthMm).toBe(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('all IDs are unique', () => {
|
|
66
|
+
const ids = Object.values(MEDIA).map(m => m.id);
|
|
67
|
+
const unique = new Set(ids);
|
|
68
|
+
expect(unique.size).toBe(ids.length);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createBitmap } from '@mbtech-nl/bitmap';
|
|
3
|
+
import {
|
|
4
|
+
buildInvalidate,
|
|
5
|
+
buildInitialize,
|
|
6
|
+
buildRasterMode,
|
|
7
|
+
buildRasterRow,
|
|
8
|
+
buildPrintCommand,
|
|
9
|
+
buildPrintInfo,
|
|
10
|
+
buildZeroRow,
|
|
11
|
+
buildCompression,
|
|
12
|
+
buildStatusNotification,
|
|
13
|
+
buildVariousMode,
|
|
14
|
+
buildExpandedMode,
|
|
15
|
+
encodeJob,
|
|
16
|
+
} from '../protocol.js';
|
|
17
|
+
import { type PageData } from '../types.js';
|
|
18
|
+
import { MEDIA } from '../media.js';
|
|
19
|
+
|
|
20
|
+
describe('buildInvalidate', () => {
|
|
21
|
+
it('returns exactly 200 zero bytes', () => {
|
|
22
|
+
const buf = buildInvalidate();
|
|
23
|
+
expect(buf.length).toBe(200);
|
|
24
|
+
expect(buf.every(b => b === 0)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('buildInitialize', () => {
|
|
29
|
+
it('returns [0x1B, 0x40]', () => {
|
|
30
|
+
expect(Array.from(buildInitialize())).toEqual([0x1b, 0x40]);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('buildRasterRow', () => {
|
|
35
|
+
it('single-color black: [0x67][0x00][len][data]', () => {
|
|
36
|
+
const payload = new Uint8Array(90).fill(0xaa);
|
|
37
|
+
const row = buildRasterRow(payload, 'black');
|
|
38
|
+
expect(row[0]).toBe(0x67);
|
|
39
|
+
expect(row[1]).toBe(0x00);
|
|
40
|
+
expect(row[2]).toBe(90);
|
|
41
|
+
expect(row.length).toBe(93);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('two-color black: [0x77][0x01][len][data]', () => {
|
|
45
|
+
const payload = new Uint8Array(90);
|
|
46
|
+
const row = buildRasterRow(payload, 'black', true);
|
|
47
|
+
expect(row[0]).toBe(0x77);
|
|
48
|
+
expect(row[1]).toBe(0x01);
|
|
49
|
+
expect(row[2]).toBe(90);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('two-color red: [0x77][0x02][len][data]', () => {
|
|
53
|
+
const payload = new Uint8Array(90);
|
|
54
|
+
const row = buildRasterRow(payload, 'red', true);
|
|
55
|
+
expect(row[0]).toBe(0x77);
|
|
56
|
+
expect(row[1]).toBe(0x02);
|
|
57
|
+
expect(row[2]).toBe(90);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('buildPrintCommand', () => {
|
|
62
|
+
it('last page returns [0x1A]', () => {
|
|
63
|
+
expect(Array.from(buildPrintCommand(true))).toEqual([0x1a]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('non-last page returns [0x0C]', () => {
|
|
67
|
+
expect(Array.from(buildPrintCommand(false))).toEqual([0x0c]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('buildPrintInfo', () => {
|
|
72
|
+
it('byte 5 contains correct media width, bytes 7-8 correct row count LE', () => {
|
|
73
|
+
const media = MEDIA[259]!; // 62mm continuous
|
|
74
|
+
const buf = buildPrintInfo(media, 200, 0);
|
|
75
|
+
expect(buf[5]).toBe(62); // widthMm
|
|
76
|
+
// rowCount 200 = 0xC8 at bytes 7-8
|
|
77
|
+
expect(buf[7]).toBe(0xc8);
|
|
78
|
+
expect(buf[8]).toBe(0x00);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('page index is at byte 9', () => {
|
|
82
|
+
const media = MEDIA[259]!;
|
|
83
|
+
const buf = buildPrintInfo(media, 100, 3);
|
|
84
|
+
expect(buf[9]).toBe(3);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('die-cut media sets mediaType byte to 0x0b', () => {
|
|
88
|
+
const media = MEDIA[271]!; // 29x90mm die-cut
|
|
89
|
+
const buf = buildPrintInfo(media, 100, 0);
|
|
90
|
+
expect(buf[4]).toBe(0x0b);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('encodeJob', () => {
|
|
95
|
+
const media62 = MEDIA[259]!; // 62mm, 696 printAreaDots
|
|
96
|
+
|
|
97
|
+
function makePage(widthPx: number, heightPx: number): PageData {
|
|
98
|
+
const bitmap = createBitmap(widthPx, heightPx);
|
|
99
|
+
return { bitmap, media: media62 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
it('single page: contains ESC i a + 200 zero invalidate, ends with 0x1A', () => {
|
|
103
|
+
const page = makePage(696, 50);
|
|
104
|
+
const buf = encodeJob([page]);
|
|
105
|
+
// Starts with ESC i a 01 (raster mode, 4 bytes) then 200 zero invalidate bytes
|
|
106
|
+
expect(buf[0]).toBe(0x1b);
|
|
107
|
+
expect(buf[1]).toBe(0x69);
|
|
108
|
+
expect(buf[2]).toBe(0x61);
|
|
109
|
+
expect(buf[3]).toBe(0x01);
|
|
110
|
+
for (let i = 4; i < 204; i++) expect(buf[i]).toBe(0);
|
|
111
|
+
expect(buf.at(-1)).toBe(0x1a);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('two-page job: second page has control codes, ends with 0x1A', () => {
|
|
115
|
+
const page1 = makePage(696, 10);
|
|
116
|
+
const page2 = makePage(696, 10);
|
|
117
|
+
const buf = encodeJob([page1, page2]);
|
|
118
|
+
expect(buf.at(-1)).toBe(0x1a);
|
|
119
|
+
// 0x0C should appear as the inter-page print command somewhere before the last byte
|
|
120
|
+
const printCmds = [];
|
|
121
|
+
for (let i = 0; i < buf.length; i++) {
|
|
122
|
+
if (buf[i] === 0x0c) printCmds.push(i);
|
|
123
|
+
}
|
|
124
|
+
expect(printCmds.length).toBeGreaterThan(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('two-color job: black rows [0x77,0x01,len], red rows [0x77,0x02,len], interleaved', () => {
|
|
128
|
+
const bitmap = createBitmap(696, 5);
|
|
129
|
+
const redBitmap = createBitmap(696, 5);
|
|
130
|
+
const page: PageData = { bitmap, redBitmap, media: media62 };
|
|
131
|
+
const buf = encodeJob([page]);
|
|
132
|
+
const rowLen = 90;
|
|
133
|
+
const blackRows: number[] = [];
|
|
134
|
+
const redRows: number[] = [];
|
|
135
|
+
for (let i = 0; i < buf.length - 2; i++) {
|
|
136
|
+
if (buf[i] === 0x77 && buf[i + 1] === 0x01 && buf[i + 2] === rowLen) blackRows.push(i);
|
|
137
|
+
if (buf[i] === 0x77 && buf[i + 1] === 0x02 && buf[i + 2] === rowLen) redRows.push(i);
|
|
138
|
+
}
|
|
139
|
+
expect(blackRows.length).toBe(5);
|
|
140
|
+
expect(redRows.length).toBe(5);
|
|
141
|
+
// Verify interleaving: each black row is immediately followed by a red row (93 bytes apart)
|
|
142
|
+
for (let r = 0; r < 5; r++) {
|
|
143
|
+
expect(redRows[r]! - blackRows[r]!).toBe(93); // 3 header + 90 data
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('two-color job: throws if red bitmap dimensions mismatch', () => {
|
|
148
|
+
const bitmap = createBitmap(720, 5);
|
|
149
|
+
const redBitmap = createBitmap(720, 10); // different height
|
|
150
|
+
const page: PageData = { bitmap, redBitmap, media: media62 };
|
|
151
|
+
expect(() => encodeJob([page])).toThrow('identical dimensions');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('copies option repeats the job', () => {
|
|
155
|
+
const page = makePage(720, 2);
|
|
156
|
+
const buf1 = encodeJob([page]);
|
|
157
|
+
const buf2 = encodeJob([page], { copies: 2 });
|
|
158
|
+
expect(buf2.length).toBeGreaterThan(buf1.length);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('compress option includes compression command [0x4D, 0x02]', () => {
|
|
162
|
+
const page: PageData = { ...makePage(696, 5), options: { compress: true } };
|
|
163
|
+
const buf = encodeJob([page]);
|
|
164
|
+
let found = false;
|
|
165
|
+
for (let i = 0; i < buf.length - 1; i++) {
|
|
166
|
+
if (buf[i] === 0x4d && buf[i + 1] === 0x02) {
|
|
167
|
+
found = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
expect(found).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('buildRasterMode', () => {
|
|
176
|
+
it('returns correct bytes', () => {
|
|
177
|
+
expect(Array.from(buildRasterMode())).toEqual([0x1b, 0x69, 0x61, 0x01]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('buildStatusNotification', () => {
|
|
182
|
+
it('disabled returns [0x1B, 0x69, 0x21, 0x00]', () => {
|
|
183
|
+
expect(Array.from(buildStatusNotification(false))).toEqual([0x1b, 0x69, 0x21, 0x00]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('enabled returns [0x1B, 0x69, 0x21, 0x01]', () => {
|
|
187
|
+
expect(Array.from(buildStatusNotification(true))).toEqual([0x1b, 0x69, 0x21, 0x01]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('buildVariousMode', () => {
|
|
192
|
+
it('autoCut=true returns 0x40 flag', () => {
|
|
193
|
+
expect(buildVariousMode(true)[3]).toBe(0x40);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('autoCut=false returns 0x00 flag', () => {
|
|
197
|
+
expect(buildVariousMode(false)[3]).toBe(0x00);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('buildExpandedMode', () => {
|
|
202
|
+
it('cutAtEnd=true sets bit 3', () => {
|
|
203
|
+
expect((buildExpandedMode(true, false)[3] ?? 0) & 0x08).toBe(0x08);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('highRes=true sets bit 4', () => {
|
|
207
|
+
expect((buildExpandedMode(false, true)[3] ?? 0) & 0x10).toBe(0x10);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('twoColor=true sets bit 0', () => {
|
|
211
|
+
expect((buildExpandedMode(false, false, true)[3] ?? 0) & 0x01).toBe(0x01);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('both false returns 0x00 flags', () => {
|
|
215
|
+
expect(buildExpandedMode(false, false)[3]).toBe(0x00);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('buildPrintInfo twoColor flag', () => {
|
|
220
|
+
it('always uses 0xCE valid flags (Python brother_ql behaviour for two-color capable models)', () => {
|
|
221
|
+
const media = MEDIA[259]!;
|
|
222
|
+
const buf = buildPrintInfo(media, 100, 0);
|
|
223
|
+
expect(buf[3]).toBe(0xce);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('buildZeroRow', () => {
|
|
228
|
+
it('returns [0x5A]', () => {
|
|
229
|
+
expect(Array.from(buildZeroRow())).toEqual([0x5a]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('buildCompression', () => {
|
|
234
|
+
it('enabled returns [0x4D, 0x02]', () => {
|
|
235
|
+
expect(Array.from(buildCompression(true))).toEqual([0x4d, 0x02]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('disabled returns [0x4D, 0x00]', () => {
|
|
239
|
+
expect(Array.from(buildCompression(false))).toEqual([0x4d, 0x00]);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseStatus, STATUS_REQUEST } from '../status.js';
|
|
3
|
+
|
|
4
|
+
function makeStatusBytes(
|
|
5
|
+
overrides?: Partial<{
|
|
6
|
+
errInfo1: number;
|
|
7
|
+
errInfo2: number;
|
|
8
|
+
mediaWidthMm: number;
|
|
9
|
+
mediaLengthMm: number;
|
|
10
|
+
mediaTypeByte: number;
|
|
11
|
+
statusType: number;
|
|
12
|
+
}>,
|
|
13
|
+
): Uint8Array {
|
|
14
|
+
const bytes = new Uint8Array(32);
|
|
15
|
+
bytes[0] = 0x80;
|
|
16
|
+
bytes[1] = 0x20;
|
|
17
|
+
bytes[2] = 0x42;
|
|
18
|
+
bytes[3] = 0x30;
|
|
19
|
+
bytes[8] = overrides?.errInfo1 ?? 0;
|
|
20
|
+
bytes[9] = overrides?.errInfo2 ?? 0;
|
|
21
|
+
bytes[10] = overrides?.mediaWidthMm ?? 62;
|
|
22
|
+
bytes[11] = overrides?.mediaTypeByte ?? 0x0a;
|
|
23
|
+
bytes[17] = overrides?.mediaLengthMm ?? 0;
|
|
24
|
+
bytes[18] = overrides?.statusType ?? 0x00;
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('parseStatus', () => {
|
|
29
|
+
it('throws on short input', () => {
|
|
30
|
+
expect(() => parseStatus(new Uint8Array(10))).toThrow('too short');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns ready=true with no errors', () => {
|
|
34
|
+
const status = parseStatus(makeStatusBytes());
|
|
35
|
+
expect(status.ready).toBe(true);
|
|
36
|
+
expect(status.errors).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('parses media width and type', () => {
|
|
40
|
+
const status = parseStatus(makeStatusBytes({ mediaWidthMm: 62, mediaTypeByte: 0x0a }));
|
|
41
|
+
expect(status.mediaWidthMm).toBe(62);
|
|
42
|
+
expect(status.mediaType).toBe('continuous');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('parses media length from byte 17 (0 for continuous, mm for die-cut)', () => {
|
|
46
|
+
const cont = parseStatus(makeStatusBytes({ mediaLengthMm: 0 }));
|
|
47
|
+
expect(cont.mediaLengthMm).toBe(0);
|
|
48
|
+
const diecut = parseStatus(makeStatusBytes({ mediaTypeByte: 0x0b, mediaLengthMm: 90 }));
|
|
49
|
+
expect(diecut.mediaLengthMm).toBe(90);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('parses die-cut media type', () => {
|
|
53
|
+
const status = parseStatus(makeStatusBytes({ mediaTypeByte: 0x0b }));
|
|
54
|
+
expect(status.mediaType).toBe('die-cut');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns null mediaType for unknown byte', () => {
|
|
58
|
+
const status = parseStatus(makeStatusBytes({ mediaTypeByte: 0xff }));
|
|
59
|
+
expect(status.mediaType).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('parses error info 1 bits', () => {
|
|
63
|
+
const status = parseStatus(makeStatusBytes({ errInfo1: 0b00000001 })); // No media
|
|
64
|
+
expect(status.errors).toContain('No media');
|
|
65
|
+
expect(status.ready).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('parses error info 2 bits', () => {
|
|
69
|
+
const status = parseStatus(makeStatusBytes({ errInfo2: 0b00010000 })); // Cover open
|
|
70
|
+
expect(status.errors).toContain('Cover open');
|
|
71
|
+
expect(status.ready).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('ready=false when statusType is error (0x02)', () => {
|
|
75
|
+
const status = parseStatus(makeStatusBytes({ statusType: 0x02 }));
|
|
76
|
+
expect(status.ready).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rawBytes contains the full 32-byte input', () => {
|
|
80
|
+
const bytes = makeStatusBytes();
|
|
81
|
+
const status = parseStatus(bytes);
|
|
82
|
+
expect(status.rawBytes).toEqual(bytes);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('editorLiteMode is false (detected at discovery time, not from status bytes)', () => {
|
|
86
|
+
const status = parseStatus(makeStatusBytes());
|
|
87
|
+
expect(status.editorLiteMode).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('STATUS_REQUEST', () => {
|
|
92
|
+
it('is the correct byte sequence', () => {
|
|
93
|
+
expect(Array.from(STATUS_REQUEST)).toEqual([0x1b, 0x69, 0x53]);
|
|
94
|
+
});
|
|
95
|
+
});
|