From 17d51ac34fb8186d4249fb31e0257fbcf1e0d51d Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 1 Jun 2026 14:24:06 +0700 Subject: [PATCH] refactor: optimize BLE flow control, improve receipt parsing logic, and refine feed settings for thermal printers --- static/src/js/bluetooth_printer_manager.js | 34 ++++++------- static/src/js/escpos_generator.js | 4 +- static/src/js/pos_receipt_printer.js | 55 ++++++++++++++++------ 3 files changed, 60 insertions(+), 33 deletions(-) diff --git a/static/src/js/bluetooth_printer_manager.js b/static/src/js/bluetooth_printer_manager.js index 8e2d816..b6aa0ca 100755 --- a/static/src/js/bluetooth_printer_manager.js +++ b/static/src/js/bluetooth_printer_manager.js @@ -469,35 +469,37 @@ export class BluetoothPrinterManager { console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk, mode: ${isGraphicsMode ? 'graphics' : 'text'})`); - // Determine write method based on characteristic properties - const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse; + // 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'); } - // OPTIMIZED: Safe delays between BLE writes - const delay = isGraphicsMode ? - (useWriteWithoutResponse ? 15 : 5) : // Delays for graphics chunks - (useWriteWithoutResponse ? 20 : 10); // Delays for text chunks + const writeWithAck = useWrite; // True if we can write with acknowledgment + console.log(`[Bluetooth] Transmission write mode: ${writeWithAck ? 'write-with-response (Ack)' : 'write-without-response'}`); // Send each chunk for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; try { - if (useWriteWithoutResponse) { - // Faster but no acknowledgment - await this.characteristic.writeValueWithoutResponse(chunk); - } else { - // Slower but with acknowledgment + if (writeWithAck) { + // Safe write with response (OS/device level acknowledgment) await this.characteristic.writeValue(chunk); - } - - // OPTIMIZED: Minimal delay between chunks - if (delay > 0) { - await this._sleep(delay); + // Minimal safety sleep of 5ms between chunks + await this._sleep(5); + } else { + // Write without response (needs explicit flow control to prevent 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); + } else { + await this._sleep(35); + } } // Progress logging every 20% diff --git a/static/src/js/escpos_generator.js b/static/src/js/escpos_generator.js index 14c339a..77bd744 100755 --- a/static/src/js/escpos_generator.js +++ b/static/src/js/escpos_generator.js @@ -384,8 +384,8 @@ export class EscPosGenerator { } } - // Feed and cut (set feed to 6 lines to ensure the footer clears the tear bar) - cmds.push(...this.feedAndCut(6)); + // Feed and cut (set feed to 4 lines to ensure the footer clears the tear bar without excess blank space) + cmds.push(...this.feedAndCut(4)); return new Uint8Array(cmds); } diff --git a/static/src/js/pos_receipt_printer.js b/static/src/js/pos_receipt_printer.js index fb78127..9a69dd4 100755 --- a/static/src/js/pos_receipt_printer.js +++ b/static/src/js/pos_receipt_printer.js @@ -226,6 +226,9 @@ patch(PosPrinterService.prototype, { // Last resort - try original method return await originalPrintHtml.apply(this, arguments); } + } finally { + this._currentPrintOrder = null; + this._currentPrintBasic = false; } }, @@ -478,11 +481,13 @@ patch(PosPrinterService.prototype, { // Prefer direct model data over HTML parsing — HTML parsing fails in basic_receipt mode // because prices and totals are not rendered in the DOM when basic_receipt=true. let receiptData; - if (this._currentPrintOrder) { + const isKitchenReceipt = !!el.querySelector('.new-changes, .preset-name, .o-employee-name, .order-ref-prefix'); + + if (this._currentPrintOrder && !isKitchenReceipt) { console.log('[BluetoothPrint] Building receipt data from POS order model (accurate)...'); receiptData = this._buildReceiptDataFromOrder(this._currentPrintOrder); } else { - console.log('[BluetoothPrint] No order captured, falling back to HTML parsing...'); + console.log('[BluetoothPrint] Kitchen receipt or no order captured, parsing HTML...'); receiptData = this._parseReceiptDataFromHtml(el); } console.log('[BluetoothPrint] Receipt data:', JSON.stringify(receiptData, null, 2)); @@ -882,7 +887,18 @@ patch(PosPrinterService.prototype, { // Parse order info // Odoo 19: order reference is in .pos-receipt-vat, date in #order-date, cashier in .cashier - let orderName = getText('.pos-receipt-vat') || getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || ''; + let orderName = ''; + const orderRefPrefixEl = el.querySelector('.order-ref-prefix'); + if (orderRefPrefixEl) { + const parentEl = orderRefPrefixEl.parentElement; + const fullText = parentEl.textContent.trim(); + orderName = fullText.replace(/^(Order|Ticket|Ref|Invoice)\s*/i, '').trim(); + console.log('[BluetoothPrint] Extracted orderName from order-ref-prefix parent:', orderName); + } + + if (!orderName) { + orderName = getText('.pos-receipt-vat') || getText('.pos-receipt-order-name') || getText('.order-name') || getText('.pos-receipt-order-data') || ''; + } if (!orderName) { const possibleOrderText = findTextByPattern(/\b(Order|Ticket|Ref|Invoice)\b/i); if (possibleOrderText) { @@ -891,6 +907,12 @@ patch(PosPrinterService.prototype, { } let tableName = getText('.pos-receipt-table') || getText('.table-name') || ''; + if (!tableName && orderName) { + const match = orderName.match(/(Table\s+\d+)/i); + if (match) { + tableName = match[1]; + } + } if (!tableName) { const possibleTableText = findTextByPattern(/\bTable\s+\d+/i) || findTextByPattern(/\bTable\b/i); if (possibleTableText) { @@ -932,26 +954,29 @@ patch(PosPrinterService.prototype, { // Check if this element represents a combo sub-line const isComboSubLine = line.classList.contains('orderline-combo') || line.closest('.orderline-combo') || - line.querySelector('.orderline-combo'); + line.querySelector('.orderline-combo') || + line.classList.contains('ms-5'); - // Get qty from the dedicated .qty span - const qtySpan = line.querySelector('.qty'); + // Get qty from the dedicated .qty span, or sibling spans on kitchen tickets (e.g. .me-3) + const qtySpan = line.querySelector('.qty') || + line.querySelector('span.me-3') || + line.querySelector('span.me-2') || + line.querySelector('span.me-1') || + line.querySelector('span'); // Fallback to first span const qtyText = qtySpan ? qtySpan.textContent.trim() : '1'; // Get product name - const nameSpan = line.querySelector('.product-name .text-wrap'); + const nameSpan = line.querySelector('.product-name .text-wrap') || line.querySelector('.product-name'); let productName = ''; if (nameSpan) { productName = nameSpan.textContent.trim(); } else { - const productNameEl = line.querySelector('.product-name'); - if (productNameEl) { - const fullText = productNameEl.textContent.trim(); - const qtyPrefix = qtyText; - productName = fullText.startsWith(qtyPrefix) - ? fullText.slice(qtyPrefix.length).trim() - : fullText; - } + // Heuristic fallback to line text content without quantity prefix + const fullText = line.textContent.trim(); + const qtyPrefix = qtyText; + productName = fullText.startsWith(qtyPrefix) + ? fullText.slice(qtyPrefix.length).trim() + : fullText; } if (!productName) return;