// ESC/POS Commands Utility const ESC = 0x1B; const GS = 0x1D; const EscPosCommands = { // Formatting INIT: [ESC, 0x40], // Initialize printer ALIGN_CENTER: [ESC, 0x61, 0x01], // Align text center ALIGN_LEFT: [ESC, 0x61, 0x00], // Align text left ALIGN_RIGHT: [ESC, 0x61, 0x02], // Align text right BOLD_ON: [ESC, 0x45, 0x01], // Bold text on BOLD_OFF: [ESC, 0x45, 0x00], // Bold text off DOUBLE_HEIGHT_ON: [ESC, 0x21, 0x10], // Double height on DOUBLE_HEIGHT_OFF: [ESC, 0x21, 0x00], // Double height off NORMAL_TEXT: [ESC, 0x21, 0x00], // Normal text size FEED_LINE: [0x0A], // Feed line FEED_LINES: (n) => [ESC, 0x64, n], // Feed n lines FULL_CUT: [GS, 0x56, 0x00], // Full paper cut PARTIAL_CUT: [GS, 0x56, 0x01], // Partial paper cut // Barcode BARCODE_TEXT_BELOW: [GS, 0x48, 0x02], // HRI barcode chars below BARCODE_HEIGHT: (n) => [GS, 0x68, n], // Barcode height n dots // Character conversion to bytes textToBytes: (text) => { const encoder = new TextEncoder('utf-8'); return encoder.encode(text); }, // Combine commands and text concatenate: (...arrays) => { let totalLength = 0; arrays.forEach(arr => { if (arr instanceof Uint8Array) { totalLength += arr.length; } else if (Array.isArray(arr)) { totalLength += arr.length; } }); const result = new Uint8Array(totalLength); let offset = 0; arrays.forEach(arr => { if (arr instanceof Uint8Array) { result.set(arr, offset); offset += arr.length; } else if (Array.isArray(arr)) { result.set(new Uint8Array(arr), offset); offset += arr.length; } }); return result; } }; // Main Bluetooth Printing Class class BluetoothPrinter { constructor() { this.device = null; this.characteristic = null; } // Connect to Bluetooth printer async connect() { try { // Check if Web Bluetooth is supported if (!navigator.bluetooth) { throw new Error("Web Bluetooth API is not available on your browser."); } // Request device with serial port service this.device = await navigator.bluetooth.requestDevice({ filters: [ { services: ['000018f0-0000-1000-8000-00805f9b34fb'] }, // Generic printer service { namePrefix: 'Printer' }, { namePrefix: 'BT' } ], optionalServices: ['0000ffe0-0000-1000-8000-00805f9b34fb'] // Common service for thermal printers }); if (!this.device) { throw new Error("No printer was selected."); } // Connect to the device const server = await this.device.gatt.connect(); // Get primary service const service = await server.getPrimaryService('0000ffe0-0000-1000-8000-00805f9b34fb'); // Get characteristic for writing this.characteristic = await service.getCharacteristic('0000ffe1-0000-1000-8000-00805f9b34fb'); return true; } catch (error) { console.error("Bluetooth connection error:", error); throw error; } } // Disconnect from printer async disconnect() { if (this.device && this.device.gatt.connected) { await this.device.gatt.disconnect(); } this.device = null; this.characteristic = null; } // Send data to printer async sendData(data) { if (!this.characteristic) { throw new Error("Printer not connected."); } // For large data, we need to chunk it const CHUNK_SIZE = 512; // Adjust based on printer specifications for (let i = 0; i < data.length; i += CHUNK_SIZE) { const chunk = data.slice(i, i + CHUNK_SIZE); await this.characteristic.writeValue(chunk); // Small delay between chunks to avoid buffer overflow await new Promise(resolve => setTimeout(resolve, 50)); } } // Generate and print receipt async printReceipt(shopName, cartItems, cartTotal, itemsCount, date) { try { // Initialize printer let receiptData = EscPosCommands.concatenate( EscPosCommands.INIT, EscPosCommands.ALIGN_CENTER, EscPosCommands.BOLD_ON, EscPosCommands.DOUBLE_HEIGHT_ON, EscPosCommands.textToBytes(shopName), EscPosCommands.FEED_LINE, EscPosCommands.NORMAL_TEXT, EscPosCommands.BOLD_OFF, EscPosCommands.textToBytes(date), EscPosCommands.FEED_LINES(2), EscPosCommands.ALIGN_LEFT ); // Add header receiptData = EscPosCommands.concatenate( receiptData, EscPosCommands.BOLD_ON, EscPosCommands.textToBytes("ARTICLE".padEnd(25) + "PRIX"), EscPosCommands.BOLD_OFF, EscPosCommands.FEED_LINE, EscPosCommands.textToBytes("----------------------------------------"), EscPosCommands.FEED_LINE ); // Add items cartItems.forEach(item => { // Format reference and price with proper padding const reference = (item.brand !== "Article manuel") ? item.reference : "Article manuel"; const itemLine = reference.padEnd(25).substring(0, 25) + item.price.toFixed(2) + " CHF"; receiptData = EscPosCommands.concatenate( receiptData, EscPosCommands.textToBytes(itemLine), EscPosCommands.FEED_LINE ); }); // Add total receiptData = EscPosCommands.concatenate( receiptData, EscPosCommands.FEED_LINE, EscPosCommands.textToBytes("----------------------------------------"), EscPosCommands.FEED_LINE, EscPosCommands.BOLD_ON, EscPosCommands.textToBytes("Articles: " + itemsCount), EscPosCommands.FEED_LINE, EscPosCommands.textToBytes("TOTAL: " + cartTotal.toFixed(2) + " CHF"), EscPosCommands.BOLD_OFF, EscPosCommands.FEED_LINES(3), EscPosCommands.ALIGN_CENTER, EscPosCommands.textToBytes("Merci pour votre achat!"), EscPosCommands.FEED_LINES(4), EscPosCommands.PARTIAL_CUT ); // Send to printer await this.sendData(receiptData); return true; } catch (error) { console.error("Print error:", error); throw error; } } } // Initialize the printer button once Wix is ready $w.onReady(function() { // Add a print button to your UI const printButton = $w('#printReceiptButton'); // Make sure to add this button in Wix editor if (printButton) { printButton.onClick(async function() { try { // Get our cart data from the existing variables // These variables are defined in your scanning code const cart = $w.Globals?.cart || window.cart; const total = $w.Globals?.total || window.total; if (!cart || cart.length === 0) { $w('#resultBox').text = "The cart is empty. Nothing to print."; return; } // Store name, can be customized const shopName = "YOUR SHOP NAME"; const currentDate = new Date().toLocaleString(); const itemsCount = cart.length; // Show connecting message $w('#resultBox').text = "Connecting to printer..."; const printer = new BluetoothPrinter(); await printer.connect(); $w('#resultBox').text = "Printing receipt..."; await printer.printReceipt(shopName, cart, total, itemsCount, currentDate); await printer.disconnect(); $w('#resultBox').text = "Printing completed!"; } catch (error) { $w('#resultBox').text = "Error: " + error.message; console.error("Full error:", error); } }); } }); // ESC/POS Commands Utility for Thermal Printer const EscPos = { INIT: new Uint8Array([0x1B, 0x40]), CENTER: new Uint8Array([0x1B, 0x61, 0x01]), LEFT: new Uint8Array([0x1B, 0x61, 0x00]), RIGHT: new Uint8Array([0x1B, 0x61, 0x02]), BOLD_ON: new Uint8Array([0x1B, 0x45, 0x01]), BOLD_OFF: new Uint8Array([0x1B, 0x45, 0x00]), LINE: new Uint8Array([0x0A]), CUT: new Uint8Array([0x1D, 0x56, 0x01]), // Convert text to bytes text: function(text) { return new TextEncoder().encode(text); }, // Concatenate Uint8Arrays concat: function(...arrays) { let length = 0; arrays.forEach(arr => { length += arr.byteLength; }); const result = new Uint8Array(length); let offset = 0; arrays.forEach(arr => { result.set(arr, offset); offset += arr.byteLength; }); return result; } }; // Bluetooth Printer Class class ThermalPrinter { constructor() { this.device = null; this.characteristic = null; } async connect() { try { console.log("Requesting Bluetooth device..."); this.device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [ '000018f0-0000-1000-8000-00805f9b34fb', '0000ffe0-0000-1000-8000-00805f9b34fb', '49535343-fe7d-4ae5-8fa9-9fafd205e455' ] }); console.log("Connecting to GATT server..."); const server = await this.device.gatt.connect(); // Try different services let service; try { service = await server.getPrimaryService('0000ffe0-0000-1000-8000-00805f9b34fb'); console.log("Using service: 0000ffe0-0000-1000-8000-00805f9b34fb"); } catch (e) { try { service = await server.getPrimaryService('000018f0-0000-1000-8000-00805f9b34fb'); console.log("Using service: 000018f0-0000-1000-8000-00805f9b34fb"); } catch (e2) { service = await server.getPrimaryService('49535343-fe7d-4ae5-8fa9-9fafd205e455'); console.log("Using service: 49535343-fe7d-4ae5-8fa9-9fafd205e455"); } } // Get all characteristics const characteristics = await service.getCharacteristics(); console.log("Available characteristics:", characteristics.length); // Find a writable characteristic for (const char of characteristics) { if (char.properties.write || char.properties.writeWithoutResponse) { this.characteristic = char; console.log("Found writable characteristic:", char.uuid); break; } } // If not found, try known UUIDs if (!this.characteristic) { try { this.characteristic = await service.getCharacteristic('0000ffe1-0000-1000-8000-00805f9b34fb'); console.log("Using specific characteristic: 0000ffe1-0000-1000-8000-00805f9b34fb"); } catch (e) { this.characteristic = await service.getCharacteristic('00002af1-0000-1000-8000-00805f9b34fb'); console.log("Using specific characteristic: 00002af1-0000-1000-8000-00805f9b34fb"); } } return true; } catch (error) { console.error("Bluetooth connection error:", error); throw error; } } async disconnect() { if (this.device && this.device.gatt.connected) { await this.device.gatt.disconnect(); } } async write(data) { if (!this.characteristic) { throw new Error("Printer not connected"); } const CHUNK_SIZE = 100; for (let i = 0; i < data.byteLength; i += CHUNK_SIZE) { const chunk = data.slice(i, i + CHUNK_SIZE); console.log(`Writing chunk of size ${chunk.byteLength}`); try { if (this.characteristic.properties.writeWithoutResponse) { await this.characteristic.writeValueWithoutResponse(chunk); } else { await this.characteristic.writeValue(chunk); } } catch (error) { console.error("Write error:", error); throw error; } await new Promise(resolve => setTimeout(resolve, 100)); } } async printReceipt(shopName, items, total, count) { const currentDate = new Date().toLocaleString(); // Create header let receipt = EscPos.concat( EscPos.INIT, EscPos.CENTER, EscPos.BOLD_ON, EscPos.text(shopName), EscPos.LINE, EscPos.BOLD_OFF, EscPos.text(currentDate), EscPos.LINE, EscPos.LINE, EscPos.LEFT ); // Add items for (const item of items) { const ref = item.reference || "Produit"; const line = `${ref.padEnd(24).substring(0, 24)}${item.price.toFixed(2)} CHF`; receipt = EscPos.concat(receipt, EscPos.text(line), EscPos.LINE); } // Add total receipt = EscPos.concat( receipt, EscPos.LINE, EscPos.text("----------------------------------------"), EscPos.LINE, EscPos.BOLD_ON, EscPos.text(`Articles: ${count}`), EscPos.LINE, EscPos.text(`TOTAL: ${total.toFixed(2)} CHF`), EscPos.BOLD_OFF, EscPos.LINE, EscPos.LINE, EscPos.CENTER, EscPos.text("Merci pour votre achat!"), EscPos.LINE, EscPos.LINE, EscPos.LINE, EscPos.LINE, EscPos.CUT ); await this.write(receipt); } } // Function to initialize printing function initBtPrinting() { console.log("Initializing Bluetooth printing..."); // Add the print function to window so it can be accessed by page code window.printReceipt = async function(resultBox) { try { resultBox.text = "Loading scanner data..."; // Wait for cart and result variables to be defined in window if (typeof window.cart === 'undefined') { resultBox.text = "Error: Cart data not found"; return; } const items = window.cart; const totalPrice = window.total || 0; if (!items || items.length === 0) { resultBox.text = "Cart is empty. Nothing to print."; return; } resultBox.text = "Connecting to printer..."; const printer = new ThermalPrinter(); await printer.connect(); resultBox.text = "Printing receipt..."; await printer.printReceipt( "YOUR SHOP NAME", // Replace with your shop name items, totalPrice, items.length ); await printer.disconnect(); resultBox.text = "Printing complete!"; } catch (error) { console.error("Print error:", error); if (resultBox) { resultBox.text = "Print error: " + error.message; } } }; console.log("Bluetooth printing initialized"); } // Initialize once the document is ready if (document.readyState === "complete") { initBtPrinting(); } else { window.addEventListener("load", initBtPrinting); }
top of page

Prêt à scanner

Panier vide

Articles: 0 Total: 0.00 CHF

...

bottom of page