wangxichen 1 неделя назад
Родитель
Сommit
63ae1c6700

+ 211 - 0
SNMP-GUIDE.md

@@ -0,0 +1,211 @@
+# SNMP 打印监控使用说明
+
+## 功能说明
+
+通过 SNMP 协议监控 HP 打印机的物理打印完成状态,而不是仅依赖打印队列的任务提交。
+
+## 工作原理
+
+1. **提交打印** → `job.print()` 阻塞,等待任务进入打印队列
+2. **任务入队** → 返回,记录当前打印机页数计数器 `baseCount`
+3. **SNMP 轮询** → 每 2 秒查询一次打印机页数计数器
+4. **检测完成** → 当前页数 >= `baseCount + 1` 时,触发回调
+5. **超时保护** → 默认 60 秒超时,超时后也触发回调
+
+## 配置步骤
+
+### 1. 打印机端配置
+
+HP 3004dn 需要开启 SNMP 支持(出厂默认开启):
+
+1. 进入打印机 Web 管理界面(浏览器访问打印机 IP)
+2. 导航:`网络` → `SNMP`
+3. 确认:
+   - `启用 SNMP v1/v2c` 已勾选
+   - `团体名称(读取)` 为 `public`(默认值)
+   - `UDP 端口` 为 `161`(默认值)
+
+### 2. 客户端配置
+
+编辑 `config/print.properties`:
+
+```properties
+printer.type=hp
+printer.name=HP LaserJet 3004
+
+# 添加打印机 IP(替换为实际 IP)
+printer.ip=192.168.1.100
+```
+
+### 3. 网络配置
+
+确保客户端能访问打印机的 UDP 161 端口:
+
+```bash
+# Windows 测试连通性
+ping 192.168.1.100
+
+# 如果有防火墙,需放行 UDP 161 端口
+```
+
+## 测试 SNMP 连接
+
+```bash
+# 在 mes-print 项目目录
+cd C:/Users/jcxa/Desktop/T09/khd/mes-print
+
+# 编译测试程序
+javac -encoding UTF-8 -d build/classes -cp "lib/*" src/com/mes/print/TestSnmp.java
+
+# 运行测试(替换为实际打印机 IP)
+java -cp "build/classes;lib/*" com.mes.print.TestSnmp 192.168.1.100
+```
+
+**预期输出**:
+```
+=== SNMP 打印机监控测试 ===
+打印机 IP: 192.168.1.100
+
+1. 测试连接...
+[PrinterMonitor] 已连接到打印机:192.168.1.100
+[PrinterMonitor] 测试连接 - 页数=1234, 状态=3
+
+2. 查询打印机状态...
+  总打印页数: 1234
+  打印机状态: 3 (3=空闲, 4=打印中, 5=预热)
+
+3. 开始监控打印完成(请手动发送打印任务)...
+  按 Ctrl+C 退出监控
+```
+
+然后手动打印一张测试页,程序会检测到页数变化并输出:
+```
+[PrinterMonitor] 当前页数=1234,目标=1235
+[PrinterMonitor] 当前页数=1235,目标=1235
+[PrinterMonitor] 打印完成检测到!页数从 1234 → 1235
+
+✓ 检测到打印完成!
+```
+
+## 故障排查
+
+### 1. 连接失败
+
+**错误**:`连接失败,请检查...`
+
+**排查**:
+- 检查打印机 IP 是否正确(能 ping 通)
+- 检查打印机 SNMP 是否启用
+- 检查防火墙是否拦截 UDP 161 端口
+- 尝试用 Windows 自带工具测试:
+  ```bash
+  # 安装 SNMP 工具(如果没有)
+  # 控制面板 → 程序 → 启用或关闭 Windows 功能 → SNMP 工具
+  
+  # 测试查询
+  snmpget -v2c -c public 192.168.1.100 1.3.6.1.2.1.43.10.2.1.4.1.1
+  ```
+
+### 2. 页数不变化
+
+**现象**:打印了但页数计数器不增加
+
+**原因**:
+- 某些打印机型号的 OID 不同
+- 打印任务被取消或失败
+
+**解决**:
+- 尝试其他 OID(参考打印机 MIB 文档)
+- 检查打印机实际是否出纸
+
+### 3. 监控超时
+
+**现象**:60 秒后仍未检测到完成
+
+**原因**:
+- 打印任务卡在队列(打印机离线/缺纸/卡纸)
+- 大文件打印耗时超过 60 秒
+
+**解决**:
+- 增加超时时间(修改 `HpPrinter.java:90` 的 60 改为更大值)
+- 检查打印机物理状态
+
+## 性能影响
+
+- **轮询间隔**:2 秒/次
+- **网络开销**:每次查询 ~200 字节
+- **CPU 占用**:极低(单独线程,非阻塞)
+- **延迟**:物理打印完成后 0-2 秒内检测到
+
+## 禁用 SNMP 监控
+
+如果不需要物理打印完成检测,只需注释掉配置:
+
+```properties
+# printer.ip=192.168.1.100
+```
+
+此时打印任务提交到队列后立即返回(原有行为)。
+
+## API 文档
+
+### PrinterMonitor
+
+```java
+// 创建监控器
+PrinterMonitor monitor = new PrinterMonitor("192.168.1.100");
+monitor.init();
+
+// 查询页数
+Long count = monitor.getPageCount();
+
+// 查询状态
+Integer status = monitor.getPrinterStatus();
+// 1=其他, 3=空闲, 4=打印中, 5=预热
+
+// 监控打印完成
+monitor.waitForPrintComplete(
+    1,                    // 预期页数增量
+    () -> {               // 完成回调
+        System.out.println("打印完成");
+    },
+    60                    // 超时秒数
+);
+
+// 关闭监控
+monitor.close();
+```
+
+### HpPrinter
+
+```java
+HpPrinter printer = new HpPrinter();
+
+// 启用 SNMP 监控
+printer.enableSnmpMonitor("192.168.1.100");
+
+// 设置完成回调
+printer.setOnPrintComplete(() -> {
+    System.out.println("物理打印完成");
+});
+
+// 打印(会阻塞直到物理打印完成)
+printer.printFlowCard(data);
+```
+
+## 依赖库
+
+- **snmp4j-3.7.7.jar** (631 KB)
+- 需要添加到项目 classpath
+
+## 支持的打印机型号
+
+理论上支持所有实现了 **Printer MIB (RFC 3805)** 的网络打印机,包括:
+- HP LaserJet 系列
+- Canon 网络打印机
+- Epson 网络打印机
+- 其他企业级网络打印机
+
+不支持:
+- USB 本地连接打印机(无网络接口)
+- 不支持 SNMP 的低端打印机

