diff --git a/README.md b/README.md index b2efb1f..12e7f6d 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,13 @@ npx cap sync * [`requestPermissions()`](#requestpermissions) * [`scanBluetooth()`](#scanbluetooth) * [`scanUsb()`](#scanusb) +* [`connectUsb()`](#connectusb) +* [`scanNetwork()`](#scannetwork) * [`printBluetooth(...)`](#printbluetooth) * [`printNetwork(...)`](#printnetwork) * [`printUsb(...)`](#printusb) +* [`printIntent(...)`](#printintent) +* [`checkPrinterStatus(...)`](#checkprinterstatus) * [Interfaces](#interfaces) @@ -76,6 +80,32 @@ Scan connected USB devices -------------------- +### connectUsb() + +```typescript +connectUsb() => Promise +``` + +Connect to USB printer + +**Returns:** Promise<UsbConnectionResult> + +-------------------- + + +### scanNetwork() + +```typescript +scanNetwork() => Promise<{ devices: NetworkPrinter[]; }> +``` + +Scan Wi-Fi / LAN printers + +**Returns:** Promise<{ devices: NetworkPrinter[]; }> + +-------------------- + + ### printBluetooth(...) ```typescript @@ -127,6 +157,36 @@ Print via USB OTG -------------------- +### printIntent(...) + +```typescript +printIntent(options: PrintIntentOptions) => Promise +``` + +| Param | Type | +| ------------- | ----------------------------------------------------------------- | +| **`options`** | PrintIntentOptions | + +-------------------- + + +### checkPrinterStatus(...) + +```typescript +checkPrinterStatus(options: PrinterStatusOptions) => Promise +``` + +Check printer online/offline status + +| Param | Type | +| ------------- | --------------------------------------------------------------------- | +| **`options`** | PrinterStatusOptions | + +**Returns:** Promise<PrinterStatusResult> + +-------------------- + + ### Interfaces @@ -146,6 +206,24 @@ Print via USB OTG | **`address`** | string | +#### UsbConnectionResult + +| Prop | Type | +| --------------- | -------------------- | +| **`name`** | string | +| **`address`** | string | +| **`connected`** | boolean | + + +#### NetworkPrinter + +| Prop | Type | +| ------------- | ------------------- | +| **`name`** | string | +| **`address`** | string | +| **`port`** | number | + + #### BluetoothPrintOptions | Prop | Type | @@ -170,4 +248,41 @@ Print via USB OTG | **`data`** | number[] | | **`deviceId`** | string | + +#### PrintIntentOptions + +| Prop | Type | +| ----------------------- | ------------------- | +| **`receiptText`** | string | +| **`estimateText`** | string | +| **`logoImage`** | string | +| **`tokenDataSales`** | any[] | +| **`tokenDataEstimate`** | any[] | +| **`orderId`** | string | +| **`estimateOrderId`** | string | +| **`footerPrint`** | string | +| **`upiId`** | string | +| **`totalAmount`** | string | +| **`estimateAmount`** | string | + + +#### PrinterStatusResult + +| Prop | Type | +| --------------- | ---------------------------------- | +| **`connected`** | boolean | +| **`status`** | 'online' \| 'offline' | +| **`type`** | string | +| **`address`** | string | +| **`name`** | string | +| **`message`** | string | + + +#### PrinterStatusOptions + +| Prop | Type | +| ------------- | -------------------------------------------- | +| **`type`** | 'USB' \| 'Bluetooth' \| 'Wi-Fi' | +| **`address`** | string | + diff --git a/android/build.gradle b/android/build.gradle index 1d45b99..31f288d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -52,6 +52,9 @@ repositories { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':capacitor-android') + implementation 'com.google.code.gson:gson:2.14.0' + implementation 'com.github.dantsu:escpos-thermalprinter-android:3.4.0' + implementation "com.google.zxing:core:3.5.4" implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" diff --git a/android/src/main/java/com/pozo/printer/BluetoothPrinter.java b/android/src/main/java/com/pozo/printer/BluetoothPrinter.java index 7d96628..8d23876 100644 --- a/android/src/main/java/com/pozo/printer/BluetoothPrinter.java +++ b/android/src/main/java/com/pozo/printer/BluetoothPrinter.java @@ -1,11 +1,25 @@ package com.pozo.printer; +import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import androidx.core.content.ContextCompat; + import com.getcapacitor.JSObject; +import com.pozo.printer.helper.LogoCache; +import com.pozo.printer.model.TokenItem; +import com.pozo.printer.utils.EscPosUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -13,72 +27,455 @@ import java.util.UUID; public class BluetoothPrinter { - private static final UUID SPP_UUID = - UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + private static byte[] CUT_PAPER = new byte[] { 0x1D, 0x56, 0x00 }; - public static List scanDevices(Context context) { - List result = new ArrayList<>(); - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter == null || !adapter.isEnabled()) return result; - Set paired = adapter.getBondedDevices(); - for (BluetoothDevice device : paired) { - JSObject obj = new JSObject(); - obj.put("name", device.getName() != null ? device.getName() : "Unknown"); - obj.put("address", device.getAddress()); - result.add(obj); + private static final UUID PRINTER_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + + public static List scanDevices(Context context) throws Exception { + List result = new ArrayList<>(); + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + + if (adapter == null) { + return result; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && + ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT) + != PackageManager.PERMISSION_GRANTED) { + + throw new Exception("Bluetooth permission not granted"); + } + + Set paired = adapter.getBondedDevices(); + + for (BluetoothDevice device : paired) { + + JSObject obj = new JSObject(); + + obj.put( + "name", + device.getName() != null + ? device.getName() + : "Unknown"); + + obj.put( + "address", + device.getAddress()); + + result.add(obj); + } + return result; } - return result; - } - public static void print(String address, byte[] data) throws Exception { - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter == null || !adapter.isEnabled()) { - throw new Exception("Bluetooth is not available or not enabled"); - } + public static void print(String address, byte[] data) throws Exception { + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + if (adapter == null || !adapter.isEnabled()) { + throw new Exception("Bluetooth is not available or not enabled"); + } - BluetoothDevice device = adapter.getRemoteDevice(address); - adapter.cancelDiscovery(); + BluetoothDevice device = adapter.getRemoteDevice(address); + adapter.cancelDiscovery(); - // ── Retry up to 3 times ── - int maxRetries = 3; - int attempt = 0; - Exception lastError = null; + // ── Retry up to 3 times ── + int maxRetries = 3; + int attempt = 0; + Exception lastError = null; - while (attempt < maxRetries) { - BluetoothSocket socket = null; - try { - socket = device.createRfcommSocketToServiceRecord(SPP_UUID); - socket.connect(); + while (attempt < maxRetries) { + BluetoothSocket socket = null; + try { + socket = device.createRfcommSocketToServiceRecord(PRINTER_UUID); + socket.connect(); - OutputStream out = socket.getOutputStream(); - int chunkSize = 512; - int offset = 0; - while (offset < data.length) { - int size = Math.min(chunkSize, data.length - offset); - out.write(data, offset, size); - out.flush(); - offset += size; - Thread.sleep(10); - } - Thread.sleep(500); - return; // ── success, exit ── + OutputStream out = socket.getOutputStream(); + int chunkSize = 512; + int offset = 0; + while (offset < data.length) { + int size = Math.min(chunkSize, data.length - offset); + out.write(data, offset, size); + out.flush(); + offset += size; + Thread.sleep(10); + } + Thread.sleep(500); + return; // ── success, exit ── - } catch (Exception e) { - lastError = e; - attempt++; - if (socket != null) { - try { socket.close(); } catch (Exception ignored) {} - } - if (attempt < maxRetries) { - Thread.sleep(1000); // wait before retry - } - } finally { - if (socket != null) { - try { socket.close(); } catch (Exception ignored) {} - } + } catch (Exception e) { + lastError = e; + attempt++; + if (socket != null) { + try { + socket.close(); + } catch (Exception ignored) { + } + } + if (attempt < maxRetries) { + Thread.sleep(1000); // wait before retry + } + } finally { + if (socket != null) { + try { + socket.close(); + } catch (Exception ignored) { + } + } + } + } + + throw new Exception("Failed after " + maxRetries + " attempts: " + lastError.getMessage()); } - } - throw new Exception("Failed after " + maxRetries + " attempts: " + lastError.getMessage()); -} + public static void printBluetooth( + Context context, + String macAddress, + String receiptText, + String estimateText, + List> salesTokens, + List> estimateTokens, + String logoImage, + String totalAmount, + String estimateAmount, + String footerPrint, + String upiId, + String orderId, + String estimateOrderId, + String salesId, + String salesIdEst) throws Exception { + + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + + BluetoothDevice device = adapter.getRemoteDevice(macAddress); + + BluetoothSocket socket = connectToDevice(context, device); + + OutputStream outputStream = socket.getOutputStream(); + + long printStartTime = System.currentTimeMillis(); + + Log.d("PRINT_TIME", "Printing Started"); + + if (logoImage != null && !logoImage.isEmpty()) { + + byte[] imageData = LogoCache.getBluetoothLogo(logoImage); + + if (imageData != null) { + + writeInChunks(outputStream, imageData); + + Thread.sleep(2); + } + } + + if (receiptText != null && !receiptText.isEmpty()) { + + byte[] parsedText = parseFormattedTextToEscPos(receiptText, footerPrint); + + writeInChunks(outputStream, parsedText); + + Thread.sleep(2); + + byte[] qrData = generateQRCodeByte(upiId, totalAmount, footerPrint); + + writeInChunks(outputStream, qrData); + + outputStream.write(CUT_PAPER); + + } + + if (estimateText != null && !estimateText.isEmpty()) { + + byte[] parsedText = parseFormattedTextToEscPos(estimateText, footerPrint); + + writeInChunks(outputStream, parsedText); + + Thread.sleep(2); + + byte[] qrData = generateQRCodeByte(upiId, estimateAmount, footerPrint); + + writeInChunks(outputStream, qrData); + + outputStream.write(CUT_PAPER); + + } + + if (salesTokens != null && !salesTokens.isEmpty()) { + + printBluetoothTokens( + outputStream, + salesTokens, + orderId, + "Sales", + salesId); + } + + if (estimateTokens != null && !estimateTokens.isEmpty()) { + + printBluetoothTokens( + outputStream, + estimateTokens, + estimateOrderId, + "Estimate", + salesIdEst); + } + + outputStream.flush(); + outputStream.close(); + socket.close(); + + long printEndTime = System.currentTimeMillis(); + + Log.d("PRINT_TIME", "Printing Completed"); + + Log.d("PRINT_TIME", + "Total Print Time = " + + (printEndTime - printStartTime) + + " ms"); + } + + private static BluetoothSocket connectToDevice( + Context context, + BluetoothDevice device) { + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (bluetoothAdapter == null) { + Log.e("PrinterLog", "Bluetooth not supported"); + return null; + } + + try { + + // Android 12+ permission check + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + + throw new SecurityException("BLUETOOTH_CONNECT permission not granted"); + } + + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) { + + bluetoothAdapter.cancelDiscovery(); + } + + } else { + + bluetoothAdapter.cancelDiscovery(); + } + + Log.d("BT_TIME", "Before create socket"); + + BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(PRINTER_UUID); + + Log.d("BT_TIME", "Before connect"); + + socket.connect(); + + Log.d("BT_TIME", "After connect"); + + Log.d("PrinterLog", "Bluetooth Connected Successfully"); + + return socket; + + } catch (SecurityException e) { + + Log.e("PrinterLog", "Bluetooth permission missing", e); + + } catch (IOException e) { + + Log.e("PrinterLog", "Bluetooth connection failed", e); + + } catch (Exception e) { + + Log.e("PrinterLog", "Unexpected Bluetooth error", e); + + } + + return null; + } + + private static void printBluetoothTokens(OutputStream outputStream, List> tokensList, + String orderId, String SalesorEstimate, + String salesId) throws Exception { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + for (List token : tokensList) { + + buffer.reset(); + + String tokenText = EscPosUtils.buildTokenDesign(token, orderId, salesId, SalesorEstimate); + + byte[] tokenBytes = parseFormattedTextToEscPos(tokenText, ""); + + writeInChunks(outputStream, tokenBytes); + + outputStream.write(new byte[]{0x0A, 0x0A, 0x0A}); + + outputStream.write(CUT_PAPER); + + } + + } + + private static byte[] parseFormattedTextToEscPos(String text, String footerPrint) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try { + String[] lines = text.split("\n"); + + buffer.write(new byte[] { 0x1B, 0x40 }); /* Reset the printer */ + + for (String line : lines) { + // Skip empty lines + if (line.trim().isEmpty()) { + buffer.write("\n".getBytes()); + continue; + } + + // Skip HTML comments + if (line.trim().startsWith("", "") + .replaceAll("|", "") + .replaceAll("|", "") + .replaceAll("]*>|", "") + .replace("[L]", "") + .replace("[R]", "") + .replace("[C]", "") + .replace("–", "-") + .trim(); + + // Only write if there's content after cleaning + if (!cleanLine.isEmpty()) { + buffer.write(cleanLine.getBytes(StandardCharsets.UTF_8)); + buffer.write(0x0A); + + buffer.write(new byte[] { 0x1B, 0x45, 0x00 }); // Bold OFF /*This one For reset a bold. bold off*/ + buffer.write(new byte[] { 0x1B, 0x2D, 0x00 }); // Underline OFF /* This one for reset the Underline + // to off*/ + buffer.write(new byte[] { 0x1D, 0x21, 0x00 }); // Normal size /*This One for normal size*/ + } + } + + // Reset to left alignment at end + buffer.write(new byte[] { 0x1B, 0x61, 0x00 }); + + } catch (Exception e) { + Log.e("ParseError", "Error parsing formatted text", e); + } + + return buffer.toByteArray(); + } + + private static byte[] generateQRCodeByte(String upiId, String amount, String footerPrint) { + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try { + + if (upiId != null && !upiId.isEmpty()) { + + buffer.write(new byte[] { 0x1B, 0x61, 0x01 }); + + String upiString = "upi://pay?pa=" + upiId + "&pn=Merchant&am=" + amount + "&cu=INR"; + + buffer.write(EscPosUtils.generateQRCodeEscPos(upiString)); + buffer.write("Scan To Pay\n".getBytes(StandardCharsets.UTF_8)); + buffer.write(("Rs: " + amount + "\n").getBytes(StandardCharsets.UTF_8)); + + buffer.write(new byte[] { 0x1B, 0x61, 0x00 }); + + } + + buffer.write(EscPosUtils.generateFooter(footerPrint)); + + } catch (Exception e) { + Log.e("QR", "QR Error", e); + } + + return buffer.toByteArray(); + } + + private static void writeInChunks(OutputStream out, byte[] data) throws Exception { + + int offset = 0; + + int chunkSize = 512; + + while (offset < data.length) { + + int len = Math.min(chunkSize, data.length - offset); + + out.write(data, offset, len); + + out.flush(); + + Log.d("BT_CHUNK", "Sent: " + offset + " to " + (offset + len)); + + offset += len; + + Thread.sleep(2); + } + } } \ No newline at end of file diff --git a/android/src/main/java/com/pozo/printer/NetworkPrinter.java b/android/src/main/java/com/pozo/printer/NetworkPrinter.java index 4e5be36..968442e 100644 --- a/android/src/main/java/com/pozo/printer/NetworkPrinter.java +++ b/android/src/main/java/com/pozo/printer/NetworkPrinter.java @@ -1,19 +1,346 @@ package com.pozo.printer; +import android.content.Context; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.text.format.Formatter; +import android.util.Log; + +import com.dantsu.escposprinter.EscPosPrinter; +import com.dantsu.escposprinter.connection.tcp.TcpConnection; +import com.getcapacitor.JSArray; +import com.getcapacitor.JSObject; +import com.pozo.printer.helper.ReceiptBuilder; +import com.pozo.printer.model.TokenItem; +import com.pozo.printer.utils.EscPosUtils; + import java.io.OutputStream; +import java.net.InetSocketAddress; import java.net.Socket; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class NetworkPrinter { - public static void print(String host, int port, byte[] data) throws Exception { - Socket socket = new Socket(host, port); - socket.setSoTimeout(10000); - try { - OutputStream out = socket.getOutputStream(); - out.write(data); - out.flush(); - } finally { - socket.close(); + private static final String TAG = "NETWORK_PRINTER"; + private static final int PORT = 9100; + private static final int TIMEOUT = 1000; + private static final int THREAD_COUNT = 20; + private static final int RETRY_COUNT = 2; + + /** + * Scan current subnet for ESC/POS printers. + */ + public static JSArray scan(Context context) throws Exception { + + JSArray printers = new JSArray(); + + WifiManager wifiManager = + (WifiManager) context.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + + if (wifiManager == null) { + throw new Exception("WifiManager unavailable"); + } + + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + + if (wifiInfo == null) { + throw new Exception("Not connected to Wi-Fi"); + } + + String myIp = Formatter.formatIpAddress(wifiInfo.getIpAddress()); + + if (myIp == null || myIp.equals("0.0.0.0")) { + throw new Exception("Invalid IP Address"); + } + + Log.d(TAG, "My IP : " + myIp); + + String subnet = + myIp.substring(0, myIp.lastIndexOf('.') + 1); + + Log.d(TAG, "Subnet : " + subnet); + + ExecutorService executor = + Executors.newFixedThreadPool(THREAD_COUNT); + + CountDownLatch latch = + new CountDownLatch(254); + + Set foundHosts = + Collections.synchronizedSet(new HashSet<>()); + + for (int i = 1; i <= 254; i++) { + + final String host = subnet + i; + + executor.execute(() -> { + + try { + + boolean connected = false; + + for (int retry = 0; retry < RETRY_COUNT; retry++) { + + Socket socket = new Socket(); + + try { + + socket.connect( + new InetSocketAddress(host, PORT), + TIMEOUT); + + connected = true; + + break; + + } catch (Exception ignored) { + + } finally { + + try { + socket.close(); + } catch (Exception ignored) { + } + + } + + Thread.sleep(100); + + } + + if (connected && foundHosts.add(host)) { + + Log.d(TAG, "Printer Found : " + host); + + synchronized (printers) { + + JSObject obj = new JSObject(); + + obj.put("name", "Network Printer"); + obj.put("address", host); + obj.put("port", PORT); + + printers.put(obj); + + } + + } + + } catch (Exception ignored) { + + } finally { + + latch.countDown(); + + } + + }); + + } + + latch.await(); + + executor.shutdown(); + + Log.d(TAG, "Total Printers : " + printers.length()); + + return printers; + } + + /** + * Print raw ESC/POS data. + */ + public static void print( + String host, + int port, + byte[] data) throws Exception { + + Socket socket = new Socket(); + + socket.connect( + new InetSocketAddress(host, port), + 5000); + + socket.setSoTimeout(10000); + + try { + + OutputStream out = + socket.getOutputStream(); + + out.write(data); + + out.flush(); + + } finally { + + socket.close(); + + } + } + + /** + * Check whether a printer is reachable. + */ + public static boolean isReachable( + String host, + int port) { + + try { + + Socket socket = new Socket(); + + socket.connect( + new InetSocketAddress(host, port), + 500); + + socket.close(); + + return true; + + } catch (Exception e) { + + return false; + } + } + + public static void printNetwork( + String host, + int port, + String receiptText, + String estimateText, + List> salesTokens, + List> estimateTokens, + String logoImage, + String totalAmount, + String estimateAmount, + String footerPrint, + String upiId, + String orderId, + String estimateOrderId, + String salesId, + String salesIdEst) throws Exception { + + Log.d("NETWORK_PRINT", "========== START =========="); + Log.d("NETWORK_PRINT", "Host : " + host); + Log.d("NETWORK_PRINT", "Port : " + port); + + try { + + Log.d("NETWORK_PRINT", "Creating TcpConnection..."); + + TcpConnection connection = new TcpConnection(host, port, 5000); + + Log.d("NETWORK_PRINT", "Creating EscPosPrinter..."); + + EscPosPrinter printer = new EscPosPrinter( + connection, + 203, + 80f, + 45); + + Log.d("NETWORK_PRINT", "Printer Connected"); + + // ---------------- Receipt ---------------- + + if (receiptText != null && !receiptText.isEmpty()) { + + Log.d("NETWORK_PRINT", "Building Receipt..."); + + String receipt = ReceiptBuilder.buildReceipt( + printer, + receiptText, + logoImage, + upiId, + totalAmount, + footerPrint); + + Log.d("NETWORK_PRINT", "Receipt Length : " + receipt.length()); + + Log.d("NETWORK_PRINT", "Printing Receipt..."); + + printer.printFormattedTextAndCut(receipt); + + Log.d("NETWORK_PRINT", "Receipt Printed"); + } + + // ---------------- Estimate ---------------- + + if (estimateText != null && !estimateText.isEmpty()) { + + Log.d("NETWORK_PRINT", "Building Estimate..."); + + String estimate = ReceiptBuilder.buildReceipt( + printer, + estimateText, + logoImage, + upiId, + estimateAmount, + footerPrint); + + Log.d("NETWORK_PRINT", "Estimate Length : " + estimate.length()); + + Log.d("NETWORK_PRINT", "Printing Estimate..."); + + printer.printFormattedTextAndCut(estimate); + + Log.d("NETWORK_PRINT", "Estimate Printed"); + } + + // ---------------- Sales Tokens ---------------- + + if (salesTokens != null && !salesTokens.isEmpty()) { + + Log.d("NETWORK_PRINT", + "Printing Sales Tokens : " + salesTokens.size()); + + EscPosUtils.printUsbOrNetworkTokens( + printer, + salesTokens, + orderId, + salesId, + "Sales"); + + Log.d("NETWORK_PRINT", "Sales Tokens Printed"); + } + + // ---------------- Estimate Tokens ---------------- + + if (estimateTokens != null && !estimateTokens.isEmpty()) { + + Log.d("NETWORK_PRINT", + "Printing Estimate Tokens : " + estimateTokens.size()); + + EscPosUtils.printUsbOrNetworkTokens( + printer, + estimateTokens, + estimateOrderId, + salesIdEst, + "Estimate"); + + Log.d("NETWORK_PRINT", "Estimate Tokens Printed"); + } + + Log.d("NETWORK_PRINT", "Disconnecting Printer..."); + + printer.disconnectPrinter(); + + Log.d("NETWORK_PRINT", "Printer Disconnected"); + + } catch (Exception e) { + + Log.e("NETWORK_PRINT", "Print Failed", e); + + throw e; + } + + Log.d("NETWORK_PRINT", "=========== END ==========="); } - } } \ No newline at end of file diff --git a/android/src/main/java/com/pozo/printer/PrinterPlugin.java b/android/src/main/java/com/pozo/printer/PrinterPlugin.java index 25f7460..9b9c4c5 100644 --- a/android/src/main/java/com/pozo/printer/PrinterPlugin.java +++ b/android/src/main/java/com/pozo/printer/PrinterPlugin.java @@ -1,7 +1,18 @@ package com.pozo.printer; import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; +import android.os.Build; +import android.util.Log; + +import androidx.core.content.ContextCompat; + +import com.dantsu.escposprinter.connection.bluetooth.BluetoothConnection; import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; import com.getcapacitor.PermissionState; @@ -11,197 +22,530 @@ import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; import com.getcapacitor.annotation.PermissionCallback; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import com.pozo.printer.model.TokenItem; + +import java.lang.reflect.Type; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -@CapacitorPlugin( - name = "Printer", - permissions = { - @Permission( - strings = { - Manifest.permission.BLUETOOTH, - Manifest.permission.BLUETOOTH_ADMIN, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN - }, - alias = "bluetooth" - ), - @Permission( - strings = { - Manifest.permission.INTERNET, - Manifest.permission.ACCESS_NETWORK_STATE - }, - alias = "network" - ) - } -) +@CapacitorPlugin(name = "Printer", permissions = { + @Permission(strings = { + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.BLUETOOTH_SCAN + }, alias = "bluetooth"), + @Permission(strings = { + Manifest.permission.INTERNET, + Manifest.permission.ACCESS_NETWORK_STATE + }, alias = "network") +}) public class PrinterPlugin extends Plugin { - // ─── Request Permissions ──────────────────────────────────────── + // ─── Request Permissions ──────────────────────────────────────── - @PluginMethod - public void requestPermissions(PluginCall call) { - requestAllPermissions(call, "permissionsCallback"); - } - - @PermissionCallback - private void permissionsCallback(PluginCall call) { - JSObject result = new JSObject(); - result.put("bluetooth", getPermissionState("bluetooth").toString().toLowerCase()); - result.put("network", getPermissionState("network").toString().toLowerCase()); - call.resolve(result); - } - - // ─── Bluetooth Scan ───────────────────────────────────────────── - - @PluginMethod - public void scanBluetooth(PluginCall call) { - if ( - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && - getContext().checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) - != PackageManager.PERMISSION_GRANTED - ) { - requestPermissionForAlias("bluetooth", call, "bluetoothScanCallback"); - return; + @PluginMethod + public void requestPermissions(PluginCall call) { + requestAllPermissions(call, "permissionsCallback"); } - performBluetoothScan(call); - } - @PermissionCallback - private void bluetoothScanCallback(PluginCall call) { - if (getPermissionState("bluetooth") == PermissionState.GRANTED) { - performBluetoothScan(call); - } else { - call.reject("Bluetooth permission denied"); - } - } - - private void performBluetoothScan(PluginCall call) { - try { - List devices = BluetoothPrinter.scanDevices(getContext()); - JSObject result = new JSObject(); - JSArray arr = new JSArray(); - for (JSObject d : devices) arr.put(d); - result.put("devices", arr); - call.resolve(result); - } catch (Exception e) { - call.reject("Bluetooth scan failed: " + e.getMessage()); - } - } - - // ─── Bluetooth Print ───────────────────────────────────────────── - - @PluginMethod - public void printBluetooth(PluginCall call) { - if ( - android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && - getContext().checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) - != PackageManager.PERMISSION_GRANTED - ) { - requestPermissionForAlias("bluetooth", call, "bluetoothPrintCallback"); - return; - } - performBluetoothPrint(call); - } - - @PermissionCallback - private void bluetoothPrintCallback(PluginCall call) { - if (getPermissionState("bluetooth") == PermissionState.GRANTED) { - performBluetoothPrint(call); - } else { - call.reject("Bluetooth permission denied"); - } - } - - private void performBluetoothPrint(PluginCall call) { - String address = call.getString("address"); - JSArray dataArray = call.getArray("data"); - if (address == null || dataArray == null) { - call.reject("Missing address or data"); - return; - } - new Thread(() -> { - try { - byte[] data = jsArrayToBytes(dataArray); - BluetoothPrinter.print(address, data); + @PermissionCallback + private void permissionsCallback(PluginCall call) { JSObject result = new JSObject(); - result.put("success", true); + result.put("bluetooth", getPermissionState("bluetooth").toString().toLowerCase()); + result.put("network", getPermissionState("network").toString().toLowerCase()); call.resolve(result); - } catch (Exception e) { - call.reject("Bluetooth print failed: " + e.getMessage()); - } - }).start(); - } - - // ─── USB Scan ─────────────────────────────────────────────────── - - @PluginMethod - public void scanUsb(PluginCall call) { - try { - List devices = UsbPrinter.scanDevices(getContext()); - JSObject result = new JSObject(); - JSArray arr = new JSArray(); - for (JSObject d : devices) arr.put(d); - result.put("devices", arr); - call.resolve(result); - } catch (Exception e) { - call.reject("USB scan failed: " + e.getMessage()); } - } - // ─── USB Print ────────────────────────────────────────────────── + // ─── Bluetooth Scan ───────────────────────────────────────────── - @PluginMethod - public void printUsb(PluginCall call) { - JSArray dataArray = call.getArray("data"); - String deviceId = call.getString("deviceId", null); - if (dataArray == null) { - call.reject("Missing data"); - return; + @PluginMethod + public void scanBluetooth(PluginCall call) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && + getContext().checkSelfPermission( + Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + requestPermissionForAlias("bluetooth", call, "bluetoothScanCallback"); + return; + } + performBluetoothScan(call); } - new Thread(() -> { - try { - byte[] data = jsArrayToBytes(dataArray); - UsbPrinter.print(getContext(), data, deviceId); + + @PermissionCallback + private void bluetoothScanCallback(PluginCall call) { + if (getPermissionState("bluetooth") == PermissionState.GRANTED) { + performBluetoothScan(call); + } else { + call.reject("Bluetooth permission denied"); + } + } + + private void performBluetoothScan(PluginCall call) { + try { + List devices = BluetoothPrinter.scanDevices(getContext()); + JSObject result = new JSObject(); + JSArray arr = new JSArray(); + for (JSObject d : devices) + arr.put(d); + result.put("devices", arr); + call.resolve(result); + } catch (Exception e) { + call.reject("Bluetooth scan failed: " + e.getMessage()); + } + } + + // ─── Bluetooth Print ───────────────────────────────────────────── + + @PluginMethod + public void printBluetooth(PluginCall call) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S && + getContext().checkSelfPermission( + Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + requestPermissionForAlias("bluetooth", call, "bluetoothPrintCallback"); + return; + } + performBluetoothPrint(call); + } + + @PluginMethod + public void scanNetwork(PluginCall call) { + + new Thread(() -> { + + try { + + JSObject ret = new JSObject(); + ret.put("devices", NetworkPrinter.scan(getContext())); + + call.resolve(ret); + + } catch (Exception e) { + + call.reject(e.getMessage(), e); + + } + + }).start(); + } + + @PermissionCallback + private void bluetoothPrintCallback(PluginCall call) { + if (getPermissionState("bluetooth") == PermissionState.GRANTED) { + performBluetoothPrint(call); + } else { + call.reject("Bluetooth permission denied"); + } + } + + private void performBluetoothPrint(PluginCall call) { + String address = call.getString("address"); + JSArray dataArray = call.getArray("data"); + if (address == null || dataArray == null) { + call.reject("Missing address or data"); + return; + } + new Thread(() -> { + try { + byte[] data = jsArrayToBytes(dataArray); + BluetoothPrinter.print(address, data); + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + } catch (Exception e) { + call.reject("Bluetooth print failed: " + e.getMessage()); + } + }).start(); + } + + // ─── USB Scan ─────────────────────────────────────────────────── + + @PluginMethod + public void scanUsb(PluginCall call) { + try { + List devices = UsbPrinter.scanDevices(getContext()); + JSObject result = new JSObject(); + JSArray arr = new JSArray(); + for (JSObject d : devices) + arr.put(d); + result.put("devices", arr); + call.resolve(result); + } catch (Exception e) { + call.reject("USB scan failed: " + e.getMessage()); + } + } + + // ─── USB Connect ─────────────────────────────────────────────────── + + @PluginMethod + public void connectUsb(PluginCall call) { + + try { + Log.d("USB_CONNECT" , "USB" + "connectUsb Triggered"); + UsbPrinter.connectUsb( + getContext(), + new UsbPrinter.ConnectCallback() { + @Override + public void onConnected(JSObject printer) { + call.resolve(printer); + } + + @Override + public void onError(String message) { + call.reject(message); + } + + }); + + } catch (Exception e) { + + call.reject(e.getMessage(), e); + + } + } + + // ─── USB Print ────────────────────────────────────────────────── + + @PluginMethod + public void printUsb(PluginCall call) { + JSArray dataArray = call.getArray("data"); + String deviceId = call.getString("deviceId", null); + if (dataArray == null) { + call.reject("Missing data"); + return; + } + + byte[] data; + try { + data = jsArrayToBytes(dataArray); + } catch (Exception e) { + call.reject("Invalid data"); + return; + } + + UsbPrinter.printWithPermission(getContext(), data, deviceId, new UsbPrinter.PrintCallback() { + @Override + public void onSuccess() { + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + } + + @Override + public void onError(String message) { + call.reject("USB print failed: " + message); + } + }); + } + + // ─── Network Print ────────────────────────────────────────────── + + @PluginMethod + public void printNetwork(PluginCall call) { + String host = call.getString("host"); + int port = call.getInt("port", 9100); + JSArray dataArray = call.getArray("data"); + if (host == null || dataArray == null) { + call.reject("Missing host or data"); + return; + } + new Thread(() -> { + try { + byte[] data = jsArrayToBytes(dataArray); + NetworkPrinter.print(host, port, data); + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + } catch (Exception e) { + call.reject("Network print failed: " + e.getMessage()); + } + }).start(); + } + + // ─── Helper ───────────────────────────────────────────────────── + + private byte[] jsArrayToBytes(JSArray arr) throws Exception { + byte[] bytes = new byte[arr.length()]; + for (int i = 0; i < arr.length(); i++) { + bytes[i] = (byte) arr.getInt(i); + } + return bytes; + } + + // ─── Intent ───────────────────────────────────────────────────── + + @PluginMethod + public void printIntent(PluginCall call) { + + String receiptText = call.getString("receiptText", ""); + String estimateText = call.getString("estimateText", ""); + String logoImage = call.getString("logoImage", ""); + + JSArray tokenDataSales = call.getArray("tokenDataSales"); + JSArray tokenDataEstimate = call.getArray("tokenDataEstimate"); + + String orderId = call.getString("orderId", ""); + String estimateOrderId = call.getString("estimateOrderId", ""); + String salesId = call.getString("salesIdNormal", ""); + String salesIdEst = call.getString("salesIdEst", ""); + String footerPrint = call.getString("footerPrint", ""); + String upiId = call.getString("upiId", ""); + String totalAmount = call.getString("totalAmount", ""); + String estimateAmount = call.getString("estimateAmount", ""); + + String macAddress = call.getString("macAddress", ""); + String usbDeviceId = call.getString("usbAddress", ""); + String wifiAddress = call.getString("wifiAddress", ""); + Log.d("AddressUI", "macAddress" + macAddress); + Log.d("AddressUI", "usbAddress" + usbDeviceId); + Log.d("AddressUI", "wifiAddress" + wifiAddress); + Log.d("AddressUI", "totalAmount" + totalAmount); + Log.d("PluginData", "receiptText = " + receiptText); + Log.d("PluginData", "estimateText = " + estimateText); + Log.d("PluginData", "orderId = " + orderId); + Log.d("PluginData", "estimateOrderId = " + estimateOrderId); + Gson gson = new Gson(); + + Type tokenType = new TypeToken>>() { + }.getType(); + + List> salesTokens = new ArrayList<>(); + List> estimateTokens = new ArrayList<>(); + + if (tokenDataSales != null) { + + salesTokens = gson.fromJson(tokenDataSales.toString(), tokenType); + Log.d("TokenDebug", "Sales Tokens Count: " + salesTokens.size()); + + } + + Log.d("TokenDebug", "tokenDataEstimate: " + tokenDataEstimate); + + if (tokenDataEstimate != null) { + + estimateTokens = gson.fromJson(tokenDataEstimate.toString(), tokenType); + Log.d("TokenDebug", "Estimate Tokens Count: " + estimateTokens.size()); + + } + + try { + + if (macAddress != null && !macAddress.trim().isEmpty()) { + + BluetoothPrinter.printBluetooth( + getContext(), + macAddress, + receiptText, + estimateText, + salesTokens, + estimateTokens, + logoImage, + totalAmount, + estimateAmount, + footerPrint, + upiId, + orderId, + estimateOrderId, + salesId, + salesIdEst); + + } else if (usbDeviceId != null && !usbDeviceId.trim().isEmpty()) { + + UsbPrinter.printUsb( + getContext(), + usbDeviceId, + receiptText, + estimateText, + logoImage, + totalAmount, + estimateAmount, + footerPrint, + upiId, + salesTokens, + estimateTokens, + orderId, + estimateOrderId, + salesId, + salesIdEst); + + } else if (wifiAddress != null && !wifiAddress.trim().isEmpty()) { + String[] parts = wifiAddress.split(":"); + String host = parts[0]; + int port = Integer.parseInt(parts[1]); + NetworkPrinter.printNetwork(host, port, receiptText, estimateText, salesTokens, estimateTokens, + logoImage, totalAmount, + estimateAmount, footerPrint, upiId, orderId, estimateOrderId, salesId, salesIdEst); + } else { + + call.reject("No printer selected"); + return; + } + + call.resolve(); + + } catch (Exception e) { + + call.reject(e.getMessage(), e); + + } + + } + + @PluginMethod + public void checkPrinterStatus(PluginCall call) { + JSObject result = new JSObject(); - result.put("success", true); - call.resolve(result); - } catch (Exception e) { - call.reject("USB print failed: " + e.getMessage()); - } - }).start(); - } - // ─── Network Print ────────────────────────────────────────────── + try { - @PluginMethod - public void printNetwork(PluginCall call) { - String host = call.getString("host"); - int port = call.getInt("port", 9100); - JSArray dataArray = call.getArray("data"); - if (host == null || dataArray == null) { - call.reject("Missing host or data"); - return; + String type = call.getString("type", ""); + String address = call.getString("address", ""); + + if (type.isEmpty() || address.isEmpty()) { + call.reject("Type and address are required."); + return; + } + + // -------------------------------------------------- + // USB + // -------------------------------------------------- + if ("USB".equalsIgnoreCase(type)) { + + UsbManager usbManager = (UsbManager) getContext().getSystemService(Context.USB_SERVICE); + + HashMap devices = usbManager.getDeviceList(); + + for (UsbDevice device : devices.values()) { + + if (String.valueOf(device.getDeviceId()).equals(address)) { + + result.put("connected", true); + result.put("status", "online"); + result.put("type", "USB"); + result.put("address", address); + result.put("name", device.getProductName()); + + call.resolve(result); + return; + } + } + + result.put("connected", false); + result.put("status", "offline"); + result.put("type", "USB"); + result.put("address", address); + + call.resolve(result); + return; + } + + // -------------------------------------------------- + // Bluetooth + // -------------------------------------------------- + if ("Bluetooth".equalsIgnoreCase(type)) { + + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); + + if (adapter == null) { + result.put("connected", false); + result.put("status", "offline"); + result.put("message", "Bluetooth not supported"); + call.resolve(result); + return; + } + + BluetoothDevice device = adapter.getRemoteDevice(address); + + try { + + BluetoothConnection connection = new BluetoothConnection(device); + + connection.connect(); + connection.disconnect(); + + result.put("connected", true); + result.put("status", "online"); + + } catch (Exception e) { + + result.put("connected", false); + result.put("status", "offline"); + + } + + result.put("type", "Bluetooth"); + result.put("address", address); + String name = ""; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || + ContextCompat.checkSelfPermission( + getContext(), + Manifest.permission.BLUETOOTH_CONNECT + ) == PackageManager.PERMISSION_GRANTED) { + + name = device.getName(); + } + + result.put("name", name); + + call.resolve(result); + return; + } + + // -------------------------------------------------- + // Wi-Fi + // -------------------------------------------------- + if ("Wi-Fi".equalsIgnoreCase(type)) { + + String[] split = address.split(":"); + + Socket socket = new Socket(); + + try { + + socket.connect( + new InetSocketAddress(split[0], Integer.parseInt(split[1])), + 1500); + + socket.close(); + + result.put("connected", true); + result.put("status", "online"); + + } catch (Exception e) { + + result.put("connected", false); + result.put("status", "offline"); + } + + result.put("type", "Wi-Fi"); + result.put("address", address); + + call.resolve(result); + return; + } + + call.reject("Unsupported printer type."); + + } catch (Exception e) { + + call.reject(e.getMessage()); + + } } - new Thread(() -> { - try { - byte[] data = jsArrayToBytes(dataArray); - NetworkPrinter.print(host, port, data); - JSObject result = new JSObject(); - result.put("success", true); - call.resolve(result); - } catch (Exception e) { - call.reject("Network print failed: " + e.getMessage()); - } - }).start(); - } - // ─── Helper ───────────────────────────────────────────────────── + @Override + public void load() { - private byte[] jsArrayToBytes(JSArray arr) throws Exception { - byte[] bytes = new byte[arr.length()]; - for (int i = 0; i < arr.length(); i++) { - bytes[i] = (byte) arr.getInt(i); + super.load(); + + UsbPrinter.initialize(getContext()); } - return bytes; - } + } \ No newline at end of file diff --git a/android/src/main/java/com/pozo/printer/UsbPrinter.java b/android/src/main/java/com/pozo/printer/UsbPrinter.java index 4835293..a881abf 100644 --- a/android/src/main/java/com/pozo/printer/UsbPrinter.java +++ b/android/src/main/java/com/pozo/printer/UsbPrinter.java @@ -1,90 +1,662 @@ package com.pozo.printer; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.hardware.usb.*; +import android.os.Build; +import android.util.Log; + +import androidx.core.content.ContextCompat; + +import com.dantsu.escposprinter.EscPosPrinter; +import com.dantsu.escposprinter.connection.usb.UsbConnection; import com.getcapacitor.JSObject; +import com.pozo.printer.helper.ReceiptBuilder; +import com.pozo.printer.model.TokenItem; +import com.pozo.printer.utils.EscPosUtils; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class UsbPrinter { - public static List scanDevices(Context context) { - List result = new ArrayList<>(); - UsbManager manager = - (UsbManager) context.getSystemService(Context.USB_SERVICE); - if (manager == null) return result; - HashMap devices = manager.getDeviceList(); - for (UsbDevice device : devices.values()) { - JSObject obj = new JSObject(); - obj.put( - "name", - device.getProductName() != null ? device.getProductName() : "USB Device" - ); - obj.put("address", String.valueOf(device.getDeviceId())); - result.add(obj); - } - return result; - } + private static final String ACTION_USB_PERMISSION = + "com.pozo.printer.USB_PERMISSION"; - public static void print( - Context context, - byte[] data, - String deviceIdStr - ) throws Exception { - UsbManager manager = - (UsbManager) context.getSystemService(Context.USB_SERVICE); - if (manager == null) throw new Exception("USB service not available"); + public static UsbDevice connectedUsbDevice; - HashMap devices = manager.getDeviceList(); - UsbDevice targetDevice = null; + private static ConnectCallback pendingConnectCallback; - if (deviceIdStr != null) { - int deviceId = Integer.parseInt(deviceIdStr); - for (UsbDevice device : devices.values()) { - if (device.getDeviceId() == deviceId) { - targetDevice = device; - break; + private static final BroadcastReceiver usbReceiver = + new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + + if (!ACTION_USB_PERMISSION.equals(intent.getAction())) { + return; + } + + if (pendingConnectCallback == null) { + return; + } + + boolean granted = + intent.getBooleanExtra( + UsbManager.EXTRA_PERMISSION_GRANTED, + false); + + UsbDevice device; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + + device = intent.getParcelableExtra( + UsbManager.EXTRA_DEVICE, + UsbDevice.class); + + } else { + + device = intent.getParcelableExtra( + UsbManager.EXTRA_DEVICE); + } + + if (granted && device != null) { + + connectedUsbDevice = device; + + JSObject obj = new JSObject(); + + obj.put( + "name", + device.getProductName() != null + ? device.getProductName() + : "USB Printer"); + + obj.put( + "address", + String.valueOf(device.getDeviceId())); + + obj.put("connected", true); + + pendingConnectCallback.onConnected(obj); + + } else { + + pendingConnectCallback.onError( + "USB Permission Denied"); + } + + pendingConnectCallback = null; + } + }; + + public static void initialize(Context context) { + + IntentFilter filter = + new IntentFilter(ACTION_USB_PERMISSION); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + + context.registerReceiver( + usbReceiver, + filter, + Context.RECEIVER_NOT_EXPORTED); + + } else { + + context.registerReceiver( + usbReceiver, + filter); } - } - } else { - if (!devices.isEmpty()) { - targetDevice = devices.values().iterator().next(); - } } - if (targetDevice == null) throw new Exception("USB printer not found"); - if (!manager.hasPermission(targetDevice)) - throw new Exception("USB permission denied — prompt user first"); + public static UsbDevice findUsbPrinter(Context context) { - UsbInterface intf = targetDevice.getInterface(0); - UsbEndpoint endpoint = null; - for (int i = 0; i < intf.getEndpointCount(); i++) { - UsbEndpoint ep = intf.getEndpoint(i); - if (ep.getDirection() == UsbConstants.USB_DIR_OUT) { - endpoint = ep; - break; - } + UsbManager manager = + (UsbManager) context.getSystemService(Context.USB_SERVICE); + + if (manager == null) + return null; + + for (UsbDevice device : manager.getDeviceList().values()) { + + for (int i = 0; i < device.getInterfaceCount(); i++) { + + if (device.getInterface(i).getInterfaceClass() + == UsbConstants.USB_CLASS_PRINTER) { + + return device; + } + } + } + + return null; } - if (endpoint == null) throw new Exception("No OUT endpoint on USB device"); - UsbDeviceConnection connection = manager.openDevice(targetDevice); - if (connection == null) throw new Exception("Cannot open USB device"); + // ─── Callback interface ───────────────────────────────────────── - connection.claimInterface(intf, true); - try { - int chunkSize = endpoint.getMaxPacketSize(); - int offset = 0; - while (offset < data.length) { - int size = Math.min(chunkSize, data.length - offset); - byte[] chunk = new byte[size]; - System.arraycopy(data, offset, chunk, 0, size); - connection.bulkTransfer(endpoint, chunk, size, 5000); - offset += size; - } - } finally { - connection.releaseInterface(intf); - connection.close(); + public interface ConnectCallback { + + void onConnected(JSObject printer); + + void onError(String message); } - } + + public interface PrintCallback { + void onSuccess(); + + void onError(String message); + } + + // ─── Scan ─────────────────────────────────────────────────────── + + public static List scanDevices(Context context) { + List result = new ArrayList<>(); + + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + if (manager == null) return result; + + for (UsbDevice device : manager.getDeviceList().values()) { + + boolean isPrinter = false; + + for (int i = 0; i < device.getInterfaceCount(); i++) { + UsbInterface usbInterface = device.getInterface(i); + + if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_PRINTER) { + isPrinter = true; + break; + } + } + + if (isPrinter) { + JSObject obj = new JSObject(); + obj.put("name", device.getProductName() != null ? + device.getProductName() : "USB Printer"); + obj.put("address", String.valueOf(device.getDeviceId())); + + result.add(obj); + } + } + + return result; + } + + private static final String TAG = "USB_CONNECT"; + + public static void connectUsb( + Context context, + ConnectCallback callback) throws Exception { + + Log.d(TAG, "===== connectUsb() called ====="); + + UsbManager manager = + (UsbManager) context.getSystemService(Context.USB_SERVICE); + + Log.d(TAG, "UsbManager: " + manager); + + if (manager == null) { + Log.e(TAG, "UsbManager unavailable"); + throw new Exception("UsbManager unavailable"); + } + + Log.d(TAG, "Searching for USB printer..."); + + UsbDevice printer = findUsbPrinter(context); + + Log.d(TAG, "Printer found: " + printer); + + if (printer == null) { + Log.e(TAG, "No USB printer found"); + throw new Exception("No USB printer found"); + } + + Log.d(TAG, "Device Name: " + printer.getDeviceName()); + Log.d(TAG, "Product Name: " + printer.getProductName()); + Log.d(TAG, "Vendor ID: " + printer.getVendorId()); + Log.d(TAG, "Product ID: " + printer.getProductId()); + Log.d(TAG, "Device ID: " + printer.getDeviceId()); + + boolean hasPermission = manager.hasPermission(printer); + Log.d(TAG, "Has USB Permission: " + hasPermission); + + // Already has permission + if (hasPermission) { + + Log.d(TAG, "Permission already granted."); + + connectedUsbDevice = printer; + + JSObject obj = new JSObject(); + + obj.put( + "name", + printer.getProductName() != null + ? printer.getProductName() + : "USB Printer"); + + obj.put( + "address", + String.valueOf(printer.getDeviceId())); + + obj.put("connected", true); + + Log.d(TAG, "Calling callback.onConnected()"); + callback.onConnected(obj); + + Log.d(TAG, "connectUsb() completed successfully."); + return; + } + + Log.d(TAG, "Permission NOT granted. Requesting permission..."); + + // Save callback until permission result arrives + pendingConnectCallback = callback; + + int flags; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + + flags = + PendingIntent.FLAG_UPDATE_CURRENT + | PendingIntent.FLAG_MUTABLE; + + Log.d(TAG, "Using FLAG_MUTABLE"); + + } else { + + flags = PendingIntent.FLAG_UPDATE_CURRENT; + + Log.d(TAG, "Using FLAG_UPDATE_CURRENT"); + } + + PendingIntent permissionIntent = + PendingIntent.getBroadcast( + context, + 0, + new Intent(ACTION_USB_PERMISSION), + flags); + + Log.d(TAG, "PendingIntent created."); + + manager.requestPermission( + printer, + permissionIntent); + + Log.d(TAG, "requestPermission() called. Waiting for BroadcastReceiver..."); + } + + // ─── Find device ──────────────────────────────────────────────── + + public static UsbDevice findDevice(UsbManager manager, String deviceIdStr) { + HashMap devices = manager.getDeviceList(); + if (deviceIdStr != null) { + int id = Integer.parseInt(deviceIdStr); + for (UsbDevice d : devices.values()) { + if (d.getDeviceId() == id) return d; + } + return null; + } + return devices.isEmpty() ? null : devices.values().iterator().next(); + } + + // ─── Print with auto permission ───────────────────────────────── + + public static void printWithPermission( + Context context, + byte[] data, + String deviceIdStr, + PrintCallback callback + ) { + UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); + if (manager == null) { + callback.onError("USB service not available"); + return; + } + + UsbDevice target = findDevice(manager, deviceIdStr); + if (target == null) { + callback.onError("USB printer not found"); + return; + } + + if (manager.hasPermission(target)) { + // ── Already permitted → print immediately ── + new Thread(() -> { + try { + sendData(manager, target, data); + callback.onSuccess(); + } catch (Exception e) { + callback.onError(e.getMessage()); + } + }).start(); + } else { + // ── No permission → show dialog, then print on grant ── + PendingIntent pi = PendingIntent.getBroadcast( + context, 0, + new Intent(ACTION_USB_PERMISSION), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context ctx, Intent intent) { + if (!ACTION_USB_PERMISSION.equals(intent.getAction())) return; + context.unregisterReceiver(this); + + boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); + if (granted) { + new Thread(() -> { + try { + sendData(manager, target, data); + callback.onSuccess(); + } catch (Exception e) { + callback.onError(e.getMessage()); + } + }).start(); + } else { + callback.onError("USB permission denied by user"); + } + } + }; + + context.registerReceiver(receiver, new IntentFilter(ACTION_USB_PERMISSION)); + manager.requestPermission(target, pi); + } + } + + // ─── Raw data transfer ────────────────────────────────────────── + + private static void sendData(UsbManager manager, UsbDevice device, byte[] data) throws Exception { + Log.d("Gokullog", "sendData: " + device.getProductName()); + + UsbInterface intf = device.getInterface(0); + UsbEndpoint endpoint = null; + for (int i = 0; i < intf.getEndpointCount(); i++) { + UsbEndpoint ep = intf.getEndpoint(i); + if (ep.getDirection() == UsbConstants.USB_DIR_OUT) { + endpoint = ep; + break; + } + } + if (endpoint == null) throw new Exception("No OUT endpoint on USB device"); + + UsbDeviceConnection connection = manager.openDevice(device); + if (connection == null) throw new Exception("Cannot open USB device"); + + connection.claimInterface(intf, true); + try { + int chunkSize = endpoint.getMaxPacketSize(); + int offset = 0; + while (offset < data.length) { + int size = Math.min(chunkSize, data.length - offset); + byte[] chunk = new byte[size]; + System.arraycopy(data, offset, chunk, 0, size); + int transferred = connection.bulkTransfer(endpoint, chunk, size, 5000); + if (transferred < 0) throw new Exception("Transfer failed at offset " + offset); + offset += size; + } + } finally { + connection.releaseInterface(intf); + connection.close(); + } + } + + public static void printUsb( + Context context, + String usbDeviceId, + String receiptText, + String estimateText, + String logoImage, + String totalAmount, + String estimateAmount, + String footerPrint, + String upiId, + List> salesTokens, + List> estimateTokens, + String orderId, + String estimateOrderId, + String salesId, + String salesIdEst) throws Exception { + + UsbManager usbManager = + (UsbManager) context.getSystemService(Context.USB_SERVICE); + + HashMap deviceList = + usbManager.getDeviceList(); + + UsbDevice selectedDevice = null; + + for (UsbDevice device : deviceList.values()) { + + if (String.valueOf(device.getDeviceId()).equals(usbDeviceId)) { + + selectedDevice = device; + break; + } + } + + if (selectedDevice == null) { + throw new Exception("USB printer not found"); + } + + if (!usbManager.hasPermission(selectedDevice)) { + + PendingIntent permissionIntent = + PendingIntent.getBroadcast( + context, + 0, + new Intent(ACTION_USB_PERMISSION), + PendingIntent.FLAG_IMMUTABLE); + + BroadcastReceiver usbReceiver = + new BroadcastReceiver() { + + @Override + public void onReceive( + Context ctx, + Intent intent) { + + if (!ACTION_USB_PERMISSION.equals( + intent.getAction())) { + return; + } + + try { + ctx.unregisterReceiver(this); + } catch (Exception ignored) { + } + + UsbDevice device; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + + device = intent.getParcelableExtra( + UsbManager.EXTRA_DEVICE, + UsbDevice.class); + + } else { + + device = intent.getParcelableExtra( + UsbManager.EXTRA_DEVICE); + } + + boolean granted = + intent.getBooleanExtra( + UsbManager.EXTRA_PERMISSION_GRANTED, + false); + + if (!granted || device == null) { + + Log.e("USB", "Permission denied"); + + return; + } + + try { + + printUsbInternal( + usbManager, + device, + receiptText, + estimateText, + logoImage, + totalAmount, + estimateAmount, + footerPrint, + upiId, + salesTokens, + estimateTokens, + orderId, + estimateOrderId, + salesId, + salesIdEst); + + } catch (Exception e) { + + Log.e("USB", "Print failed", e); + + } + } + }; + + ContextCompat.registerReceiver( + context, + usbReceiver, + new IntentFilter(ACTION_USB_PERMISSION), + ContextCompat.RECEIVER_NOT_EXPORTED); + + usbManager.requestPermission( + selectedDevice, + permissionIntent); + + return; + } + + printUsbInternal( + usbManager, + selectedDevice, + receiptText, + estimateText, + logoImage, + totalAmount, + estimateAmount, + footerPrint, + upiId, + salesTokens, + estimateTokens, + orderId, + estimateOrderId, + salesId, + salesIdEst); + } + + private static void printUsbInternal( + UsbManager usbManager, + UsbDevice selectedDevice, + String receiptText, + String estimateText, + String logoImage, + String totalAmount, + String estimateAmount, + String footerPrint, + String upiId, + List> salesTokens, + List> estimateTokens, + String orderId, + String estimateOrderId, + String salesId, + String salesIdEst) throws Exception { + + UsbConnection connection = + new UsbConnection( + usbManager, + selectedDevice); + + long t = System.currentTimeMillis(); + + EscPosPrinter printer = + new EscPosPrinter( + connection, + 203, + 80f, + 45); + + Log.d("USB_TIME", + "Printer = " + + (System.currentTimeMillis() - t)); + + if (receiptText != null && + !receiptText.isEmpty()) { + + t = System.currentTimeMillis(); + + String receipt = + ReceiptBuilder.buildReceipt( + printer, + receiptText, + logoImage, + upiId, + totalAmount, + footerPrint); + + Log.d("USB_TIME", + "Receipt = " + + (System.currentTimeMillis() - t)); + + t = System.currentTimeMillis(); + + printer.printFormattedTextAndCut(receipt); + + Log.d("USB_TIME", + "Print = " + + (System.currentTimeMillis() - t)); + } + + if (estimateText != null && + !estimateText.isEmpty()) { + + String estimate = + ReceiptBuilder.buildReceipt( + printer, + estimateText, + logoImage, + upiId, + estimateAmount, + footerPrint); + + printer.printFormattedTextAndCut(estimate); + } + + if (salesTokens != null && + !salesTokens.isEmpty()) { + + EscPosUtils.printUsbOrNetworkTokens( + printer, + salesTokens, + orderId, + salesId, + "Sales"); + } + + if (estimateTokens != null && + !estimateTokens.isEmpty()) { + + EscPosUtils.printUsbOrNetworkTokens( + printer, + estimateTokens, + estimateOrderId, + salesIdEst, + "Estimate"); + } + + printer.disconnectPrinter(); + + Log.d("USB", "Print Success"); + } + } \ No newline at end of file diff --git a/android/src/main/java/com/pozo/printer/helper/LogoCache.java b/android/src/main/java/com/pozo/printer/helper/LogoCache.java new file mode 100644 index 0000000..153821f --- /dev/null +++ b/android/src/main/java/com/pozo/printer/helper/LogoCache.java @@ -0,0 +1,155 @@ +package com.pozo.printer.helper; + + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; + +import com.dantsu.escposprinter.EscPosPrinter; +import com.dantsu.escposprinter.textparser.PrinterTextParserImg; +import com.pozo.printer.utils.EscPosUtils; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class LogoCache { + + private static byte[] cachedBluetoothLogo; + + // USB (DantSu) hex string + private static String cachedUsbLogoHex; + + private static String cachedLogoUrl; + + public static Bitmap cachedBitmap; + + public static Bitmap getCachedBitmap(String url) { + + try { + + if (cachedBitmap != null + && !cachedBitmap.isRecycled() + && url.equals(cachedLogoUrl)) { + + Log.d("LOGO_CACHE", "Using cached bitmap"); + return cachedBitmap; + } + + Log.d("LOGO_CACHE", "Downloading logo"); + + URL logoUrl = new URL(url); + + HttpURLConnection connection = (HttpURLConnection) logoUrl.openConnection(); + + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + connection.setDoInput(true); + connection.connect(); + + InputStream input = connection.getInputStream(); + + Bitmap bitmap = BitmapFactory.decodeStream(input); + + input.close(); + connection.disconnect(); + + cachedBitmap = bitmap; + cachedLogoUrl = url; + + // invalidate converted caches + cachedBluetoothLogo = null; + cachedUsbLogoHex = null; + + return cachedBitmap; + + } catch (Exception e) { + + Log.e("Bitmap", "Download failed", e); + return null; + } + } + + public static byte[] getBluetoothLogo(String logoUrl) throws Exception { + + if (cachedBluetoothLogo != null && + logoUrl.equals(cachedLogoUrl)) { + + Log.d("LOGO_CACHE", "Bluetooth logo cache"); + return cachedBluetoothLogo; + } + + Bitmap bitmap = getCachedBitmap(logoUrl); + + if (bitmap == null) + return null; + + Bitmap scaled = Bitmap.createScaledBitmap( + bitmap, + 220, + 220, + true); + + Bitmap rgb565 = scaled.copy( + Bitmap.Config.RGB_565, + false); + + Bitmap centered = EscPosUtils.centerBitmap( + rgb565, + 576); + + cachedBluetoothLogo = EscPosUtils.convertBitmapToEscPos(centered); + + // recycle temporary bitmaps only + if (scaled != bitmap) + scaled.recycle(); + + if (rgb565 != scaled) + rgb565.recycle(); + + if (centered != rgb565) + centered.recycle(); + + return cachedBluetoothLogo; + } + + public static String getUsbLogo( + EscPosPrinter printer, + String logoUrl) throws Exception { + + if (cachedUsbLogoHex != null && + logoUrl.equals(cachedLogoUrl)) { + + Log.d("LOGO_CACHE", "USB logo cache"); + return cachedUsbLogoHex; + } + + Bitmap bitmap = getCachedBitmap(logoUrl); + + if (bitmap == null) + return ""; + + Bitmap scaled = Bitmap.createScaledBitmap( + bitmap, + 170, + 170, + true); + + // Center on 576-dot canvas (80mm printer) + Bitmap centered = EscPosUtils.centerBitmap(scaled, 576); + + cachedUsbLogoHex = PrinterTextParserImg.bitmapToHexadecimalString( + printer, + centered); + + if (centered != scaled) { + centered.recycle(); + } + + if (scaled != bitmap) { + scaled.recycle(); + } + + return cachedUsbLogoHex; + } +} diff --git a/android/src/main/java/com/pozo/printer/helper/ReceiptBuilder.java b/android/src/main/java/com/pozo/printer/helper/ReceiptBuilder.java new file mode 100644 index 0000000..174e0ed --- /dev/null +++ b/android/src/main/java/com/pozo/printer/helper/ReceiptBuilder.java @@ -0,0 +1,158 @@ +package com.pozo.printer.helper; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.Log; + +import com.dantsu.escposprinter.EscPosPrinter; +import com.dantsu.escposprinter.textparser.PrinterTextParserImg; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +public class ReceiptBuilder { + + public static String buildReceipt( + EscPosPrinter printer, + String receiptText, + String logoImage, + String upiId, + String amount, + String footerPrint) { + + String imageDisplay = ""; + String qrCodeSection = ""; + String footerVisible = ""; + + // ----------------------------- + // LOGO + // ----------------------------- + if (logoImage != null && !logoImage.trim().isEmpty()) { + + try { + + long t = System.currentTimeMillis(); + + String logoHex = LogoCache.getUsbLogo(printer, logoImage); + + Log.d("USB_TIME", "Logo = " + (System.currentTimeMillis() - t)); + + if (logoHex != null && !logoHex.isEmpty()) { + + imageDisplay = "[C]" + + logoHex + + "\n"; + } + + } catch (Exception e) { + + Log.e("Printer", "Logo Error", e); + + } + } + + // ----------------------------- + // UPI QR + // ----------------------------- + Log.d("GokulLog", "Upi" + upiId); + Log.d("GokulLog", "amount" + amount); + if (upiId != null && !upiId.trim().isEmpty() + && amount != null + && !amount.trim().isEmpty()) { + + try { + + String upiString = "upi://pay?pa=" + + upiId + + "&pn=Merchant" + + "&am=" + + amount + + "&cu=INR"; + + Bitmap qrBitmap = generateQRCode(upiString); + + qrCodeSection = "[C]" + + PrinterTextParserImg.bitmapToHexadecimalString( + printer, + qrBitmap) + + "\n" + + "[C]Scan To Pay:\n" + + "[C]Rs :- " + amount + "\n"; + + qrBitmap.recycle(); + + } catch (WriterException e) { + + Log.e("Receipt", "QR Generation Error", e); + + } catch (Exception e) { + + Log.e("Receipt", "Unexpected QR Error", e); + + } + } + + // ----------------------------- + // FOOTER + // ----------------------------- + if ("Yes".equalsIgnoreCase(footerPrint)) { + + footerVisible = "[C]Thank You! Visit Again\n"; + + } else { + + footerVisible = ""; + + } + + // ----------------------------- + // FINAL RECEIPT + // ----------------------------- + return imageDisplay + + receiptText + + qrCodeSection + + footerVisible + + "[L]\n\n\n"; + } + + private static Bitmap generateQRCode(String text) throws WriterException { + + BitMatrix bitMatrix = new MultiFormatWriter().encode( + text, + BarcodeFormat.QR_CODE, + 450, + 450); + + int width = bitMatrix.getWidth(); + int height = bitMatrix.getHeight(); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + + for (int x = 0; x < width; x++) { + for (int y = 0; y < height; y++) { + bitmap.setPixel( + x, + y, + bitMatrix.get(x, y) + ? Color.BLACK + : Color.WHITE); + } + } + + return bitmap; + } + + public static String trimText(String text, int maxLength) { + + if (text == null) { + return ""; + } + + if (text.length() <= maxLength) { + return text; + } + + return text.substring(0, maxLength); + } +} diff --git a/android/src/main/java/com/pozo/printer/model/TokenItem.java b/android/src/main/java/com/pozo/printer/model/TokenItem.java new file mode 100644 index 0000000..d82f720 --- /dev/null +++ b/android/src/main/java/com/pozo/printer/model/TokenItem.java @@ -0,0 +1,52 @@ +package com.pozo.printer.model; + +public class TokenItem { + public int slNo; + public String foodName; + public double qty; + + public double rate; + public double price; + + public int getSlNo() { + return slNo; + } + + public void setSlNo(int slNo) { + this.slNo = slNo; + } + + public String getFoodName() { + return foodName; + } + + public void setFoodName(String foodName) { + this.foodName = foodName; + } + + public double getQty() { + return qty; + } + + public void setQty(double qty) { + this.qty = qty; + } + + public double getRate() { + return rate; + } + + public void setRate(double rate) { + this.rate = rate; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + +} \ No newline at end of file diff --git a/android/src/main/java/com/pozo/printer/utils/EscPosUtils.java b/android/src/main/java/com/pozo/printer/utils/EscPosUtils.java new file mode 100644 index 0000000..a582c04 --- /dev/null +++ b/android/src/main/java/com/pozo/printer/utils/EscPosUtils.java @@ -0,0 +1,230 @@ +package com.pozo.printer.utils; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.util.Log; + +import com.dantsu.escposprinter.EscPosPrinter; +import com.pozo.printer.helper.ReceiptBuilder; +import com.pozo.printer.model.TokenItem; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class EscPosUtils { + + public static Bitmap centerBitmap(Bitmap bitmap, int printerWidth) { + + Bitmap output = Bitmap.createBitmap( + printerWidth, + bitmap.getHeight(), + Bitmap.Config.RGB_565); + + Canvas canvas = new Canvas(output); + + // White background + canvas.drawColor(Color.WHITE); + + int left = (printerWidth - bitmap.getWidth()) / 2; + + canvas.drawBitmap(bitmap, left, 0, null); + + return output; + } + + public static byte[] generateFooter(String footerPrint) { + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try { + if ("Yes".equalsIgnoreCase(footerPrint)) { + buffer.write(new byte[] { 0x1B, 0x61, 0x01 }); + buffer.write("\n".getBytes(StandardCharsets.UTF_8)); + buffer.write("Thank You! Visit Again\n\n".getBytes(StandardCharsets.UTF_8)); + + buffer.write("\n\n".getBytes(StandardCharsets.UTF_8)); + buffer.write(new byte[] { 0x1B, 0x61, 0x00 }); + } + } catch (Exception e) { + Log.e("Footer", "Footer Error", e); + } + + return buffer.toByteArray(); + } + + public static byte[] convertBitmapToEscPos(Bitmap bitmap) { + + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + try { + + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + + int bytesPerLine = (width + 7) / 8; + byte[] imageData = new byte[bytesPerLine * height]; + + int index = 0; + + for (int y = 0; y < height; y++) { + + for (int x = 0; x < bytesPerLine; x++) { + + byte slice = 0; + + for (int b = 0; b < 8; b++) { + + int pixelX = x * 8 + b; + + if (pixelX < width) { + + int pixel = bitmap.getPixel(pixelX, y); + + int r = Color.red(pixel); + int g = Color.green(pixel); + int bl = Color.blue(pixel); + + int gray = (r + g + bl) / 3; + + if (gray < 128) { + slice |= (byte) (1 << (7 - b)); + } + } + } + + imageData[index++] = slice; + } + } + + // Center align + stream.write(new byte[] { 0x1B, 0x61, 0x01 }); + + // GS v 0 command + stream.write(new byte[] { + 0x1D, + 0x76, + 0x30, + 0x00, + (byte) (bytesPerLine % 256), + (byte) (bytesPerLine / 256), + (byte) (height % 256), + (byte) (height / 256) + }); + + Log.d("IMAGE_DEBUG", "ImageData Size : " + imageData.length); + stream.write(imageData); + + stream.write(new byte[] { 0x0A, 0x0A }); + + // Left align + stream.write(new byte[] { 0x1B, 0x61, 0x00 }); + + } catch (Exception e) { + Log.e("BitmapError", "Error converting bitmap", e); + } + + return stream.toByteArray(); + } + + public static byte[] generateQRCodeEscPos(String data) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + try { + byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); + int dataLength = dataBytes.length; + + // QR Code model + buffer.write(new byte[] { 0x1D, 0x28, 0x6B, 0x04, 0x00, 0x31, 0x41, 0x32, 0x00 }); // Model 2 + + // QR Code size (1-8, 5 is good for mobile) + buffer.write(new byte[] { 0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, 0x05 }); // Size 5 + + // QR Code error correction level (L=48, M=49, Q=50, H=51) + buffer.write(new byte[] { 0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x45, 0x31 }); // Level M + + // Store QR code data + int pL = (dataLength + 3) % 256; + int pH = (dataLength + 3) / 256; + buffer.write(new byte[] { 0x1D, 0x28, 0x6B, (byte) pL, (byte) pH, 0x31, 0x50, 0x30 }); + buffer.write(dataBytes); + + // Print QR code + buffer.write(new byte[] { 0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30 }); + + } catch (Exception e) { + Log.e("QRCodeError", "Error generating QR code", e); + } + + return buffer.toByteArray(); + } + + public static void printUsbOrNetworkTokens(EscPosPrinter printer, + List> salesTokens, + String orderId, + String salesId, + String salesOrEstimate) throws Exception { + Log.d("TokenPrint", "salesTokens" + salesTokens); + Log.d("TokenPrint", "orderId" + orderId); + if (salesTokens == null || salesTokens.isEmpty()) { + return; + } + + for (List token : salesTokens) { + + String tokenText = EscPosUtils.buildTokenDesign(token, orderId, salesId, salesOrEstimate); + + printer.printFormattedTextAndCut(tokenText); + } + } + + public static String buildTokenDesign( + List token, + String orderId, + String salesId, + String salesOrEstimate) { + + String currentDate = new SimpleDateFormat( + "dd-MM-yyyy", + Locale.ENGLISH).format(new Date()); + + String currentTime = new SimpleDateFormat( + "hh:mm a", + Locale.ENGLISH).format(new Date()); + + StringBuilder bookingData = new StringBuilder(); + + for (TokenItem item : token) { + + bookingData.append(String.format(Locale.ENGLISH, + "%2d %-20s %4.0f %6.0f %7.0f\n", + item.slNo, + ReceiptBuilder.trimText(item.foodName, 20), + item.qty, + item.rate, + item.price)); + } + + String tokenHeading = ""; + + if ("Estimate".equalsIgnoreCase(salesOrEstimate)) { + tokenHeading = " (" + salesOrEstimate + ")"; + } + + return "[C]Token" + tokenHeading + "\n" + + "[C]\n" + + "[L]Bill No : " + orderId + "" + + "[R]" + currentDate + ", " + currentTime + "\n" + + "[C]------------------------------------------------\n" + + "[L]S.No Item Qty Rate Price\n" + + "[C]------------------------------------------------\n" + + bookingData + + "[C]------------------------------------------------\n" + + "[C]\n" + + "[C]YOUR ORDER NO : " + salesId + "\n"; + } +} diff --git a/android/src/main/java/com/pozo/printer/views/ReceiptView.kt b/android/src/main/java/com/pozo/printer/views/ReceiptView.kt new file mode 100644 index 0000000..e69de29 diff --git a/android/src/main/java/com/pozo/printer/views/ReceiptViewStyle2.kt b/android/src/main/java/com/pozo/printer/views/ReceiptViewStyle2.kt new file mode 100644 index 0000000..e69de29 diff --git a/android/src/main/java/com/pozo/printer/views/ReceiptViewStyle3.kt b/android/src/main/java/com/pozo/printer/views/ReceiptViewStyle3.kt new file mode 100644 index 0000000..e69de29 diff --git a/android/src/main/java/com/pozo/printer/views/ReceiptViewToken.kt b/android/src/main/java/com/pozo/printer/views/ReceiptViewToken.kt new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index 20835c8..929eabd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,50 +117,6 @@ "@capacitor/core": "^8.2.0" } }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1326,9 +1282,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -1426,35 +1382,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", - "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^11.0.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2361,9 +2288,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -2578,9 +2505,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3285,18 +3212,6 @@ "dev": true, "license": "ISC" }, - "node_modules/java-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/java-parser/-/java-parser-3.0.1.tgz", - "integrity": "sha512-sDIR7u9b7O2JViNUxiZRhnRz7URII/eE7g2B+BmGxDeS6Ex3OYAcCyz5oh0H4LQ+hL/BS8OJTz8apMy9xtGmrQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "chevrotain": "11.0.3", - "chevrotain-allstar": "0.3.1", - "lodash": "4.17.21" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3305,10 +3220,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3418,20 +3343,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3823,9 +3734,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3873,13 +3784,13 @@ } }, "node_modules/prettier-plugin-java": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.8.1.tgz", - "integrity": "sha512-tkteH5OSCEb0E7wKnhhUSitr1pGUCUt9M//CwerSNhoalL/qv0jXTeSVBPZ36KC+kZl3nbq4dxh144NuGchACg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-java/-/prettier-plugin-java-2.10.0.tgz", + "integrity": "sha512-lDmVgJU/TCBri3kITMcNyr712Ac5Totb1fOp9GW1KCUdv4SsPfbFP7I+mUdRFEoPzE72ymf8MOgIsd/v0yA9CA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "java-parser": "3.0.1" + "web-tree-sitter": "0.26.9" }, "peerDependencies": { "prettier": "^3.0.0" @@ -4756,6 +4667,13 @@ "punycode": "^2.1.0" } }, + "node_modules/web-tree-sitter": { + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.9.tgz", + "integrity": "sha512-YJwSHANl6XFgeEjB8nitgj0qZYt5gkIesJ4w2srS2wcLB4GUa4xcOkM0YaMsU6WNR53YVIkDSY7Ej4pf3IXtCA==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/definitions.ts b/src/definitions.ts index 6443218..fb661ff 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -24,6 +24,50 @@ export interface PermissionResult { network: 'granted' | 'denied' | 'prompt'; } +export interface NetworkPrinter { + name: string; + address: string; + port: number; +} + +export interface PrintIntentOptions { + receiptText: string; + estimateText: string; + logoImage: string; + + tokenDataSales: any[]; + tokenDataEstimate: any[]; + + orderId: string; + estimateOrderId: string; + + footerPrint: string; + upiId: string; + + totalAmount: string; + estimateAmount: string; +} + +export interface UsbConnectionResult { + name: string; + address: string; + connected: boolean; +} + +export interface PrinterStatusOptions { + type: 'USB' | 'Bluetooth' | 'Wi-Fi'; + address: string; +} + +export interface PrinterStatusResult { + connected: boolean; + status: 'online' | 'offline'; + type: string; + address: string; + name?: string; + message?: string; +} + /** * @capacitor-pozo-printer */ @@ -39,6 +83,16 @@ export interface PrinterPlugin { */ scanUsb(): Promise<{ devices: PrinterDevice[] }>; + /** + * Connect to USB printer + */ + connectUsb(): Promise; + + /** + * Scan Wi-Fi / LAN printers + */ + scanNetwork(): Promise<{ devices: NetworkPrinter[] }>; + /** * Print via Bluetooth SPP */ @@ -54,5 +108,13 @@ export interface PrinterPlugin { */ printUsb(options: UsbPrintOptions): Promise<{ success: boolean }>; - + + printIntent(options: PrintIntentOptions): Promise; + + /** + * Check printer online/offline status + */ + checkPrinterStatus( + options: PrinterStatusOptions + ): Promise; } \ No newline at end of file diff --git a/src/web.ts b/src/web.ts index b5f4547..ebb3e87 100644 --- a/src/web.ts +++ b/src/web.ts @@ -2,10 +2,14 @@ import { WebPlugin } from '@capacitor/core'; import type { PrinterPlugin, PrinterDevice, + NetworkPrinter, + UsbConnectionResult, BluetoothPrintOptions, NetworkPrintOptions, UsbPrintOptions, - PermissionResult + PermissionResult, + PrinterStatusOptions, + PrinterStatusResult } from './definitions'; export class PrinterWeb extends WebPlugin implements PrinterPlugin { @@ -19,6 +23,21 @@ export class PrinterWeb extends WebPlugin implements PrinterPlugin { return { devices: [] }; } + async connectUsb(): Promise { + console.warn('USB connection not available on web'); + + return { + name: '', + address: '', + connected: false + }; + } + + async scanNetwork(): Promise<{ devices: NetworkPrinter[] }> { + console.warn('Network scan not available on web'); + return { devices: [] }; + } + async printBluetooth(_options: BluetoothPrintOptions): Promise<{ success: boolean }> { console.warn('Bluetooth printing not available on web'); return { success: false }; @@ -37,4 +56,24 @@ export class PrinterWeb extends WebPlugin implements PrinterPlugin { async requestPermissions(): Promise { return { bluetooth: 'granted', network: 'granted' }; } + + async printIntent(_options: any): Promise { + console.warn('printIntent not available on web'); + } + + async checkPrinterStatus( + _options: PrinterStatusOptions + ): Promise { + + console.warn("Printer status not available on web"); + + return { + connected: false, + status: "offline", + type: _options.type, + address: _options.address, + message: "Not supported on web" + }; + + } }