Last active
November 26, 2025 17:40
-
-
Save emibloque/d57a44c7a149e31e8f9e50b685b9e9e1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // epos2escpos.js | |
| const https = require('https'); | |
| const fs = require('fs'); | |
| const xml2js = require('xml2js'); | |
| const qz = require('qz-tray'); | |
| const WebSocket = require('ws'); | |
| const crypto = require('crypto'); | |
| // --- QZ setup --- | |
| qz.api.setWebSocketType(WebSocket); | |
| qz.api.setPromiseType(r => new Promise(r)); | |
| qz.api.setSha256Type(data => crypto.createHash('sha256').update(data).digest('hex')); | |
| const PRINTER_NAME = "Ticket"; // adjust to your QZ printer name | |
| async function ensureQz() { | |
| if (!qz.websocket.isActive()) { | |
| await qz.websocket.connect(); | |
| } | |
| } | |
| async function sendEscpos(buffer) { | |
| await ensureQz(); | |
| const cfg = qz.configs.create(PRINTER_NAME, { encoding: "binary" }); | |
| const data = [{ type: 'raw', format: 'base64', data: buffer.toString('base64') }]; | |
| return qz.print(cfg, data); | |
| } | |
| // ESC/POS building helpers | |
| function textLine(str) { | |
| return Buffer.from(str + "\n", 'utf8'); | |
| } | |
| function alignCmd(mode) { | |
| // mode: 0=left,1=center,2=right | |
| return Buffer.from([0x1B, 0x61, mode]); | |
| } | |
| function drawerPulse() { | |
| return Buffer.from([0x1B, 0x70, 0x00, 0x3C, 0xFF]); | |
| } | |
| function cut() { | |
| // Full cut | |
| return Buffer.from([0x1D, 0x56, 0x41, 0x00]); | |
| } | |
| function init() { | |
| return Buffer.from([0x1B, 0x40]); | |
| } | |
| function imageCmd(width, height, rasterBuf) { | |
| // ESC/POS: GS v 0 m xL xH yL yH d1..dk | |
| const bytesPerRow = Math.ceil(width / 8); | |
| const xL = bytesPerRow & 0xFF; | |
| const xH = (bytesPerRow >> 8) & 0xFF; | |
| const yL = height & 0xFF; | |
| const yH = (height >> 8) & 0xFF; | |
| if (rasterBuf.length < bytesPerRow * height) { | |
| console.warn("[IMAGE] Raster data shorter than expected:", rasterBuf.length, | |
| "expected at least", bytesPerRow * height); | |
| } | |
| const header = Buffer.from([0x1D, 0x76, 0x30, 0x00, xL, xH, yL, yH]); | |
| return Buffer.concat([header, rasterBuf]); | |
| } | |
| // Translate ePOS-XML object → ESC/POS buffer | |
| function eposXmlToEscpos(xmlObj) { | |
| let bufParts = []; | |
| bufParts.push(init()); // ESC @ | |
| let foundSomething = false; | |
| // ------------- SOAP Envelope Extraction ------------- | |
| let root = null; | |
| try { | |
| const env = xmlObj["s:Envelope"] || xmlObj["Envelope"]; | |
| if (env && env["s:Body"]) { | |
| const body = env["s:Body"][0]; | |
| if (body["epos-print"]) { | |
| root = body["epos-print"][0]; | |
| console.log("[XML] Found <epos-print> inside SOAP envelope"); | |
| } | |
| } | |
| } catch (e) { | |
| console.error("[XML] SOAP parsing error:", e); | |
| } | |
| if (!root) { | |
| console.error("[XML] Could not locate <epos-print> root. Parsed object:", JSON.stringify(xmlObj, null, 2)); | |
| return Buffer.concat(bufParts); | |
| } | |
| // ------------- Recursive XML Walker ------------- | |
| function walk(node, nodeName = "") { | |
| if (typeof node === "string") { | |
| // raw text node | |
| console.log("[XML] TEXT:", JSON.stringify(node)); | |
| bufParts.push(textLine(node.trimEnd())); | |
| foundSomething = true; | |
| return; | |
| } | |
| if (Array.isArray(node)) { | |
| node.forEach(n => walk(n, nodeName)); | |
| return; | |
| } | |
| if (typeof node !== "object") return; | |
| for (const key in node) { | |
| const val = node[key]; | |
| console.log("[XML] tag:", key); | |
| switch (key) { | |
| // -------- TEXT -------- | |
| case "text": | |
| const textNode = val[0]; | |
| const attrs = textNode.$ || {}; | |
| const alignVal = attrs.align; | |
| if (alignVal) { | |
| let mode = 0; | |
| if (alignVal === "center") mode = 1; | |
| else if (alignVal === "right") mode = 2; | |
| console.log("[XML] ALIGN:", alignVal); | |
| bufParts.push(alignCmd(mode)); // ESC a x | |
| } | |
| // text value | |
| const textContent = (typeof textNode === "string") | |
| ? textNode | |
| : textNode._ || ""; // xml2js stores inner text in "._" | |
| bufParts.push(textLine(textContent.replace(/ /g, "\n"))); | |
| foundSomething = true; | |
| break; | |
| // -------- FEED -------- | |
| case "feed": | |
| const feedAttr = val[0].$ || {}; | |
| const count = parseInt(feedAttr.line || "1", 10); | |
| console.log("[XML] FEED lines:", count); | |
| for (let i = 0; i < count; i++) bufParts.push(textLine("")); | |
| foundSomething = true; | |
| break; | |
| // -------- CUT -------- | |
| case "cut": | |
| console.log("[XML] CUT"); | |
| bufParts.push(cut()); | |
| foundSomething = true; | |
| break; | |
| // -------- DRAWER -------- | |
| case "drawer": | |
| console.log("[XML] DRAWER"); | |
| bufParts.push(drawerPulse()); | |
| foundSomething = true; | |
| break; | |
| case "image": | |
| const imgNode = val[0]; | |
| const attrs = imgNode.$ || {}; | |
| const w = parseInt(attrs.width || "0", 10); | |
| const h = parseInt(attrs.height || "0", 10); | |
| // xml2js stores inner text as "._" or as string | |
| let base64 = ""; | |
| if (typeof imgNode === "string") { | |
| base64 = imgNode; | |
| } else if (typeof imgNode._ === "string") { | |
| base64 = imgNode._; | |
| } | |
| // remove whitespace & decode | |
| base64 = base64.replace(/\s+/g, ""); | |
| if (!w || !h || !base64) { | |
| console.warn("[IMAGE] Missing width/height or data, skipping."); | |
| break; | |
| } | |
| console.log("[IMAGE] width:", w, "height:", h, "data len:", base64.length); | |
| const raster = Buffer.from(base64, "base64"); | |
| bufParts.push(imageCmd(w, h, raster)); | |
| foundSomething = true; | |
| break; | |
| default: | |
| // dive deeper into nested body | |
| if (Array.isArray(val) || typeof val === "object") { | |
| walk(val, key); | |
| } | |
| } | |
| } | |
| } | |
| walk(root); | |
| const finalBuf = Buffer.concat(bufParts); | |
| console.log("[ESC/POS] Final buffer length:", finalBuf.length, "foundSomething:", foundSomething); | |
| return finalBuf; | |
| } | |
| // --- HTTP(S) server --- | |
| const port = 9100; | |
| const options = { | |
| key: fs.readFileSync("./private.key"), | |
| cert: fs.readFileSync("./certificate.pem") | |
| }; | |
| const server = https.createServer(options, (req, res) => { | |
| if (req.method !== 'POST') { | |
| res.writeHead(405, {'Allow': 'POST'}); | |
| res.end('Only POST allowed'); | |
| return; | |
| } | |
| let body = []; | |
| req.on('data', chunk => body.push(chunk)); | |
| req.on('end', async () => { | |
| const raw = Buffer.concat(body).toString('utf8'); | |
| // parse XML | |
| xml2js.parseString(raw, (err, xmlObj) => { | |
| if (err) { | |
| console.error("Error parsing XML:", err); | |
| res.writeHead(400); | |
| res.end("Bad XML"); | |
| return; | |
| } | |
| try { | |
| const esc = eposXmlToEscpos(xmlObj); | |
| sendEscpos(esc) | |
| .then(() => { | |
| res.writeHead(200); | |
| res.end("OK"); | |
| }) | |
| .catch(e => { | |
| console.error("Print failed", e); | |
| res.writeHead(500); | |
| res.end("Print Error"); | |
| }); | |
| } catch (e) { | |
| console.error("Translate error", e); | |
| res.writeHead(500); | |
| res.end("Translate Error"); | |
| } | |
| }); | |
| }); | |
| }); | |
| server.listen(port, () => console.log(`Listening for ePOS-XML POST on port ${port}`)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // server.js | |
| const net = require('net'); | |
| const fs = require('fs'); | |
| const crypto = require('crypto'); | |
| const qz = require('qz-tray'); | |
| const RSVP = require('rsvp'); | |
| // --- Load local key & certificate ------------------------------------ | |
| const privateKey = fs.readFileSync('./private.key').toString(); | |
| const certificate = fs.readFileSync('./certificate.pem').toString(); | |
| // --- QZ security setup ----------------------------------------------- | |
| qz.api.setPromiseType(resolver => new RSVP.Promise(resolver)); | |
| qz.security.setCertificatePromise((resolve, reject) => { | |
| resolve(certificate); | |
| }); | |
| qz.security.setSignaturePromise((toSign) => { | |
| const sign = crypto.createSign("SHA512"); | |
| sign.update(toSign); | |
| return Promise.resolve(sign.sign(privateKey, "base64")); | |
| }); | |
| // --- QZ helper -------------------------------------------------------- | |
| async function ensureQz() { | |
| if (!qz.websocket.isActive()) { | |
| await qz.websocket.connect(); | |
| console.log("Connected to QZ Tray"); | |
| } | |
| } | |
| // CHANGE THIS to your printer’s name shown in QZ Tray | |
| const PRINTER_NAME = "Serial Port - COM2 @ 19200"; | |
| // Load your fixed ESC/POS binary sample | |
| const sample = fs.readFileSync('./escpos_sample.bin'); // <--- put your .bin here | |
| const sampleHex = | |
| "1b401b613143616e6173746f74612c204e592031333033320a0a68747470733a" + | |
| "2f2f717a2e696f0a0a0a4d61792031382c20323031362031303a333020414d0a" + | |
| "0a0a0a5472616e73616374696f6e202320313233343536205265676973746572" + | |
| "3a20330a0a0a0a1b613042616b6c617661202851747920342920202020202020" + | |
| "392e30301b7413c2aa0a58585858585858585858585858585858585858585858" + | |
| "58585858585858585858585858580a1b450d48657265277320736f6d6520626f" + | |
| "6c642074657874211b450a0a0a1b61321b21304452494e4b204d451b210a1b45" + | |
| "0a0a0a1b61302d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d" + | |
| "2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a1b4d31454154204d450a1b4d302d2d" + | |
| "2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d" + | |
| "2d2d2d2d2d2d2d2d0a6e6f726d616c20746578741b61300a0a0a0a0a0a0a1b69" + | |
| "1014010005"; | |
| const sampleBuffer = Buffer.from(sampleHex, "hex"); | |
| async function printSample() { | |
| await ensureQz(); | |
| const cfg = qz.configs.create(PRINTER_NAME, { encoding: "binary" }); | |
| const data = [{ | |
| type: "raw", | |
| format: "base64", | |
| data: sampleBuffer.toString("base64") | |
| }]; | |
| console.log("Printing fixed sample..."); | |
| await qz.print(cfg, data); | |
| console.log("Printed!"); | |
| } | |
| // --- TCP server listening on port 9100 ------------------------------- | |
| const server = net.createServer((socket) => { | |
| console.log("Odoo connected", socket.remoteAddress, socket.remotePort); | |
| socket.on('data', () => { | |
| // We IGNORE the ePOS data completely | |
| }); | |
| socket.on('end', async () => { | |
| try { | |
| await printSample(); | |
| } catch (e) { | |
| console.error("Print error:", e); | |
| } | |
| }); | |
| socket.on('error', (err) => { | |
| console.error("Socket error:", err); | |
| }); | |
| }); | |
| server.listen(9100, () => { | |
| console.log("Listening on TCP 9100. Every connection will print escpos_sample.bin"); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // server.js | |
| const qz = require("qz-tray"); | |
| const WebSocket = require("ws"); | |
| const crypto = require("crypto"); | |
| // 1) Override WebSocket — required in Node | |
| qz.api.setWebSocketType(WebSocket); | |
| // 2) Override Promises — native Promise | |
| qz.api.setPromiseType((resolver) => new Promise(resolver)); | |
| // 3) Override SHA-256 hashing (to satisfy QZ security) | |
| qz.api.setSha256Type((data) => { | |
| return crypto.createHash("sha256").update(data).digest("hex"); | |
| }); | |
| // 4) Try connecting, listing printers | |
| (async () => { | |
| try { | |
| await qz.websocket.connect(); | |
| const printers = await qz.printers.getDefault(); | |
| console.log("Connected to QZ Tray. Default printer:", printers); | |
| await qz.websocket.disconnect(); | |
| console.log("Disconnected QZ Tray. OK."); | |
| } catch (err) { | |
| console.error("QZ Tray error:", err); | |
| } | |
| })(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // server.js | |
| const net = require('net'); | |
| const fs = require('fs'); | |
| const qz = require('qz-tray'); | |
| const WebSocket = require('ws'); | |
| const crypto = require('crypto'); | |
| // --- QZ setup -------------------------------- | |
| qz.api.setWebSocketType(WebSocket); | |
| qz.api.setPromiseType(resolver => new Promise(resolver)); | |
| qz.api.setSha256Type(data => crypto.createHash("sha256").update(data).digest("hex")); | |
| // Printer name in QZ (update if needed) | |
| const PRINTER_NAME = "Ticket"; | |
| const sampleHex = | |
| "1b401b613143616e6173746f74612c204e592031333033320a0a68747470733a" + | |
| "2f2f717a2e696f0a0a0a4d61792031382c20323031362031303a333020414d0a" + | |
| "0a0a0a5472616e73616374696f6e202320313233343536205265676973746572" + | |
| "3a20330a0a0a0a1b613042616b6c617661202851747920342920202020202020" + | |
| "392e30301b7413c2aa0a58585858585858585858585858585858585858585858" + | |
| "58585858585858585858585858580a1b450d48657265277320736f6d6520626f" + | |
| "6c642074657874211b450a0a0a1b61321b21304452494e4b204d451b210a1b45" + | |
| "0a0a0a1b61302d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d" + | |
| "2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a1b4d31454154204d450a1b4d302d2d" + | |
| "2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d" + | |
| "2d2d2d2d2d2d2d2d0a6e6f726d616c20746578741b61300a0a0a0a0a0a0a1b69" + | |
| "1014010005"; | |
| const sampleBuffer = Buffer.from(sampleHex, "hex"); | |
| // Ensure QZ connection | |
| async function connectQz() { | |
| if (!qz.websocket.isActive()) { | |
| await qz.websocket.connect(); | |
| console.log("QZ Tray connected."); | |
| } | |
| } | |
| // Print the sample | |
| async function printSample() { | |
| await connectQz(); | |
| const cfg = qz.configs.create(PRINTER_NAME, { encoding: "binary" }); | |
| const data = [{ | |
| type: 'raw', | |
| format: 'base64', | |
| data: sampleBuffer.toString('base64'), | |
| }]; | |
| console.log("Printing sample..."); | |
| await qz.print(cfg, data); | |
| console.log("Print job sent."); | |
| } | |
| printSample(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // server.js | |
| const https = require('https'); | |
| const fs = require('fs'); | |
| const qz = require('qz-tray'); | |
| const WebSocket = require('ws'); | |
| const crypto = require('crypto'); | |
| // QZ Tray setup | |
| qz.api.setWebSocketType(WebSocket); | |
| qz.api.setPromiseType(resolver => new Promise(resolver)); | |
| qz.api.setSha256Type(data => crypto.createHash('sha256').update(data).digest('hex')); | |
| const PRINTER_NAME = "Ticket"; // or whatever your QZ printer name is | |
| const sampleHex = | |
| "1b401b613143616e6173746f74612c204e592031333033320a0a68747470733a" + | |
| "2f2f717a2e696f0a0a0a4d61792031382c20323031362031303a333020414d0a" + | |
| "0a0a0a5472616e73616374696f6e202320313233343536205265676973746572" + | |
| "3a20330a0a0a0a1b613042616b6c617661202851747920342920202020202020" + | |
| "392e30301b7413c2aa0a58585858585858585858585858585858585858585858" + | |
| "58585858585858585858585858580a1b450d48657265277320736f6d6520626f" + | |
| "6c642074657874211b450a0a0a1b61321b21304452494e4b204d451b210a1b45" + | |
| "0a0a0a1b61302d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d" + | |
| "2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a1b4d31454154204d450a1b4d302d2d" + | |
| "2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d" + | |
| "2d2d2d2d2d2d2d2d0a6e6f726d616c20746578741b61300a0a0a0a0a0a0a1b69" + | |
| "1014010005"; | |
| const sampleBuffer = Buffer.from(sampleHex, "hex"); | |
| async function ensureQz() { | |
| if (!qz.websocket.isActive()) { | |
| await qz.websocket.connect(); | |
| console.log("QZ Tray connected"); | |
| } | |
| } | |
| async function printReceipt() { | |
| await ensureQz(); | |
| const cfg = qz.configs.create(PRINTER_NAME, { encoding: "binary" }); | |
| const data = [{ type: "raw", format: "base64", data: sampleBuffer.toString("base64") }]; | |
| console.log("Sending print job…"); | |
| await qz.print(cfg, data); | |
| console.log("Print job done."); | |
| } | |
| // HTTPS server options | |
| const options = { | |
| key: fs.readFileSync("./private.key.txt"), | |
| cert: fs.readFileSync("./certificate.pem.txt") | |
| }; | |
| https.createServer(options, (req, res) => { | |
| console.log("HTTP request:", req.method, req.url); | |
| // Optionally read POST data if you want to inspect it | |
| let body = []; | |
| req.on('data', chunk => body.push(chunk)); | |
| req.on('end', async () => { | |
| const raw = Buffer.concat(body).toString('utf8'); | |
| console.log("Body:", raw); | |
| // Always print the receipt on any request | |
| try { | |
| await printReceipt(); | |
| res.writeHead(200, { "Content-Type": "text/plain" }); | |
| res.end("Printed\n"); | |
| } catch (err) { | |
| console.error("Print error:", err); | |
| res.writeHead(500, { "Content-Type": "text/plain" }); | |
| res.end("Error\n"); | |
| } | |
| }); | |
| }).listen(9100, () => { | |
| console.log("HTTPS server listening on port 9100"); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment