Skip to content

Instantly share code, notes, and snippets.

@emibloque
Last active November 26, 2025 17:40
Show Gist options
  • Select an option

  • Save emibloque/d57a44c7a149e31e8f9e50b685b9e9e1 to your computer and use it in GitHub Desktop.

Select an option

Save emibloque/d57a44c7a149e31e8f9e50b685b9e9e1 to your computer and use it in GitHub Desktop.
// 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(/&#10;/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}`));
// 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");
});
// 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);
}
})();
// 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();
// 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