refactor: optimize BLE flow control, improve receipt parsing logic, and refine feed settings for thermal printers
This commit is contained in:
parent
17e1b4cc60
commit
17d51ac34f
@ -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%
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user