feat: Add Bluetooth printer connection functionality and UI alongside existing USB printer support.

This commit is contained in:
Yoga Pangestu 2026-01-04 22:51:01 +07:00
parent 823cb18311
commit c4a66fcf9a

View File

@ -4,19 +4,29 @@
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
@can('create order')
<div class="flex" x-data="{ isConnected: !!localStorage.getItem('printerDevice') }" @printer-status-change.window="isConnected = $event.detail.connected">
<div class="flex" x-data="{
isConnected: !!(localStorage.getItem('printerDevice') || localStorage.getItem('btPrinterDevice'))
}" @printer-status-change.window="isConnected = $event.detail.connected">
<flux:button href="{{ route('studio.manage.order.create') }}" variant="primary" wire:navigate class="text-sm">
Tambah
</flux:button>
<template x-if="!isConnected">
<flux:button variant="primary" color="cyan" class="ms-2" wire:click="$dispatch('connectUSB')">
Connect USB
</flux:button>
<div class="flex gap-2 ms-2">
<flux:button variant="primary" color="cyan" wire:click="$dispatch('connectUSB')">
Connect USB
</flux:button>
<flux:button variant="primary" color="indigo" wire:click="$dispatch('connectBluetooth')">
Connect Bluetooth
</flux:button>
</div>
</template>
<template x-if="isConnected">
<div class="ms-2 flex items-center gap-2 text-green-600 bg-green-50 px-3 py-1 rounded-lg border border-green-200">
<div
class="ms-2 flex items-center gap-2 text-green-600 bg-green-50 px-3 py-1 rounded-lg border border-green-200">
<flux:icon.check-circle class="size-4" />
<span class="text-sm font-medium">Printer Connected</span>
{{-- <flux:button variant="ghost" size="xs" color="danger" class="ms-1" icon="x-mark"
x-on:click="localStorage.removeItem('printerDevice'); localStorage.removeItem('btPrinterDevice'); window.printerDevice = null; window.btCharacteristic = null; window.dispatchEvent(new CustomEvent('printer-status-change', { detail: { connected: false } }))" /> --}}
</div>
</template>
</div>
@ -67,11 +77,13 @@
<script>
document.addEventListener('livewire:navigated', () => {
window.printerDevice = null;
window.btCharacteristic = null;
let currentOrder = null;
// Check if device is already stored
const savedDevice = localStorage.getItem('printerDevice');
if (savedDevice) {
const savedUSB = localStorage.getItem('printerDevice');
const savedBT = localStorage.getItem('btPrinterDevice');
if (savedUSB || savedBT) {
// Dispatch event to update UI
setTimeout(() => window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
@ -100,13 +112,12 @@
vendorId: device.vendorId,
productId: device.productId,
productName: device.productName,
manufacturerName: device.manufacturerName,
serialNumber: device.serialNumber
};
localStorage.setItem('printerDevice', JSON.stringify(deviceInfo));
localStorage.removeItem('btPrinterDevice'); // Clear BT if USB is connected
console.log('Connected to:', device.productName);
console.log('Connected to USB:', device.productName);
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: true
@ -114,51 +125,138 @@
}));
} catch (err) {
console.error('Gagal connect:', err);
console.error('Gagal connect USB:', err);
}
});
Livewire.on('connectBluetooth', async (event) => {
try {
const device = await navigator.bluetooth.requestDevice({
filters: [{
services: ['000018f0-0000-1000-8000-00805f9b34fb']
},
{
services: ['0000ff00-0000-1000-8000-00805f9b34fb']
},
{
services: ['49535343-fe7d-4ae5-8fa9-9fafd205e455']
}, // microchip
{
namePrefix: 'TP'
},
{
namePrefix: 'RP'
},
{
namePrefix: 'MPT'
},
{
namePrefix: 'InnerPrinter'
},
{
namePrefix: 'Bluetooth'
}
],
optionalServices: [
'00001101-0000-1000-8000-00805f9b34fb',
'000018f0-0000-1000-8000-00805f9b34fb',
'0000ff00-0000-1000-8000-00805f9b34fb',
'49535343-fe7d-4ae5-8fa9-9fafd205e455'
]
});
const server = await device.gatt.connect();
// Try to find the write characteristic
const services = await server.getPrimaryServices();
let characteristic = null;
for (const service of services) {
const characteristics = await service.getCharacteristics();
for (const char of characteristics) {
if (char.properties.write || char.properties.writeWithoutResponse) {
characteristic = char;
break;
}
}
if (characteristic) break;
}
if (!characteristic) {
throw new Error("Tidak menemukan characteristic untuk menulis data.");
}
window.btCharacteristic = characteristic;
const deviceInfo = {
name: device.name,
id: device.id
};
localStorage.setItem('btPrinterDevice', JSON.stringify(deviceInfo));
localStorage.removeItem('printerDevice'); // Clear USB if BT is connected
console.log('Connected to Bluetooth:', device.name);
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: true
}
}));
device.addEventListener('gattserverdisconnected', () => {
console.log('Bluetooth disconnected');
window.btCharacteristic = null;
localStorage.removeItem('btPrinterDevice');
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: false
}
}));
});
} catch (err) {
console.error('Gagal connect Bluetooth:', err);
alert('Gagal menghubungkan Bluetooth: ' + err.message);
}
});
async function autoReconnect() {
const saved = localStorage.getItem('printerDevice');
const savedUSB = localStorage.getItem('printerDevice');
const savedBT = localStorage.getItem('btPrinterDevice');
if (!saved) return;
if (savedUSB) {
const {
vendorId,
productId
} = JSON.parse(savedUSB);
try {
const devices = await navigator.usb.getDevices();
const target = devices.find(d => d.vendorId === vendorId && d.productId === productId);
const {
vendorId,
productId
} = JSON.parse(saved);
const devices = await navigator.usb.getDevices();
const target = devices.find(d => d.vendorId === vendorId && d.productId === productId);
if (!target) return;
try {
await target.open();
if (target.configuration === null) {
await target.selectConfiguration(1);
if (target) {
await target.open();
if (target.configuration === null) {
await target.selectConfiguration(1);
}
await target.claimInterface(0);
window.printerDevice = target;
console.log('Auto reconnected to USB:', target.productName);
}
} catch (err) {
console.error('Gagal autoreconnect USB:', err);
}
await target.claimInterface(0);
window.printerDevice = target;
console.log('Auto reconnected to:', target.productName);
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: true
}
}));
} catch (err) {
console.error('Gagal autoreconnect:', err);
// If fail, remove storage
localStorage.removeItem('printerDevice');
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: false
}
}));
} else if (savedBT) {
// Bluetooth doesn't support silent reconnect in many browsers without user gesture
// But we can check if it's still connected or just clear it if it needs manual action
console.log('Bluetooth device saved, but may need manual reconnection if disconnected.');
}
const isConnected = !!(window.printerDevice || window.btCharacteristic);
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: isConnected
}
}));
}
autoReconnect();
@ -218,25 +316,20 @@ function getMaxChar(widthMM) {
});
async function printReceipt(order, paperSize = 58) {
if (!window.printerDevice) {
// Try auto reconnect if not connected but saved
if (!window.printerDevice && !window.btCharacteristic) {
// Try auto reconnect or prompt
await autoReconnect();
if (!window.printerDevice) {
alert("Printer belum terkoneksi via USB.");
if (!window.printerDevice && !window.btCharacteristic) {
alert("Printer belum terkoneksi via USB atau Bluetooth.");
return;
}
}
const encoder = new TextEncoder();
const device = window.printerDevice;
// ESC/POS Commands
const ESC = "\x1B";
const GS = "\x1D";
const reset = new Uint8Array([0x1B, 0x40]);
const alignLeft = new Uint8Array([0x1B, 0x61, 0]);
const alignCenter = new Uint8Array([0x1B, 0x61, 1]);
const alignRight = new Uint8Array([0x1B, 0x61, 2]);
const cut = new Uint8Array([0x1D, 0x56, 0x00]);
const MAX = getMaxChar(paperSize);
@ -305,7 +398,7 @@ function getMaxChar(widthMM) {
}
// Add extra newline if not packed tight
text += "\n";
text += "\n";
});
text += line;
@ -331,10 +424,29 @@ function getMaxChar(widthMM) {
const textBytes = encoder.encode(text);
// SEND
await device.transferOut(1, reset);
await device.transferOut(1, textBytes);
await device.transferOut(1, cut);
// SEND DATA FUNCTION
const sendData = async (data) => {
if (window.printerDevice) {
await window.printerDevice.transferOut(1, data);
} else if (window.btCharacteristic) {
// Bluetooth often has a limit on packet size (typically 20-512 bytes)
// Split data into chunks if necessary
const chunkSize = 100;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
await window.btCharacteristic.writeValue(chunk);
}
}
};
try {
await sendData(reset);
await sendData(textBytes);
await sendData(cut);
} catch (err) {
console.error('Gagal mencetak:', err);
alert('Gagal mencetak: ' + err.message);
}
}
});
</script>