feat: add print mode configuration to support both graphics and text printing options

This commit is contained in:
Suherdy Yacob 2026-05-28 22:03:25 +07:00
parent 464bbc3067
commit 4ee383c672
5 changed files with 102 additions and 12 deletions

View File

@ -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
}
};

View File

@ -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;
}
/**

View File

@ -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<void>}
* @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...');

View File

@ -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));
})

View File

@ -130,6 +130,21 @@
</small>
</div>
<!-- Print Mode -->
<div class="form-group">
<label for="printMode">Print Mode</label>
<select id="printMode"
class="form-control"
t-model="state.printMode"
t-on-change="onPrintModeChange">
<option value="graphics">Graphics Mode (Exact Layout)</option>
<option value="text">Text Mode (Extremely Compatible)</option>
</select>
<small class="form-text text-muted">
Choose Graphics Mode for exact layouts, or Text Mode if print is garbled
</small>
</div>
<!-- Paper Width (characters) - Auto-adjusted -->
<div class="form-group">
<label for="paperWidth">Characters Per Line</label>