From a87aac59cecf3b27a6556e140928ecd1bf8ed87f Mon Sep 17 00:00:00 2001 From: fengge <844143714@qq.com> Date: Tue, 21 Apr 2026 23:54:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E5=8D=B0=E6=9C=BA=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/print_demo.html | 166 +++++++++++++++++- .../com/stand/standapp/AndroidInterface.kt | 152 +++++++++++++++- .../main/java/com/stand/standapp/AppConfig.kt | 2 +- .../java/com/stand/standapp/LoginActivity.kt | 8 +- .../stand/standapp/printer/PrinterManager.kt | 100 +++++++++++ app/src/main/res/layout/dialog_settings.xml | 2 +- 6 files changed, 419 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/stand/standapp/printer/PrinterManager.kt diff --git a/app/src/main/assets/print_demo.html b/app/src/main/assets/print_demo.html index af2fc23..ffbb7ba 100644 --- a/app/src/main/assets/print_demo.html +++ b/app/src/main/assets/print_demo.html @@ -76,10 +76,44 @@
设备信息:
应用版本: Unknown
- 环境检测: Browser + 环境检测: Browser
+ 打印机: 未连接
- + +
+
基础打印测试
+
+ + + + +
+
+ +
+
高级业务样式
+
+ + + + +
+
+ +
+
硬件控制与维护
+
+ + + + +
+
+ +
+ +

(支持 PDF 导出、网络打印机、A4 适配)

