thermalprinter
Version:
Use node.js to communicate with Adafruit/Sparkfun Thermal Printer
514 lines (464 loc) • 13.5 kB
JavaScript
'use strict';
var util = require('util'),
EventEmitter = require('events').EventEmitter,
fs = require('fs'),
getPixels = require('get-pixels'),
deasync = require('deasync'),
async = require('async'),
helpers = require('./helpers'),
specialChars = require('./specialChars.js'),
codePage = require('./codePage').PC437;
/*
* Printer opts.
*
* maxPrintingDots = 0-255. Max heat dots, Unit (8dots), Default: 7 (64 dots)
* heatingTime = 3-255. Heating time, Unit (10us), Default: 80 (800us)
* heatingInterval = 0-255. Heating interval, Unit (10µs), Default: 2 (20µs)
*
* The more max heating dots, the more peak current will cost when printing,
* the faster printing speed. The max heating dots is 8*(n+1).
*
* The more heating time, the more density, but the slower printing speed.
* If heating time is too short, blank page may occur.
*
* The more heating interval, the more clear, but the slower printing speed.
*
* Example with default values.
*
* var Printer = require('thermalprinter'),
* opts = {
* maxPrintingDots : 7,
* heatingTime : 80,
* heatingInterval : 2,
* commandDelay: 0,
* charset: 0
* };
* var printer = new Printer(mySerialPort, opts);
*/
var Printer = function(serialPort, opts) {
EventEmitter.call(this);
// Serial port used by printer
if (!serialPort.write || !serialPort.drain) throw new Error('The serial port object must have write and drain functions');
this.serialPort = serialPort;
opts = opts || {};
// Max printing dots (0-255), unit: (n+1)*8 dots, default: 7 ((7+1)*8 = 64 dots)
this.maxPrintingDots = opts.maxPrintingDots || 7;
// Heating time (3-255), unit: 10µs, default: 80 (800µs)
this.heatingTime = opts.heatingTime || 80;
// Heating interval (0-255), unit: 10µs, default: 2 (20µs)
this.heatingInterval = opts.heatingInterval || 2;
// delay between 2 commands (in µs)
this.commandDelay = opts.commandDelay || 0;
// chinese firmware, for some reasons some thermal printers come with a firmware that don't handle all latin chars
// but we can do some hacky stuff to have almost every chars
this.chineseFirmware = opts.chineseFirmware || false;
// charset (USA: 0, by default, will switch to print special chars)
this.charset = 0;
// command queue
this.commandQueue = [];
// printmode bytes (normal by default)
this.printMode = 0;
var _self = this;
this.reset().sendPrintingParams().setCharset(this.charset).print(function() {
_self.emit('ready');
});
};
util.inherits(Printer, EventEmitter);
Printer.prototype.print = function(callback) {
var _self = this;
async.eachSeries(
_self.commandQueue,
function(command, callback) {
function write() {
_self.serialPort.write(command, function() {
_self.serialPort.drain(callback);
});
}
if (_self.commandDelay === 0) {
write();
}
else {
setTimeout(write, Math.ceil(_self.commandDelay / 1000));
}
},
function(err) {
_self.commandQueue = [];
if(callback) callback(err);
}
);
};
Printer.prototype.writeCommand = function(command) {
var buf;
if (!Buffer.isBuffer(command)) {
buf = new Buffer(1);
buf.writeUInt8(command, 0);
}
else {
buf = command;
}
this.commandQueue.push(buf);
return this;
};
Printer.prototype.writeCommands = function(commands) {
commands.forEach(function(command) {
this.writeCommand(command);
}, this);
return this;
};
Printer.prototype.reset = function() {
var commands = [27, 64];
return this.writeCommands(commands);
};
Printer.prototype.setCharset = function(code) {
this.charset = code;
var commands = [27, 82, this.charset];
return this.writeCommands(commands);
};
Printer.prototype.setCharCodeTable = function(code) {
var commands = [27, 116, code];
return this.writeCommands(commands);
};
Printer.prototype.testPage = function() {
var commands = [18, 84];
return this.writeCommands(commands);
};
Printer.prototype.hasPaper = function(callback) {
var command = new Buffer([27, 118, 0]);
var _self = this;
// waits for the printer answer
_self.serialPort.once('data', function(data) {
if (data) {
var returnCode = data.toString('utf-8');
// the return code $ means no paper
if (returnCode === '$') {
callback(false);
}
else {
callback(true);
}
}
});
_self.serialPort.write(command, function() {
_self.serialPort.drain();
});
};
Printer.prototype.sendPrintingParams = function() {
var commands = [27,55,this.maxPrintingDots, this.heatingTime, this.heatingInterval];
return this.writeCommands(commands);
};
Printer.prototype.lineFeed = function (linesToFeed) {
var commands = linesToFeed ? [27, 100, linesToFeed] : [10];
return this.writeCommands(commands);
};
Printer.prototype.addPrintMode = function(mode) {
this.printMode |= mode;
return this.writeCommands([27, 33, this.printMode]);
};
Printer.prototype.removePrintMode = function(mode) {
this.printMode &= ~mode;
return this.writeCommands([27, 33, this.printMode]);
};
Printer.prototype.bold = function (onOff) {
return onOff ? this.addPrintMode(8) : this.removePrintMode(8);
};
Printer.prototype.big = function (onOff) {
return onOff ? this.addPrintMode(56) : this.removePrintMode(56);
};
Printer.prototype.underline = function(dots){
var commands = [27, 45, dots];
return this.writeCommands(commands);
};
Printer.prototype.small = function(onOff){
var commands = [27, 33, (onOff === true ? 1 : 0)];
return this.writeCommands(commands);
};
Printer.prototype.upsideDown = function(onOff){
var commands = [27, 123, (onOff === true ? 1 : 0)];
return this.writeCommands(commands);
};
Printer.prototype.inverse = function (onOff) {
var commands = onOff ? [29, 66, 1] : [29, 66, 0];
return this.writeCommands(commands);
};
Printer.prototype.left = function () {
var commands = [27, 97, 0];
return this.writeCommands(commands);
};
Printer.prototype.right = function () {
var commands = [27, 97, 2];
return this.writeCommands(commands);
};
Printer.prototype.center = function () {
var commands = [27, 97, 1];
return this.writeCommands(commands);
};
Printer.prototype.indent = function(columns) {
if (columns < 0 || columns > 31) {
columns = 0;
}
var commands = [27, 66, columns];
return this.writeCommands(commands);
};
Printer.prototype.setLineSpacing = function(lineSpacing) {
var commands = [27, 51, lineSpacing];
return this.writeCommands(commands);
};
Printer.prototype.horizontalLine = function(length) {
var commands = [];
if (length > 0) {
if (length > 32) {
length = 32;
}
for (var i = 0; i < length; i++) {
commands.push(196);
}
commands.push(10);
}
return this.writeCommands(commands);
};
Printer.prototype.printText = Printer.prototype.addText = function (text) {
var _self = this;
var chars = text.split('');
var commands = [];
chars.forEach(function(char, index) {
// handle chinese firmaware
if (_self.chineseFirmware) {
var currentChar = specialChars[char];
// if this is a special character
if (currentChar) {
// if the current charset is the same as the one of the special character
if (currentChar.charset === _self.charset) {
commands.push(specialChars[char].code);
}
// if the current charset is different of the one of the special character
else {
var oldCharset = _self.charset;
// set the charset to the one where the special character exists
commands.push.apply(commands, [27, 82, currentChar.charset]);
// push the special character in the command queue
commands.push(specialChars[char].code);
// reset the old charset
commands.push.apply(commands, [27, 82, oldCharset]);
}
}
// we guess it is not a special character and push it to the command queue
else {
commands.push(new Buffer(char));
}
}
// handle normal firmware
else {
var charPos = codePage.indexOf(char);
// if the char is in the code table
if(charPos != -1){
// get the hex value and push it into new commands list
commands.push(charPos + 128);
}
else {
// otherwise it's probably normal text
commands.push(new Buffer(char));
}
}
});
return this.writeCommands(commands);
};
Printer.prototype.printLine = function (text) {
return this.printText(text).writeCommand(10);
};
Printer.prototype.printImage = function(path, type){
var done = false;
let params = [path]
// if we recieved a buffer
if (typeof path === 'object') {
if (type === undefined){
throw new Error('You must provide a MIME type when passing a buffer. Ex. (image/png)');
}
// add required mime type
params[1] = type
}
var _self = this;
getPixels(...params, function(err, pixels){
if(!err){
var width = pixels.shape[0];
var height = pixels.shape[1];
if (width != 384 || height > 65635) {
throw new Error('Image width must be 384px, height cannot exceed 65635px.');
}
// contruct an array of Uint8Array,
// each Uint8Array contains 384/8 pixel samples, corresponding to a whole line
var imgData = [];
for (var y = 0; y < height; y++) {
imgData[y] = new Uint8Array(width/8);
for (var x = 0; x < (width/8); x++) {
imgData[y][x] = 0;
for (var n = 0; n < 8; n++) {
var r = pixels.get(x*8+n, y, 0);
var g = pixels.get(x*8+n, y, 1);
var b = pixels.get(x*8+n, y, 2);
var brightness = helpers.rgbToHsl(r, g, b)[2];
// only print dark stuff
if (brightness < 0.6) {
imgData[y][x] += (1 << n);
}
}
}
}
// send the commands and buffers to the printer
_self.printImageData(width, height, imgData);
// tell deasync getPixels is done
done = true;
}
else {
throw new Error(err);
}
});
// deasync getPixels
while(!done) {
deasync.runLoopOnce();
}
return this;
};
Printer.prototype.printImageData =function(width, height, imgData){
if (width != 384 || height > 65635) {
throw new Error('Image width must be 384px, height cannot exceed 65635px.');
}
// send the commands and buffers to the printer
var commands = [18, 118, height & 255, height >> 8];
for (var y = 0; y < imgData.length; y++) {
var buf = helpers.uint8ArrayToBuffer(imgData[y]);
commands.push(buf);
}
this.writeCommands(commands);
return this;
};
// Barcodes
// Set barcodeTextPosition
//
// Position can be:
// 0: Not printed
// 1: Above the barcode
// 2: Below the barcode
// 3: Both above and below the barcode
Printer.prototype.barcodeTextPosition = function(pos) {
if(pos > 3 || pos < 0) {
throw new Error('Position must be 0, 1, 2 or 3');
}
var commands = [29, 72, pos];
return this.writeCommands(commands);
};
// Set barcode height
// 0 < h < 255 (default = 50)
Printer.prototype.barcodeHeight = function(h) {
if(h > 255 || h < 0) {
throw new Error('Height must be 0 < height > 255');
}
var commands = [29, 104, h];
return this.writeCommands(commands);
};
Printer.BARCODE_CHARSETS = {
NUMS: function(n) { return n >= 48 && n <= 57; },
ASCII: function(n) { return n >= 0 && n <= 127; }
};
// These are all valid barcode types.
// Pass this object to printer.barcode() as type:
// printer.barcode(Printer.BARCODE_TYPES.UPCA, 'data');
Printer.BARCODE_TYPES = {
UPCA : {
code: 65,
size: function(n) { return n === 11 || n === 12; },
chars: Printer.BARCODE_CHARSETS.NUMS
},
UPCE : {
code: 66,
size: function(n) { return n === 11 || n === 12; },
chars: Printer.BARCODE_CHARSETS.NUMS
},
EAN13 : {
code: 67,
size: function(n) { return n === 12 || n === 13; },
chars: Printer.BARCODE_CHARSETS.NUMS
},
EAN8 : {
code: 68,
size: function(n) { return n === 7 || n === 8; },
chars: Printer.BARCODE_CHARSETS.NUMS
},
CODE39 : {
code: 69,
size: function(n) { return n > 1; },
chars: function(n) {
// " $%+-./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
return (
n === 32 ||
n === 36 ||
n === 37 ||
n === 43 ||
(n >= 45 && n <= 57) ||
(n >= 65 && n <= 90)
);
}
},
I25 : {
code: 70,
size: function(n) { return n > 1 && n % 2 === 0; },
chars: Printer.BARCODE_CHARSETS.NUMS
},
CODEBAR : {
code: 71,
size: function(n) { return n > 1; },
chars: function(n) {
// "$+-./0123456789:ABCD"
return (
n === 36 ||
n === 43 ||
(n >= 45 && n <= 58) ||
(n >= 65 && n <= 68)
);
}
},
CODE93 : {
code: 72,
size: function(n) { return n > 1; },
chars: Printer.BARCODE_CHARSETS.ASCII
},
CODE128 : {
code: 73,
size: function(n) { return n > 1; },
chars: Printer.BARCODE_CHARSETS.ASCII
},
CODE11 : {
code: 74,
size: function(n) { return n > 1; },
chars: Printer.BARCODE_CHARSETS.NUMS
},
MSI : {
code: 75,
size: function(n) { return n > 1; },
chars: Printer.BARCODE_CHARSETS.NUMS
}
};
Printer.prototype.barcode = function(type, data) {
var error;
var commands = [29, 107];
commands.push(type.code);
commands.push(data.length);
// Validate size
if(!type.size(data.length)) {
error = new Error('Data length does not match specification for this type of barcode');
error.name = "invalid_data_size";
throw error;
}
// validate that the chars to be printed are supported for this type of barcode
for(var i=0; i < data.length; i++) {
var code = data.charCodeAt(i);
if(!type.chars(code)) {
error = new Error('Character ' + code + ' is not valid for this type of barcode');
error.name = "invalid_character";
error.char = code;
throw error;
}
commands.push(code);
}
return this.writeCommands(commands);
};
module.exports = Printer;