feat: add print mode configuration to support both graphics and text printing options
This commit is contained in:
parent
464bbc3067
commit
4ee383c672
@ -41,6 +41,7 @@ export class BluetoothPrinterConfig extends Component {
|
|||||||
paperWidthMm: 58, // Paper width in millimeters (58mm or 80mm)
|
paperWidthMm: 58, // Paper width in millimeters (58mm or 80mm)
|
||||||
autoReconnect: true,
|
autoReconnect: true,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
|
printMode: 'graphics',
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
showConfiguration: false,
|
showConfiguration: false,
|
||||||
@ -87,6 +88,7 @@ export class BluetoothPrinterConfig extends Component {
|
|||||||
this.state.paperWidthMm = config.settings.paperWidthMm || 58;
|
this.state.paperWidthMm = config.settings.paperWidthMm || 58;
|
||||||
this.state.autoReconnect = config.settings.autoReconnect !== false;
|
this.state.autoReconnect = config.settings.autoReconnect !== false;
|
||||||
this.state.timeout = config.settings.timeout || 10000;
|
this.state.timeout = config.settings.timeout || 10000;
|
||||||
|
this.state.printMode = config.settings.printMode || 'graphics';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.showConfiguration = true;
|
this.state.showConfiguration = true;
|
||||||
@ -265,6 +267,15 @@ export class BluetoothPrinterConfig extends Component {
|
|||||||
this._saveConfiguration();
|
this._saveConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle print mode change
|
||||||
|
* @param {Event} event - Change event
|
||||||
|
*/
|
||||||
|
onPrintModeChange(event) {
|
||||||
|
this.state.printMode = event.target.value;
|
||||||
|
this._saveConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
onBypassChange(event) {
|
onBypassChange(event) {
|
||||||
this.state.bypassBluetooth = event.target.checked;
|
this.state.bypassBluetooth = event.target.checked;
|
||||||
this.storageManager.setBypassStatus(this.posConfigId, this.state.bypassBluetooth);
|
this.storageManager.setBypassStatus(this.posConfigId, this.state.bypassBluetooth);
|
||||||
@ -344,7 +355,8 @@ export class BluetoothPrinterConfig extends Component {
|
|||||||
paperWidth: this.state.paperWidth,
|
paperWidth: this.state.paperWidth,
|
||||||
paperWidthMm: this.state.paperWidthMm,
|
paperWidthMm: this.state.paperWidthMm,
|
||||||
autoReconnect: this.state.autoReconnect,
|
autoReconnect: this.state.autoReconnect,
|
||||||
timeout: this.state.timeout
|
timeout: this.state.timeout,
|
||||||
|
printMode: this.state.printMode
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -148,14 +148,54 @@ export class EscPosGenerator {
|
|||||||
return new Uint8Array(0);
|
return new Uint8Array(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For CP437 and similar single-byte character sets,
|
const result = new Uint8Array(text.length);
|
||||||
// we can use a simple encoding approach
|
const charSet = (this.characterSet || 'CP437').toUpperCase();
|
||||||
// For production, you might want to use a proper encoding library
|
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
// CP437 mapping for common characters above 127
|
||||||
const encoded = encoder.encode(text);
|
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
|
||||||
|
};
|
||||||
|
|
||||||
return encoded;
|
// 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 result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -155,8 +155,14 @@ patch(PosPrinterService.prototype, {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Attempt bluetooth printing
|
// Attempt bluetooth printing
|
||||||
console.log('[BluetoothPrint] Attempting bluetooth print...');
|
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);
|
await this._printViaBluetoothFromHtml(el, services, config);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[BluetoothPrint] Print completed successfully');
|
console.log('[BluetoothPrint] Print completed successfully');
|
||||||
return true;
|
return true;
|
||||||
@ -401,7 +407,7 @@ patch(PosPrinterService.prototype, {
|
|||||||
console.log('[BluetoothPrint] Falling back to text mode...');
|
console.log('[BluetoothPrint] Falling back to text mode...');
|
||||||
|
|
||||||
// Fallback to text mode if graphics fails
|
// 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
|
* @private
|
||||||
* @param {HTMLElement} el - Receipt HTML element
|
* @param {HTMLElement} el - Receipt HTML element
|
||||||
* @param {Object} services - Bluetooth services
|
* @param {Object} services - Bluetooth services
|
||||||
|
* @param {Object} [config] - Printer configuration
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If printing fails
|
* @throws {Error} If printing fails
|
||||||
*/
|
*/
|
||||||
async _printViaBluetoothTextMode(el, services) {
|
async _printViaBluetoothTextMode(el, services, config = null) {
|
||||||
const { bluetoothManager, escposGenerator } = services;
|
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
|
// Parse receipt data from HTML element
|
||||||
console.log('[BluetoothPrint] Parsing receipt data from HTML...');
|
console.log('[BluetoothPrint] Parsing receipt data from HTML...');
|
||||||
|
|||||||
@ -227,6 +227,10 @@ describe('Error Handling Scenarios', () => {
|
|||||||
test('sendData with timeout throws TimeoutError', async () => {
|
test('sendData with timeout throws TimeoutError', async () => {
|
||||||
// Create a mock that takes longer than timeout
|
// Create a mock that takes longer than timeout
|
||||||
const mockCharacteristic = {
|
const mockCharacteristic = {
|
||||||
|
properties: {
|
||||||
|
write: true,
|
||||||
|
writeWithoutResponse: false
|
||||||
|
},
|
||||||
writeValue: jest.fn().mockImplementation(() => {
|
writeValue: jest.fn().mockImplementation(() => {
|
||||||
return new Promise(resolve => setTimeout(resolve, 200));
|
return new Promise(resolve => setTimeout(resolve, 200));
|
||||||
})
|
})
|
||||||
|
|||||||
@ -130,6 +130,21 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</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 -->
|
<!-- Paper Width (characters) - Auto-adjusted -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="paperWidth">Characters Per Line</label>
|
<label for="paperWidth">Characters Per Line</label>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user