// SPDX-License-Identifier: MIT package com.rnthermalprinter import android.util.Log import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule import com.rnthermalprinter.connection.ConnectionTester import com.rnthermalprinter.discovery.BluetoothDiscovery import com.rnthermalprinter.printing.PrintingModule import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * React Native Thermal Printer Module * Bridge between React Native and native printing functionality */ @ReactModule(name = RNThermalPrinterModule.NAME) class RNThermalPrinterModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { companion object { const val NAME = "RNThermalPrinter" private const val TAG = "RNThermalPrinter" } // Module instances private val bluetoothDiscovery = BluetoothDiscovery(reactContext) private val connectionTester = ConnectionTester(reactContext) private val printingModule = PrintingModule() private val moduleScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) override fun getName() = NAME // ===== DISCOVERY APIs ===== /** * Scan for Bluetooth devices * Returns both paired and nearby devices */ @ReactMethod fun scanBluetoothDevices(promise: Promise) { try { Log.d(TAG, "[Native:Android] scanBluetoothDevices START") val result = bluetoothDiscovery.scanDevices() Log.d(TAG, "[Native:Android] scanBluetoothDevices SUCCESS: ${result.getArray("devices")?.size() ?: 0} devices found") promise.resolve(result) } catch (e: Exception) { Log.e(TAG, "[Native:Android] scanBluetoothDevices ERROR: ${e.message}", e) promise.reject("SCAN_ERROR", e.message, e) } } /** * Stop Bluetooth device scanning */ @ReactMethod fun stopScanDevices(promise: Promise) { try { Log.d(TAG, "[Native:Android] stopScanDevices") bluetoothDiscovery.stopScan() promise.resolve(true) } catch (e: Exception) { Log.e(TAG, "[Native:Android] stopScanDevices ERROR: ${e.message}", e) promise.reject("STOP_SCAN_ERROR", e.message, e) } } // ===== NEW SIMPLIFIED APIs ===== /** * Test printer connection * Checks if printer exists and is reachable */ @ReactMethod fun testConnection(address: String, promise: Promise) { Log.d(TAG, "[Native:Android] testConnection START: addr=$address") moduleScope.launch { try { val result = connectionTester.testPrinter(address) val resultMap = connectionTester.testResultToWritableMap(result) withContext(Dispatchers.Main) { promise.resolve(resultMap) } } catch (e: Exception) { Log.e(TAG, "[Native:Android] testConnection ERROR: ${e.message}", e) withContext(Dispatchers.Main) { val errorMap = Arguments.createMap() errorMap.putBoolean("success", false) errorMap.putString("error", e.message) promise.resolve(errorMap) // Resolve with error, don't reject } } } } /** * Print raw ESC/POS commands * Handles connection lifecycle automatically */ @ReactMethod fun printRaw(address: String, data: ReadableArray, options: ReadableMap?, promise: Promise) { Log.d(TAG, "[Native:Android] printRaw START: addr=$address, size=${data.size()}b") // ===== CONVERT DATA ===== val bytes = ByteArray(data.size()) for (i in 0 until data.size()) { bytes[i] = data.getInt(i).toByte() } // ===== PARSE OPTIONS ===== val keepAlive = options?.getBoolean("keepAlive") ?: false val timeoutMs = if (options?.hasKey("timeout") == true) { options.getInt("timeout").toLong() } else { 10000L } // ===== EXECUTE PRINT JOB ===== moduleScope.launch { try { val result = printingModule.printRaw( address = address, data = bytes, keepAlive = keepAlive, timeoutMs = timeoutMs ) Log.d(TAG, "[Native:Android] printRaw SUCCESS") withContext(Dispatchers.Main) { promise.resolve(result.toWritableMap()) } } catch (e: Exception) { Log.e(TAG, "[Native:Android] printRaw ERROR: ${e.message}", e) withContext(Dispatchers.Main) { val errorMap = Arguments.createMap() errorMap.putBoolean("success", false) errorMap.putString("error", e.message ?: "Print failed") promise.resolve(errorMap) } } } } /** * Print image from file path * Loads image directly from disk for efficiency */ @ReactMethod fun printImage(address: String, imagePath: String, options: ReadableMap?, promise: Promise) { Log.d(TAG, "[Native:Android] printImage START: addr=$address, path=$imagePath") // ===== PARSE OPTIONS ===== val widthPx = options?.getInt("widthPx") ?: 384 val paperWidthMm = if (options?.hasKey("paperWidthMm") == true) { options.getInt("paperWidthMm") } else { null } val keepAlive = options?.getBoolean("keepAlive") ?: false val timeoutMs = if (options?.hasKey("timeout") == true) { options.getInt("timeout").toLong() } else { 10000L } val isCutPaper = options?.getBoolean("isCutPaper") ?: false val marginMm = options?.getDouble("marginMm") ?: 0.0 val alignNum = options?.getInt("alignNum") ?: 0 // 0=left, 1=center, 2=right // ===== EXECUTE PRINT JOB ===== moduleScope.launch { try { val result = printingModule.printImage( address = address, imagePath = imagePath, widthPx = widthPx, paperWidthMm = paperWidthMm, keepAlive = keepAlive, timeoutMs = timeoutMs, isCutPaper = isCutPaper, marginMm = marginMm, align = alignNum ) Log.d(TAG, "[Native:Android] printImage SUCCESS") withContext(Dispatchers.Main) { promise.resolve(result.toWritableMap()) } } catch (e: Exception) { Log.e(TAG, "[Native:Android] printImage ERROR: ${e.message}", e) withContext(Dispatchers.Main) { val errorMap = Arguments.createMap() errorMap.putBoolean("success", false) errorMap.putString("error", e.message ?: "Print image failed") promise.resolve(errorMap) } } } } /** * Disconnect printer connections * Pass null to disconnect all printers */ @ReactMethod fun disconnect(address: String?, promise: Promise) { try { val target = address ?: "all" Log.d(TAG, "[Native:Android] disconnect: $target") printingModule.disconnect(if (address.isNullOrEmpty()) null else address) promise.resolve(true) } catch (e: Exception) { Log.e(TAG, "[Native:Android] disconnect ERROR: ${e.message}", e) promise.reject("DISCONNECT_ERROR", e.message, e) } } /** * Cleanup resources when module is destroyed */ override fun onCatalystInstanceDestroy() { super.onCatalystInstanceDestroy() Log.d(TAG, "[Native:Android] onCatalystInstanceDestroy: Cleaning up") printingModule.cleanup() moduleScope.cancel() } }