NativeFastPrinterModule.java 15.9 KB
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);
    }
}