299 lines
7.8 KiB
JavaScript
299 lines
7.8 KiB
JavaScript
/*
|
|
* Menggunakan library@mmote/niimbluelib
|
|
*/
|
|
import { ref, computed } from 'vue';
|
|
import {
|
|
NiimbotBluetoothClient,
|
|
NiimbotSerialClient,
|
|
Utils,
|
|
ImageEncoder,
|
|
LabelType
|
|
} from '@mmote/niimbluelib';
|
|
|
|
export function useNiimbotPrinter() {
|
|
// State
|
|
const printerClient = ref(null);
|
|
const connectionState = ref('disconnected'); // 'disconnected' | 'connecting' | 'connected'
|
|
const connectedPrinterName = ref('');
|
|
const printerInfo = ref(null);
|
|
const printerMeta = ref(null);
|
|
const heartbeatData = ref(null);
|
|
const connectionType = ref('bluetooth'); // 'bluetooth' | 'serial'
|
|
const printProgress = ref(0);
|
|
const isPrinting = ref(false);
|
|
|
|
// Computed
|
|
const isConnected = computed(() => connectionState.value === 'connected');
|
|
const isDisconnected = computed(() => connectionState.value === 'disconnected');
|
|
const featureSupport = computed(() => Utils.getAvailableTransports());
|
|
|
|
/**
|
|
* Inisialisasi printer client
|
|
*/
|
|
const initClient = (type = 'bluetooth') => {
|
|
connectionType.value = type;
|
|
|
|
// Disconnect existing client
|
|
if (printerClient.value) {
|
|
printerClient.value.disconnect();
|
|
}
|
|
|
|
// Create new client based on type
|
|
if (type === 'bluetooth') {
|
|
printerClient.value = new NiimbotBluetoothClient();
|
|
} else if (type === 'serial') {
|
|
printerClient.value = new NiimbotSerialClient();
|
|
}
|
|
|
|
// Setup event listeners
|
|
printerClient.value.on('connect', (e) => {
|
|
console.log('Printer connected:', e);
|
|
connectionState.value = 'connected';
|
|
connectedPrinterName.value = e.info.deviceName || 'Niimbot Printer';
|
|
});
|
|
|
|
printerClient.value.on('connect', (e) => {
|
|
console.log('Printer connected (Serial):', e);
|
|
console.log('Device details:', e.info);
|
|
connectionState.value = 'connected';
|
|
connectedPrinterName.value = e.info.deviceName || 'Niimbot Printer';
|
|
});
|
|
|
|
printerClient.value.on('packetreceived', (e) => {
|
|
console.log('<< Packet received (Serial):', Utils.bufToHex(e.packet.toBytes()), e);
|
|
});
|
|
|
|
printerClient.value.on('disconnect', () => {
|
|
console.log('Printer disconnected');
|
|
connectionState.value = 'disconnected';
|
|
connectedPrinterName.value = '';
|
|
printerInfo.value = null;
|
|
printerMeta.value = null;
|
|
});
|
|
|
|
printerClient.value.on('printerinfofetched', (e) => {
|
|
console.log('Printer info fetched:', e.info);
|
|
printerInfo.value = e.info;
|
|
printerMeta.value = printerClient.value.getModelMetadata();
|
|
});
|
|
|
|
printerClient.value.on('heartbeat', (e) => {
|
|
heartbeatData.value = e.data;
|
|
});
|
|
|
|
printerClient.value.on('printprogress', (e) => {
|
|
printProgress.value = Math.floor(
|
|
(e.page / e.totalPages) * ((e.pagePrintProgress + e.pageFeedProgress) / 2)
|
|
);
|
|
});
|
|
|
|
// Log packets for debugging
|
|
printerClient.value.on('packetsent', (e) => {
|
|
console.log('>> Packet sent:', Utils.bufToHex(e.packet.toBytes()));
|
|
});
|
|
|
|
// printerClient.value.on('packetreceived', (e) => {
|
|
// console.log('<< Packet received:', Utils.bufToHex(e.packet.toBytes()));
|
|
// });
|
|
};
|
|
|
|
/**
|
|
* Connect to printer
|
|
*/
|
|
// const connect = async () => {
|
|
// if (!printerClient.value) {
|
|
// initClient(connectionType.value);
|
|
// }
|
|
|
|
// connectionState.value = 'connecting';
|
|
|
|
// try {
|
|
// await printerClient.value.connect();
|
|
// // Connection state will be updated by 'connect' event
|
|
// } catch (error) {
|
|
// console.error('Failed to connect to printer:', error);
|
|
// connectionState.value = 'disconnected';
|
|
// throw error;
|
|
// }
|
|
// };
|
|
|
|
const connect = async () => {
|
|
if (!printerClient.value) {
|
|
initClient(connectionType.value);
|
|
}
|
|
|
|
connectionState.value = 'connecting';
|
|
try {
|
|
const port = await printerClient.value.connect();
|
|
console.log('Selected serial port:', port);
|
|
} catch (error) {
|
|
console.error('Failed to connect to printer (Serial):', error);
|
|
connectionState.value = 'disconnected';
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Disconnect from printer
|
|
*/
|
|
const disconnect = () => {
|
|
if (printerClient.value) {
|
|
printerClient.value.disconnect();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Print QR Code image
|
|
* @param {string} imageDataUrl - Data URL of QR code image
|
|
* @param {object} options - Print options
|
|
*/
|
|
const printQRCode = async (imageDataUrl, options = {}) => {
|
|
if (!printerClient.value || !isConnected.value) {
|
|
throw new Error('Printer not connected. Please connect to printer first.');
|
|
}
|
|
|
|
const {
|
|
density = 3,
|
|
quantity = 1,
|
|
labelType = LabelType.WithGaps,
|
|
printTaskName = 'D110'
|
|
} = options;
|
|
|
|
isPrinting.value = true;
|
|
printProgress.value = 0;
|
|
|
|
try {
|
|
// Stop heartbeat during print
|
|
printerClient.value.stopHeartbeat();
|
|
|
|
// Create print task
|
|
const printTask = printerClient.value.abstraction.newPrintTask(printTaskName, {
|
|
totalPages: quantity,
|
|
density,
|
|
labelType,
|
|
statusPollIntervalMs: 100,
|
|
statusTimeoutMs: 8000,
|
|
});
|
|
|
|
// Load image into canvas
|
|
const canvas = await loadImageToCanvas(imageDataUrl);
|
|
|
|
// Encode canvas to printer format
|
|
const encoded = ImageEncoder.encodeCanvas(canvas, 'top'); // 'top' = print direction
|
|
|
|
// Initialize print
|
|
await printTask.printInit();
|
|
|
|
// Send print data
|
|
await printTask.printPage(encoded, quantity);
|
|
|
|
// Wait for print to finish
|
|
await printTask.waitForFinished();
|
|
|
|
// End print
|
|
await printTask.printEnd();
|
|
|
|
printProgress.value = 100;
|
|
|
|
// Restart heartbeat
|
|
printerClient.value.startHeartbeat();
|
|
|
|
return { success: true, message: 'QR Code printed successfully' };
|
|
|
|
} catch (error) {
|
|
console.error('Print failed:', error);
|
|
|
|
// Try to end print gracefully
|
|
try {
|
|
await printerClient.value.abstraction.printEnd();
|
|
printerClient.value.startHeartbeat();
|
|
} catch (endError) {
|
|
console.error('Failed to end print:', endError);
|
|
}
|
|
|
|
throw error;
|
|
} finally {
|
|
isPrinting.value = false;
|
|
printProgress.value = 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Load image data URL into canvas
|
|
* @param {string} dataUrl - Image data URL
|
|
* @returns {HTMLCanvasElement}
|
|
*/
|
|
const loadImageToCanvas = (dataUrl) => {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = 'white';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
resolve(canvas);
|
|
};
|
|
|
|
img.onerror = () => {
|
|
reject(new Error('Failed to load image'));
|
|
};
|
|
|
|
img.src = dataUrl;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Apply threshold to canvas for better print quality
|
|
* @param {HTMLCanvasElement} canvas
|
|
* @param {number} threshold - 0-255
|
|
*/
|
|
const applyThreshold = (canvas, threshold = 140) => {
|
|
const ctx = canvas.getContext('2d');
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
const data = imageData.data;
|
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
|
|
const val = avg < threshold ? 0 : 255;
|
|
data[i] = val; // red
|
|
data[i + 1] = val; // green
|
|
data[i + 2] = val; // blue
|
|
}
|
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
return canvas;
|
|
};
|
|
|
|
return {
|
|
// State
|
|
printerClient,
|
|
connectionState,
|
|
connectedPrinterName,
|
|
printerInfo,
|
|
printerMeta,
|
|
heartbeatData,
|
|
connectionType,
|
|
printProgress,
|
|
isPrinting,
|
|
|
|
// Computed
|
|
isConnected,
|
|
isDisconnected,
|
|
featureSupport,
|
|
|
|
// Methods
|
|
initClient,
|
|
connect,
|
|
disconnect,
|
|
printQRCode,
|
|
loadImageToCanvas,
|
|
applyThreshold,
|
|
};
|
|
}
|