UNPKG

@seiton/printer

Version:

Driver para las impresoras Seiton, incluye todas las funciones basicas para poder imprimir tickets

769 lines (768 loc) 35.1 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Seiton = void 0; const fs = __importStar(require("fs")); const net = __importStar(require("net")); const sharp_1 = __importDefault(require("sharp")); const usb_1 = require("usb"); const commands_enum_1 = require("./enum/commands.enum"); const image_alignment_enum_1 = require("./enum/image-alignment.enum"); class Seiton { config; ticket = Buffer.alloc(0); lastOperation = Promise.resolve(); /** * Inicializa una nueva instancia de la clase Seiton para control de impresoras termicas. * * @param config - Configuración para la conexion con la impresora * @throws {Error} Si la configuracion es invalida o incompleta * * @example * // Configuracion para conexión USB en Windows * const seiton = new Seiton({ * usb: { * windows: { * vendorId: SEITON_VID.V2, * productId: SEITON_PID.V2 * } * } * }); * * @example * // Configuracion para conexion USB en Linux * const seiton = new Seiton({ * usb: { * linux: { * devicePath: '/dev/usb/lp0' * } * } * }); * * @example * // Configuracion para conexion Ethernet * const seiton = new Seiton({ * ethernet: { * ip: '192.168.1.100', * port: 9100 * } * }); * */ constructor(config) { this.config = config; this.validarConfiguracion(); this.setDefaultStyles(); } validarConfiguracion() { // Validamos que exista la configuración if (!this.config) { throw new Error('La configuración es requerida para instanciar la clase Seiton'); } // Validamos que se proporcione al menos una configuración (USB o Ethernet) if (!this.config.usb && !this.config.ethernet) { throw new Error('Debe proporcionar la configuración para USB o Ethernet'); } // Si hay configuración USB, validamos sus propiedades if (this.config.usb) { // Validamos que solo se configure un sistema operativo if (this.config.usb.windows && this.config.usb.linux) { throw new Error('Solo puede configurar un sistema operativo a la vez (Windows o Linux)'); } // Validamos que se configure al menos un sistema operativo if (!this.config.usb.windows && !this.config.usb.linux) { throw new Error('Debe configurar al menos un sistema operativo para la conexión USB'); } // Validaciones específicas para Windows if (this.config.usb.windows) { if (typeof this.config.usb.windows !== 'object') { throw new Error('La configuración de Windows debe ser un objeto'); } if (!this.config.usb.windows.vendorId) { throw new Error('Para la configuración de Windows, el atributo vendorId es requerido'); } if (!this.config.usb.windows.productId) { throw new Error('Para la configuración de Windows, el atributo productId es requerido'); } // Validamos que el vendorId y productId sean números válidos en el rango de USB if (typeof this.config.usb.windows.vendorId !== 'number' || this.config.usb.windows.vendorId < 0x0000 || this.config.usb.windows.vendorId > 0xffff) { throw new Error('El vendorId debe ser un número hexadecimal válido entre 0x0000 y 0xFFFF'); } if (typeof this.config.usb.windows.productId !== 'number' || this.config.usb.windows.productId < 0x0000 || this.config.usb.windows.productId > 0xffff) { throw new Error('El productId debe ser un número hexadecimal válido entre 0x0000 y 0xFFFF'); } } // Validaciones específicas para Linux if (this.config.usb.linux) { if (typeof this.config.usb.linux !== 'object') { throw new Error('La configuración de Linux debe ser un objeto'); } if (!this.config.usb.linux.devicePath) { throw new Error('Para la configuración de Linux, el atributo devicePath es requerido'); } if (typeof this.config.usb.linux.devicePath !== 'string') { throw new Error('El atributo devicePath debe ser una cadena de texto'); } if (this.config.usb.linux.devicePath.trim() === '') { throw new Error('El atributo devicePath no puede estar vacío'); } } } // Si hay configuración Ethernet, validamos sus propiedades if (this.config.ethernet) { if (typeof this.config.ethernet !== 'object') { throw new Error('La configuración de Ethernet debe ser un objeto'); } if (!this.config.ethernet.ip) { throw new Error('Para la configuración de Ethernet, el atributo ip es requerido'); } if (typeof this.config.ethernet.ip !== 'string' || this.config.ethernet.ip.trim() === '') { throw new Error('La dirección IP debe ser una cadena de texto válida'); } if (!this.config.ethernet.port) { throw new Error('Para la configuración de Ethernet, el atributo port es requerido'); } if (typeof this.config.ethernet.port !== 'number' || this.config.ethernet.port < 1 || this.config.ethernet.port > 65535) { throw new Error('El puerto debe ser un número entre 1 y 65535'); } } } /** * Agrega un texto al ticket actual, agregando automaticamente el salto de linea al final. * * @param text - El texto a agregar al ticket. * @returns La instancia actual de Seiton para continuar armando el ticket */ addText(text) { return this.chain(() => { const mappedText = this.mapSpecialCharacters(text); const textBuffer = Buffer.from(`${mappedText}\n`, 'binary'); this.ticket = Buffer.concat([this.ticket, textBuffer]); }); } /** * Restablece los estilos predeterminados de la impresora a los valores por defecto. * * Cualquier texto/imagen/codigo de barras/codigo QR agregado despues de esta llamada se verá afectado por los estilos predeterminados. * * @returns La instancia actual de Seiton para continuar armando el ticket */ setDefaultStyles() { return this.chain(() => { const setDefaultStylesCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.INICIALIZAR_IMPRESORA, 'binary'); this.ticket = Buffer.concat([this.ticket, setDefaultStylesCommand]); }); } /** * Alinea todo el contenido a ser insertado **despues** de la llamada a esta funcion a la derecha. * @returns La instancia actual de Seiton para continuar armando el ticket */ alignToRight() { return this.chain(() => { const alignRightCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.ALINEACION_DERECHA, 'binary'); this.ticket = Buffer.concat([this.ticket, alignRightCommand]); }); } /** * Centra todo el contenido a ser insertado **despues** de la llamada a esta funcion. * @returns La instancia actual de Seiton para continuar armando el ticket */ alignToCenter() { return this.chain(() => { const alignCenterCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.ALINEACION_CENTRADA, 'binary'); this.ticket = Buffer.concat([this.ticket, alignCenterCommand]); }); } /** * Avanza el papel una línea. * @returns La instancia actual de Seiton para continuar armando el ticket */ feedLine() { return this.chain(() => { const lineFeedCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.AVANCE_DE_LINEA, 'binary'); this.ticket = Buffer.concat([this.ticket, lineFeedCommand]); }); } /** * Avanza el papel un número específico de líneas. * @param {number} lines Número de líneas a avanzar. * @returns La instancia actual de Seiton para continuar armando el ticket */ feedLines(lines) { return this.chain(() => { const feedLinesCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.AVANCE_DE_LINEAS + String.fromCharCode(lines)); this.ticket = Buffer.concat([this.ticket, feedLinesCommand]); }); } /** * Activa la cuchilla de la impresora para cortar el papel. * @returns La instancia actual de Seiton para continuar armando el ticket */ cutPaper() { return this.chain(() => { const cutCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.CORTAR_PAPEL, 'binary'); this.ticket = Buffer.concat([this.ticket, cutCommand]); }); } /** * Avanza el papel una linea y luego lo corta. * @returns La instancia actual de Seiton para continuar armando el ticket */ feedAndCutPaper() { return this.chain(() => { const feedAndCutPaperCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.AVANCE_Y_CORTE_PAPEL, 'binary'); this.ticket = Buffer.concat([this.ticket, feedAndCutPaperCommand]); }); } /** * Aplica el formato de texto doble alto y doble ancho. * @returns La instancia actual de Seiton para continuar armando el ticket */ doubleTallDoubleWide() { return this.chain(() => { const doubleTallDoubleWideCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.TAMANO_DOBLE_ANCHO_Y_DOBLE_ALTURA, 'binary'); this.ticket = Buffer.concat([this.ticket, doubleTallDoubleWideCommand]); }); } /** * Aplica el formato de texto doble alto. * @returns La instancia actual de Seiton para continuar armando el ticket */ doubleTall() { return this.chain(() => { const doubleTallCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.TAMANO_DOBLE_ALTURA, 'binary'); this.ticket = Buffer.concat([this.ticket, doubleTallCommand]); }); } /** * Aplica el formato de texto doble ancho. * @returns La instancia actual de Seiton para continuar armando el ticket */ doubleWide() { return this.chain(() => { const doubleWideCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.TAMANO_DOBLE_ANCHO, 'binary'); this.ticket = Buffer.concat([this.ticket, doubleWideCommand]); }); } /** * Aplica el formato de texto doble alto y negrita. * @returns La instancia actual de Seiton para continuar armando el ticket */ doubleTallBold() { return this.chain(() => { const doubleTallBoldCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.DOBLE_ALTO_NEGRITA, 'binary'); this.ticket = Buffer.concat([this.ticket, doubleTallBoldCommand]); }); } /** * Activa el buzzer de la impresora y hace que emita un número especificado de pitidos * con una duración determinada para cada uno. * * @param {number} beepCount Número de pitidos a emitir (1-8). * @param {number} beepMillis Duración de cada pitido (1-10). * @returns La instancia actual de Seiton para continuar armando el ticket */ posBeep(beepCount, beepMillis) { return this.chain(() => { if (beepCount < 1 || beepCount > 8) { this.inicializarBuffer(); throw new Error('La cantidad de pitidos debe estar entre 1 y 8.'); } if (beepMillis < 1 || beepMillis > 10) { this.inicializarBuffer(); throw new Error('La duración del pitido debe estar entre 1 y 10.'); } const beepCommand = Buffer.from([...Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.POS_BEEP), beepCount, beepMillis]); this.ticket = Buffer.concat([this.ticket, beepCommand]); }); } /** * Genera un código QR de tamaño y contenido configurable. * * @param {string} content - El contenido que se almacenará en el código QR. * @param {number} size - Tamaño del módulo del QR (valor entre 1 y 16). * @throws {Error} Si el tamaño está fuera del rango permitido (1-16). * @returns {Seiton} La instancia actual de Seiton para continuar armando el ticket * * @example * // Generar un código QR de tamaño 4 con el contenido "Hola Mundo" * addQRCode("Hola Mundo", 4); */ addQRCode(content, size) { return this.chain(() => { if (size < 1 || size > 16) { this.inicializarBuffer(); throw new Error('El tamaño del codigo QR debe estar entre 1 y 16.'); } const sizeCommand = Buffer.from([ ...Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_INICIO, 'ascii'), ...Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_PARAM_LONGITUD_MODULO, 'ascii'), ...Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_CONFIG_MODULO, 'ascii'), size, ]); const storeCommand = Buffer.concat([ Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_INICIO, 'ascii'), // Calcula la longitud total del contenido (datos QR) más los 3 bytes adicionales requeridos por el comando QR. // La longitud se divide en dos bytes: el primer byte contiene los 8 bits menos significativos (& 0xff), // y el segundo byte contiene los 8 bits más significativos (>> 8) para representar el valor como un entero de 16 bits. Buffer.from([(content.length + 3) & 0xff, ((content.length + 3) >> 8) & 0xff]), Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_ALMACENAR_DATOS, 'ascii'), Buffer.from(content, 'utf8'), ]); const printCommand = Buffer.from([ ...Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_INICIO, 'ascii'), ...Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_PARAM_LONGITUD_MODULO, 'ascii'), ...Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.QR_IMPRIMIR, 'ascii'), ]); const qrCommand = Buffer.concat([sizeCommand, storeCommand, printCommand]); this.ticket = Buffer.concat([this.ticket, qrCommand]); }); } /** * Genera un código de barras CODE128. * Permite ajustar el tamaño del código de barras (ancho y altura), así como el ancho de las barras. * @param barcodeData - El contenido del código de barras en formato ASCII. * @param width - El ancho del código de barras (1 a 6). * @param height - La altura del código de barras (1 a 255). * @returns La instancia actual de Seiton para continuar armando el ticket. */ addBarcode(barcodeData, width = 3, height = 100) { return this.chain(() => { if (!barcodeData) { this.inicializarBuffer(); throw new Error('El contenido del código de barras no puede estar vacío.'); } if (width < 1 || width > 6) { this.inicializarBuffer(); throw new Error('El ancho del código de barras debe estar entre 1 y 6.'); } if (height < 1 || height > 255) { this.inicializarBuffer(); throw new Error('La altura del código de barras debe estar entre 1 y 255.'); } // Configurar ancho this.ticket = Buffer.concat([this.ticket, Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.ANCHO_CODIGO_BARRAS), Buffer.from([width])]); // Configurar altura this.ticket = Buffer.concat([this.ticket, Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.ALTO_CODIGO_BARRAS), Buffer.from([height])]); // Configurar fuente HRI this.ticket = Buffer.concat([this.ticket, Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.FUENTE_TEXTO_BARCODE)]); // Imprimir código de barras CODE39 const barcodeBytes = Buffer.from(barcodeData.toUpperCase(), 'ascii'); this.ticket = Buffer.concat([ this.ticket, Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.INICIA_CODIGO_BARRAS_CODE39), barcodeBytes, Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.FIN_CODIGO_BARRAS), ]); }); } /** * Método para imprimir la página de autodiagnóstico de la impresora. * @returns La instancia actual de Seiton para continuar armando el ticket */ printSelfTest() { return this.chain(() => { const selfTestCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.PRINT_SELF_TEST, 'binary'); this.ticket = Buffer.concat([this.ticket, selfTestCommand]); }); } /** * Agrega una imagen al ticket. La imagen puede ser redimensionada según los parámetros especificados. * @param imagePath - Ruta al archivo de imagen * @param width - Ancho deseado de la imagen (opcional, por defecto usa el ancho de la impresora 384) * @param height - Alto deseado de la imagen (opcional, mantiene la proporción si no se especifica) * @param alignment - Alineación de la imagen (opcional, por defecto es CENTER = 2, LEFT = 0, RIGHT = 1) * @returns La instancia actual de Seiton para continuar armando el ticket */ addImage(imagePath, width, height, alignment = image_alignment_enum_1.IMAGE_ALIGNMENT.CENTER) { return this.chain(async () => { try { const printerWidth = 384; // Ancho común en impresoras térmicas const resizeWidth = width || printerWidth; const resizeHeight = height || null; // null mantiene el aspect ratio const buffer = await (0, sharp_1.default)(imagePath).resize(resizeWidth, resizeHeight).grayscale().png().toBuffer(); const { PNG } = require('pngjs'); const png = PNG.sync.read(buffer); const printerBuffer = this.printImageBuffer(png.width, png.height, png.data); const alignmentCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.ALINEACION + String.fromCharCode(alignment)); this.ticket = Buffer.concat([ this.ticket, Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.INICIALIZAR_IMPRESORA, 'binary'), alignmentCommand, printerBuffer, Buffer.from('\n', 'binary'), ]); } catch (error) { console.error('Error detallado:', error); this.inicializarBuffer(); throw new Error(`Error al procesar la imagen: ${error}`); } }); } /** * Mapea los caracteres especiales a sus códigos de impresora * @param text Texto a mapear * @returns texto con caracteres especiales mapeados */ mapSpecialCharacters(text) { const specialChars = { á: '\xA0', é: '\x82', í: '\xA1', ó: '\xA2', ú: '\xA3', ñ: '\xA4', Ñ: '\xA5', }; return text .split('') .map((char) => specialChars[char] || char) .join(''); } /** * Abre el cajón de dinero conectado a la impresora * @returns La instancia actual de Seiton para continuar armando el ticket */ openCashDrawer() { return this.chain(() => { this.ticket = Buffer.concat([this.ticket, Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.ABRIR_CAJON)]); }); } /** * Aplica el formato de texto en negrita. * @returns La instancia actual de Seiton para continuar armando el ticket */ enableBold() { return this.chain(() => { const boldCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.HABILITAR_NEGRITA, 'binary'); this.ticket = Buffer.concat([this.ticket, boldCommand]); }); } /** * Desactiva el formato de texto en negrita. * @returns La instancia actual de Seiton para continuar armando el ticket */ disableBold() { return this.chain(() => { const boldOffCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.DESHABILITAR_NEGRITA, 'binary'); this.ticket = Buffer.concat([this.ticket, boldOffCommand]); }); } /** * Aplica el formato de texto subrayado. * @returns La instancia actual de Seiton para continuar armando el ticket */ enableUnderline() { return this.chain(() => { const underlineCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.HABILITAR_SUBRAYADO, 'binary'); this.ticket = Buffer.concat([this.ticket, underlineCommand]); }); } /** * Desactiva el formato de texto subrayado. * @returns La instancia actual de Seiton para continuar armando el ticket */ disableUnderline() { return this.chain(() => { const underlineOffCommand = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.DESHABILITAR_SUBRAYADO, 'binary'); this.ticket = Buffer.concat([this.ticket, underlineOffCommand]); }); } printImageBuffer(width, height, data) { let outputBuffer = Buffer.from([0x1d, 0x76, 0x30, 48]); // Get pixel rgba in 2D array const pixels = []; for (let i = 0; i < height; i++) { const line = []; for (let j = 0; j < width; j++) { const idx = (width * i + j) << 2; line.push({ r: data[idx], g: data[idx + 1], b: data[idx + 2], a: data[idx + 3], }); } pixels.push(line); } const imageBufferArray = []; for (let i = 0; i < height; i++) { for (let j = 0; j < Math.ceil(width / 8); j++) { let byte = 0x0; for (let k = 0; k < 8; k++) { let pixel = pixels[i][j * 8 + k]; // Image overflow if (pixel === undefined) { pixel = { a: 0, r: 0, g: 0, b: 0, }; } if (pixel.a > 126) { // checking transparency const grayscale = Number(0.2126 * pixel.r + 0.7152 * pixel.g + 0.0722 * pixel.b); if (grayscale < 128) { // checking color const mask = 1 << (7 - k); // setting bitwise mask byte |= mask; // setting the correct bit to 1 } } } imageBufferArray.push(byte); } } const imageBuffer = Buffer.from(imageBufferArray); // Check if width/8 is decimal if (width % 8 != 0) { width += 8; } outputBuffer = Buffer.concat([outputBuffer, Buffer.from([(width >> 3) & 0xff])]); outputBuffer = Buffer.concat([outputBuffer, Buffer.from([0x00])]); outputBuffer = Buffer.concat([outputBuffer, Buffer.from([height & 0xff])]); outputBuffer = Buffer.concat([outputBuffer, Buffer.from([(height >> 8) & 0xff])]); // append data outputBuffer = Buffer.concat([outputBuffer, imageBuffer]); return outputBuffer; } /** * Verifica si la impresora está conectada por USB o Red en base a la configuracion de la clase Seiton. * @returns {boolean} true si la impresora está conectada, false si no lo está. */ async isPrinterConnected() { try { if (this.config.usb) { this.ticket = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.STATUS_REQUEST, 'binary'); this.enviarComandosAImpresora(); return true; } if (this.config.ethernet) { return this.isHostConnected(this.config.ethernet.ip, this.config.ethernet.port); } return false; } catch (error) { return false; } } /** * Configura la impresora para obtener una dirección IP dinámica mediante DHCP. * @returns {Seiton} La instancia actual de Seiton para permitir el encadenamiento de métodos. */ setDHCP() { return this.chain(() => { const command = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.CONFIGURAR_IP_DINAMICA, 'binary'); this.ticket = Buffer.concat([this.ticket, command]); }); } /** * Configura la impresora con una dirección IP estática. * Este método envía el comando necesario para establecer una IP fija, máscara de subred, * puerta de enlace y servidores DNS en la impresora. * * @param {string} ip - La dirección IP estática a asignar. * @param {string} mask - La máscara de subred asociada a la IP. * @param {string} gateway - La dirección de la puerta de enlace. * @param {string} dns1 - La dirección del primer servidor DNS. * @param {string} dns2 - La dirección del segundo servidor DNS. * @returns {Seiton} La instancia actual de Seiton para permitir el encadenamiento de métodos. * @throws {Error} Si alguna de las direcciones no tiene 4 octetos. */ setStaticIP(ip, mask, gateway, dns1, dns2) { return this.chain(() => { // Parsear y validar las direcciones IP const ipParts = this.parseAndValidateIP(ip); const maskParts = this.parseAndValidateIP(mask); const gatewayParts = this.parseAndValidateIP(gateway); const dns1Parts = this.parseAndValidateIP(dns1); const dns2Parts = this.parseAndValidateIP(dns2); const header = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.CABECERA_IP_FIJA, 'binary'); const config = Buffer.from(commands_enum_1.PRINTER_COMMANDS_ESC_POS.CONFIGURAR_IP_FIJA, 'binary'); const allParts = [...ipParts, ...maskParts, ...gatewayParts, ...dns1Parts, ...dns2Parts]; const checksum = this.calculateChecksum([...config, ...allParts]); const command = Buffer.concat([header, config, Buffer.from(allParts), Buffer.from([checksum])]); this.ticket = Buffer.concat([this.ticket, command]); }); } parseAndValidateIP(ip) { const parts = ip.split('.').map((part) => parseInt(part)); if (parts.length !== 4) { throw new Error('Todas las direcciones deben tener 4 octetos'); } return parts; } calculateChecksum(parts) { return parts.reduce((xor, part) => xor ^ part, 0x1f ^ 0x49); } /** * Imprime el ticket en el puerto USB especificado en el constructor de la clase. * @throws Lanzará un error si el ticket no se puede imprimir. */ async print() { try { // Esperamos a que se complete la última operación await this.lastOperation; // Enviamos los comandos a la impresora this.enviarComandosAImpresora(); } catch (error) { console.error(`Ocurrió un error al intentar imprimir el ticket: ${error}`); this.inicializarBuffer(); throw error; } } chain(operation) { this.lastOperation = this.lastOperation.then(async () => { await operation(); }); return this; } inicializarBuffer() { this.ticket = Buffer.alloc(0); this.lastOperation = Promise.resolve(); } enviarComandosAImpresora() { if (this.config.usb) { if (this.config.usb.linux) { try { fs.writeFileSync(this.config.usb.linux.devicePath, this.ticket, { encoding: 'binary' }); this.inicializarBuffer(); return; } catch (error) { console.error('No se pudo encontrar la impresora en Linux'); this.inicializarBuffer(); throw error; } } if (this.config.usb.windows) { try { let outEndpoint; let counter = 0; const device = (0, usb_1.findByIds)(this.config.usb.windows.vendorId, this.config.usb.windows.productId); if (!device) { this.inicializarBuffer(); throw new Error(`No se encontró la impresora con Vendor ID ${this.config.usb.windows.vendorId} y Product ID ${this.config.usb.windows.productId} conectada por USB`); } device.open(); if (!device.interfaces) { this.inicializarBuffer(); throw new Error(`No se encontraron interfaces en la impresora con Vendor ID ${this.config.usb.windows.vendorId} y Product ID ${this.config.usb.windows.productId} conectada por USB`); } for (const usbInterface of device.interfaces) { try { usbInterface.claim(); outEndpoint = usbInterface.endpoints.find((endpoint) => endpoint.direction === 'out'); if (++counter === device.interfaces.length && !outEndpoint) { this.inicializarBuffer(); throw new Error(`No se encontro el endpoint para imprimir en la impresora con Vendor ID ${this.config.usb.windows.vendorId} y Product ID ${this.config.usb.windows.productId}`); } } catch (error) { this.inicializarBuffer(); throw new Error(`No se pudo obtener acceso a la impresora con Vendor ID ${this.config.usb.windows.vendorId} y Product ID ${this.config.usb.windows.productId}, puede que este ocupada por otro proceso`); } } outEndpoint.transfer(this.ticket); this.inicializarBuffer(); } catch (error) { console.error(`No se pudo encontrar la impresora en Windows con Vendor ID ${this.config.usb.windows.vendorId} y Product ID ${this.config.usb.windows.productId}`); this.inicializarBuffer(); throw error; } return; } } if (this.config.ethernet) { const impresora = new net.Socket(); const desconectarImpresora = () => { try { impresora.destroy(); } catch (err) { // Se ignora cualquier error en caso de que la conexion ya este cerrada. } }; impresora.on('error', (error) => { desconectarImpresora(); console.error(`Error de conexión con la impresora: ${error.message}`); }); impresora.on('timeout', () => { desconectarImpresora(); console.error('Tiempo de espera agotado al conectar con la impresora'); }); impresora.on('connect', () => { try { impresora.write(this.ticket); this.inicializarBuffer(); impresora.end(); } catch (error) { desconectarImpresora(); console.error(`Error al enviar comando de impresion a la impresora: ${JSON.stringify(error)}`); } }); impresora.setTimeout(5000); // Timeout de 5 segundos impresora.connect({ host: this.config.ethernet.ip, port: this.config.ethernet.port }); } } async isHostConnected(host, port) { const timeout = 2000; const socket = new net.Socket(); return new Promise((resolve) => { socket.on('connect', () => { socket.destroy(); resolve(true); }); socket.on('error', () => { socket.destroy(); resolve(false); }); socket.setTimeout(timeout); socket.on('timeout', () => { socket.destroy(); resolve(false); }); socket.connect(port, host); }); } } exports.Seiton = Seiton;