@thermal-label/brother-ql-core 0.6.1 → 0.6.3

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.
Files changed (49) hide show
  1. package/README.md +11 -11
  2. package/data/devices.json +146 -46
  3. package/dist/__tests__/devices.test.js +45 -21
  4. package/dist/__tests__/devices.test.js.map +1 -1
  5. package/dist/__tests__/network-status.test.d.ts +2 -0
  6. package/dist/__tests__/network-status.test.d.ts.map +1 -0
  7. package/dist/__tests__/network-status.test.js +132 -0
  8. package/dist/__tests__/network-status.test.js.map +1 -0
  9. package/dist/__tests__/protocol.test.js +103 -0
  10. package/dist/__tests__/protocol.test.js.map +1 -1
  11. package/dist/__tests__/status.test.js +13 -1
  12. package/dist/__tests__/status.test.js.map +1 -1
  13. package/dist/devices.d.ts +36 -24
  14. package/dist/devices.d.ts.map +1 -1
  15. package/dist/devices.generated.d.ts +36 -23
  16. package/dist/devices.generated.d.ts.map +1 -1
  17. package/dist/devices.generated.js +41 -23
  18. package/dist/devices.generated.js.map +1 -1
  19. package/dist/devices.js +0 -12
  20. package/dist/devices.js.map +1 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/network-status.d.ts +45 -0
  26. package/dist/network-status.d.ts.map +1 -0
  27. package/dist/network-status.js +140 -0
  28. package/dist/network-status.js.map +1 -0
  29. package/dist/protocol.d.ts.map +1 -1
  30. package/dist/protocol.js +34 -6
  31. package/dist/protocol.js.map +1 -1
  32. package/dist/status.d.ts +5 -3
  33. package/dist/status.d.ts.map +1 -1
  34. package/dist/status.js +21 -6
  35. package/dist/status.js.map +1 -1
  36. package/dist/types.d.ts +5 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +2 -2
  39. package/src/__tests__/devices.test.ts +47 -24
  40. package/src/__tests__/network-status.test.ts +165 -0
  41. package/src/__tests__/protocol.test.ts +129 -2
  42. package/src/__tests__/status.test.ts +15 -1
  43. package/src/devices.generated.ts +41 -23
  44. package/src/devices.ts +0 -16
  45. package/src/index.ts +3 -1
  46. package/src/network-status.ts +173 -0
  47. package/src/protocol.ts +41 -7
  48. package/src/status.ts +26 -6
  49. package/src/types.ts +5 -1
@@ -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] = 0x30;
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
  ]);
