| Server IP : 173.209.174.21 / Your IP : 216.73.216.89 Web Server : Apache/2.4.58 (Ubuntu) System : Linux wcfs-server 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : nodor ( 1000) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/wcfs/NAOPOS/src/lib/ |
Upload File : |
/**
* ESC/POS printer utility for Epson TM series via WebSerial API.
* Handles receipt printing and cash drawer kick.
*/
const ESC = 0x1b;
const GS = 0x1d;
const LF = 0x0a;
// ESC/POS commands
const CMD = {
INIT: [ESC, 0x40], // Initialize printer
BOLD_ON: [ESC, 0x45, 0x01],
BOLD_OFF: [ESC, 0x45, 0x00],
ALIGN_CENTER: [ESC, 0x61, 0x01],
ALIGN_LEFT: [ESC, 0x61, 0x00],
ALIGN_RIGHT: [ESC, 0x61, 0x02],
DOUBLE_HEIGHT_ON: [GS, 0x21, 0x11], // Double width + height
DOUBLE_HEIGHT_OFF: [GS, 0x21, 0x00],
CUT_PAPER: [GS, 0x56, 0x42, 0x03], // Partial cut with feed
DRAWER_KICK_PIN2: [ESC, 0x70, 0x00, 0x19, 0x78], // Kick drawer pin 2
DRAWER_KICK_PIN5: [ESC, 0x70, 0x01, 0x19, 0x78], // Kick drawer pin 5
FEED_LINES: (n: number) => [ESC, 0x64, n],
};
const encoder = new TextEncoder();
function textBytes(text: string): number[] {
return Array.from(encoder.encode(text));
}
function padLine(left: string, right: string, width = 42): string {
const space = width - left.length - right.length;
return left + " ".repeat(Math.max(1, space)) + right;
}
function dashLine(width = 42): string {
return "-".repeat(width);
}
export interface ReceiptData {
storeName: string;
items: {
name: string;
quantity: number;
price: number;
taxRate?: number;
taxAmount?: number;
}[];
subtotal: number;
tax: number;
total: number;
paymentMethod: "cash" | "credit";
amountTendered?: number;
changeDue?: number;
timestamp: Date;
}
function buildReceiptBytes(data: ReceiptData): number[] {
const bytes: number[] = [];
const push = (...b: number[]) => bytes.push(...b);
// Initialize
push(...CMD.INIT);
// Store name (centered, bold, double)
push(...CMD.ALIGN_CENTER);
push(...CMD.BOLD_ON);
push(...CMD.DOUBLE_HEIGHT_ON);
push(...textBytes(data.storeName));
push(LF);
push(...CMD.DOUBLE_HEIGHT_OFF);
push(...CMD.BOLD_OFF);
// Date/time
push(...textBytes(data.timestamp.toLocaleDateString() + " " + data.timestamp.toLocaleTimeString()));
push(LF);
push(...CMD.ALIGN_LEFT);
push(...textBytes(dashLine()));
push(LF);
// Items
for (const item of data.items) {
const lineTotal = item.price * item.quantity;
const name = item.quantity > 1 ? `${item.name} x${item.quantity}` : item.name;
push(...textBytes(padLine(name, `$${lineTotal.toFixed(2)}`)));
push(LF);
if (item.taxRate && item.taxAmount && item.taxAmount > 0) {
push(...textBytes(padLine(` Tax ${item.taxRate}%`, `$${item.taxAmount.toFixed(2)}`)));
push(LF);
}
}
push(...textBytes(dashLine()));
push(LF);
// Totals
push(...textBytes(padLine("Subtotal", `$${data.subtotal.toFixed(2)}`)));
push(LF);
if (data.tax > 0) {
push(...textBytes(padLine("Tax", `$${data.tax.toFixed(2)}`)));
push(LF);
}
push(...CMD.BOLD_ON);
push(...CMD.DOUBLE_HEIGHT_ON);
push(...textBytes(padLine("TOTAL", `$${data.total.toFixed(2)}`)));
push(LF);
push(...CMD.DOUBLE_HEIGHT_OFF);
push(...CMD.BOLD_OFF);
push(...textBytes(dashLine()));
push(LF);
// Payment info
const methodLabel = data.paymentMethod === "cash" ? "Cash" : "Credit Card";
push(...textBytes(padLine("Payment", methodLabel)));
push(LF);
if (data.paymentMethod === "cash" && data.amountTendered !== undefined) {
push(...textBytes(padLine("Tendered", `$${data.amountTendered.toFixed(2)}`)));
push(LF);
if (data.changeDue !== undefined && data.changeDue > 0) {
push(...CMD.BOLD_ON);
push(...textBytes(padLine("Change", `$${data.changeDue.toFixed(2)}`)));
push(LF);
push(...CMD.BOLD_OFF);
}
}
// Footer
push(LF);
push(...CMD.ALIGN_CENTER);
push(...textBytes("Thank you!"));
push(LF);
push(...CMD.FEED_LINES(4));
push(...CMD.CUT_PAPER);
return bytes;
}
// ---------- Serial port management ----------
let serialPort: any = null;
let writer: any = null;
export function isWebSerialSupported(): boolean {
return "serial" in (navigator as any);
}
export async function connectPrinter(): Promise<boolean> {
if (!isWebSerialSupported()) {
throw new Error("WebSerial API is not supported in this browser. Please use Chrome or Edge.");
}
try {
// Prompt user to select the printer's serial port
serialPort = await (navigator as any).serial.requestPort();
await serialPort.open({ baudRate: 9600 });
writer = serialPort.writable!.getWriter();
return true;
} catch (err) {
console.error("Failed to connect to printer:", err);
serialPort = null;
writer = null;
return false;
}
}
export async function disconnectPrinter(): Promise<void> {
try {
if (writer) {
writer.releaseLock();
writer = null;
}
if (serialPort) {
await serialPort.close();
serialPort = null;
}
} catch (err) {
console.error("Error disconnecting printer:", err);
}
}
export function isPrinterConnected(): boolean {
return serialPort !== null && writer !== null;
}
async function sendBytes(data: Uint8Array): Promise<void> {
if (!writer) throw new Error("Printer not connected");
await writer.write(data);
}
export async function kickDrawer(): Promise<void> {
await sendBytes(new Uint8Array(CMD.DRAWER_KICK_PIN2));
}
export async function printReceipt(data: ReceiptData): Promise<void> {
const bytes = buildReceiptBytes(data);
await sendBytes(new Uint8Array(bytes));
}
export async function printReceiptAndKickDrawer(data: ReceiptData): Promise<void> {
// Kick drawer first, then print
await kickDrawer();
await printReceipt(data);
}