package com.mes.component; import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType; import com.github.xingshuangs.iot.protocol.s7.service.S7PLC; import com.mes.ui.MesClient; import com.mes.util.SnSlot; import com.mes.util.TCPClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.nio.charset.StandardCharsets; import java.util.Timer; import java.util.TimerTask; /** * 扫码面板(每台 PLC 一个,92 发 1、2 号枪,93 发 3 号枪,由 PLC 决定) * DB7010.0.0 请求扫码 / DB7010.0.1 扫码完成反馈 / DB7010.6(INT) 扫码枪号;0.2、2、4 为预留,不读不写 * 枪号 0=未发;正常同一时间 92、93 只有一个非 0,PLC 收到扫码完成反馈后清零 * 请求扫码时 PLC 给几号就触发几号枪,读到码(不校验长度)存入本 PLC 的正面扫码码 * 92 扫的码给 OP130A-E 取,93 扫的码给 OP130F(93 DB7004)取 * 推码给 PLC(pushAddrs 按枪号对应 S7String 地址):扫到码后留存 15s 写入,到期清空,独立定时器,失败只记日志 */ public class WorkJPanelScan extends JPanel { public static final Logger log = LoggerFactory.getLogger(WorkJPanelScan.class); public String oprno = "OP130"; public String plcIp; public String plcName; // 92 / 93,用于界面和日志 public String plcDb = "DB7010"; public String gunAddr = "DB7010.6"; public S7PLC s7PLC; public SnSlot slot; public Integer workWidth = 470; public Integer workHeight = 300; private boolean scanDone = false; // 本次请求已扫码成功,等 PLC 复位请求 private String lastGunState = ""; // 上次读到的枪号状态,变化时才记日志 public JButton status_menu; public JTextField productSnFeeding; // 扫码槽当前码 public JTextField gunField; // 本次扫码使用的枪 public JButton resetFeedingBtn; public JLabel feedinglabel; // 0.0 请求扫码 public JLabel feedingmeslabel; // 0.1 扫码完成反馈 public JLabel gunNoLabel; // DB7010.6 实时枪号 // 手动输入(参考 auto3,模拟扫码枪),由 设置->扫码手动输入 开关控制显示 public JLabel manualLabel; public JTextField manualSnField; public JButton manualSubmitBtn; public volatile String pendingManualSn = ""; // 待使用的手动码,下次请求扫码时代替扫码枪 public ImageIcon imageGreen; public ImageIcon imageGray; public JButton signalBtn; public PlcSignalMonitor02 signalMonitor; public Timer oprnoTimer; // 推码给 PLC:S7String 布局 [最大长度][实际长度][字符...] public static final int PUSH_SN_LENGTH = 24; public static final long PUSH_HOLD_MS = 15000; public PushPort[] pushPorts; public Timer pushTimer; // 一个推码地址的状态,holdSn/holdUntil 由扫码线程写,其余仅推码定时器线程使用 public static class PushPort { public final int gunNo; public final String addr; // 起始地址,如 DB7020.1034 public final String lenAddr; // 实际长度字节,起始地址+1 public volatile String holdSn = ""; public volatile long holdUntil = 0; public String writtenSn = null; // 初始 null,登录后首次运行必定清零一次,清掉上次残留 public JTextField echoField; PushPort(int gunNo, String addr) { this.gunNo = gunNo; this.addr = addr; int dot = addr.lastIndexOf('.'); this.lenAddr = addr.substring(0, dot + 1) + (Integer.parseInt(addr.substring(dot + 1)) + 1); } } /** * @param plcIp PLC 地址 * @param slot 扫到的码存入的码槽(92 给 92 正面机,93 给 93 DB7004 正面机) * @param pushAddrs 推码地址,第 i 个对应 i+1 号枪;不传则不推码 */ public WorkJPanelScan(String plcIp, SnSlot slot, String... pushAddrs) { this.setLayout(null); this.setBackground(Color.WHITE); this.plcIp = plcIp; this.plcName = plcIp.substring(plcIp.lastIndexOf('.') + 1); this.slot = slot; this.pushPorts = new PushPort[pushAddrs.length]; for (int i = 0; i < pushAddrs.length; i++) { pushPorts[i] = new PushPort(i + 1, pushAddrs[i]); } s7PLC = new S7PLC(EPlcType.S1500, plcIp); s7PLC.setConnectTimeout(1000); s7PLC.setReceiveTimeout(1000); initUI(); productSnFeeding.setText(slot.getSn()); setMenuStatus("等待请求扫码", 0); startOprnoTimer(); if (pushPorts.length > 0) { startPushTimer(); } } private String tag() { return oprno + "扫码" + plcName; } private void initUI() { JLabel oprnoLabel = new JLabel(oprno); oprnoLabel.setHorizontalAlignment(SwingConstants.CENTER); oprnoLabel.setForeground(Color.BLACK); oprnoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); oprnoLabel.setBounds(0, 0, workWidth / 5, 40); this.add(oprnoLabel); JLabel nameLabel = new JLabel(plcName + "扫码"); nameLabel.setHorizontalAlignment(SwingConstants.CENTER); nameLabel.setForeground(Color.BLACK); nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14)); nameLabel.setBounds(0, 17, workWidth / 5, 40); this.add(nameLabel); status_menu = new JButton("等待请求扫码"); status_menu.setBounds(workWidth / 5, 0, workWidth * 4 / 5, 40); status_menu.setForeground(Color.GREEN); status_menu.setFont(new Font("微软雅黑", Font.PLAIN, 16)); status_menu.setBackground(Color.BLACK); this.add(status_menu); addRowLabel("扫码", 50); productSnFeeding = createField(50, workWidth * 3 / 5); productSnFeeding.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { String sn = productSnFeeding.getText().trim(); if (!sn.isEmpty()) { MesClient.createQRCode(sn, tag()); } } }); resetFeedingBtn = new JButton("清空"); resetFeedingBtn.setFont(new Font("微软雅黑", Font.PLAIN, 22)); resetFeedingBtn.setBounds(workWidth * 4 / 5 + 10, 50, workWidth / 5 - 10, 40); resetFeedingBtn.addActionListener(e -> { String sn = slot.getSn(); if (sn.isEmpty()) { return; } if (confirm("确定清空扫码结果" + sn + "吗?\n请求扫码仍在时会重新扫码")) { resetScan(); } }); this.add(resetFeedingBtn); addRowLabel("扫码枪", 100); gunField = createField(100, workWidth * 4 / 5); gunField.setFont(new Font("微软雅黑", Font.PLAIN, 14)); gunField.setText("未扫码"); imageGreen = scaledIcon("/bg/green_dot.png"); imageGray = scaledIcon("/bg/grey_dot.png"); feedinglabel = createLight("请求扫码", 0, 150); feedingmeslabel = createLight("扫码完成反馈", workWidth / 3, 150); feedingmeslabel.addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { boolean ret = readBoolean(plcDb + ".0.1"); if (confirm("确定修改" + plcName + "扫码完成反馈(" + plcDb + ".0.1)为" + !ret + "吗?")) { writeScanFinish(!ret); } } }); // 实时显示 PLC 给的枪号(DB7010.6),每轮刷新 gunNoLabel = new JLabel("PLC枪号:-"); gunNoLabel.setHorizontalAlignment(SwingConstants.LEFT); gunNoLabel.setFont(new Font("微软雅黑", Font.BOLD, 14)); gunNoLabel.setForeground(Color.DARK_GRAY); gunNoLabel.setBounds(workWidth * 2 / 3, 150, workWidth / 3, 30); this.add(gunNoLabel); // 手动输入行 manualLabel = new JLabel("手动"); manualLabel.setHorizontalAlignment(SwingConstants.CENTER); manualLabel.setForeground(Color.BLACK); manualLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); manualLabel.setBounds(0, 185, workWidth / 5, 40); this.add(manualLabel); manualSnField = new JTextField(""); manualSnField.setHorizontalAlignment(SwingConstants.CENTER); manualSnField.setForeground(Color.BLACK); manualSnField.setFont(new Font("微软雅黑", Font.PLAIN, 16)); manualSnField.setBounds(workWidth / 5, 185, workWidth * 3 / 5, 40); manualSnField.addActionListener(e -> submitManualSn()); // 回车提交 this.add(manualSnField); manualSubmitBtn = new JButton("确认"); manualSubmitBtn.setFont(new Font("微软雅黑", Font.PLAIN, 22)); manualSubmitBtn.setBounds(workWidth * 4 / 5 + 10, 185, workWidth / 5 - 10, 40); manualSubmitBtn.addActionListener(e -> submitManualSn()); this.add(manualSubmitBtn); setManualMode(MesClient.manualScanEnabled); // 回显行:每个推码地址一个框,显示从 PLC 读回的值 int footerY = 230; if (pushPorts.length > 0) { JLabel echoLabel = new JLabel("回显"); echoLabel.setHorizontalAlignment(SwingConstants.CENTER); echoLabel.setForeground(Color.BLACK); echoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 20)); echoLabel.setBounds(0, 230, workWidth / 5, 32); this.add(echoLabel); int w = (workWidth * 4 / 5 - 5 * (pushPorts.length - 1)) / pushPorts.length; for (int i = 0; i < pushPorts.length; i++) { PushPort p = pushPorts[i]; p.echoField = new JTextField(""); p.echoField.setHorizontalAlignment(SwingConstants.CENTER); p.echoField.setForeground(Color.BLACK); p.echoField.setFont(new Font("微软雅黑", Font.PLAIN, 12)); p.echoField.setEditable(false); p.echoField.setToolTipText(p.gunNo + "号枪 → " + p.addr); p.echoField.setBounds(workWidth / 5 + i * (w + 5), 230, w, 32); this.add(p.echoField); } footerY = 268; } JLabel ipLabel = new JLabel("PLC " + plcIp + " | " + plcDb + " | 枪号 " + gunAddr); ipLabel.setFont(new Font("微软雅黑", Font.PLAIN, 12)); ipLabel.setForeground(Color.DARK_GRAY); ipLabel.setBounds(5, footerY, workWidth * 2 / 3 + 55, 26); this.add(ipLabel); signalMonitor = new PlcSignalMonitor02(s7PLC, plcDb, gunAddr, tag(), MesClient.mesClientFrame); signalBtn = signalMonitor.createSignalButton(); signalBtn.setBounds(workWidth * 2 / 3 + 63, footerY, 90, 26); this.add(signalBtn); } private void addRowLabel(String text, int y) { JLabel label = new JLabel(text); label.setHorizontalAlignment(SwingConstants.CENTER); label.setForeground(Color.BLACK); label.setFont(new Font("微软雅黑", Font.PLAIN, 20)); label.setBounds(0, y, workWidth / 5, 40); this.add(label); } private JTextField createField(int y, int width) { JTextField field = new JTextField(""); field.setHorizontalAlignment(SwingConstants.CENTER); field.setForeground(Color.BLACK); field.setFont(new Font("微软雅黑", Font.PLAIN, 16)); field.setEditable(false); field.setBounds(workWidth / 5, y, width, 40); this.add(field); return field; } private JLabel createLight(String text, int x, int y) { JLabel label = new JLabel(text); label.setHorizontalAlignment(SwingConstants.LEFT); label.setIcon(imageGray); label.setForeground(Color.BLACK); label.setBounds(x, y, workWidth / 3, 30); this.add(label); return label; } private static ImageIcon scaledIcon(String path) { ImageIcon icon = new ImageIcon(MesClient.class.getResource(path)); icon.setImage(icon.getImage().getScaledInstance(20, 20, Image.SCALE_SMOOTH)); return icon; } private static boolean confirm(String msg) { return JOptionPane.showConfirmDialog(null, msg, "操作确认", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION; } public void setMenuStatus(String msg, int error) { status_menu.setForeground(error == 0 ? Color.GREEN : Color.RED); status_menu.setText(msg); } // 显示/隐藏手动输入行;关闭时丢弃未使用的手动码 public void setManualMode(boolean enabled) { manualLabel.setVisible(enabled); manualSnField.setVisible(enabled); manualSubmitBtn.setVisible(enabled); if (enabled) { return; } manualSnField.setText(""); if (!pendingManualSn.isEmpty()) { pendingManualSn = ""; setMenuStatus("已关闭手动输入,未使用的手动码已丢弃", 0); } } // 提交手动码:非空即挂起,等下一次请求扫码时当作扫码结果 public void submitManualSn() { String sn = manualSnField.getText().trim(); if (sn.isEmpty()) { setMenuStatus("手动码不能为空", -1); return; } pendingManualSn = sn; manualSnField.setText(""); log.info("<{}>手动输入工件码:{},操作人:{}", tag(), sn, MesClient.user_menu.getText()); setMenuStatus("手动码已就绪,等待请求扫码:" + sn, 0); } // scan.ips 里配了这把枪即有效,PLC 发几号就用几号 private boolean isValidGun(int no) { return no >= 1 && no <= MesClient.scanClients.length; } // 定时任务 ----------------------------------------------------------------------------------------------------- public void startOprnoTimer() { if (oprnoTimer != null) { oprnoTimer.cancel(); } oprnoTimer = new Timer(); oprnoTimer.schedule(new TimerTask() { public void run() { try { if (MesClient.isLogin == 1) { cycle(); } } catch (Exception e) { log.error("<{}>轮询异常", tag(), e); } } }, 100, 1500); } // 推码定时器(独立于扫码主流程):留存期内写码,到期清空,并读回显 public void startPushTimer() { if (pushTimer != null) { pushTimer.cancel(); } pushTimer = new Timer(); pushTimer.schedule(new TimerTask() { public void run() { if (MesClient.isLogin != 1) { return; } for (PushPort p : pushPorts) { pushCycle(p); } } }, 1000, 500); } private void pushCycle(PushPort p) { try { String target = System.currentTimeMillis() <= p.holdUntil ? p.holdSn : ""; if (!target.equals(p.writtenSn)) { if (target.isEmpty()) { // writeString 不支持空串:从实际长度字节起连同字符一起写 0,最大长度字节不动 s7PLC.writeByte(p.lenAddr, new byte[1 + PUSH_SN_LENGTH]); } else { s7PLC.writeString(p.addr, target); } p.writtenSn = target; log.info("<{}>推码{}-->成功,写入内容:{}", tag(), p.addr, target.isEmpty() ? "清空" : target); } p.echoField.setText(readPushSn(p)); } catch (Exception e) { p.echoField.setText("通讯失败"); log.error("<{}>推码{}-->失败:{}", tag(), p.addr, e.getMessage()); } } // 按原始字节读 S7String,实际长度为 0 时返回空串(不用 readString) private String readPushSn(PushPort p) { byte[] bs = s7PLC.readByte(p.addr, 2 + PUSH_SN_LENGTH); int len = Math.min(bs[1] & 0xFF, PUSH_SN_LENGTH); return new String(bs, 2, len, StandardCharsets.US_ASCII).trim(); } // 留存扫到的码,只改内存不碰 PLC;没有对应推码地址的枪号不推 private void holdPushSn(int gunNo, String sn) { if (gunNo < 1 || gunNo > pushPorts.length) { return; } PushPort p = pushPorts[gunNo - 1]; p.holdSn = sn; // 先写码再写截止时间,定时器不会读到新时间配旧码 p.holdUntil = System.currentTimeMillis() + PUSH_HOLD_MS; } private void cycle() { boolean retScan = readBoolean(plcDb + ".0.0"); boolean retFinish = readBoolean(plcDb + ".0.1"); feedinglabel.setIcon(retScan ? imageGreen : imageGray); feedingmeslabel.setIcon(retFinish ? imageGreen : imageGray); Integer plcGunNo = readGunNo(); showGunNo(plcGunNo); // 显示扫码槽当前码,被正面机台取走后变空 productSnFeeding.setText(slot.getSn()); if (!retScan) { scanDone = false; if (retFinish) { writeScanFinish(false); } return; } if (scanDone) { if (!retFinish) { writeScanFinish(true); } return; } doScan(plcGunNo); } // 刷新界面上的 PLC 枪号:0=未发(灰),1-3 绿色,其他值/读不到红色 private void showGunNo(Integer no) { if (no == null) { gunNoLabel.setForeground(Color.RED); gunNoLabel.setText("PLC枪号:读取失败"); gunNoLabel.setToolTipText(gunAddr + " 读取失败,不扫码"); return; } if (no == 0) { gunNoLabel.setForeground(Color.GRAY); gunNoLabel.setText("PLC枪号:0(未发)"); gunNoLabel.setToolTipText("本PLC未发枪号"); return; } boolean valid = isValidGun(no); gunNoLabel.setForeground(valid ? new Color(0, 128, 0) : Color.RED); gunNoLabel.setText("PLC枪号:" + no + (valid ? "" : "(无效)")); gunNoLabel.setToolTipText(valid ? no + "号枪 " + MesClient.scanClients[no - 1].serverIP : "有效范围 1-" + MesClient.scanClients.length); } private void doScan(Integer plcGunNo) { // 有手动码时优先使用并跳过本次扫码枪,用完即清(同 auto3) String manualSn = pendingManualSn; if (!manualSn.isEmpty()) { pendingManualSn = ""; log.info("<{}>手动码执行:{}", tag(), manualSn); gunField.setText("手动输入"); // 手动码按 PLC 当前枪号推码,枪号 0 或读不到则不推 saveScan(manualSn, "手动输入(" + plcName + ")", plcGunNo == null ? 0 : plcGunNo); return; } // 读不到枪号不扫码 if (plcGunNo == null) { gunField.setText("未读到枪号"); setMenuStatus("读取" + gunAddr + "失败,不扫码", -1); return; } int gunNo = plcGunNo; // 0 = PLC 还没发枪号,等下一轮 if (gunNo == 0) { setMenuStatus("请求扫码,等待PLC发枪号", 0); return; } if (!isValidGun(gunNo)) { gunField.setText("枪号 " + gunNo + " 无效"); setMenuStatus("扫码枪号无效:" + gunNo + "(" + gunAddr + " 应为 1-" + MesClient.scanClients.length + ")", -1); return; } TCPClient client = MesClient.scanClients[gunNo - 1]; gunField.setText(gunNo + "号枪 " + client.serverIP + ":" + client.serverPort); setMenuStatus(gunNo + "号枪扫码中...", 0); String sn = MesClient.scanQrcode(gunNo); if (sn.isEmpty()) { setMenuStatus(gunNo + "号枪扫码失败(未读到码),重试中", -1); return; } saveScan(sn, gunNo + "号枪(" + plcName + ")", gunNo); } // 存入正面扫码码并回写扫码完成反馈,同时留存推码 private void saveScan(String sn, String source, int gunNo) { slot.put(sn, source); productSnFeeding.setText(sn); writeScanFinish(true); holdPushSn(gunNo, sn); scanDone = true; setMenuStatus("扫码成功" + (source.startsWith("手动") ? "(手动码)" : ""), 0); } public void resetScan() { slot.clear("人工清空(" + plcName + ")"); productSnFeeding.setText(""); scanDone = false; writeScanFinish(false); setMenuStatus("扫码结果已清空,等待请求扫码", 0); } // 与设备交互 ----------------------------------------------------------------------------------------- // 读扫码枪号,读失败返回 null;每轮都读,只在值变化时记日志 private Integer readGunNo() { Integer no = null; String err = ""; try { no = (int) s7PLC.readInt16(gunAddr); } catch (Exception e) { err = e.getMessage(); } String state = no == null ? "ERR" : String.valueOf(no); if (!state.equals(lastGunState)) { lastGunState = state; if (no == null) { log.warn("<{}>读取{}-->失败,不扫码:{}", tag(), gunAddr, err); } else { log.info("<{}>读取{}-->枪号变为:{}", tag(), gunAddr, no); } } return no; } public void writeScanFinish(boolean b) { writeBoolean(plcDb + ".0.1", b); } public boolean readBoolean(String addr) { try { return s7PLC.readBoolean(addr); } catch (Exception e) { log.error("<{}>读取{}-->失败:{}", tag(), addr, e.getMessage()); return false; } } public boolean writeBoolean(String addr, boolean value) { try { s7PLC.writeBoolean(addr, value); log.info("<{}>写入{}-->成功,写入内容:{}", tag(), addr, value); return true; } catch (Exception e) { log.error("<{}>写入{}-->失败,写入内容:{},{}", tag(), addr, value, e.getMessage()); return false; } } }