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.8"; private static final String BUILD_TIME = String.valueOf(System.currentTimeMillis()); 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(); /** * Some builds of NativePrintDebugState.attachTo() do not expose stage/lastError in JSON. * Keep an explicit copy here so JS can always read it for diagnosis. */ private static volatile String LAST_STAGE = ""; private static volatile String LAST_ERROR = ""; private static volatile int LAST_COMMAND_BYTES = 0; private static volatile long LAST_WRITE_MS = 0L; private static void setStage(String stage) { LAST_STAGE = stage == null ? "" : stage; DEBUG_STATE.setStage(LAST_STAGE); } private static void setError(String error) { LAST_ERROR = error == null ? "" : error; DEBUG_STATE.setError(LAST_ERROR); } private static void clearError() { LAST_ERROR = ""; DEBUG_STATE.clearError(); } private static void setCommandBytes(int n) { LAST_COMMAND_BYTES = Math.max(0, n); } private static void setWriteMs(long ms) { LAST_WRITE_MS = Math.max(0L, ms); } 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(); setStage("disconnect:ok"); 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)); String outputTransport = SafeJson.getString(params, "outputTransport", "bluetooth"); final String uposPrefer = SafeJson.getString(params, "uposPrefer", "builtin"); final String uposSerialPath = SafeJson.getString(params, "uposSerialPath", ""); final int uposBaudrate = Math.max(1200, SafeJson.getInt(params, "uposBaudrate", 9600)); final boolean useUpos = "upos".equalsIgnoreCase(outputTransport); if (templateJson == null || templateJson.trim().isEmpty()) { if (callback != null) { callback.invoke(errorResult(9011006, "Template json is empty.").toJsonString()); } return; } if (!useUpos) { PluginResult connectResult = ensureConnected(deviceId, deviceName); if (!connectResult.success) { if (callback != null) callback.invoke(connectResult.toJsonString()); return; } } setStage("printTemplate:queued"); if (callback != null) { String did = useUpos ? "builtin-upos" : DEBUG_STATE.getCurrentDeviceId(); String dname = useUpos ? "Built-in UPOS" : DEBUG_STATE.getCurrentDeviceName(); callback.invoke(debugResult(PluginResult.ok(true, did, dname, "printTemplate:queued")).toJsonString()); } PRINT_EXECUTOR.execute(new Runnable() { @Override public void run() { try { DEBUG_STATE.resetBuildMetrics(); 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); if (buildResult.bytes == null || buildResult.bytes.length == 0) { setError("built command bytes empty"); setStage("printTemplate:error"); return; } if (useUpos) { setCommandBytes(buildResult.bytes.length); setStage("write-upos-command"); boolean ok = dispatchUposPrint(buildResult.bytes, uposPrefer, uposSerialPath, uposBaudrate); if (!ok) { setStage("printTemplate:error"); if (DEBUG_STATE.getLastError() == null || DEBUG_STATE.getLastError().isEmpty()) { setError("upos write failed"); } return; } DEBUG_STATE.markPrinted(System.currentTimeMillis()); setStage("printTemplate:ok"); clearError(); return; } long writeStarted = System.currentTimeMillis(); synchronized (LOCK) { if (!BLUETOOTH_TRANSPORT.isConnected()) { errorResult(9011005, "Bluetooth printer transport is not ready."); return; } setStage("write-command"); boolean ok = BLUETOOTH_TRANSPORT.write(buildResult.bytes); if (!ok) { errorResult(9011011, "Printer writeDataImmediately returned false."); return; } } setCommandBytes(buildResult.bytes.length); DEBUG_STATE.setWriteMs(Math.max(0L, System.currentTimeMillis() - writeStarted)); DEBUG_STATE.markPrinted(System.currentTimeMillis()); setStage("printTemplate:ok"); clearError(); } catch (Throwable e) { setError(ThrowableUtils.unwrap(e)); 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; } 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) { setError("decoded bytes empty"); setStage("printCommandBytes:error"); return; } long writeStarted = System.currentTimeMillis(); synchronized (LOCK) { if (!BLUETOOTH_TRANSPORT.isConnected()) { errorResult(9011005, "Bluetooth printer transport is not ready."); return; } 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()); setStage("printCommandBytes:ok"); clearError(); } catch (Throwable e) { setError(ThrowableUtils.unwrap(e)); 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; } 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) { setError("decoded bytes empty"); setStage("printUposCommandBytes:error"); return; } setCommandBytes(bytes.length); boolean ok = dispatchUposPrint(bytes, prefer, serialPath, baudrate); DEBUG_STATE.markPrinted(System.currentTimeMillis()); if (ok) { setStage("printUposCommandBytes:ok"); clearError(); if (callback != null) { callback.invoke(debugResult(PluginResult.ok(true, "", "", "printUposCommandBytes:ok")).toJsonString()); } } else { setStage("printUposCommandBytes:error"); if (DEBUG_STATE.getLastError() == null || DEBUG_STATE.getLastError().isEmpty()) { setError("upos write failed"); } if (callback != null) { callback.invoke(errorResult(9011020, "UPOS print failed: " + DEBUG_STATE.getLastError()).toJsonString()); } } } catch (Throwable e) { setError(ThrowableUtils.unwrap(e)); setStage("printUposCommandBytes:error"); if (callback != null) { callback.invoke(errorResult(9011021, "UPOS print exception: " + DEBUG_STATE.getLastError()).toJsonString()); } } } }); } /** * 内置/串口 UPOS 写出(与 printUposCommandBytes 同源逻辑)。 */ private static boolean dispatchUposPrint(byte[] bytes, String prefer, String serialPath, int baudrate) { if (bytes == null || bytes.length == 0) { setError("bytes empty"); return false; } long writeStarted = System.currentTimeMillis(); boolean ok; if ("serial".equalsIgnoreCase(prefer)) { ok = writeBySerial(bytes, serialPath, baudrate); } else { ok = writeByBuiltinPrinter(bytes); if (!ok) { ok = writeBySerial(bytes, serialPath, baudrate); } } setWriteMs(Math.max(0L, System.currentTimeMillis() - writeStarted)); return ok; } private static boolean writeByBuiltinPrinter(byte[] bytes) { try { setStage("upos:builtin:write"); boolean ok = runUposWriteWithTimeout(new UposWriteOp() { @Override public boolean run() { return UPOS_BUILTIN_TRANSPORT.write(bytes); } }, estimateUposWriteTimeoutMs(bytes == null ? 0 : bytes.length), "upos:builtin:write:timeout"); if (!ok) { setError("upos builtin failed: " + UPOS_BUILTIN_TRANSPORT.getLastError()); } return ok; } catch (Throwable e) { 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) { setStage("upos:serial:open:" + usePath); UPOS_SERIAL_TRANSPORT.close(); if (!UPOS_SERIAL_TRANSPORT.open(usePath, baudrate)) { setError("upos serial open failed: " + UPOS_SERIAL_TRANSPORT.getLastError()); return false; } setStage("upos:serial:write"); boolean ok = runUposWriteWithTimeout(new UposWriteOp() { @Override public boolean run() { return UPOS_SERIAL_TRANSPORT.write(bytes); } }, estimateUposWriteTimeoutMs(bytes == null ? 0 : bytes.length), "upos:serial:write:timeout"); if (!ok) setError("upos serial write failed: " + UPOS_SERIAL_TRANSPORT.getLastError()); UPOS_SERIAL_TRANSPORT.close(); return ok; } for (String candidate : SERIAL_PORT_CANDIDATES) { setStage("upos:serial:open:" + candidate); UPOS_SERIAL_TRANSPORT.close(); if (!UPOS_SERIAL_TRANSPORT.open(candidate, baudrate)) { continue; } setStage("upos:serial:write"); boolean ok = runUposWriteWithTimeout(new UposWriteOp() { @Override public boolean run() { return UPOS_SERIAL_TRANSPORT.write(bytes); } }, estimateUposWriteTimeoutMs(bytes == null ? 0 : bytes.length), "upos:serial:write:timeout"); if (!ok) { UPOS_SERIAL_TRANSPORT.close(); continue; } UPOS_SERIAL_TRANSPORT.close(); return true; } setError("upos serial open failed: no candidate ports worked"); return false; } catch (Throwable e) { setError(ThrowableUtils.unwrap(e)); return false; } } /** * UnifiedPOS/Serial 写入在部分机型/驱动上可能“卡死不返回且无异常”。 * 为避免 JS Promise 长时间悬挂,增加超时保护:超时则尝试 close,并返回 false(上层会回调错误)。 */ private interface UposWriteOp { boolean run(); } private static long estimateUposWriteTimeoutMs(int byteLen) { int n = Math.max(0, byteLen); // 基础 15s + 按体量加成(整页光栅可达数百 KB);上限 3min,与 JS printUposCommandBytes 300s 对齐。 long base = 15000L; long extra = (long) Math.min(150000L, (n / 1024L) * 900L); long ms = base + extra; if (ms < 15000L) ms = 15000L; if (ms > 180000L) ms = 180000L; return ms; } private static boolean runUposWriteWithTimeout(UposWriteOp op, long timeoutMs, String timeoutStage) { if (op == null) return false; final boolean[] done = new boolean[]{false}; final boolean[] ok = new boolean[]{false}; Thread t = new Thread(new Runnable() { @Override public void run() { try { ok[0] = op.run(); } catch (Throwable e) { ok[0] = false; setError(ThrowableUtils.unwrap(e)); } finally { done[0] = true; } } }, "upos-write"); t.setDaemon(true); t.start(); try { t.join(Math.max(1000L, timeoutMs)); } catch (InterruptedException ignored) { // keep going } if (!done[0]) { setStage(timeoutStage); setError("upos write timeout"); try { UPOS_SERIAL_TRANSPORT.close(); } catch (Throwable ignored) { } try { UPOS_BUILTIN_TRANSPORT.close(); } catch (Throwable ignored) { } return false; } return ok[0]; } 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) { setError(message == null ? "" : message); return debugResult(PluginResult.error(code, message)); } private static PluginResult debugResult(PluginResult result) { PluginResult r = DEBUG_STATE.attachTo(result); // Always expose minimal diagnostics for JS (even if attachTo omits some fields). r.withMeta("backend", BACKEND) .withMeta("pluginVersion", PLUGIN_VERSION) .withMeta("buildTime", BUILD_TIME) .withMeta("stage", LAST_STAGE) .withMeta("lastError", LAST_ERROR) .withMeta("commandBytes", LAST_COMMAND_BYTES) .withMeta("writeMs", LAST_WRITE_MS); return r; } }