From 0ca500fc01c7714c98cbe6b749067a62d1770d01 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 1 Jun 2026 22:14:01 +0700 Subject: [PATCH] perf: optimize BLE transmission throughput and fix layout truncation on basic receipts --- static/src/js/bluetooth_printer_manager.js | 37 +++++++++++----------- static/src/js/escpos_generator.js | 28 ++++++++-------- static/src/js/pos_receipt_printer.js | 13 +++++++- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/static/src/js/bluetooth_printer_manager.js b/static/src/js/bluetooth_printer_manager.js index b6aa0ca..5485280 100755 --- a/static/src/js/bluetooth_printer_manager.js +++ b/static/src/js/bluetooth_printer_manager.js @@ -455,24 +455,24 @@ export class BluetoothPrinterManager { try { this.isPrinting = true; const startTime = performance.now(); - - // OPTIMIZED: Use safe chunks depending on transmission mode - // Large chunks (e.g. 512 bytes) overflow standard BLE serial buffers, truncating long receipts. - // Graphics data is sent in highly compatible 128-byte chunks; text is sent in safe 20-byte chunks. - const isGraphicsMode = isGraphics || escposData.length > 5000; - const chunkSize = isGraphicsMode ? 128 : 20; - + + // Use 128-byte chunks for all modes — this is the standard BLE ATT MTU payload size + // and is well within the serial buffer of all common thermal printers. + // Using only 20 bytes (the BLE minimum) is 6× slower and causes connection timeouts + // on long receipts (many items), resulting in missing lines at the end. + const chunkSize = 128; + const chunks = []; for (let i = 0; i < escposData.length; i += chunkSize) { chunks.push(escposData.slice(i, i + chunkSize)); } - console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk, mode: ${isGraphicsMode ? 'graphics' : 'text'})`); + console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk)`); // Prefer write with response if available for natural BLE flow control, fallback to write without response const useWrite = this.characteristic.properties.write; const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse; - + if (!useWrite && !useWriteWithoutResponse) { throw new Error('Characteristic does not support write operations'); } @@ -483,25 +483,24 @@ export class BluetoothPrinterManager { // Send each chunk for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; - + try { if (writeWithAck) { - // Safe write with response (OS/device level acknowledgment) + // Write with response: OS provides ack, minimal sleep needed await this.characteristic.writeValue(chunk); - // Minimal safety sleep of 5ms between chunks await this._sleep(5); } else { - // Write without response (needs explicit flow control to prevent buffer overrun) + // Write without response: explicit flow control to prevent printer buffer overrun await this.characteristic.writeValueWithoutResponse(chunk); - - // Adaptive sleep: base delay of 35ms, with 120ms flush every 8 chunks - if ((i + 1) % 8 === 0) { - await this._sleep(120); + + // 30ms between chunks; 80ms flush pause every 10 chunks + if ((i + 1) % 10 === 0) { + await this._sleep(80); } else { - await this._sleep(35); + await this._sleep(30); } } - + // Progress logging every 20% if (i % Math.ceil(chunks.length / 5) === 0) { const progress = Math.round((i / chunks.length) * 100); diff --git a/static/src/js/escpos_generator.js b/static/src/js/escpos_generator.js index f96e857..0c7a513 100755 --- a/static/src/js/escpos_generator.js +++ b/static/src/js/escpos_generator.js @@ -231,7 +231,7 @@ export class EscPosGenerator { if (receiptData.orderData) { const o = receiptData.orderData; if (receiptData.isBasicReceipt) { - // Basic receipt (table checker): all info at double height for readability + // Basic receipt (table checker): double-height for readability, keep full 32-char width if (o.orderName) cmds.push(...this.addLine(`Order: ${o.orderName}`, { height: 2, bold: true })); if (o.date) cmds.push(...this.addLine(o.date, { height: 2 })); if (o.cashier) cmds.push(...this.addLine(`By: ${o.cashier}`, { height: 2 })); @@ -256,16 +256,15 @@ export class EscPosGenerator { if (receiptData.isBasicReceipt) { // ── Basic receipt / table checker ────────────────────────── - // Show only qty + product name (no price/total), at double-width - // double-height for maximum readability on 58mm paper. - // At width=2, each char is 2x wide, so effective cols = W/2 = 16 - const halfW = Math.floor(W / 2); - const qtyLabel = qtyStr.padEnd(2) + ' '; - const nameMaxLen = halfW - qtyLabel.length; + // Show qty + product name at double HEIGHT only (keeps full 32-char width). + // This ensures long product names are NOT truncated at 13 chars. + // Format: "1 Pangsit Mie Ayam (Biasa)" + const qtyLabel = qtyStr.padEnd(2) + ' '; + const nameMaxLen = W - qtyLabel.length; const displayName = name.length > nameMaxLen ? name.substring(0, nameMaxLen - 1) + '.' : name; - cmds.push(...this.addLine(qtyLabel + displayName, { bold: true, width: 2, height: 2 })); + cmds.push(...this.addLine(qtyLabel + displayName, { bold: true, height: 2 })); } else { // ── Full receipt ─────────────────────────────────────────── // Line 1: product name @@ -289,10 +288,9 @@ export class EscPosGenerator { const subQtyStr = subQty % 1 === 0 ? subQty.toFixed(0) : subQty.toFixed(2); if (receiptData.isBasicReceipt) { - // Double-height sub-lines on basic receipt - const halfW = Math.floor(W / 2); - const prefix = ` ${subQtyStr}x `; - const maxSubLen = halfW - prefix.length; + // Double-height sub-lines, full 32-char width + const prefix = ` ${subQtyStr}x `; + const maxSubLen = W - prefix.length; const displaySub = subName.length > maxSubLen ? subName.substring(0, maxSubLen - 1) + '.' : subName; @@ -314,8 +312,9 @@ export class EscPosGenerator { const rawNote = String(line.note); const rawLines = rawNote.split('\n'); const wrappedLines = []; + // For basic receipt: full W chars available (height:2 only, not width:2) const maxNoteWidth = W - 2; - + rawLines.forEach(rawLine => { const words = rawLine.split(/\s+/); let currentLine = ''; @@ -339,9 +338,10 @@ export class EscPosGenerator { wrappedLines.push(currentLine); } }); - + wrappedLines.forEach((l, i) => { const prefix = i === 0 ? '* ' : ' '; + // Both modes: bold, and double-height on basic receipt if (receiptData.isBasicReceipt) { cmds.push(...this.addLine(prefix + l, { bold: true, height: 2 })); } else { diff --git a/static/src/js/pos_receipt_printer.js b/static/src/js/pos_receipt_printer.js index 4067b4c..69d3af1 100755 --- a/static/src/js/pos_receipt_printer.js +++ b/static/src/js/pos_receipt_printer.js @@ -723,8 +723,19 @@ patch(PosPrinterService.prototype, { ? order.getOrderlines() : (order.lines || []); + const isBasic = !!this._currentPrintBasic; const lines = orderlines - .filter(line => !line.combo_parent_id) // skip combo sub-lines + .filter(line => { + if (line.combo_parent_id) return false; // skip combo sub-lines + // On basic receipt: mirror the XML filter — exclude reward lines and + // lines with negative price/qty (same as receipt_overrides.xml t-if) + if (isBasic) { + if (line.is_reward_line) return false; + if ((line.price_unit || 0) < 0) return false; + if ((line.qty || 0) < 0) return false; + } + return true; + }) .map(line => { const qty = line.qty || 0;