// DOM Elements const connectBtn = document.getElementById('connectBtn'); const printBtn = document.getElementById('printBtn'); const statusText = document.getElementById('statusText'); const statusDot = document.getElementById('statusDot'); const labelForm = document.getElementById('labelForm'); const labelType = document.getElementById('labelType'); // Inputs const itemNameLabel = document.getElementById('itemNameLabel'); const itemName = document.getElementById('itemName'); const itemPriceLabel = document.getElementById('itemPriceLabel'); const itemPrice = document.getElementById('itemPrice'); const itemNotesLabel = document.getElementById('itemNotesLabel'); const itemNotes = document.getElementById('itemNotes'); // Preview Elements const previewName = document.getElementById('previewName'); const previewPrice = document.getElementById('previewPrice'); const previewNotes = document.getElementById('previewNotes'); // Bluetooth State let printDevice = null; let printCharacteristic = null; // Live Preview Updater const labelPreviewContainer = document.getElementById('labelPreview'); const updatePreview = () => { const isCable = labelType.value === 'cable'; const repeats = isCable ? 3 : 1; labelPreviewContainer.innerHTML = ''; // Clear current // Toggle Labels & Placeholders if (isCable) { itemNameLabel.textContent = "Source / Near-End"; itemName.placeholder = "e.g. SW-CORE-01 P12"; itemPriceLabel.textContent = "Destination / Far-End"; itemPrice.placeholder = "e.g. SRV-WEB ETH0"; itemNotesLabel.textContent = "Cable Info (Optional)"; itemNotes.placeholder = "e.g. CAT6 / VLAN 20"; } else { itemNameLabel.textContent = "Item Name"; itemName.placeholder = "e.g. Premium Coffee Beans"; itemPriceLabel.textContent = "Price"; itemPrice.placeholder = "e.g. Rp 50.000"; itemNotesLabel.textContent = "Additional Notes (Optional)"; itemNotes.placeholder = "e.g. 500g, Exp: 24/12/26"; } let blockHtml = ''; if (isCable) { const nameVal = itemName.value || 'SW-CORE-01 P12'; const priceVal = itemPrice.value || 'SRV-WEB ETH0'; const notesVal = itemNotes.value || 'CAT6'; blockHtml = `
[SRC] ${nameVal.toUpperCase()}
[DST] ${priceVal.toUpperCase()}
(${notesVal})
--------------------
`; } else { const nameVal = itemName.value || 'ITEM NAME'; const priceVal = itemPrice.value || 'Rp 0'; const notesVal = itemNotes.value || 'Notes here'; blockHtml = `
LABEL STORE
${nameVal.toUpperCase()}
====================
${priceVal}
====================
`; if (notesVal) { blockHtml += `
${notesVal}
`; } } const blockDiv = document.createElement('div'); blockDiv.innerHTML = blockHtml; labelPreviewContainer.appendChild(blockDiv); }; labelType.addEventListener('change', updatePreview); itemName.addEventListener('input', updatePreview); itemPrice.addEventListener('input', updatePreview); itemNotes.addEventListener('input', updatePreview); // Handle BT Connection connectBtn.addEventListener('click', async () => { try { if (!navigator.bluetooth) { alert('Web Bluetooth API is not available on your browser. Please use Chrome on Android or Desktop.'); return; } connectBtn.textContent = 'Connecting...'; const device = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [ '000018f0-0000-1000-8000-00805f9b34fb', // standard SPP 'e7810a71-73ae-499d-8c15-faa9aef0c3f2', '49535343-fe7d-4ae5-8fa9-9fafd205e455', // iOS serial / generic clone '0000ff00-0000-1000-8000-00805f9b34fb' // generic custom ] }); printDevice = device; device.addEventListener('gattserverdisconnected', onDisconnected); statusText.textContent = 'Pairing...'; const server = await device.gatt.connect(); statusText.textContent = 'Finding Services...'; const services = await server.getPrimaryServices(); // Find the characteristic that supports WRITE without response (or write) let foundChar = null; for (const service of services) { const characteristics = await service.getCharacteristics(); for (const char of characteristics) { if (char.properties.write || char.properties.writeWithoutResponse) { foundChar = char; break; } } if (foundChar) break; } if (!foundChar) { throw new Error('Writable characteristic not found on this device'); } printCharacteristic = foundChar; // UI Update on Success statusDot.className = 'dot connected'; statusText.textContent = `Connected: ${device.name || 'Printer'}`; connectBtn.style.display = 'none'; printBtn.disabled = false; } catch (error) { console.error('Connection Error:', error); alert(`Connection Failed: ${error.message}`); resetUI(); } }); function onDisconnected() { console.log('Device Disconnected'); printDevice = null; printCharacteristic = null; resetUI(); } function resetUI() { statusDot.className = 'dot disconnected'; statusText.textContent = 'Disconnected'; connectBtn.style.display = 'inline-flex'; connectBtn.innerHTML = ' Connect to Printer'; printBtn.disabled = true; } // Convert string to bytes function encodeToBytes(str) { const arr = []; for (let i = 0; i < str.length; i++) { let code = str.charCodeAt(i); if (code > 255) code = 63; // '?' arr.push(code); } return arr; } labelForm.addEventListener('submit', async (e) => { e.preventDefault(); console.log("Submit triggered. Characteristic:", printCharacteristic); if (!printCharacteristic) { alert("Printer is not fully connected. Please ensure you click 'Connect to Printer' first and the indicator is Green (Connected)."); return; } printBtn.disabled = true; printBtn.textContent = 'Printing...'; // Formatting the Print Job using CPCL (Comtec Printer Control Language) // CPCL uses text based commands, ending with PRINT let cpcl = []; // Initialize CPCL label format // Format: ! offset <200|x|y> // 200 dpi, 200 dpi, height ~ 800 dots (100mm) cpcl.push("! 0 200 200 800 1\r\n"); // PAGE-WIDTH 450 (Maximize printable area on 58mm) cpcl.push("PAGE-WIDTH 480\r\n"); let currentY = 20; const isCable = labelType.value === 'cable'; // Print Single Block Logic const generateTextBlock = (startY) => { let y = startY; if (isCable) { // --- Source --- cpcl.push(`TEXT 7 1 0 ${y} [SRC] ${itemName.value.toUpperCase()}\r\n`); y += 50; // --- Destination --- cpcl.push(`TEXT 7 1 0 ${y} [DST] ${itemPrice.value.toUpperCase()}\r\n`); y += 50; // --- Cable Info --- if (itemNotes.value) { cpcl.push(`TEXT 7 0 0 ${y} (${itemNotes.value})\r\n`); y += 50; } // --- Divider --- cpcl.push(`LINE 0 ${y} 430 ${y} 2\r\n`); } else { // --- Header --- cpcl.push(`TEXT 7 0 0 ${y} ITEM NAME\r\n`); y += 40; cpcl.push(`TEXT 7 0 0 ${y} ${itemName.value.toUpperCase()}\r\n`); y += 50; // --- Divider --- cpcl.push(`LINE 0 ${y} 430 ${y} 2\r\n`); y += 20; // --- Price --- cpcl.push(`TEXT 7 1 0 ${y} ${itemPrice.value}\r\n`); y += 60; // --- Divider --- cpcl.push(`LINE 0 ${y} 430 ${y} 2\r\n`); y += 20; // --- Notes --- if (itemNotes.value) { cpcl.push(`TEXT 7 0 0 ${y} ${itemNotes.value}\r\n`); y += 40; } } }; if (labelType.value === 'cable') { // Cable Wrap Style: Print 1 block at the very top. // The bottom 80% of the label will be blank because it overlaps/wraps around the cable. generateTextBlock(20); } else { // Standard Label Style: Single block at top generateTextBlock(20); } // Finish rendering and print cpcl.push("FORM\r\n"); cpcl.push("PRINT\r\n"); // Combine to single string and encode const printString = cpcl.join(""); console.log("Sending CPCL Command:\n", printString); const printData = encodeToBytes(printString); // Recursive Write Queue const MAX_CHUNK = 20; // 20-byte MTU limit for maximum standard BLE compatibility let chunks = []; for (let i = 0; i < printData.length; i += MAX_CHUNK) { chunks.push(new Uint8Array(printData.slice(i, i + MAX_CHUNK))); } const writeNextChunk = async () => { if (chunks.length === 0) { console.log("Print job completed successfully."); printBtn.disabled = false; printBtn.innerHTML = ' Print via Bluetooth'; return; } const chunk = chunks.shift(); try { console.log("Writing chunk...", chunk.length, "bytes. Char props:", Object.keys(printCharacteristic.properties).filter(k => printCharacteristic.properties[k]).join(',')); // Prioritize writeWithoutResponse for cheap clones - they often stall on writeValue if (printCharacteristic.properties && printCharacteristic.properties.writeWithoutResponse) { await printCharacteristic.writeValueWithoutResponse(chunk); } else if (printCharacteristic.properties && printCharacteristic.properties.write) { await printCharacteristic.writeValue(chunk); } else { await printCharacteristic.writeValue(chunk); } setTimeout(writeNextChunk, 100); // Wait 100ms before next 20 byte chunk to ensure buffer digests } catch (error) { console.error("Critical Write Error:", error); alert(`Printing failed: ${error.message}`); printBtn.disabled = false; printBtn.innerHTML = ' Retry Print'; } }; // Kick off printing writeNextChunk(); }); // Initialize the preview on load updatePreview();