feat: Add Bluetooth printer connection functionality and UI alongside existing USB printer support.
This commit is contained in:
parent
823cb18311
commit
c4a66fcf9a
@ -4,19 +4,29 @@
|
|||||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||||
</div>
|
</div>
|
||||||
@can('create order')
|
@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">
|
<flux:button href="{{ route('studio.manage.order.create') }}" variant="primary" wire:navigate class="text-sm">
|
||||||
Tambah
|
Tambah
|
||||||
</flux:button>
|
</flux:button>
|
||||||
<template x-if="!isConnected">
|
<template x-if="!isConnected">
|
||||||
<flux:button variant="primary" color="cyan" class="ms-2" wire:click="$dispatch('connectUSB')">
|
<div class="flex gap-2 ms-2">
|
||||||
Connect USB
|
<flux:button variant="primary" color="cyan" wire:click="$dispatch('connectUSB')">
|
||||||
</flux:button>
|
Connect USB
|
||||||
|
</flux:button>
|
||||||
|
<flux:button variant="primary" color="indigo" wire:click="$dispatch('connectBluetooth')">
|
||||||
|
Connect Bluetooth
|
||||||
|
</flux:button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="isConnected">
|
<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" />
|
<flux:icon.check-circle class="size-4" />
|
||||||
<span class="text-sm font-medium">Printer Connected</span>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -67,11 +77,13 @@
|
|||||||
<script>
|
<script>
|
||||||
document.addEventListener('livewire:navigated', () => {
|
document.addEventListener('livewire:navigated', () => {
|
||||||
window.printerDevice = null;
|
window.printerDevice = null;
|
||||||
|
window.btCharacteristic = null;
|
||||||
let currentOrder = null;
|
let currentOrder = null;
|
||||||
|
|
||||||
// Check if device is already stored
|
// Check if device is already stored
|
||||||
const savedDevice = localStorage.getItem('printerDevice');
|
const savedUSB = localStorage.getItem('printerDevice');
|
||||||
if (savedDevice) {
|
const savedBT = localStorage.getItem('btPrinterDevice');
|
||||||
|
if (savedUSB || savedBT) {
|
||||||
// Dispatch event to update UI
|
// Dispatch event to update UI
|
||||||
setTimeout(() => window.dispatchEvent(new CustomEvent('printer-status-change', {
|
setTimeout(() => window.dispatchEvent(new CustomEvent('printer-status-change', {
|
||||||
detail: {
|
detail: {
|
||||||
@ -100,13 +112,12 @@
|
|||||||
vendorId: device.vendorId,
|
vendorId: device.vendorId,
|
||||||
productId: device.productId,
|
productId: device.productId,
|
||||||
productName: device.productName,
|
productName: device.productName,
|
||||||
manufacturerName: device.manufacturerName,
|
|
||||||
serialNumber: device.serialNumber
|
|
||||||
};
|
};
|
||||||
|
|
||||||
localStorage.setItem('printerDevice', JSON.stringify(deviceInfo));
|
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', {
|
window.dispatchEvent(new CustomEvent('printer-status-change', {
|
||||||
detail: {
|
detail: {
|
||||||
connected: true
|
connected: true
|
||||||
@ -114,51 +125,138 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
} catch (err) {
|
} 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() {
|
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 {
|
if (target) {
|
||||||
vendorId,
|
await target.open();
|
||||||
productId
|
if (target.configuration === null) {
|
||||||
} = JSON.parse(saved);
|
await target.selectConfiguration(1);
|
||||||
|
}
|
||||||
const devices = await navigator.usb.getDevices();
|
await target.claimInterface(0);
|
||||||
|
window.printerDevice = target;
|
||||||
const target = devices.find(d => d.vendorId === vendorId && d.productId === productId);
|
console.log('Auto reconnected to USB:', target.productName);
|
||||||
|
}
|
||||||
if (!target) return;
|
} catch (err) {
|
||||||
|
console.error('Gagal autoreconnect USB:', err);
|
||||||
try {
|
|
||||||
await target.open();
|
|
||||||
if (target.configuration === null) {
|
|
||||||
await target.selectConfiguration(1);
|
|
||||||
}
|
}
|
||||||
|
} else if (savedBT) {
|
||||||
await target.claimInterface(0);
|
// Bluetooth doesn't support silent reconnect in many browsers without user gesture
|
||||||
window.printerDevice = target;
|
// 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.');
|
||||||
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
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isConnected = !!(window.printerDevice || window.btCharacteristic);
|
||||||
|
window.dispatchEvent(new CustomEvent('printer-status-change', {
|
||||||
|
detail: {
|
||||||
|
connected: isConnected
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
autoReconnect();
|
autoReconnect();
|
||||||
@ -218,25 +316,20 @@ function getMaxChar(widthMM) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function printReceipt(order, paperSize = 58) {
|
async function printReceipt(order, paperSize = 58) {
|
||||||
if (!window.printerDevice) {
|
if (!window.printerDevice && !window.btCharacteristic) {
|
||||||
// Try auto reconnect if not connected but saved
|
// Try auto reconnect or prompt
|
||||||
await autoReconnect();
|
await autoReconnect();
|
||||||
if (!window.printerDevice) {
|
if (!window.printerDevice && !window.btCharacteristic) {
|
||||||
alert("Printer belum terkoneksi via USB.");
|
alert("Printer belum terkoneksi via USB atau Bluetooth.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const device = window.printerDevice;
|
|
||||||
|
|
||||||
// ESC/POS Commands
|
// ESC/POS Commands
|
||||||
const ESC = "\x1B";
|
const ESC = "\x1B";
|
||||||
const GS = "\x1D";
|
|
||||||
const reset = new Uint8Array([0x1B, 0x40]);
|
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 cut = new Uint8Array([0x1D, 0x56, 0x00]);
|
||||||
|
|
||||||
const MAX = getMaxChar(paperSize);
|
const MAX = getMaxChar(paperSize);
|
||||||
@ -305,7 +398,7 @@ function getMaxChar(widthMM) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add extra newline if not packed tight
|
// Add extra newline if not packed tight
|
||||||
text += "\n";
|
text += "\n";
|
||||||
});
|
});
|
||||||
|
|
||||||
text += line;
|
text += line;
|
||||||
@ -331,10 +424,29 @@ function getMaxChar(widthMM) {
|
|||||||
|
|
||||||
const textBytes = encoder.encode(text);
|
const textBytes = encoder.encode(text);
|
||||||
|
|
||||||
// SEND
|
// SEND DATA FUNCTION
|
||||||
await device.transferOut(1, reset);
|
const sendData = async (data) => {
|
||||||
await device.transferOut(1, textBytes);
|
if (window.printerDevice) {
|
||||||
await device.transferOut(1, cut);
|
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>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user