diff --git a/static/src/js/bluetooth_printer_config.js b/static/src/js/bluetooth_printer_config.js index d5fec25..8335b78 100755 --- a/static/src/js/bluetooth_printer_config.js +++ b/static/src/js/bluetooth_printer_config.js @@ -41,6 +41,7 @@ export class BluetoothPrinterConfig extends Component { paperWidthMm: 58, // Paper width in millimeters (58mm or 80mm) autoReconnect: true, timeout: 10000, + printMode: 'graphics', // UI state showConfiguration: false, @@ -87,6 +88,7 @@ export class BluetoothPrinterConfig extends Component { this.state.paperWidthMm = config.settings.paperWidthMm || 58; this.state.autoReconnect = config.settings.autoReconnect !== false; this.state.timeout = config.settings.timeout || 10000; + this.state.printMode = config.settings.printMode || 'graphics'; } this.state.showConfiguration = true; @@ -265,6 +267,15 @@ export class BluetoothPrinterConfig extends Component { this._saveConfiguration(); } + /** + * Handle print mode change + * @param {Event} event - Change event + */ + onPrintModeChange(event) { + this.state.printMode = event.target.value; + this._saveConfiguration(); + } + onBypassChange(event) { this.state.bypassBluetooth = event.target.checked; this.storageManager.setBypassStatus(this.posConfigId, this.state.bypassBluetooth); @@ -344,7 +355,8 @@ export class BluetoothPrinterConfig extends Component { paperWidth: this.state.paperWidth, paperWidthMm: this.state.paperWidthMm, autoReconnect: this.state.autoReconnect, - timeout: this.state.timeout + timeout: this.state.timeout, + printMode: this.state.printMode } }; diff --git a/static/src/js/escpos_generator.js b/static/src/js/escpos_generator.js index f4492e7..06f4798 100755 --- a/static/src/js/escpos_generator.js +++ b/static/src/js/escpos_generator.js @@ -148,14 +148,54 @@ export class EscPosGenerator { return new Uint8Array(0); } - // For CP437 and similar single-byte character sets, - // we can use a simple encoding approach - // For production, you might want to use a proper encoding library + const result = new Uint8Array(text.length); + const charSet = (this.characterSet || 'CP437').toUpperCase(); - const encoder = new TextEncoder(); - const encoded = encoder.encode(text); + // CP437 mapping for common characters above 127 + const cp437Map = { + 'ü': 0x81, 'é': 0x82, 'â': 0x83, 'ä': 0x84, 'à': 0x85, 'å': 0x86, 'ç': 0x87, 'ê': 0x88, 'ë': 0x89, 'è': 0x8A, 'ï': 0x8B, 'î': 0x8C, 'ì': 0x8D, 'Ä': 0x8E, 'Å': 0x8F, + 'É': 0x90, 'æ': 0x91, 'Æ': 0x92, 'ô': 0x93, 'ö': 0x94, 'ò': 0x95, 'û': 0x96, 'ù': 0x97, 'ÿ': 0x98, 'Ö': 0x99, 'Ü': 0x9A, '¢': 0x9B, '£': 0x9C, '¥': 0x9D, '₧': 0x9E, 'ƒ': 0x9F, + 'á': 0xA0, 'í': 0xA1, 'ó': 0xA2, 'ú': 0xA3, 'ñ': 0xA4, 'Ñ': 0xA5, 'ª': 0xA6, 'º': 0xA7, '¿': 0xA8, '⌐': 0xA9, '¬': 0xAA, '½': 0xAB, '¼': 0xAC, '¡': 0xAD, '«': 0xAE, '»': 0xAF, + 'ß': 0xE1, 'Γ': 0xE2, 'π': 0xE3, 'Σ': 0xE4, 'σ': 0xE5, 'µ': 0xE6, 'τ': 0xE7, 'Φ': 0xE8, 'Θ': 0xE9, 'Ω': 0xEA, 'δ': 0xEB, '∞': 0xEC, 'φ': 0xED, 'ε': 0xEE, '∩': 0xEF, + '≡': 0xF0, '±': 0xF1, '≥': 0xF2, '≤': 0xF3, '⌠': 0xF4, '⌡': 0xF5, '÷': 0xF6, '≈': 0xF7, '°': 0xF8, '∙': 0xF9, '·': 0xFA, '√': 0xFB, 'ⁿ': 0xFC, '²': 0xFD, '■': 0xFE, + '€': 0xEE // Mapped to approximate char or standard CP437 fallback + }; + + // CP850/CP858 mapping for common characters + const cp850Map = { + 'ü': 0x81, 'é': 0x82, 'â': 0x83, 'ä': 0x84, 'à': 0x85, 'å': 0x86, 'ç': 0x87, 'ê': 0x88, 'ë': 0x89, 'è': 0x8A, 'ï': 0x8B, 'î': 0x8C, 'ì': 0x8D, 'Ä': 0x8E, 'Å': 0x8F, + 'É': 0x90, 'æ': 0x91, 'Æ': 0x92, 'ô': 0x93, 'ö': 0x94, 'ò': 0x95, 'û': 0x96, 'ù': 0x97, 'ÿ': 0x98, 'Ö': 0x99, 'Ü': 0x9A, 'ø': 0x9B, '£': 0x9C, 'Ø': 0x9D, '×': 0x9E, 'ƒ': 0x9F, + 'á': 0xA0, 'í': 0xA1, 'ó': 0xA2, 'ú': 0xA3, 'ñ': 0xA4, 'Ñ': 0xA5, 'ª': 0xA6, 'º': 0xA7, '¿': 0xA8, '®': 0xA9, '¬': 0xAA, '½': 0xAB, '¼': 0xAC, '¡': 0xAD, '«': 0xAE, '»': 0xAF, + 'ß': 0xE1, 'µ': 0xE6, '±': 0xF1, '÷': 0xF6, '°': 0xF8, '²': 0xFD, + '€': 0xD5 // CP858/CP850 Euro symbol or placeholder + }; + + // Select active lookup map + let activeMap = cp437Map; + if (charSet === 'CP850' || charSet === 'CP858' || charSet === 'CP852') { + activeMap = cp850Map; + } + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + const code = char.charCodeAt(0); + + if (code <= 127) { + result[i] = code; + } else if (activeMap[char] !== undefined) { + result[i] = activeMap[char]; + } else { + // Normalize to strip accents as a general fallback + const normalized = char.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + if (normalized.length > 0 && normalized.charCodeAt(0) <= 127) { + result[i] = normalized.charCodeAt(0); + } else { + result[i] = 0x3F; // '?' in ASCII + } + } + } - return encoded; + return result; } /** diff --git a/static/src/js/pos_receipt_printer.js b/static/src/js/pos_receipt_printer.js index 4fb5ad2..1988a1b 100755 --- a/static/src/js/pos_receipt_printer.js +++ b/static/src/js/pos_receipt_printer.js @@ -155,8 +155,14 @@ patch(PosPrinterService.prototype, { try { // Attempt bluetooth printing - console.log('[BluetoothPrint] Attempting bluetooth print...'); - await this._printViaBluetoothFromHtml(el, services, config); + const printMode = config?.settings?.printMode || 'graphics'; + if (printMode === 'text') { + console.log('[BluetoothPrint] Using TEXT MODE as configured'); + await this._printViaBluetoothTextMode(el, services); + } else { + console.log('[BluetoothPrint] Attempting bluetooth print in GRAPHICS MODE...'); + await this._printViaBluetoothFromHtml(el, services, config); + } console.log('[BluetoothPrint] Print completed successfully'); return true; @@ -401,7 +407,7 @@ patch(PosPrinterService.prototype, { console.log('[BluetoothPrint] Falling back to text mode...'); // Fallback to text mode if graphics fails - await this._printViaBluetoothTextMode(el, services); + await this._printViaBluetoothTextMode(el, services, config); } }, @@ -411,13 +417,26 @@ patch(PosPrinterService.prototype, { * @private * @param {HTMLElement} el - Receipt HTML element * @param {Object} services - Bluetooth services + * @param {Object} [config] - Printer configuration * @returns {Promise} * @throws {Error} If printing fails */ - async _printViaBluetoothTextMode(el, services) { + async _printViaBluetoothTextMode(el, services, config = null) { const { bluetoothManager, escposGenerator } = services; - console.log('[BluetoothPrint] Using TEXT MODE (fallback)'); + console.log('[BluetoothPrint] Using TEXT MODE'); + + if (!config) { + const storage = new BluetoothPrinterStorage(); + const pos = this.env?.services?.pos; + const posConfigId = pos?.config?.id || 1; + config = storage.loadConfiguration(posConfigId); + } + + if (config && config.settings && config.settings.characterSet) { + escposGenerator.characterSet = config.settings.characterSet; + console.log('[BluetoothPrint] Setting character set on generator:', escposGenerator.characterSet); + } // Parse receipt data from HTML element console.log('[BluetoothPrint] Parsing receipt data from HTML...'); diff --git a/static/src/tests/error_handling.test.js b/static/src/tests/error_handling.test.js index 665c733..0f3de07 100755 --- a/static/src/tests/error_handling.test.js +++ b/static/src/tests/error_handling.test.js @@ -227,6 +227,10 @@ describe('Error Handling Scenarios', () => { test('sendData with timeout throws TimeoutError', async () => { // Create a mock that takes longer than timeout const mockCharacteristic = { + properties: { + write: true, + writeWithoutResponse: false + }, writeValue: jest.fn().mockImplementation(() => { return new Promise(resolve => setTimeout(resolve, 200)); }) diff --git a/static/src/xml/bluetooth_printer_config.xml b/static/src/xml/bluetooth_printer_config.xml index 9137244..7f84126 100755 --- a/static/src/xml/bluetooth_printer_config.xml +++ b/static/src/xml/bluetooth_printer_config.xml @@ -130,6 +130,21 @@ + +
+ + + + Choose Graphics Mode for exact layouts, or Text Mode if print is garbled + +
+