package com.foodlabel.nativeprinter; import com.alibaba.fastjson.JSONObject; import com.foodlabel.nativeprinter.debug.NativePrintDebugState; import com.foodlabel.nativeprinter.support.PluginResult; import com.foodlabel.nativeprinter.support.SafeJson; import com.foodlabel.nativeprinter.support.ThrowableUtils; import com.foodlabel.nativeprinter.template.NativeTemplateCommandBuilder; import com.foodlabel.nativeprinter.transport.GprinterBluetoothTransport; import com.foodlabel.nativeprinter.transport.UposBuiltinPrinterTransport; import com.foodlabel.nativeprinter.transport.UposSerialPortTransport; import com.taobao.weex.annotation.JSMethod; import com.taobao.weex.bridge.JSCallback; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import android.util.Base64; import io.dcloud.feature.uniapp.common.UniModule; public class NativeFastPrinterModule extends UniModule { private static final String BACKEND = "gprinter-sdk"; private static final String PLUGIN_VERSION = "1.2.0"; private static final Object LOCK = new Object(); private static final ExecutorService PRINT_EXECUTOR = Executors.newSingleThreadExecutor(); private static final NativePrintDebugState DEBUG_STATE = new NativePrintDebugState(BACKEND, PLUGIN_VERSION); private static final GprinterBluetoothTransport BLUETOOTH_TRANSPORT = new GprinterBluetoothTransport(); private static final UposBuiltinPrinterTransport UPOS_BUILTIN_TRANSPORT = new UposBuiltinPrinterTransport(); private static final UposSerialPortTransport UPOS_SERIAL_TRANSPORT = new UposSerialPortTransport(); private static final String[] SERIAL_PORT_CANDIDATES = new String[]{ "/dev/ttyS1", "/dev/ttyS0", "/dev/ttyMSM0", "/dev/ttyMSM1", "/dev/ttyUSB0", "/dev/ttyACM0" }; @JSMethod(uiThread = false) public void connect(JSONObject params, JSCallback callback) { String deviceId = SafeJson.getString(params, "deviceId", ""); String deviceName = SafeJson.getString(params, "deviceName", ""); PluginResult result = ensureConnected(deviceId, deviceName); if (callback != null) { callback.invoke(result.toJsonString()); } } @JSMethod(uiThread = false) public void disconnect(JSCallback callback) { synchronized (LOCK) { BLUETOOTH_TRANSPORT.disconnect(); DEBUG_STATE.clearCurrentDevice(); DEBUG_STATE.setStage("disconnect:ok"); DEBUG_STATE.clearError(); } if (callback != null) { callback.invoke(debugResult(PluginResult.ok(false, "", "", "disconnect:ok")).toJsonString()); } } @JSMethod(uiThread = false) public void isConnected(JSCallback callback) { boolean connected; synchronized (LOCK) { connected = BLUETOOTH_TRANSPORT.isConnected(); } if (callback != null) { callback.invoke(debugResult(PluginResult.ok(connected, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "isConnected:ok")).toJsonString()); } } @JSMethod(uiThread = false) public void getDebugInfo(JSCallback callback) { boolean connected; synchronized (LOCK) { connected = BLUETOOTH_TRANSPORT.isConnected(); } if (callback != null) { callback.invoke(debugResult(PluginResult.ok(connected, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "debug:ok")).toJsonString()); } } @JSMethod(uiThread = false) public void printTemplate(JSONObject params, JSCallback callback) { String deviceId = SafeJson.getString(params, "deviceId", ""); String deviceName = SafeJson.getString(params, "deviceName", ""); String templateJson = SafeJson.getString(params, "templateJson", ""); String dataJson = SafeJson.getString(params, "dataJson", "{}"); int dpi = SafeJson.getInt(params, "dpi", 203); int printQty = Math.max(1, SafeJson.getInt(params, "printQty", 1)); if (templateJson == null || templateJson.trim().isEmpty()) { if (callback != null) { callback.invoke(errorResult(9011006, "Template json is empty.").toJsonString()); } return; } PluginResult connectResult = ensureConnected(deviceId, deviceName); if (!connectResult.success) { if (callback != null) callback.invoke(connectResult.toJsonString()); return; } DEBUG_STATE.setStage("printTemplate:queued"); if (callback != null) { callback.invoke(debugResult(PluginResult.ok(true, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "printTemplate:queued")).toJsonString()); } PRINT_EXECUTOR.execute(new Runnable() { @Override public void run() { try { DEBUG_STATE.resetBuildMetrics(); DEBUG_STATE.setStage("build-command"); long buildStarted = System.currentTimeMillis(); NativeTemplateCommandBuilder.BuildResult buildResult = NativeTemplateCommandBuilder.buildWithStats(templateJson, dataJson, dpi, printQty); DEBUG_STATE.setBuildMs(Math.max(0L, System.currentTimeMillis() - buildStarted)); DEBUG_STATE.setBuildResult(buildResult); long writeStarted = System.currentTimeMillis(); synchronized (LOCK) { if (!BLUETOOTH_TRANSPORT.isConnected()) { errorResult(9011005, "Bluetooth printer transport is not ready."); return; } DEBUG_STATE.setStage("write-command"); boolean ok = BLUETOOTH_TRANSPORT.write(buildResult.bytes); if (!ok) { errorResult(9011011, "Printer writeDataImmediately returned false."); return; } } DEBUG_STATE.setWriteMs(Math.max(0L, System.currentTimeMillis() - writeStarted)); DEBUG_STATE.markPrinted(System.currentTimeMillis()); DEBUG_STATE.setStage("printTemplate:ok"); DEBUG_STATE.clearError(); } catch (Throwable e) { DEBUG_STATE.setError(ThrowableUtils.unwrap(e)); DEBUG_STATE.setStage("printTemplate:error"); } } }); } /** * 将 JS 侧已生成的 TSC/指令字节(Base64)经佳博 SDK 写出。 * 用于整页光栅等路径,避免再走 JS 经典蓝牙 socket(慢、易超时)。 */ @JSMethod(uiThread = false) public void printCommandBytes(JSONObject params, JSCallback callback) { String deviceId = SafeJson.getString(params, "deviceId", ""); String deviceName = SafeJson.getString(params, "deviceName", ""); String base64 = SafeJson.getString(params, "base64", ""); if (base64 == null || base64.trim().isEmpty()) { if (callback != null) { callback.invoke(errorResult(9011012, "base64 is empty.").toJsonString()); } return; } PluginResult connectResult = ensureConnected(deviceId, deviceName); if (!connectResult.success) { if (callback != null) { callback.invoke(connectResult.toJsonString()); } return; } DEBUG_STATE.setStage("printCommandBytes:queued"); if (callback != null) { callback.invoke(debugResult(PluginResult.ok(true, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "printCommandBytes:queued")).toJsonString()); } PRINT_EXECUTOR.execute(new Runnable() { @Override public void run() { try { byte[] bytes = Base64.decode(base64, Base64.DEFAULT); if (bytes == null || bytes.length == 0) { DEBUG_STATE.setError("decoded bytes empty"); DEBUG_STATE.setStage("printCommandBytes:error"); return; } long writeStarted = System.currentTimeMillis(); synchronized (LOCK) { if (!BLUETOOTH_TRANSPORT.isConnected()) { errorResult(9011005, "Bluetooth printer transport is not ready."); return; } DEBUG_STATE.setStage("write-raw-command"); boolean ok = BLUETOOTH_TRANSPORT.write(bytes); if (!ok) { errorResult(9011011, "Printer writeDataImmediately returned false."); return; } } DEBUG_STATE.setWriteMs(Math.max(0L, System.currentTimeMillis() - writeStarted)); DEBUG_STATE.markPrinted(System.currentTimeMillis()); DEBUG_STATE.setStage("printCommandBytes:ok"); DEBUG_STATE.clearError(); } catch (Throwable e) { DEBUG_STATE.setError(ThrowableUtils.unwrap(e)); DEBUG_STATE.setStage("printCommandBytes:error"); } } }); } /** * UnifiedPOS SDK: built-in printer / serial port printing (no Bluetooth pairing). * * Params: * - base64: required, raw command bytes * - prefer: optional, "builtin" | "serial" (default builtin) * - serialPath: optional, e.g. "/dev/ttyS1" * - baudrate: optional, default 9600 */ @JSMethod(uiThread = false) public void printUposCommandBytes(JSONObject params, JSCallback callback) { String base64 = SafeJson.getString(params, "base64", ""); String prefer = SafeJson.getString(params, "prefer", "builtin"); String serialPath = SafeJson.getString(params, "serialPath", ""); int baudrate = Math.max(1200, SafeJson.getInt(params, "baudrate", 9600)); if (base64 == null || base64.trim().isEmpty()) { if (callback != null) { callback.invoke(errorResult(9011012, "base64 is empty.").toJsonString()); } return; } DEBUG_STATE.setStage("printUposCommandBytes:queued"); PRINT_EXECUTOR.execute(new Runnable() { @Override public void run() { try { byte[] bytes = Base64.decode(base64, Base64.DEFAULT); if (bytes == null || bytes.length == 0) { DEBUG_STATE.setError("decoded bytes empty"); DEBUG_STATE.setStage("printUposCommandBytes:error"); return; } long writeStarted = System.currentTimeMillis(); boolean ok; if ("serial".equalsIgnoreCase(prefer)) { ok = writeBySerial(bytes, serialPath, baudrate); } else { ok = writeByBuiltinPrinter(bytes); if (!ok) { // fallback to serial if builtin printer open/print failed ok = writeBySerial(bytes, serialPath, baudrate); } } DEBUG_STATE.setWriteMs(Math.max(0L, System.currentTimeMillis() - writeStarted)); DEBUG_STATE.markPrinted(System.currentTimeMillis()); if (ok) { DEBUG_STATE.setStage("printUposCommandBytes:ok"); DEBUG_STATE.clearError(); if (callback != null) { callback.invoke(debugResult(PluginResult.ok(true, "", "", "printUposCommandBytes:ok")).toJsonString()); } } else { DEBUG_STATE.setStage("printUposCommandBytes:error"); if (DEBUG_STATE.getLastError() == null || DEBUG_STATE.getLastError().isEmpty()) { DEBUG_STATE.setError("upos write failed"); } if (callback != null) { callback.invoke(errorResult(9011020, "UPOS print failed: " + DEBUG_STATE.getLastError()).toJsonString()); } } } catch (Throwable e) { DEBUG_STATE.setError(ThrowableUtils.unwrap(e)); DEBUG_STATE.setStage("printUposCommandBytes:error"); if (callback != null) { callback.invoke(errorResult(9011021, "UPOS print exception: " + DEBUG_STATE.getLastError()).toJsonString()); } } } }); } private static boolean writeByBuiltinPrinter(byte[] bytes) { try { DEBUG_STATE.setStage("upos:builtin:write"); boolean ok = UPOS_BUILTIN_TRANSPORT.write(bytes); if (!ok) { DEBUG_STATE.setError("upos builtin failed: " + UPOS_BUILTIN_TRANSPORT.getLastError()); } return ok; } catch (Throwable e) { DEBUG_STATE.setError(ThrowableUtils.unwrap(e)); return false; } } private static boolean writeBySerial(byte[] bytes, String path, int baudrate) { try { String usePath = (path != null && !path.trim().isEmpty()) ? path.trim() : null; if (usePath != null) { DEBUG_STATE.setStage("upos:serial:open:" + usePath); UPOS_SERIAL_TRANSPORT.close(); if (!UPOS_SERIAL_TRANSPORT.open(usePath, baudrate)) { DEBUG_STATE.setError("upos serial open failed: " + UPOS_SERIAL_TRANSPORT.getLastError()); return false; } DEBUG_STATE.setStage("upos:serial:write"); boolean ok = UPOS_SERIAL_TRANSPORT.write(bytes); if (!ok) DEBUG_STATE.setError("upos serial write failed: " + UPOS_SERIAL_TRANSPORT.getLastError()); UPOS_SERIAL_TRANSPORT.close(); return ok; } for (String candidate : SERIAL_PORT_CANDIDATES) { DEBUG_STATE.setStage("upos:serial:open:" + candidate); UPOS_SERIAL_TRANSPORT.close(); if (!UPOS_SERIAL_TRANSPORT.open(candidate, baudrate)) { continue; } DEBUG_STATE.setStage("upos:serial:write"); boolean ok = UPOS_SERIAL_TRANSPORT.write(bytes); if (!ok) { UPOS_SERIAL_TRANSPORT.close(); continue; } UPOS_SERIAL_TRANSPORT.close(); return true; } DEBUG_STATE.setError("upos serial open failed: no candidate ports worked"); return false; } catch (Throwable e) { DEBUG_STATE.setError(ThrowableUtils.unwrap(e)); return false; } } private PluginResult ensureConnected(String deviceId, String deviceName) { synchronized (LOCK) { PluginResult result = BLUETOOTH_TRANSPORT.ensureConnected(deviceId, deviceName, DEBUG_STATE); if (!result.success) { return debugResult(result); } return debugResult(PluginResult.ok(true, DEBUG_STATE.getCurrentDeviceId(), DEBUG_STATE.getCurrentDeviceName(), "connect:ok")); } } private static PluginResult errorResult(int code, String message) { DEBUG_STATE.setError(message == null ? "" : message); return debugResult(PluginResult.error(code, message)); } private static PluginResult debugResult(PluginResult result) { return DEBUG_STATE.attachTo(result); } }