@@ -251,7 +251,7 @@ export const REGISTRY = {
251
251
  "transports": {
252
252
  "usb": {
253
253
  "vid": "0x04f9",
254
- "pid": "0x2027"
254
+ "pid": "0x2020"
255
255
  }
256
256
  },
257
257
  "engines": [
@@ -282,7 +282,7 @@ export const REGISTRY = {
282
282
  "transports": {
283
283
  "usb": {
284
284
  "vid": "0x04f9",
285
- "pid": "0x2028"
285
+ "pid": "0x202a"
286
286
  },
287
287
  "tcp": {
288
288
  "port": 9100
@@ -427,7 +427,7 @@ export const REGISTRY = {
427
427
  "transports": {
428
428
  "usb": {
429
429
  "vid": "0x04f9",
430
- "pid": "0x2013"
430
+ "pid": "0x2015"
431
431
  }
432
432
  },
433
433
  "engines": [
@@ -485,7 +485,7 @@ export const REGISTRY = {
485
485
  "transports": {
486
486
  "usb": {
487
487
  "vid": "0x04f9",
488
- "pid": "0x2018"
488
+ "pid": "0x2027"
489
489
  }
490
490
  },
491
491
  "engines": [
@@ -515,7 +515,7 @@ export const REGISTRY = {
515
515
  "transports": {
516
516
  "usb": {
517
517
  "vid": "0x04f9",
518
- "pid": "0x2019"
518
+ "pid": "0x2028"
519
519
  }
520
520
  },
521
521
  "engines": [
@@ -545,7 +545,7 @@ export const REGISTRY = {
545
545
  "transports": {
546
546
  "usb": {
547
547
  "vid": "0x04f9",
548
- "pid": "0x201b"
548
+ "pid": "0x2029"
549
549
  },
550
550
  "tcp": {
551
551
  "port": 9100
@@ -578,7 +578,7 @@ export const REGISTRY = {
578
578
  "transports": {
579
579
  "usb": {
580
580
  "vid": "0x04f9",
581
- "pid": "0x2100"
581
+ "pid": "0x20c0"
582
582
  }
583
583
  },
584
584
  "engines": [
@@ -608,7 +608,7 @@ export const REGISTRY = {
608
608
  "transports": {
609
609
  "usb": {
610
610
  "vid": "0x04f9",
611
- "pid": "0x201c"
611
+ "pid": "0x201b"
612
612
  }
613
613
  },
614
614
  "engines": [
@@ -674,7 +674,7 @@ export const REGISTRY = {
674
674
  "date": "2026-05-21",
675
675
  "result": "verified",
676
676
  "os": "Windows",
677
- "notes": "WebUSB on Edge 148 / Windows 11, 62mm continuous (DK-22205). Pre- and post-print status clean: no errors, phase advanced to printing."
677
+ "notes": "WebUSB on Edge 148 / Windows 11 with DK-22205, clean status and phase advancing to printing."
678
678
  }
679
679
  ]
680
680
  },
@@ -687,7 +687,7 @@ export const REGISTRY = {
687
687
  "transports": {
688
688
  "usb": {
689
689
  "vid": "0x04f9",
690
- "pid": "0x2044"
690
+ "pid": "0x2043"
691
691
  },
692
692
  "tcp": {
693
693
  "port": 9100
@@ -723,7 +723,7 @@ export const REGISTRY = {
723
723
  "transports": {
724
724
  "usb": {
725
725
  "vid": "0x04f9",
726
- "pid": "0x2045"
726
+ "pid": "0x2044"
727
727
  },
728
728
  "tcp": {
729
729
  "port": 9100
@@ -745,9 +745,24 @@ export const REGISTRY = {
745
745
  }
746
746
  ],
747
747
  "support": {
748
- "status": "untested"
748
+ "status": "verified",
749
+ "transports": {
750
+ "usb": "verified"
751
+ },
752
+ "lastVerified": "2026-09-19",
753
+ "packageVersion": "0.6.1",
754
+ "reports": [
755
+ {
756
+ "issue": 10,
757
+ "reporter": "@viktis",
758
+ "date": "2026-09-19",
759
+ "result": "verified",
760
+ "os": "macOS",
761
+ "notes": "WebUSB on Chromium 152 / macOS with DK-22205, clean status and phase advancing to printing; filed as QL-710W because of the PID mix-up fixed in #11."
762
+ }
763
+ ]
749
764
  },
750
- "supportStatus": "expected"
765
+ "supportStatus": "verified"
751
766
  },
752
767
  {
753
768
  "key": "QL_800",
@@ -792,7 +807,7 @@ export const REGISTRY = {
792
807
  "date": "2026-05-22",
793
808
  "result": "verified",
794
809
  "os": "Windows",
795
- "notes": "QL-800 USB print confirmed (reporter produced output). The report revealed the DK continuous margins were stored right-registered, parking the printable area off the left-seated paper; the left-registration fix (leftMarginPins=6 for 29mm) merged in PR #3 and shipped in 0.6.1."
810
+ "notes": "USB print confirmed on Windows; the report exposed right-registered DK continuous margins, fixed in PR #3 and shipped in 0.6.1."
796
811
  }
797
812
  ]
798
813
  },
@@ -838,6 +853,10 @@ export const REGISTRY = {
838
853
  {
839
854
  "key": "QL_820NWBc",
840
855
  "name": "QL-820NWBc",
856
+ "modelNames": [
857
+ "QL-820NWB",
858
+ "QL-820NWBc"
859
+ ],
841
860
  "family": "brother-ql",
842
861
  "transports": {
843
862
  "usb": {
@@ -878,18 +897,17 @@ export const REGISTRY = {
878
897
  "tcp": "verified",
879
898
  "bluetooth-spp": "untested"
880
899
  },
881
- "lastVerified": "2026-04-27",
882
- "packageVersion": "0.2.1",
883
- "quirks": "Reference test device. Two-colour printing on DK-22251 confirmed, auto-cut works on USB and TCP, WebUSB demo prints correctly. The QL-820NWB and QL-820NWBc share PID 0x209d; this entry covers both marketing names.",
900
+ "lastVerified": "2026-08-25",
901
+ "packageVersion": "0.6.1",
902
+ "quirks": "Maintainer bench reference device (Linux): two-colour printing on DK-22251 confirmed, auto-cut works on USB and TCP. The QL-820NWB and QL-820NWBc share PID 0x209d; this entry covers both marketing names.",
884
903
  "reports": [
885
904
  {
886
- "issue": 0,
887
- "reporter": "@mannes",
888
- "date": "2026-04-27",
905
+ "issue": 6,
906
+ "reporter": "@BrettJackson1987",
907
+ "date": "2026-08-25",
889
908
  "result": "verified",
890
- "os": "Linux",
891
- "selfVerified": true,
892
- "notes": "Bench verification by the maintainer at the time of seeding this entry. Issue #0 is a placeholder — replace with the real issue number when a public verification report is filed."
909
+ "os": "macOS",
910
+ "notes": "WebUSB on Chromium 150 / macOS with DK-22251 two-colour, clean status with the two-colour flag set and phase advancing to printing."
893
911
  }
894
912
  ]
895
913
  },
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, isMassStorageMode } from './devices.js';
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 {