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'})`);
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
|
||||
// OPTIMIZED: Minimal delay between chunks
|
||||
if (delay > 0) {
|
||||
await this._sleep(delay);
|
||||
// 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%
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user