Public Access
Correct the command stream against Brother's raster manual
Checked against "Raster Command Reference, QL-800/810W/820NWB v1.01". Four things were wrong, all inherited from brother_ql's older-model defaults: - Invalidate is 400 bytes for this series, not 200. - The print-quality bit (0x40) is documented as invalid for two-colour printing; it was being set regardless. - The media-length valid bit belongs to die-cut stock only. Brother's driver sends 0x86 for continuous tape, and the manual warns that asserting a valid flag the loaded media doesn't match returns a "replace media" error. - ESC i ! (automatic status notification) was missing, and the control codes now follow the manual's per-page order. Also confirms the 62 mm geometry we were already using: 12 left margin pins, 696 print area pins, 12 right.
This commit is contained in:
+50
-31
@@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Brother QL-8xx raster command builder.
|
* Brother QL-8xx raster command builder.
|
||||||
*
|
*
|
||||||
* Byte-level command sequence follows Brother's "Raster Command Reference"
|
* Byte-level command sequence follows Brother's "Raster Command Reference,
|
||||||
* for the QL-800/810W/820NWB series, cross-checked against the reference
|
* QL-800/810W/820NWB, Version 1.01". Where that manual and the reference
|
||||||
* implementation in pklaus/brother_ql.
|
* implementation in pklaus/brother_ql disagree, the manual wins — brother_ql
|
||||||
|
* targets older models and its defaults are not correct for this hardware.
|
||||||
*
|
*
|
||||||
* The QL-820NWB print head is 720 dots wide (60.96 mm at 300 dpi). Every
|
* The QL-820NWB print head is 720 dots wide (60.96 mm at 300 dpi). Every
|
||||||
* raster line transmitted is therefore exactly 90 bytes before compression,
|
* raster line transmitted is therefore exactly 90 bytes before compression,
|
||||||
@@ -13,17 +14,21 @@
|
|||||||
|
|
||||||
const PIXEL_WIDTH = 720;
|
const PIXEL_WIDTH = 720;
|
||||||
const BYTES_PER_ROW = PIXEL_WIDTH / 8; // 90
|
const BYTES_PER_ROW = PIXEL_WIDTH / 8; // 90
|
||||||
const INVALIDATE_BYTES = 200;
|
|
||||||
|
|
||||||
// Endless (continuous) media must be at least this many raster lines long,
|
// Manual section 2.1: "Sends a 400-byte invalidate command". brother_ql sends
|
||||||
// and no longer than this, or the printer rejects the job.
|
// 200, which works on older models but is not what this hardware documents.
|
||||||
const MIN_LENGTH_DOTS = 150;
|
const INVALIDATE_BYTES = 400;
|
||||||
const MAX_LENGTH_DOTS = 11811;
|
|
||||||
|
// Manual section 2.3.4. Continuous media outside this range is rejected.
|
||||||
|
const MIN_LENGTH_DOTS = 150; // 12.7 mm
|
||||||
|
const MAX_LENGTH_DOTS = 11811; // 1000 mm
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Media definitions. `printableDots` is how many of the 720 head dots
|
* Media definitions, from manual sections 2.3.2 and 2.3.5.
|
||||||
* actually land on the label; `offsetR` is the gap between the right edge
|
*
|
||||||
* of the printable area and the right edge of the head.
|
* `printableDots` is how many of the 720 head pins land on the label;
|
||||||
|
* `offsetR` is the number of right-margin pins. For 62 mm continuous the
|
||||||
|
* manual gives 12 left / 696 print area / 12 right.
|
||||||
*/
|
*/
|
||||||
const MEDIA = {
|
const MEDIA = {
|
||||||
// DK-22205 (62 mm white) and DK-22251 (62 mm black/red) — continuous.
|
// DK-22205 (62 mm white) and DK-22251 (62 mm black/red) — continuous.
|
||||||
@@ -35,7 +40,7 @@ const MEDIA = {
|
|||||||
lengthMm: 0,
|
lengthMm: 0,
|
||||||
printableDots: 696,
|
printableDots: 696,
|
||||||
offsetR: 12,
|
offsetR: 12,
|
||||||
feedMargin: 35,
|
feedMargin: 35, // 3 mm, the documented minimum for continuous tape
|
||||||
},
|
},
|
||||||
// Die-cut sizes, kept for completeness.
|
// Die-cut sizes, kept for completeness.
|
||||||
'62x29': {
|
'62x29': {
|
||||||
@@ -47,7 +52,7 @@ const MEDIA = {
|
|||||||
printableDots: 696,
|
printableDots: 696,
|
||||||
lengthDots: 271,
|
lengthDots: 271,
|
||||||
offsetR: 12,
|
offsetR: 12,
|
||||||
feedMargin: 0,
|
feedMargin: 0, // must be 0 for die-cut
|
||||||
},
|
},
|
||||||
'62x100': {
|
'62x100': {
|
||||||
id: '62x100',
|
id: '62x100',
|
||||||
@@ -80,9 +85,9 @@ function getMedia(id) {
|
|||||||
* Pack one row of the ink map into the 90 bytes the printer expects.
|
* Pack one row of the ink map into the 90 bytes the printer expects.
|
||||||
*
|
*
|
||||||
* The printer clocks each raster line out starting from the far side of the
|
* The printer clocks each raster line out starting from the far side of the
|
||||||
* head, so the first transmitted bit is the RIGHTMOST dot. brother_ql
|
* head, so the first transmitted bit is the RIGHTMOST dot. brother_ql achieves
|
||||||
* achieves this by mirroring the image before packing; we do the same thing
|
* this by mirroring the image before packing; we do the same thing directly
|
||||||
* directly with an index flip, which avoids materialising a mirrored copy.
|
* with an index flip, which avoids materialising a mirrored copy.
|
||||||
*
|
*
|
||||||
* Get this wrong and every label prints mirrored. See the byte-order tests.
|
* Get this wrong and every label prints mirrored. See the byte-order tests.
|
||||||
*
|
*
|
||||||
@@ -154,13 +159,24 @@ const CMD = {
|
|||||||
statusRequest: () => Buffer.from([0x1b, 0x69, 0x53]), // ESC i S
|
statusRequest: () => Buffer.from([0x1b, 0x69, 0x53]), // ESC i S
|
||||||
switchToRaster: () => Buffer.from([0x1b, 0x69, 0x61, 0x01]), // ESC i a 1
|
switchToRaster: () => Buffer.from([0x1b, 0x69, 0x61, 0x01]), // ESC i a 1
|
||||||
|
|
||||||
|
/** ESC i ! — automatic status notification. 0 = notify, 1 = stay quiet. */
|
||||||
|
statusNotification: (notify) =>
|
||||||
|
Buffer.from([0x1b, 0x69, 0x21, notify ? 0x00 : 0x01]),
|
||||||
|
|
||||||
/** ESC i z — media type, width, length, raster count, page flag. */
|
/** ESC i z — media type, width, length, raster count, page flag. */
|
||||||
mediaAndQuality({ media, rasterLines, firstPage, highQuality }) {
|
mediaAndQuality({ media, rasterLines, firstPage, highQuality, twoColour }) {
|
||||||
let flags = 0x80; // "recover" bit, always set
|
let flags = 0x80; // printer recovery always on
|
||||||
flags |= 1 << 1; // media type valid
|
flags |= 0x02; // media type valid
|
||||||
flags |= 1 << 2; // media width valid
|
flags |= 0x04; // media width valid
|
||||||
flags |= 1 << 3; // media length valid
|
|
||||||
if (highQuality) flags |= 1 << 6;
|
// Only meaningful for die-cut stock. Brother's own driver sends 0x86 for
|
||||||
|
// continuous tape — no length bit — and the manual warns that asserting a
|
||||||
|
// valid flag the loaded media doesn't match returns a "replace media"
|
||||||
|
// error (error information 2, bit 0).
|
||||||
|
if (media.dieCut) flags |= 0x08;
|
||||||
|
|
||||||
|
// The manual marks the print-quality bit as invalid for two-colour work.
|
||||||
|
if (highQuality && !twoColour) flags |= 0x40;
|
||||||
|
|
||||||
const buf = Buffer.alloc(13);
|
const buf = Buffer.alloc(13);
|
||||||
buf[0] = 0x1b;
|
buf[0] = 0x1b;
|
||||||
@@ -170,6 +186,8 @@ const CMD = {
|
|||||||
buf[4] = media.dieCut ? 0x0b : 0x0a;
|
buf[4] = media.dieCut ? 0x0b : 0x0a;
|
||||||
buf[5] = media.widthMm & 0xff;
|
buf[5] = media.widthMm & 0xff;
|
||||||
buf[6] = media.dieCut ? media.lengthMm & 0xff : 0x00;
|
buf[6] = media.dieCut ? media.lengthMm & 0xff : 0x00;
|
||||||
|
// For two-colour, one raster line is the whole 186-byte packet
|
||||||
|
// ('w' 01 90 <90> 'w' 02 90 <90>), so this stays a line count either way.
|
||||||
buf.writeUInt32LE(rasterLines >>> 0, 7);
|
buf.writeUInt32LE(rasterLines >>> 0, 7);
|
||||||
buf[11] = firstPage ? 0x00 : 0x01;
|
buf[11] = firstPage ? 0x00 : 0x01;
|
||||||
buf[12] = 0x00;
|
buf[12] = 0x00;
|
||||||
@@ -201,7 +219,7 @@ const CMD = {
|
|||||||
return buf;
|
return buf;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** M — compression mode (bit 1 = PackBits). */
|
/** M — compression mode (2 = PackBits). Not supported on the QL-800. */
|
||||||
compression: (enabled) => Buffer.from([0x4d, enabled ? 0x02 : 0x00]),
|
compression: (enabled) => Buffer.from([0x4d, enabled ? 0x02 : 0x00]),
|
||||||
|
|
||||||
/** 0x1A ends the last page, 0x0C ends an intermediate page. */
|
/** 0x1A ends the last page, 0x0C ends an intermediate page. */
|
||||||
@@ -246,12 +264,8 @@ function buildJob(pages, options = {}) {
|
|||||||
throw new Error('buildJob requires at least one page');
|
throw new Error('buildJob requires at least one page');
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunks = [
|
// Initialisation commands, once per job (manual section 2.1).
|
||||||
CMD.switchToRaster(),
|
const chunks = [CMD.invalidate(), CMD.initialize()];
|
||||||
CMD.invalidate(),
|
|
||||||
CMD.initialize(),
|
|
||||||
CMD.switchToRaster(),
|
|
||||||
];
|
|
||||||
|
|
||||||
pages.forEach((page, index) => {
|
pages.forEach((page, index) => {
|
||||||
validatePage(page, media);
|
validatePage(page, media);
|
||||||
@@ -260,13 +274,16 @@ function buildJob(pages, options = {}) {
|
|||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
const isLast = index === pages.length - 1;
|
const isLast = index === pages.length - 1;
|
||||||
|
|
||||||
chunks.push(CMD.statusRequest());
|
// Control codes, repeated for every page, in the manual's documented order.
|
||||||
|
chunks.push(CMD.switchToRaster());
|
||||||
|
chunks.push(CMD.statusNotification(true));
|
||||||
chunks.push(
|
chunks.push(
|
||||||
CMD.mediaAndQuality({
|
CMD.mediaAndQuality({
|
||||||
media,
|
media,
|
||||||
rasterLines: page.height,
|
rasterLines: page.height,
|
||||||
firstPage: isFirst,
|
firstPage: isFirst,
|
||||||
highQuality,
|
highQuality,
|
||||||
|
twoColour,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
chunks.push(CMD.autoCut(cut));
|
chunks.push(CMD.autoCut(cut));
|
||||||
@@ -328,7 +345,8 @@ function encodeRasterData(page, compress) {
|
|||||||
|
|
||||||
planes.forEach((row, planeIndex) => {
|
planes.forEach((row, planeIndex) => {
|
||||||
const payload = compress ? packBits(row) : row;
|
const payload = compress ? packBits(row) : row;
|
||||||
// 'w' 0x01 = black plane, 'w' 0x02 = red plane, 'g' 0x00 = monochrome.
|
// 'w' 0x01 = first colour (high energy, black), 'w' 0x02 = second colour
|
||||||
|
// (low energy, red). 'g' 0x00 is the monochrome transfer.
|
||||||
const header = twoColour
|
const header = twoColour
|
||||||
? Buffer.from([0x77, planeIndex === 0 ? 0x01 : 0x02, payload.length])
|
? Buffer.from([0x77, planeIndex === 0 ? 0x01 : 0x02, payload.length])
|
||||||
: Buffer.from([0x67, 0x00, payload.length]);
|
: Buffer.from([0x67, 0x00, payload.length]);
|
||||||
@@ -347,6 +365,7 @@ function buildStatusRequest() {
|
|||||||
export {
|
export {
|
||||||
PIXEL_WIDTH,
|
PIXEL_WIDTH,
|
||||||
BYTES_PER_ROW,
|
BYTES_PER_ROW,
|
||||||
|
INVALIDATE_BYTES,
|
||||||
MIN_LENGTH_DOTS,
|
MIN_LENGTH_DOTS,
|
||||||
MAX_LENGTH_DOTS,
|
MAX_LENGTH_DOTS,
|
||||||
MEDIA,
|
MEDIA,
|
||||||
|
|||||||
Reference in New Issue
Block a user