fix: improve BLE transmission stability by adjusting chunk sizes and delays, and enhance receipt rendering with combo sub-line support and word-wrapped notes.
This commit is contained in:
parent
3a28e7ac08
commit
17e1b4cc60
@ -424,7 +424,7 @@ export class BluetoothPrinterManager {
|
|||||||
* @throws {TransmissionError} If transmission fails
|
* @throws {TransmissionError} If transmission fails
|
||||||
* @throws {PrinterBusyError} If printer is busy
|
* @throws {PrinterBusyError} If printer is busy
|
||||||
*/
|
*/
|
||||||
async sendData(escposData) {
|
async sendData(escposData, isGraphics = false) {
|
||||||
if (!this.server || !this.server.connected) {
|
if (!this.server || !this.server.connected) {
|
||||||
const error = new PrinterNotConnectedError();
|
const error = new PrinterNotConnectedError();
|
||||||
if (this.errorNotificationService) {
|
if (this.errorNotificationService) {
|
||||||
@ -456,17 +456,18 @@ export class BluetoothPrinterManager {
|
|||||||
this.isPrinting = true;
|
this.isPrinting = true;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
// OPTIMIZED: Use larger chunks for graphics data (faster transmission)
|
// OPTIMIZED: Use safe chunks depending on transmission mode
|
||||||
// Graphics data can handle larger chunks than text commands
|
// Large chunks (e.g. 512 bytes) overflow standard BLE serial buffers, truncating long receipts.
|
||||||
const isLargeData = escposData.length > 1000;
|
// Graphics data is sent in highly compatible 128-byte chunks; text is sent in safe 20-byte chunks.
|
||||||
const chunkSize = isLargeData ? 512 : 20; // Much larger chunks for graphics
|
const isGraphicsMode = isGraphics || escposData.length > 5000;
|
||||||
|
const chunkSize = isGraphicsMode ? 128 : 20;
|
||||||
|
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
for (let i = 0; i < escposData.length; i += chunkSize) {
|
for (let i = 0; i < escposData.length; i += chunkSize) {
|
||||||
chunks.push(escposData.slice(i, i + chunkSize));
|
chunks.push(escposData.slice(i, i + chunkSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk)`);
|
console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk, mode: ${isGraphicsMode ? 'graphics' : 'text'})`);
|
||||||
|
|
||||||
// Determine write method based on characteristic properties
|
// Determine write method based on characteristic properties
|
||||||
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
|
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
|
||||||
@ -476,10 +477,10 @@ export class BluetoothPrinterManager {
|
|||||||
throw new Error('Characteristic does not support write operations');
|
throw new Error('Characteristic does not support write operations');
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPTIMIZED: Reduce delays for faster transmission
|
// OPTIMIZED: Safe delays between BLE writes
|
||||||
const delay = isLargeData ?
|
const delay = isGraphicsMode ?
|
||||||
(useWriteWithoutResponse ? 10 : 5) : // Much shorter delays for graphics
|
(useWriteWithoutResponse ? 15 : 5) : // Delays for graphics chunks
|
||||||
(useWriteWithoutResponse ? 50 : 25); // Normal delays for text
|
(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++) {
|
||||||
|
|||||||
@ -272,13 +272,59 @@ export class EscPosGenerator {
|
|||||||
cmds.push(...this.addLine(itemLine, { bold: true, height: 2 }));
|
cmds.push(...this.addLine(itemLine, { bold: true, height: 2 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note line (customer note / kitchen note) — shown on both modes
|
// Combo sub-lines detail printing (shown on both basic/checker and full receipt modes)
|
||||||
|
if (line.comboLines && line.comboLines.length > 0) {
|
||||||
|
line.comboLines.forEach(sub => {
|
||||||
|
const subName = String(sub.productName || '');
|
||||||
|
const subQty = sub.quantity || 0;
|
||||||
|
const subQtyStr = subQty % 1 === 0 ? subQty.toFixed(0) : subQty.toFixed(2);
|
||||||
|
|
||||||
|
// Format: " - 1x Mie Goreng"
|
||||||
|
const prefix = ` - ${subQtyStr}x `;
|
||||||
|
const maxSubLen = W - prefix.length;
|
||||||
|
|
||||||
|
const displayName = subName.length > maxSubLen
|
||||||
|
? subName.substring(0, maxSubLen - 1) + '.'
|
||||||
|
: subName;
|
||||||
|
cmds.push(...this.addLine(prefix + displayName, { bold: true }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note line (customer note / kitchen note) — word-wrapped to prevent truncation
|
||||||
if (line.note) {
|
if (line.note) {
|
||||||
const noteText = `* ${line.note}`;
|
const rawNote = String(line.note);
|
||||||
cmds.push(...this.addLine(
|
const rawLines = rawNote.split('\n');
|
||||||
noteText.length > W ? noteText.substring(0, W - 1) + '.' : noteText,
|
const wrappedLines = [];
|
||||||
{ bold: true }
|
const maxNoteWidth = W - 2;
|
||||||
));
|
|
||||||
|
rawLines.forEach(rawLine => {
|
||||||
|
const words = rawLine.split(/\s+/);
|
||||||
|
let currentLine = '';
|
||||||
|
for (const word of words) {
|
||||||
|
if (!word) continue;
|
||||||
|
if ((currentLine + (currentLine ? ' ' : '') + word).length <= maxNoteWidth) {
|
||||||
|
currentLine += (currentLine ? ' ' : '') + word;
|
||||||
|
} else {
|
||||||
|
if (currentLine) {
|
||||||
|
wrappedLines.push(currentLine);
|
||||||
|
}
|
||||||
|
let tempWord = word;
|
||||||
|
while (tempWord.length > maxNoteWidth) {
|
||||||
|
wrappedLines.push(tempWord.substring(0, maxNoteWidth));
|
||||||
|
tempWord = tempWord.substring(maxNoteWidth);
|
||||||
|
}
|
||||||
|
currentLine = tempWord;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLine) {
|
||||||
|
wrappedLines.push(currentLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wrappedLines.forEach((l, i) => {
|
||||||
|
const prefix = i === 0 ? '* ' : ' ';
|
||||||
|
cmds.push(...this.addLine(prefix + l, { bold: true }));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cmds.push(...this.addLine(this.divider()));
|
cmds.push(...this.addLine(this.divider()));
|
||||||
|
|||||||
@ -432,7 +432,7 @@ patch(PosPrinterService.prototype, {
|
|||||||
// Send data to printer
|
// Send data to printer
|
||||||
console.log('[BluetoothPrint] Sending graphics to printer...');
|
console.log('[BluetoothPrint] Sending graphics to printer...');
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
await bluetoothManager.sendData(escposData);
|
await bluetoothManager.sendData(escposData, true);
|
||||||
const endTime = performance.now();
|
const endTime = performance.now();
|
||||||
console.log('[BluetoothPrint] Graphics print completed successfully in', (endTime - startTime).toFixed(0), 'ms');
|
console.log('[BluetoothPrint] Graphics print completed successfully in', (endTime - startTime).toFixed(0), 'ms');
|
||||||
|
|
||||||
@ -494,7 +494,7 @@ patch(PosPrinterService.prototype, {
|
|||||||
|
|
||||||
// Send data to printer
|
// Send data to printer
|
||||||
console.log('[BluetoothPrint] Sending text to printer...');
|
console.log('[BluetoothPrint] Sending text to printer...');
|
||||||
await bluetoothManager.sendData(escposData);
|
await bluetoothManager.sendData(escposData, false);
|
||||||
console.log('[BluetoothPrint] Text print completed successfully');
|
console.log('[BluetoothPrint] Text print completed successfully');
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -534,7 +534,7 @@ patch(PosPrinterService.prototype, {
|
|||||||
|
|
||||||
// Send data to printer
|
// Send data to printer
|
||||||
console.log('[BluetoothPrint] Sending to printer...');
|
console.log('[BluetoothPrint] Sending to printer...');
|
||||||
await bluetoothManager.sendData(escposData);
|
await bluetoothManager.sendData(escposData, false);
|
||||||
console.log('[BluetoothPrint] Print completed successfully');
|
console.log('[BluetoothPrint] Print completed successfully');
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -753,12 +753,24 @@ patch(PosPrinterService.prototype, {
|
|||||||
// Combine notes (customer note takes priority; show both if both exist)
|
// Combine notes (customer note takes priority; show both if both exist)
|
||||||
const note = [customerNote, kitchenNote].filter(Boolean).join(' | ');
|
const note = [customerNote, kitchenNote].filter(Boolean).join(' | ');
|
||||||
|
|
||||||
|
// Find combo sub-lines linked to this line
|
||||||
|
const comboSubLines = orderlines
|
||||||
|
.filter(subLine => subLine.combo_parent_id === line || subLine.combo_parent_id?.cid === line.cid || (line.id && subLine.combo_parent_id?.id === line.id))
|
||||||
|
.map(subLine => {
|
||||||
|
return {
|
||||||
|
productName: subLine.full_product_name || subLine.product_id?.display_name || '',
|
||||||
|
quantity: subLine.qty || 0,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(sub => sub.productName);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
productName: line.full_product_name || line.product_id?.display_name || '',
|
productName: line.full_product_name || line.product_id?.display_name || '',
|
||||||
quantity: qty,
|
quantity: qty,
|
||||||
price: unitPrice,
|
price: unitPrice,
|
||||||
total: lineTotal,
|
total: lineTotal,
|
||||||
note,
|
note,
|
||||||
|
comboLines: comboSubLines,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(l => l.productName);
|
.filter(l => l.productName);
|
||||||
@ -911,28 +923,27 @@ patch(PosPrinterService.prototype, {
|
|||||||
|
|
||||||
console.log('[BluetoothPrint] Found', lineElements.length, 'line elements');
|
console.log('[BluetoothPrint] Found', lineElements.length, 'line elements');
|
||||||
|
|
||||||
const lines = lineElements.map((line, index) => {
|
const lines = [];
|
||||||
|
let lastParentLine = null;
|
||||||
|
|
||||||
|
lineElements.forEach((line, index) => {
|
||||||
console.log(`[BluetoothPrint] Processing line ${index}:`, line.outerHTML.substring(0, 300));
|
console.log(`[BluetoothPrint] Processing line ${index}:`, line.outerHTML.substring(0, 300));
|
||||||
|
|
||||||
// Odoo 19 orderline structure:
|
// Check if this element represents a combo sub-line
|
||||||
// div.product-name
|
const isComboSubLine = line.classList.contains('orderline-combo') ||
|
||||||
// span.qty ← quantity (e.g. "999")
|
line.closest('.orderline-combo') ||
|
||||||
// span.text-wrap ← product name only
|
line.querySelector('.orderline-combo');
|
||||||
// div.product-price.price ← line total (formatted currency, e.g. "Rp 0.00")
|
|
||||||
// ul.info-list
|
|
||||||
// li.price-per-unit ← "unit_price / UoM" (only shown when qty != 1)
|
|
||||||
|
|
||||||
// Get qty from the dedicated .qty span
|
// Get qty from the dedicated .qty span
|
||||||
const qtySpan = line.querySelector('.qty');
|
const qtySpan = line.querySelector('.qty');
|
||||||
const qtyText = qtySpan ? qtySpan.textContent.trim() : '1';
|
const qtyText = qtySpan ? qtySpan.textContent.trim() : '1';
|
||||||
|
|
||||||
// Get product name from span.text-wrap (excludes the qty span)
|
// Get product name
|
||||||
const nameSpan = line.querySelector('.product-name .text-wrap');
|
const nameSpan = line.querySelector('.product-name .text-wrap');
|
||||||
let productName = '';
|
let productName = '';
|
||||||
if (nameSpan) {
|
if (nameSpan) {
|
||||||
productName = nameSpan.textContent.trim();
|
productName = nameSpan.textContent.trim();
|
||||||
} else {
|
} else {
|
||||||
// Fallback: get .product-name text then strip the qty prefix
|
|
||||||
const productNameEl = line.querySelector('.product-name');
|
const productNameEl = line.querySelector('.product-name');
|
||||||
if (productNameEl) {
|
if (productNameEl) {
|
||||||
const fullText = productNameEl.textContent.trim();
|
const fullText = productNameEl.textContent.trim();
|
||||||
@ -942,51 +953,63 @@ patch(PosPrinterService.prototype, {
|
|||||||
: fullText;
|
: fullText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!productName) return;
|
||||||
|
|
||||||
// Line total: div.product-price.price contains the formatted currency string
|
// Parse numbers helper
|
||||||
const priceTotalEl = line.querySelector('.product-price.price, .product-price');
|
|
||||||
const priceTotalText = priceTotalEl ? priceTotalEl.textContent.trim() : '0';
|
|
||||||
|
|
||||||
// Unit price: li.price-per-unit contains "unit_price / UoM"
|
|
||||||
// Only shown when qty != 1; format: "Rp 1,234.56 / Unit"
|
|
||||||
const priceUnitEl = line.querySelector('.price-per-unit');
|
|
||||||
const priceUnitText = priceUnitEl ? priceUnitEl.textContent.trim() : '';
|
|
||||||
|
|
||||||
console.log(`[BluetoothPrint] Line ${index} raw data:`, { productName, qtyText, priceTotalText, priceUnitText });
|
|
||||||
|
|
||||||
// Parse numbers (remove currency symbols and commas, keep digits/dot/minus)
|
|
||||||
const parseNumber = (str) => {
|
const parseNumber = (str) => {
|
||||||
if (!str) return 0;
|
if (!str) return 0;
|
||||||
const cleaned = str.replace(/[^0-9.-]/g, '');
|
const cleaned = str.replace(/[^0-9.-]/g, '');
|
||||||
return parseFloat(cleaned) || 0;
|
return parseFloat(cleaned) || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const qty = parseNumber(qtyText);
|
const qty = parseNumber(qtyText);
|
||||||
const lineTotal = parseNumber(priceTotalText);
|
|
||||||
|
|
||||||
// Extract unit price from "price / UoM" string (take part before " / ")
|
// Extract customer note/kitchen note from this line element
|
||||||
let unitPrice = 0;
|
const noteEl = line.querySelector('.customer-note, .note, li.note, li.customer-note');
|
||||||
if (priceUnitText) {
|
let note = noteEl ? noteEl.textContent.trim() : '';
|
||||||
const slashIdx = priceUnitText.lastIndexOf('/');
|
|
||||||
const priceStr = slashIdx > 0 ? priceUnitText.substring(0, slashIdx) : priceUnitText;
|
if (isComboSubLine && lastParentLine) {
|
||||||
unitPrice = parseNumber(priceStr);
|
// This is a combo subline, add it to the last parent line!
|
||||||
} else if (qty > 0 && lineTotal > 0) {
|
if (!lastParentLine.comboLines) {
|
||||||
unitPrice = lineTotal / qty;
|
lastParentLine.comboLines = [];
|
||||||
|
}
|
||||||
|
lastParentLine.comboLines.push({
|
||||||
|
productName,
|
||||||
|
quantity: qty,
|
||||||
|
});
|
||||||
|
console.log(`[BluetoothPrint] Added combo subline to parent:`, productName);
|
||||||
} else {
|
} else {
|
||||||
unitPrice = lineTotal;
|
// This is a normal parent line
|
||||||
}
|
const priceTotalEl = line.querySelector('.product-price.price, .product-price');
|
||||||
|
const priceTotalText = priceTotalEl ? priceTotalEl.textContent.trim() : '0';
|
||||||
|
|
||||||
const parsedLine = {
|
const priceUnitEl = line.querySelector('.price-per-unit');
|
||||||
productName: productName,
|
const priceUnitText = priceUnitEl ? priceUnitEl.textContent.trim() : '';
|
||||||
quantity: qty,
|
|
||||||
price: unitPrice,
|
const lineTotal = parseNumber(priceTotalText);
|
||||||
total: lineTotal
|
let unitPrice = 0;
|
||||||
};
|
if (priceUnitText) {
|
||||||
|
const slashIdx = priceUnitText.lastIndexOf('/');
|
||||||
console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine);
|
const priceStr = slashIdx > 0 ? priceUnitText.substring(0, slashIdx) : priceUnitText;
|
||||||
|
unitPrice = parseNumber(priceStr);
|
||||||
return parsedLine;
|
} else if (qty > 0 && lineTotal > 0) {
|
||||||
}).filter(line => line.productName); // Filter out empty lines
|
unitPrice = lineTotal / qty;
|
||||||
|
} else {
|
||||||
|
unitPrice = lineTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedLine = {
|
||||||
|
productName,
|
||||||
|
quantity: qty,
|
||||||
|
price: unitPrice,
|
||||||
|
total: lineTotal,
|
||||||
|
note,
|
||||||
|
comboLines: [],
|
||||||
|
};
|
||||||
|
lines.push(parsedLine);
|
||||||
|
lastParentLine = parsedLine;
|
||||||
|
console.log(`[BluetoothPrint] Added parent line:`, parsedLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
console.log('[BluetoothPrint] Parsed', lines.length, 'lines from HTML');
|
console.log('[BluetoothPrint] Parsed', lines.length, 'lines from HTML');
|
||||||
console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2));
|
console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user