perf: optimize BLE transmission throughput and fix layout truncation on basic receipts
This commit is contained in:
parent
d1df33bdb2
commit
0ca500fc01
@ -455,24 +455,24 @@ export class BluetoothPrinterManager {
|
|||||||
try {
|
try {
|
||||||
this.isPrinting = true;
|
this.isPrinting = true;
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
|
|
||||||
// OPTIMIZED: Use safe chunks depending on transmission mode
|
// Use 128-byte chunks for all modes — this is the standard BLE ATT MTU payload size
|
||||||
// Large chunks (e.g. 512 bytes) overflow standard BLE serial buffers, truncating long receipts.
|
// and is well within the serial buffer of all common thermal printers.
|
||||||
// Graphics data is sent in highly compatible 128-byte chunks; text is sent in safe 20-byte chunks.
|
// Using only 20 bytes (the BLE minimum) is 6× slower and causes connection timeouts
|
||||||
const isGraphicsMode = isGraphics || escposData.length > 5000;
|
// on long receipts (many items), resulting in missing lines at the end.
|
||||||
const chunkSize = isGraphicsMode ? 128 : 20;
|
const chunkSize = 128;
|
||||||
|
|
||||||
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, mode: ${isGraphicsMode ? 'graphics' : 'text'})`);
|
console.log(`[Bluetooth] Sending ${chunks.length} chunks (${escposData.length} bytes, ${chunkSize} bytes/chunk)`);
|
||||||
|
|
||||||
// Prefer write with response if available for natural BLE flow control, fallback to write without response
|
// Prefer write with response if available for natural BLE flow control, fallback to write without response
|
||||||
const useWrite = this.characteristic.properties.write;
|
const useWrite = this.characteristic.properties.write;
|
||||||
const useWriteWithoutResponse = this.characteristic.properties.writeWithoutResponse;
|
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');
|
||||||
}
|
}
|
||||||
@ -483,25 +483,24 @@ export class BluetoothPrinterManager {
|
|||||||
// 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 (writeWithAck) {
|
if (writeWithAck) {
|
||||||
// Safe write with response (OS/device level acknowledgment)
|
// Write with response: OS provides ack, minimal sleep needed
|
||||||
await this.characteristic.writeValue(chunk);
|
await this.characteristic.writeValue(chunk);
|
||||||
// Minimal safety sleep of 5ms between chunks
|
|
||||||
await this._sleep(5);
|
await this._sleep(5);
|
||||||
} else {
|
} else {
|
||||||
// Write without response (needs explicit flow control to prevent buffer overrun)
|
// Write without response: explicit flow control to prevent printer buffer overrun
|
||||||
await this.characteristic.writeValueWithoutResponse(chunk);
|
await this.characteristic.writeValueWithoutResponse(chunk);
|
||||||
|
|
||||||
// Adaptive sleep: base delay of 35ms, with 120ms flush every 8 chunks
|
// 30ms between chunks; 80ms flush pause every 10 chunks
|
||||||
if ((i + 1) % 8 === 0) {
|
if ((i + 1) % 10 === 0) {
|
||||||
await this._sleep(120);
|
await this._sleep(80);
|
||||||
} else {
|
} else {
|
||||||
await this._sleep(35);
|
await this._sleep(30);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Progress logging every 20%
|
// Progress logging every 20%
|
||||||
if (i % Math.ceil(chunks.length / 5) === 0) {
|
if (i % Math.ceil(chunks.length / 5) === 0) {
|
||||||
const progress = Math.round((i / chunks.length) * 100);
|
const progress = Math.round((i / chunks.length) * 100);
|
||||||
|
|||||||
@ -231,7 +231,7 @@ export class EscPosGenerator {
|
|||||||
if (receiptData.orderData) {
|
if (receiptData.orderData) {
|
||||||
const o = receiptData.orderData;
|
const o = receiptData.orderData;
|
||||||
if (receiptData.isBasicReceipt) {
|
if (receiptData.isBasicReceipt) {
|
||||||
// Basic receipt (table checker): all info at double height for readability
|
// Basic receipt (table checker): double-height for readability, keep full 32-char width
|
||||||
if (o.orderName) cmds.push(...this.addLine(`Order: ${o.orderName}`, { height: 2, bold: true }));
|
if (o.orderName) cmds.push(...this.addLine(`Order: ${o.orderName}`, { height: 2, bold: true }));
|
||||||
if (o.date) cmds.push(...this.addLine(o.date, { height: 2 }));
|
if (o.date) cmds.push(...this.addLine(o.date, { height: 2 }));
|
||||||
if (o.cashier) cmds.push(...this.addLine(`By: ${o.cashier}`, { height: 2 }));
|
if (o.cashier) cmds.push(...this.addLine(`By: ${o.cashier}`, { height: 2 }));
|
||||||
@ -256,16 +256,15 @@ export class EscPosGenerator {
|
|||||||
|
|
||||||
if (receiptData.isBasicReceipt) {
|
if (receiptData.isBasicReceipt) {
|
||||||
// ── Basic receipt / table checker ──────────────────────────
|
// ── Basic receipt / table checker ──────────────────────────
|
||||||
// Show only qty + product name (no price/total), at double-width
|
// Show qty + product name at double HEIGHT only (keeps full 32-char width).
|
||||||
// double-height for maximum readability on 58mm paper.
|
// This ensures long product names are NOT truncated at 13 chars.
|
||||||
// At width=2, each char is 2x wide, so effective cols = W/2 = 16
|
// Format: "1 Pangsit Mie Ayam (Biasa)"
|
||||||
const halfW = Math.floor(W / 2);
|
const qtyLabel = qtyStr.padEnd(2) + ' ';
|
||||||
const qtyLabel = qtyStr.padEnd(2) + ' ';
|
const nameMaxLen = W - qtyLabel.length;
|
||||||
const nameMaxLen = halfW - qtyLabel.length;
|
|
||||||
const displayName = name.length > nameMaxLen
|
const displayName = name.length > nameMaxLen
|
||||||
? name.substring(0, nameMaxLen - 1) + '.'
|
? name.substring(0, nameMaxLen - 1) + '.'
|
||||||
: name;
|
: name;
|
||||||
cmds.push(...this.addLine(qtyLabel + displayName, { bold: true, width: 2, height: 2 }));
|
cmds.push(...this.addLine(qtyLabel + displayName, { bold: true, height: 2 }));
|
||||||
} else {
|
} else {
|
||||||
// ── Full receipt ───────────────────────────────────────────
|
// ── Full receipt ───────────────────────────────────────────
|
||||||
// Line 1: product name
|
// Line 1: product name
|
||||||
@ -289,10 +288,9 @@ export class EscPosGenerator {
|
|||||||
const subQtyStr = subQty % 1 === 0 ? subQty.toFixed(0) : subQty.toFixed(2);
|
const subQtyStr = subQty % 1 === 0 ? subQty.toFixed(0) : subQty.toFixed(2);
|
||||||
|
|
||||||
if (receiptData.isBasicReceipt) {
|
if (receiptData.isBasicReceipt) {
|
||||||
// Double-height sub-lines on basic receipt
|
// Double-height sub-lines, full 32-char width
|
||||||
const halfW = Math.floor(W / 2);
|
const prefix = ` ${subQtyStr}x `;
|
||||||
const prefix = ` ${subQtyStr}x `;
|
const maxSubLen = W - prefix.length;
|
||||||
const maxSubLen = halfW - prefix.length;
|
|
||||||
const displaySub = subName.length > maxSubLen
|
const displaySub = subName.length > maxSubLen
|
||||||
? subName.substring(0, maxSubLen - 1) + '.'
|
? subName.substring(0, maxSubLen - 1) + '.'
|
||||||
: subName;
|
: subName;
|
||||||
@ -314,8 +312,9 @@ export class EscPosGenerator {
|
|||||||
const rawNote = String(line.note);
|
const rawNote = String(line.note);
|
||||||
const rawLines = rawNote.split('\n');
|
const rawLines = rawNote.split('\n');
|
||||||
const wrappedLines = [];
|
const wrappedLines = [];
|
||||||
|
// For basic receipt: full W chars available (height:2 only, not width:2)
|
||||||
const maxNoteWidth = W - 2;
|
const maxNoteWidth = W - 2;
|
||||||
|
|
||||||
rawLines.forEach(rawLine => {
|
rawLines.forEach(rawLine => {
|
||||||
const words = rawLine.split(/\s+/);
|
const words = rawLine.split(/\s+/);
|
||||||
let currentLine = '';
|
let currentLine = '';
|
||||||
@ -339,9 +338,10 @@ export class EscPosGenerator {
|
|||||||
wrappedLines.push(currentLine);
|
wrappedLines.push(currentLine);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wrappedLines.forEach((l, i) => {
|
wrappedLines.forEach((l, i) => {
|
||||||
const prefix = i === 0 ? '* ' : ' ';
|
const prefix = i === 0 ? '* ' : ' ';
|
||||||
|
// Both modes: bold, and double-height on basic receipt
|
||||||
if (receiptData.isBasicReceipt) {
|
if (receiptData.isBasicReceipt) {
|
||||||
cmds.push(...this.addLine(prefix + l, { bold: true, height: 2 }));
|
cmds.push(...this.addLine(prefix + l, { bold: true, height: 2 }));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -723,8 +723,19 @@ patch(PosPrinterService.prototype, {
|
|||||||
? order.getOrderlines()
|
? order.getOrderlines()
|
||||||
: (order.lines || []);
|
: (order.lines || []);
|
||||||
|
|
||||||
|
const isBasic = !!this._currentPrintBasic;
|
||||||
const lines = orderlines
|
const lines = orderlines
|
||||||
.filter(line => !line.combo_parent_id) // skip combo sub-lines
|
.filter(line => {
|
||||||
|
if (line.combo_parent_id) return false; // skip combo sub-lines
|
||||||
|
// On basic receipt: mirror the XML filter — exclude reward lines and
|
||||||
|
// lines with negative price/qty (same as receipt_overrides.xml t-if)
|
||||||
|
if (isBasic) {
|
||||||
|
if (line.is_reward_line) return false;
|
||||||
|
if ((line.price_unit || 0) < 0) return false;
|
||||||
|
if ((line.qty || 0) < 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
.map(line => {
|
.map(line => {
|
||||||
const qty = line.qty || 0;
|
const qty = line.qty || 0;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user