BIN
lib/snmp4j-3.7.7.jar


+ 1 - 0
sources.txt

@@ -5,6 +5,7 @@ src/com/mes/print/impl/PrinterImpl.java
 src/com/mes/print/impl/TscPrinter.java
 src/com/mes/print/model/FlowCardData.java
 src/com/mes/print/model/LabelData.java
+src/com/mes/print/monitor/PrinterMonitor.java
 src/com/mes/print/Printer.java
 src/com/mes/print/PrinterException.java
 src/com/mes/print/PrinterFactory.java

+ 10 - 1
src/com/mes/print/PrinterFactory.java

@@ -23,7 +23,13 @@ class PrinterFactory {
 			if (type == null) throw new PrinterException("未配置 printer.type");
 			type = type.trim().toLowerCase();
 			if ("hp".equals(type)) {
-				impl = new HpPrinter();
+				HpPrinter hp = new HpPrinter();
+				// 如果配置了打印机 IP,启用 SNMP 监控
+				String printerIp = PrinterConfig.get("printer.ip");
+				if (printerIp != null && !printerIp.trim().isEmpty()) {
+					hp.enableSnmpMonitor(printerIp.trim());
+				}
+				impl = hp;
 			} else if ("tsc".equals(type)) {
 				impl = new TscPrinter();
 			} else if ("none".equals(type) || type.isEmpty()) {
@@ -37,6 +43,9 @@ class PrinterFactory {
 
 	/** 测试用:重置单例,重新按 config 创建 */
 	static synchronized void reset() {
+		if (impl instanceof HpPrinter) {
+			((HpPrinter) impl).close();
+		}
 		impl = null;
 	}
 }

+ 55 - 0
src/com/mes/print/TestSnmp.java

@@ -0,0 +1,55 @@
+package com.mes.print;
+
+import com.mes.print.monitor.PrinterMonitor;
+
+/**
+ * SNMP 打印机监控测试
+ * 用法:java -cp "build/classes;lib/*" com.mes.print.TestSnmp <打印机IP>
+ */
+public class TestSnmp {
+	public static void main(String[] args) throws Exception {
+		if (args.length < 1) {
+			System.out.println("用法: TestSnmp <打印机IP>");
+			System.out.println("示例: TestSnmp 192.168.1.100");
+			return;
+		}
+
+		String printerIp = args[0];
+		System.out.println("=== SNMP 打印机监控测试 ===");
+		System.out.println("打印机 IP: " + printerIp);
+		System.out.println();
+
+		PrinterMonitor monitor = new PrinterMonitor(printerIp);
+		monitor.init();
+
+		// 测试连接
+		System.out.println("1. 测试连接...");
+		boolean connected = monitor.testConnection();
+		if (!connected) {
+			System.err.println("连接失败,请检查:");
+			System.err.println("  - 打印机 IP 是否正确");
+			System.err.println("  - 打印机是否开启 SNMP(默认端口 161)");
+			System.err.println("  - 防火墙是否放行 UDP 161 端口");
+			monitor.close();
+			return;
+		}
+
+		// 查询当前状态
+		System.out.println("\n2. 查询打印机状态...");
+		Long pageCount = monitor.getPageCount();
+		Integer status = monitor.getPrinterStatus();
+		System.out.println("  总打印页数: " + pageCount);
+		System.out.println("  打印机状态: " + status + " (3=空闲, 4=打印中, 5=预热)");
+
+		// 监控打印完成
+		System.out.println("\n3. 开始监控打印完成(请手动发送打印任务)...");
+		System.out.println("  按 Ctrl+C 退出监控");
+		monitor.waitForPrintComplete(1, () -> {
+			System.out.println("\n✓ 检测到打印完成!");
+			System.exit(0);
+		}, 300); // 5 分钟超时
+
+		// 保持程序运行
+		Thread.sleep(Long.MAX_VALUE);
+	}
+}

+ 47 - 0
src/com/mes/print/impl/HpPrinter.java

@@ -17,12 +17,43 @@ import com.mes.print.model.FlowCardData;
 import com.mes.print.model.LabelData;
 import com.mes.print.template.FlowCardRenderer;
 import com.mes.print.util.PrinterUtil;
+import com.mes.print.monitor.PrinterMonitor;
 
 /**
  * HP 3004dn 及同类激光打印机:走 Java2D + PrinterJob
+ * 支持 SNMP 监控物理打印完成状态(可选)
  */
 public class HpPrinter implements PrinterImpl {
 
+	private PrinterMonitor monitor;
+	private Runnable onPrintCompleteCallback;
+
+	/**
+	 * 设置打印完成回调(可选)
+	 * 如果设置了打印机 IP,会通过 SNMP 监控物理打印完成
+	 */
+	public void setOnPrintComplete(Runnable callback) {
+		this.onPrintCompleteCallback = callback;
+	}
+
+	/**
+	 * 配置 SNMP 监控(可选)
+	 * @param printerIp 打印机 IP 地址(如 192.168.1.100)
+	 */
+	public void enableSnmpMonitor(String printerIp) {
+		if (printerIp == null || printerIp.trim().isEmpty()) {
+			return;
+		}
+		try {
+			monitor = new PrinterMonitor(printerIp.trim());
+			monitor.init();
+			System.out.println("[HpPrinter] SNMP 监控已启用: " + printerIp);
+		} catch (Exception e) {
+			System.err.println("[HpPrinter] SNMP 监控启动失败: " + e.getMessage());
+			monitor = null;
+		}
+	}
+
 	@Override
 	public void printFlowCard(FlowCardData data) {
 		if (data == null) throw new PrinterException("流转卡数据为空");
@@ -47,7 +78,17 @@ public class HpPrinter implements PrinterImpl {
 			attrs.add(MediaSizeName.ISO_A4);
 			attrs.add(new Copies(1));
 
+			// 提交打印任务(阻塞,直到任务进入打印队列)
 			job.print(attrs);
+
+			// 如果启用了 SNMP 监控,等待物理打印完成
+			if (monitor != null && onPrintCompleteCallback != null) {
+				monitor.waitForPrintComplete(1, onPrintCompleteCallback, 60);
+			} else if (onPrintCompleteCallback != null) {
+				// 未启用 SNMP 时,立即回调(队列提交完成 = 打印完成)
+				onPrintCompleteCallback.run();
+			}
+
 		} catch (PrinterException e) {
 			throw e;
 		} catch (Exception e) {
@@ -59,4 +100,10 @@ public class HpPrinter implements PrinterImpl {
 	public void printLabel(LabelData data) {
 		throw new PrinterException("HP 打印机不支持标签打印,请配置 TSC 打印机");
 	}
+
+	public void close() {
+		if (monitor != null) {
+			monitor.close();
+		}
+	}
 }

+ 195 - 0
src/com/mes/print/monitor/PrinterMonitor.java

@@ -0,0 +1,195 @@
+package com.mes.print.monitor;
+
+import org.snmp4j.CommunityTarget;
+import org.snmp4j.PDU;
+import org.snmp4j.Snmp;
+import org.snmp4j.event.ResponseEvent;
+import org.snmp4j.mp.SnmpConstants;
+import org.snmp4j.smi.Address;
+import org.snmp4j.smi.GenericAddress;
+import org.snmp4j.smi.OID;
+import org.snmp4j.smi.OctetString;
+import org.snmp4j.smi.VariableBinding;
+import org.snmp4j.transport.DefaultUdpTransportMapping;
+
+import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 打印机 SNMP 监控器
+ * 通过 SNMP 协议监控 HP 打印机的物理打印完成状态
+ */
+public class PrinterMonitor {
+
+	// SNMP OID 定义
+	private static final String OID_PRINTER_LIFE_COUNT = "1.3.6.1.2.1.43.10.2.1.4.1.1"; // 总打印页数
+	private static final String OID_PRINTER_STATUS = "1.3.6.1.2.1.25.3.5.1.1.1"; // 打印机状态
+
+	private final String printerIp;
+	private final String community; // SNMP 团体名(默认 public)
+	private final int port;
+
+	private Snmp snmp;
+	private CommunityTarget target;
+	private ScheduledExecutorService scheduler;
+
+	public PrinterMonitor(String printerIp) {
+		this(printerIp, "public", 161);
+	}
+
+	public PrinterMonitor(String printerIp, String community, int port) {
+		this.printerIp = printerIp;
+		this.community = community;
+		this.port = port;
+	}
+
+	/**
+	 * 初始化 SNMP 连接
+	 */
+	public void init() throws IOException {
+		DefaultUdpTransportMapping transport = new DefaultUdpTransportMapping();
+		snmp = new Snmp(transport);
+		transport.listen();
+
+		Address targetAddress = GenericAddress.parse("udp:" + printerIp + "/" + port);
+		target = new CommunityTarget();
+		target.setCommunity(new OctetString(community));
+		target.setAddress(targetAddress);
+		target.setRetries(2);
+		target.setTimeout(1500);
+		target.setVersion(SnmpConstants.version2c);
+
+		scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
+			Thread t = new Thread(r, "PrinterMonitor-" + printerIp);
+			t.setDaemon(true);
+			return t;
+		});
+
+		System.out.println("[PrinterMonitor] 已连接到打印机:" + printerIp);
+	}
+
+	/**
+	 * 关闭监控
+	 */
+	public void close() {
+		if (scheduler != null) {
+			scheduler.shutdownNow();
+		}
+		if (snmp != null) {
+			try {
+				snmp.close();
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
+	/**
+	 * 查询打印机总页数计数器
+	 */
+	public Long getPageCount() {
+		try {
+			PDU pdu = new PDU();
+			pdu.add(new VariableBinding(new OID(OID_PRINTER_LIFE_COUNT)));
+			pdu.setType(PDU.GET);
+
+			ResponseEvent event = snmp.send(pdu, target);
+			if (event != null && event.getResponse() != null) {
+				PDU response = event.getResponse();
+				if (response.getErrorStatus() == PDU.noError) {
+					VariableBinding vb = response.get(0);
+					return vb.getVariable().toLong();
+				}
+			}
+		} catch (Exception e) {
+			System.err.println("[PrinterMonitor] 查询页数失败: " + e.getMessage());
+		}
+		return null;
+	}
+
+	/**
+	 * 查询打印机状态
+	 * @return 1=其他, 3=空闲, 4=打印中, 5=预热
+	 */
+	public Integer getPrinterStatus() {
+		try {
+			PDU pdu = new PDU();
+			pdu.add(new VariableBinding(new OID(OID_PRINTER_STATUS)));
+			pdu.setType(PDU.GET);
+
+			ResponseEvent event = snmp.send(pdu, target);
+			if (event != null && event.getResponse() != null) {
+				PDU response = event.getResponse();
+				if (response.getErrorStatus() == PDU.noError) {
+					VariableBinding vb = response.get(0);
+					return vb.getVariable().toInt();
+				}
+			}
+		} catch (Exception e) {
+			System.err.println("[PrinterMonitor] 查询状态失败: " + e.getMessage());
+		}
+		return null;
+	}
+
+	/**
+	 * 监控打印完成(通过页数计数器变化检测)
+	 *
+	 * @param expectedPages 预期增加的页数(通常是 1)
+	 * @param callback 完成回调
+	 * @param timeoutSeconds 超时时间(秒)
+	 */
+	public void waitForPrintComplete(int expectedPages, Runnable callback, int timeoutSeconds) {
+		Long baseCount = getPageCount();
+		if (baseCount == null) {
+			System.err.println("[PrinterMonitor] 无法获取初始页数,跳过监控");
+			callback.run();
+			return;
+		}
+
+		System.out.println("[PrinterMonitor] 开始监控,当前页数=" + baseCount + ",预期增加=" + expectedPages);
+
+		final long startTime = System.currentTimeMillis();
+		final long targetCount = baseCount + expectedPages;
+
+		scheduler.scheduleAtFixedRate(() -> {
+			try {
+				// 检查超时
+				if (System.currentTimeMillis() - startTime > timeoutSeconds * 1000L) {
+					System.err.println("[PrinterMonitor] 监控超时(" + timeoutSeconds + "秒),停止监控");
+					scheduler.shutdown();
+					callback.run();
+					return;
+				}
+
+				// 查询当前页数
+				Long currentCount = getPageCount();
+				if (currentCount == null) {
+					return; // 查询失败,下次重试
+				}
+
+				System.out.println("[PrinterMonitor] 当前页数=" + currentCount + ",目标=" + targetCount);
+
+				// 页数达到预期,打印完成
+				if (currentCount >= targetCount) {
+					System.out.println("[PrinterMonitor] 打印完成检测到!页数从 " + baseCount + " → " + currentCount);
+					scheduler.shutdown();
+					callback.run();
+				}
+			} catch (Exception e) {
+				System.err.println("[PrinterMonitor] 监控异常: " + e.getMessage());
+			}
+		}, 0, 2, TimeUnit.SECONDS); // 每 2 秒查询一次
+	}
+
+	/**
+	 * 测试 SNMP 连接
+	 */
+	public boolean testConnection() {
+		Long count = getPageCount();
+		Integer status = getPrinterStatus();
+		System.out.println("[PrinterMonitor] 测试连接 - 页数=" + count + ", 状态=" + status);
+		return count != null || status != null;
+	}
+}