@@ -219,6 +253,134 @@ if (window.android.getVersionName) { versionSpan.innerText = window.android.getVersionName(); } + + updatePrinterStatus(); + // 每 3 秒同步一次打印机状态 + setInterval(updatePrinterStatus, 3000); + } + } + + function updatePrinterStatus() { + if (window.android && window.android.getPrinterStatus) { + var status = JSON.parse(window.android.getPrinterStatus()); + var span = document.getElementById('printer-status'); + var statusTxt = status.connected ? "已连接 (" + status.port + ")" : "未连接 (" + status.port + ")"; + var paperTxt = status.hasPaper ? " [有纸]" : " [⚠️缺纸]"; + + span.innerText = statusTxt + paperTxt; + span.style.color = (status.connected && status.hasPaper) ? "#10B981" : "#ef4444"; + } + } + + function callNativePrintText() { + if (window.android && window.android.printText) { + window.android.printText("消防值守台 终端打印测试\n", 2, 1, 20); + window.android.printText("当前时间: " + new Date().toLocaleString() + "\n", 1, 1, 20); + } + } + + function callNativePrintQR() { + if (window.android && window.android.printQR) { + window.android.printQR("https://www.gyouzhe.com", 6, 2); + window.android.printText("\n\n\n", 1, 1, 10); + window.android.cutPaper(false); + } + } + + function callNativePrintBarcode() { + if (window.android && window.android.printBarcode) { + window.android.printBarcode("88888888", 3, 100, 5, 2, 2); + window.android.printText("\n\n\n", 1, 1, 10); + window.android.cutPaper(false); + } + } + + function callNativePrintImg() { + if (window.android && window.android.printImage) { + window.android.printImage("1.jpg", 384, 1, 2); + window.android.printText("\n\n\n", 1, 1, 10); + window.android.cutPaper(false); + } + } + + function callNativePrintColumns() { + if (window.android && window.android.printColumns) { + window.android.printColumns("项目", 0, "单价", 180, "数量", 250, "金额", 320); + window.android.printColumnsWithHeight("测试商品", 0, "10.0", 180, "1", 250, "10.0", 320, 70); + window.android.printText("\n\n\n", 1, 1, 10); + window.android.cutPaper(false); + } + } + + function callNativePrintAlarm() { + if (window.android && window.android.printText) { + // 1. 标题 + window.android.printText("警情通知单", 2, 2, 40); + window.android.cutPaper(false); + // 2. 二维码 (居中) + window.android.printQR("ALARM_20260304172432", 7, 2); + window.android.printText("\n", 1, 1, 20); + window.android.cutPaper(false); + // 3. 详情列表 + window.android.printText("报警时间: 2026-03-04 17:24:32", 1, 1, 20); + window.android.printText("报警人: 张先生", 1, 1, 20); + window.android.printText("报警人电话: 18722727636", 1, 1, 20); + window.android.printText("灾害地址: 贵州省安顺市西秀区(赵家山村)宋旗镇赵家山村", 1, 1, 20); + window.android.printText("警情分类: 抢险救援", 1, 1, 20); + window.android.printText("报警描述: 车祸", 1, 1, 20); + window.android.printText("主管救援站联系电话: 0851-33410119", 1, 1, 20); + window.android.printText("出动车辆: ", 1, 1, 20); + + // 4. 分割线 + window.android.printText("--------------------------------", 1, 1, 10); + + // 5. 注意事项 + window.android.printText("注意事项:", 1, 1, 20); + window.android.printText("1、请专职消防队人员穿戴好防护装备,携带灭火、救援器材赶往现场科学处置。", 1, 1, 20); + window.android.printText("2、到达现场后及时上报情况,注意安全,做好个人防护和现场警戒。", 1, 1, 20); + window.android.printText("3、根据现场情况迅速组织人员疏散,并在确保安全的前提下利用灭火救援装备开展救援。", 1, 1, 20); + window.android.printText("4、待责任消防救援站力量到达现场后,主动与其汇报现场已知情况,并协助做好灭火救援工作。", 1, 1, 20); + window.android.printText("5、严格落实作战行动安全规定,防止触电、溺水、烧伤、坠落、中毒、砸伤、放射线等伤害。", 1, 1, 20); + + // 6. 结尾 + window.android.printText("--------------------------------", 1, 1, 10); + window.android.printText("\n\n\n", 1, 1, 10); + + // 7. 切纸 + window.android.cutPaper(false); + } + } + + function callNativePrintDemo(is3Inch) { + if (window.android && window.android.printText) { + var width = is3Inch ? 400 : 300; + var right = is3Inch ? 120 : 100; + window.android.printText("\nStandAPP 业务测试单\n", 2, 1, 30); + window.android.printText("--------------------------------\n", 1, 1, 10); + window.android.printColumns("名称", 0, "单价", 180, "数量", 250, "金额", width); + window.android.printColumnsWithHeight("演示商品A", 0, "9.9", 180, "1", 250, "9.90", width, right); + window.android.printText("--------------------------------\n", 1, 1, 10); + window.android.printQR("http://www.standapp.com", 6, 2); + window.android.printText("\n\n\n", 1, 1, 10); + window.android.cutPaper(false); + } + } + + function callNativeTestPage() { + if (window.android && window.android.printTestPage) { + window.android.printTestPage(); + } + } + + function callNativeCheckPaper() { + if (window.android && window.android.checkPaper) { + window.android.checkPaper(); + } + } + + function callNativeCutPaper(allCut) { + if (window.android && window.android.cutPaper) { + window.android.cutPaper(allCut); } } diff --git a/app/src/main/java/com/stand/standapp/AndroidInterface.kt b/app/src/main/java/com/stand/standapp/AndroidInterface.kt index 3cc63ce..d9f9892 100644 --- a/app/src/main/java/com/stand/standapp/AndroidInterface.kt +++ b/app/src/main/java/com/stand/standapp/AndroidInterface.kt @@ -9,11 +9,152 @@ import android.print.PrintAttributes import android.print.PrintManager import com.stand.standapp.R import com.stand.standapp.utils.LogManager +import com.stand.standapp.printer.PrinterManager import timber.log.Timber import org.json.JSONObject class AndroidInterface(private val activity: MainActivity) { + /** + * JS 调用:获取打印机当前连接状态 + */ + @JavascriptInterface + fun getPrinterStatus(): String { + return try { + val json = JSONObject() + val connected = PrinterManager.isConnected() + json.put("connected", connected) + json.put("hasPaper", PrinterManager.isPaperReady()) + json.put("port", AppConfig.getSerialPort(activity)) + json.toString() + } catch (e: Exception) { + "{\"connected\":false, \"hasPaper\":false, \"error\":\"${e.message}\"}" + } + } + + /** + * JS 调用:通用打印文本 + */ + @JavascriptInterface + fun printText(text: String?, size: Int, align: Int, space: Int) { + if (text.isNullOrEmpty()) return + try { + PrinterManager.getFactory()?.let { + if (it.isConnection) { + it.PrintText(text, size, align, space) + } + } + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "printText failed") + } + } + + /** + * JS 调用:打印条码 + */ + @JavascriptInterface + fun printBarcode(data: String?, size: Int, height: Int, align: Int, text_pos: Int, type: Int) { + if (data.isNullOrEmpty()) return + try { + PrinterManager.getFactory()?.PrintBarcode(data, size, height, align, text_pos, type) + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "printBarcode failed") + } + } + + /** + * JS 调用:打印二维码 + */ + @JavascriptInterface + fun printQR(data: String?, size: Int, l_m: Int) { + if (data.isNullOrEmpty()) return + try { + PrinterManager.getFactory()?.PrintQR(data, size, l_m) + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "printQR failed") + } + } + + /** + * JS 调用:打印图片 (从 Assets 获取) + */ + @JavascriptInterface + fun printImage(fileName: String?, width: Int, align: Int, mode: Int) { + if (fileName.isNullOrEmpty()) return + try { + val stream = activity.assets.open(fileName) + val bitmap = android.graphics.BitmapFactory.decodeStream(stream) + PrinterManager.getFactory()?.PrintImage(bitmap, width, align, mode) + stream.close() + } catch (e: Exception) { + Timber.tag("JS_Interface").e(e, "printImage failed: $fileName") + } + } + + /** + * JS 调用:分栏打印 (4列) + */ + @JavascriptInterface + fun printColumns(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int) { + try { + PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4) + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "printColumns failed") + } + } + + /** + * JS 调用:分栏打印 (带行高) + */ + @JavascriptInterface + fun printColumnsWithHeight(c1: String?, p1: Int, c2: String?, p2: Int, c3: String?, p3: Int, c4: String?, p4: Int, height: Int) { + try { + PrinterManager.getFactory()?.Printcolumncontent(c1 ?: "", p1, c2 ?: "", p2, c3 ?: "", p3, c4 ?: "", p4, height) + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "printColumnsWithHeight failed") + } + } + + /** + * JS 调用:切纸 + */ + @JavascriptInterface + fun cutPaper(allCut: Boolean) { + try { + PrinterManager.getFactory()?.let { + if (it.isConnection) { + if (allCut) it.PaperAllCut() else it.PaperCut() + } + } + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "cutPaper failed") + } + } + + /** + * JS 调用:打印自检页 + */ + @JavascriptInterface + fun printTestPage() { + try { + PrinterManager.getFactory()?.PrintTestPage() + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "printTestPage failed") + } + } + + /** + * JS 调用:主动检查纸张状态 (会触发回调) + */ + @JavascriptInterface + fun checkPaper() { + try { + PrinterManager.getFactory()?.Check_Paper() + } catch (e: Throwable) { + Timber.tag("JS_Interface").e(e, "checkPaper failed") + } + } + /** * JS 调用:压缩并导出日志 * 返回压缩后的文件路径 @@ -76,15 +217,15 @@ class AndroidInterface(private val activity: MainActivity) { /** * JS 调用:原始数据打印 * 适用于热敏小票打印机 (蓝牙、USB、网络) - * @param data 可以是 JSON 字符串,包含打印的指令或文本 + * @param data String 可以是 JSON 字符串,包含打印的指令或文本 */ @JavascriptInterface - fun printRawData(data: String) { + fun printRawData(data: String?) { + if (data.isNullOrEmpty()) return try { Timber.tag("Print").d("收到 H5 原始打印数据: $data") Handler(Looper.getMainLooper()).post { try { - // 这里可以弹出一个选择框让用户选择打印机,或者根据配置自动连接 Toast.makeText(activity, "H5指令打印: $data", Toast.LENGTH_SHORT).show() } catch (e: Exception) { Timber.tag("Print").e(e, "Toast显示失败") @@ -99,7 +240,7 @@ class AndroidInterface(private val activity: MainActivity) { * JS 调用:在线升级 (手动触发检查) */ @JavascriptInterface - fun startUpdate(apkUrl: String) { + fun startUpdate(apkUrl: String?) { Handler(Looper.getMainLooper()).post { try { UpdateManager(activity).checkUpdate() @@ -172,7 +313,8 @@ class AndroidInterface(private val activity: MainActivity) { * JS 调用:保存消息记录 */ @JavascriptInterface - fun saveMessage(message: String) { + fun saveMessage(message: String?) { + if (message.isNullOrEmpty()) return try { val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE) val currentMsgs = prefs.getString("history", "") ?: "" diff --git a/app/src/main/java/com/stand/standapp/AppConfig.kt b/app/src/main/java/com/stand/standapp/AppConfig.kt index 9664f7e..4d2f411 100644 --- a/app/src/main/java/com/stand/standapp/AppConfig.kt +++ b/app/src/main/java/com/stand/standapp/AppConfig.kt @@ -13,7 +13,7 @@ object AppConfig { // 默认配置 const val DEFAULT_SERVER_URL = "https://template.gyouzhe.com" - const val DEFAULT_SERIAL_PORT = "/dev/ttyACM0" + const val DEFAULT_SERIAL_PORT = "ttyACM0" private fun getPrefs(context: Context): SharedPreferences { return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) diff --git a/app/src/main/java/com/stand/standapp/LoginActivity.kt b/app/src/main/java/com/stand/standapp/LoginActivity.kt index 75677d6..4eb8375 100644 --- a/app/src/main/java/com/stand/standapp/LoginActivity.kt +++ b/app/src/main/java/com/stand/standapp/LoginActivity.kt @@ -127,8 +127,12 @@ class LoginActivity : AppCompatActivity() { AppConfig.setSavedPassword(this@LoginActivity, "") } - Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show() - startActivity(Intent(this@LoginActivity, MainActivity::class.java)) + Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show() + + // 初始化打印机 (自动重连) + com.stand.standapp.printer.PrinterManager.init(this@LoginActivity) + + startActivity(Intent(this@LoginActivity, MainActivity::class.java)) finish() } else { val msg = json.optString("msg", "登录失败") diff --git a/app/src/main/java/com/stand/standapp/printer/PrinterManager.kt b/app/src/main/java/com/stand/standapp/printer/PrinterManager.kt new file mode 100644 index 0000000..2fd3025 --- /dev/null +++ b/app/src/main/java/com/stand/standapp/printer/PrinterManager.kt @@ -0,0 +1,100 @@ +package com.stand.standapp.printer + +import android.content.Context +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import com.dp.dp_serialportlist.Serialport_Factory +import com.stand.standapp.AppConfig +import timber.log.Timber +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +object PrinterManager { + private var sf: Serialport_Factory? = null + private var isConnecting = false + private val executor = Executors.newSingleThreadScheduledExecutor() + private var mContext: Context? = null + private var hasPaper: Boolean = true // 默认为有纸 + + private val mHandler = Handler(Looper.getMainLooper()) { msg -> + if (msg.what == Serialport_Factory.PAPER_STATE) { + hasPaper = msg.data.getBoolean("PAPER_STATE") + val statusStr = if (hasPaper) "有纸" else "缺纸" + Timber.tag("Printer").i("打印机状态变更: %s", statusStr) + + // 在 Android 系统层面主动提示 + mContext?.let { + if (!hasPaper) { + android.widget.Toast.makeText(it, "⚠️ 警告:打印机缺纸!", android.widget.Toast.LENGTH_LONG).show() + } + } + } + true + } + + fun isPaperReady(): Boolean = hasPaper + + fun init(context: Context) { + if (sf != null) return + mContext = context.applicationContext + + // 全异步初始化,不阻塞主线程跳转 + executor.execute { + try { + Timber.tag("Printer").i("开始异步初始化打印机组件...") + sf = Serialport_Factory.getSerialport_Factory(mContext, mHandler) + + // 启动自动重连定时任务 + executor.scheduleWithFixedDelay({ + checkAndConnect() + }, 0, 10, TimeUnit.SECONDS) + + Timber.tag("Printer").i("打印机组件初始化任务已分发") + } catch (e: Exception) { + Timber.tag("Printer").e(e, "打印机组件初始化失败") + } + } + } + + @Synchronized + private fun checkAndConnect() { + val factory = sf ?: return + if (factory.isConnection) return + + val path = AppConfig.getSerialPort(mContext!!) + val baudrate = "9600" + + Timber.tag("Printer").d("尝试自动连接打印机: %s @ %s", path, baudrate) + try { + if (factory.OpenPort(path, baudrate)) { + Timber.tag("Printer").i("打印机自动连接成功") + } + } catch (e: Throwable) { + Timber.tag("Printer").e(e, "打印机自动连接异常") + } + } + + fun getFactory(): Serialport_Factory? = sf + + fun isConnected(): Boolean = sf?.isConnection ?: false + + /** + * 提供便捷的打印方法 + */ + fun printText(text: String, size: Int = 1, align: Int = 1, space: Int = 10) { + sf?.let { + if (it.isConnection) { + it.PrintText(text, size, align, space) + } + } + } + + fun cutPaper() { + sf?.let { + if (it.isConnection) { + it.PaperCut() + } + } + } +} diff --git a/app/src/main/res/layout/dialog_settings.xml b/app/src/main/res/layout/dialog_settings.xml index c45faf0..e253aea 100644 --- a/app/src/main/res/layout/dialog_settings.xml +++ b/app/src/main/res/layout/dialog_settings.xml @@ -76,7 +76,7 @@ android:layout_width="match_parent" android:layout_height="56dp" android:background="@drawable/bg_input_field" - android:hint="/dev/ttyACM0" + android:hint="ttyACM0" android:inputType="text" android:paddingHorizontal="16dp" android:textColor="#0F172A"