refactor: optimize BLE flow control, improve receipt parsing logic, and refine feed settings for thermal printers

This commit is contained in:
Suherdy Yacob 2026-06-01 14:24:06 +07:00
parent 17e1b4cc60
commit 17d51ac34f
3 changed files with 60 additions and 33 deletions

View File

@ -469,35 +469,37 @@ export class BluetoothPrinterManager {
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, mode: ${isGraphicsMode ? 'graphics' : 'text'})`);
// Determine write method based on characteristic properties // Prefer write with response if available for natural BLE flow control, fallback to write without response
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
const useWrite = this.characteristic.properties.write; const useWrite = this.characteristic.properties.write;
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
if (!useWrite && !useWriteWithoutResponse) { if (!useWrite && !useWriteWithoutResponse) {
throw new Error('Characteristic does not support write operations'); throw new Error('Characteristic does not support write operations');
} }
// OPTIMIZED: Safe delays between BLE writes const writeWithAck = useWrite; // True if we can write with acknowledgment
const delay = isGraphicsMode ? console.log(`[Bluetooth] Transmission write mode: ${writeWithAck ? 'write-with-response (Ack)' : 'write-without-response'}`);
(useWriteWithoutResponse ? 15 : 5) : // Delays for graphics chunks
(useWriteWithoutResponse ? 20 : 10); // Delays for text chunks
// Send each chunk // Send each chunk
for (let i = 0; i < chunks.length; i++) { for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i]; const chunk = chunks[i];
try { try {
if (useWriteWithoutResponse) { if (writeWithAck) {
// Faster but no acknowledgment // Safe write with response (OS/device level acknowledgment)
await this.characteristic.writeValueWithoutResponse(chunk);
} else {
// Slower but with acknowledgment
await this.characteristic.writeValue(chunk); await this.characteristic.writeValue(chunk);
} // Minimal safety sleep of 5ms between chunks
await this._sleep(5);
// OPTIMIZED: Minimal delay between chunks } else {
if (delay > 0) { // Write without response (needs explicit flow control to prevent buffer overrun)
await this._sleep(delay); 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% // Progress logging every 20%

View File

@ -384,8 +384,8 @@ export class EscPosGenerator {
} }
} }
// Feed and cut (set feed to 6 lines to ensure the footer clears the tear bar) // Feed and cut (set feed to 4 lines to ensure the footer clears the tear bar without excess blank space)
cmds.push(...this.feedAndCut(6)); cmds.push(...this.feedAndCut(4));
return new Uint8Array(cmds); return new Uint8Array(cmds);
} }

View File

@ -226,6 +226,9 @@ patch(PosPrinterService.prototype, {
// Last resort - try original method // Last resort - try original method
return await originalPrintHtml.apply(this, arguments); 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 // 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. // because prices and totals are not rendered in the DOM when basic_receipt=true.
let receiptData; 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)...'); console.log('[BluetoothPrint] Building receipt data from POS order model (accurate)...');
receiptData = this._buildReceiptDataFromOrder(this._currentPrintOrder); receiptData = this._buildReceiptDataFromOrder(this._currentPrintOrder);
} else { } 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); receiptData = this._parseReceiptDataFromHtml(el);
} }
console.log('[BluetoothPrint] Receipt data:', JSON.stringify(receiptData, null, 2)); console.log('[BluetoothPrint] Receipt data:', JSON.stringify(receiptData, null, 2));
@ -882,7 +887,18 @@ patch(PosPrinterService.prototype, {
// Parse order info // Parse order info
// Odoo 19: order reference is in .pos-receipt-vat, date in #order-date, cashier in .cashier // 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) { if (!orderName) {
const possibleOrderText = findTextByPattern(/\b(Order|Ticket|Ref|Invoice)\b/i); const possibleOrderText = findTextByPattern(/\b(Order|Ticket|Ref|Invoice)\b/i);
if (possibleOrderText) { if (possibleOrderText) {
@ -891,6 +907,12 @@ patch(PosPrinterService.prototype, {
} }
let tableName = getText('.pos-receipt-table') || getText('.table-name') || ''; 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) { if (!tableName) {
const possibleTableText = findTextByPattern(/\bTable\s+\d+/i) || findTextByPattern(/\bTable\b/i); const possibleTableText = findTextByPattern(/\bTable\s+\d+/i) || findTextByPattern(/\bTable\b/i);
if (possibleTableText) { if (possibleTableText) {
@ -932,26 +954,29 @@ patch(PosPrinterService.prototype, {
// Check if this element represents a combo sub-line // Check if this element represents a combo sub-line
const isComboSubLine = line.classList.contains('orderline-combo') || const isComboSubLine = line.classList.contains('orderline-combo') ||
line.closest('.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 // Get qty from the dedicated .qty span, or sibling spans on kitchen tickets (e.g. .me-3)
const qtySpan = line.querySelector('.qty'); 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'; const qtyText = qtySpan ? qtySpan.textContent.trim() : '1';
// Get product name // 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 = ''; let productName = '';
if (nameSpan) { if (nameSpan) {
productName = nameSpan.textContent.trim(); productName = nameSpan.textContent.trim();
} else { } else {
const productNameEl = line.querySelector('.product-name'); // Heuristic fallback to line text content without quantity prefix
if (productNameEl) { const fullText = line.textContent.trim();
const fullText = productNameEl.textContent.trim(); const qtyPrefix = qtyText;
const qtyPrefix = qtyText; productName = fullText.startsWith(qtyPrefix)
productName = fullText.startsWith(qtyPrefix) ? fullText.slice(qtyPrefix.length).trim()
? fullText.slice(qtyPrefix.length).trim() : fullText;
: fullText;
}
} }
if (!productName) return; if (!productName) return;