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 {PrinterBusyError} If printer is busy
|
||||
*/
|
||||
async sendData(escposData) {
|
||||
async sendData(escposData, isGraphics = false) {
|
||||
if (!this.server || !this.server.connected) {
|
||||
const error = new PrinterNotConnectedError();
|
||||
if (this.errorNotificationService) {
|
||||
@ -456,17 +456,18 @@ export class BluetoothPrinterManager {
|
||||
this.isPrinting = true;
|
||||
const startTime = performance.now();
|
||||
|
||||
// OPTIMIZED: Use larger chunks for graphics data (faster transmission)
|
||||
// Graphics data can handle larger chunks than text commands
|
||||
const isLargeData = escposData.length > 1000;
|
||||
const chunkSize = isLargeData ? 512 : 20; // Much larger chunks for graphics
|
||||
// OPTIMIZED: Use safe chunks depending on transmission mode
|
||||
// Large chunks (e.g. 512 bytes) overflow standard BLE serial buffers, truncating long receipts.
|
||||
// Graphics data is sent in highly compatible 128-byte chunks; text is sent in safe 20-byte chunks.
|
||||
const isGraphicsMode = isGraphics || escposData.length > 5000;
|
||||
const chunkSize = isGraphicsMode ? 128 : 20;
|
||||
|
||||
const chunks = [];
|
||||
for (let i = 0; i < escposData.length; 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
|
||||
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
|
||||
@ -476,10 +477,10 @@ export class BluetoothPrinterManager {
|
||||
throw new Error('Characteristic does not support write operations');
|
||||
}
|
||||
|
||||
// OPTIMIZED: Reduce delays for faster transmission
|
||||
const delay = isLargeData ?
|
||||
(useWriteWithoutResponse ? 10 : 5) : // Much shorter delays for graphics
|
||||
(useWriteWithoutResponse ? 50 : 25); // Normal delays for text
|
||||
// OPTIMIZED: Safe delays between BLE writes
|
||||
const delay = isGraphicsMode ?
|
||||
(useWriteWithoutResponse ? 15 : 5) : // Delays for graphics chunks
|
||||
(useWriteWithoutResponse ? 20 : 10); // Delays for text chunks
|
||||
|
||||
// Send each chunk
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
|
||||
@ -272,13 +272,59 @@ export class EscPosGenerator {
|
||||
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) {
|
||||
const noteText = `* ${line.note}`;
|
||||
cmds.push(...this.addLine(
|
||||
noteText.length > W ? noteText.substring(0, W - 1) + '.' : noteText,
|
||||
{ bold: true }
|
||||
));
|
||||
const rawNote = String(line.note);
|
||||
const rawLines = rawNote.split('\n');
|
||||
const wrappedLines = [];
|
||||
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()));
|
||||
|
||||
@ -432,7 +432,7 @@ patch(PosPrinterService.prototype, {
|
||||
// Send data to printer
|
||||
console.log('[BluetoothPrint] Sending graphics to printer...');
|
||||
const startTime = performance.now();
|
||||
await bluetoothManager.sendData(escposData);
|
||||
await bluetoothManager.sendData(escposData, true);
|
||||
const endTime = performance.now();
|
||||
console.log('[BluetoothPrint] Graphics print completed successfully in', (endTime - startTime).toFixed(0), 'ms');
|
||||
|
||||
@ -494,7 +494,7 @@ patch(PosPrinterService.prototype, {
|
||||
|
||||
// Send data 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');
|
||||
},
|
||||
|
||||
@ -534,7 +534,7 @@ patch(PosPrinterService.prototype, {
|
||||
|
||||
// Send data to printer
|
||||
console.log('[BluetoothPrint] Sending to printer...');
|
||||
await bluetoothManager.sendData(escposData);
|
||||
await bluetoothManager.sendData(escposData, false);
|
||||
console.log('[BluetoothPrint] Print completed successfully');
|
||||
},
|
||||
|
||||
@ -753,12 +753,24 @@ patch(PosPrinterService.prototype, {
|
||||
// Combine notes (customer note takes priority; show both if both exist)
|
||||
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 {
|
||||
productName: line.full_product_name || line.product_id?.display_name || '',
|
||||
quantity: qty,
|
||||
price: unitPrice,
|
||||
total: lineTotal,
|
||||
note,
|
||||
comboLines: comboSubLines,
|
||||
};
|
||||
})
|
||||
.filter(l => l.productName);
|
||||
@ -911,28 +923,27 @@ patch(PosPrinterService.prototype, {
|
||||
|
||||
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));
|
||||
|
||||
// Odoo 19 orderline structure:
|
||||
// div.product-name
|
||||
// span.qty ← quantity (e.g. "999")
|
||||
// span.text-wrap ← product name only
|
||||
// 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)
|
||||
// Check if this element represents a combo sub-line
|
||||
const isComboSubLine = line.classList.contains('orderline-combo') ||
|
||||
line.closest('.orderline-combo') ||
|
||||
line.querySelector('.orderline-combo');
|
||||
|
||||
// Get qty from the dedicated .qty span
|
||||
const qtySpan = line.querySelector('.qty');
|
||||
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');
|
||||
let productName = '';
|
||||
if (nameSpan) {
|
||||
productName = nameSpan.textContent.trim();
|
||||
} else {
|
||||
// Fallback: get .product-name text then strip the qty prefix
|
||||
const productNameEl = line.querySelector('.product-name');
|
||||
if (productNameEl) {
|
||||
const fullText = productNameEl.textContent.trim();
|
||||
@ -942,29 +953,39 @@ patch(PosPrinterService.prototype, {
|
||||
: fullText;
|
||||
}
|
||||
}
|
||||
if (!productName) return;
|
||||
|
||||
// Line total: div.product-price.price contains the formatted currency string
|
||||
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)
|
||||
// Parse numbers helper
|
||||
const parseNumber = (str) => {
|
||||
if (!str) return 0;
|
||||
const cleaned = str.replace(/[^0-9.-]/g, '');
|
||||
return parseFloat(cleaned) || 0;
|
||||
};
|
||||
|
||||
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
|
||||
const noteEl = line.querySelector('.customer-note, .note, li.note, li.customer-note');
|
||||
let note = noteEl ? noteEl.textContent.trim() : '';
|
||||
|
||||
if (isComboSubLine && lastParentLine) {
|
||||
// This is a combo subline, add it to the last parent line!
|
||||
if (!lastParentLine.comboLines) {
|
||||
lastParentLine.comboLines = [];
|
||||
}
|
||||
lastParentLine.comboLines.push({
|
||||
productName,
|
||||
quantity: qty,
|
||||
});
|
||||
console.log(`[BluetoothPrint] Added combo subline to parent:`, productName);
|
||||
} else {
|
||||
// This is a normal parent line
|
||||
const priceTotalEl = line.querySelector('.product-price.price, .product-price');
|
||||
const priceTotalText = priceTotalEl ? priceTotalEl.textContent.trim() : '0';
|
||||
|
||||
const priceUnitEl = line.querySelector('.price-per-unit');
|
||||
const priceUnitText = priceUnitEl ? priceUnitEl.textContent.trim() : '';
|
||||
|
||||
const lineTotal = parseNumber(priceTotalText);
|
||||
let unitPrice = 0;
|
||||
if (priceUnitText) {
|
||||
const slashIdx = priceUnitText.lastIndexOf('/');
|
||||
@ -977,16 +998,18 @@ patch(PosPrinterService.prototype, {
|
||||
}
|
||||
|
||||
const parsedLine = {
|
||||
productName: productName,
|
||||
productName,
|
||||
quantity: qty,
|
||||
price: unitPrice,
|
||||
total: lineTotal
|
||||
total: lineTotal,
|
||||
note,
|
||||
comboLines: [],
|
||||
};
|
||||
|
||||
console.log(`[BluetoothPrint] Line ${index} parsed:`, parsedLine);
|
||||
|
||||
return parsedLine;
|
||||
}).filter(line => line.productName); // Filter out empty lines
|
||||
lines.push(parsedLine);
|
||||
lastParentLine = parsedLine;
|
||||
console.log(`[BluetoothPrint] Added parent line:`, parsedLine);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[BluetoothPrint] Parsed', lines.length, 'lines from HTML');
|
||||
console.log('[BluetoothPrint] All lines:', JSON.stringify(lines, null, 2));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user