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'})`);
// 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%

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)
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);
}

View File

@ -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;