S7Util.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  1. package com.mes.ui;
  2. import com.github.s7connector.api.DaveArea;
  3. import com.github.s7connector.api.factory.S7ConnectorFactory;
  4. import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
  5. import com.mes.util.JdbcUtils;
  6. import org.slf4j.Logger;
  7. import org.slf4j.LoggerFactory;
  8. import javax.swing.*;
  9. import java.math.BigInteger;
  10. import java.text.DecimalFormat;
  11. public class S7Util {
  12. private static final Logger log = LoggerFactory.getLogger(S7Util.class);
  13. private static final int DB9051 = 9051;
  14. private static final int DB9051_LEN = 38;
  15. private static final int DB9052 = 9052;
  16. /** 连接失败后的重试间隔 */
  17. private static final long RECONNECT_COOLDOWN_MS = 5000L;
  18. private static volatile long nextReconnectAt = 0L;
  19. private static volatile boolean connected = false;
  20. /** 实测本设备 slot=2 可通;成功后优先复用 */
  21. private static volatile int lastSuccessSlot = 2;
  22. /**
  23. * 监控线程入口:读 DB9051 一次,刷新界面并执行业务状态机
  24. */
  25. public static void pollDeviceCycle() {
  26. try {
  27. if (!ensureConnected()) {
  28. SwingUtilities.invokeLater(() -> MesClient.updatePlcStatus(false));
  29. return;
  30. }
  31. SwingUtilities.invokeLater(() -> MesClient.updatePlcStatus(true));
  32. byte[] db9051;
  33. try {
  34. db9051 = MesClient.s7Connector.read(DaveArea.DB, DB9051, DB9051_LEN, 0);
  35. } catch (Exception e) {
  36. markBroken("读DB9051", e);
  37. SwingUtilities.invokeLater(() -> MesClient.updatePlcStatus(false));
  38. return;
  39. }
  40. if (db9051 == null) {
  41. return;
  42. }
  43. updateRealtimeDisplay(db9051);
  44. getDeviceState(db9051);
  45. } catch (Exception e) {
  46. log.error("[PLC异常] pollDeviceCycle", e);
  47. markBroken("pollDeviceCycle", e);
  48. SwingUtilities.invokeLater(() -> MesClient.updatePlcStatus(false));
  49. }
  50. }
  51. public static synchronized boolean isConnected() {
  52. return connected && MesClient.s7Connector != null;
  53. }
  54. public static synchronized void disconnect() {
  55. connected = false;
  56. if (MesClient.s7Connector != null) {
  57. try {
  58. MesClient.s7Connector.close();
  59. } catch (Exception ignored) {
  60. }
  61. MesClient.s7Connector = null;
  62. }
  63. }
  64. public static synchronized boolean connect() {
  65. disconnect();
  66. // Unexpected function code 常见于 rack/slot 不匹配:
  67. // 旧版默认 slot=2;S7-1200 常用 slot=1。按历史成功值优先,再依次尝试。
  68. int[] slots;
  69. if (lastSuccessSlot == 1) {
  70. slots = new int[]{1, 2};
  71. } else if (lastSuccessSlot == 2) {
  72. slots = new int[]{2, 1};
  73. } else {
  74. slots = new int[]{2, 1};
  75. }
  76. Exception lastError = null;
  77. for (int slot : slots) {
  78. try {
  79. log.info("[PLC连接] 尝试 {} rack=0 slot={} port=102 timeout=5000", MesClient.plcUrl, slot);
  80. MesClient.s7Connector = S7ConnectorFactory
  81. .buildTCPConnector()
  82. .withHost(MesClient.plcUrl)
  83. .withPort(102)
  84. .withRack(0)
  85. .withSlot(slot)
  86. .withTimeout(5000)
  87. .build();
  88. byte[] probe = MesClient.s7Connector.read(DaveArea.DB, DB9051, DB9051_LEN, 0);
  89. if (probe == null || probe.length < DB9051_LEN) {
  90. throw new IllegalStateException("探测读取DB9051返回空数据");
  91. }
  92. connected = true;
  93. lastSuccessSlot = slot;
  94. nextReconnectAt = 0L;
  95. log.info("[PLC连接] 连接成功 {} rack=0 slot={}", MesClient.plcUrl, slot);
  96. return true;
  97. } catch (Exception e) {
  98. lastError = e;
  99. log.error("[PLC连接] rack=0 slot={} 失败: {}", slot, rootCause(e));
  100. disconnect();
  101. try {
  102. Thread.sleep(800);
  103. } catch (InterruptedException ie) {
  104. Thread.currentThread().interrupt();
  105. break;
  106. }
  107. }
  108. }
  109. log.error("[PLC连接] 全部 slot 尝试失败 {}: {}", MesClient.plcUrl,
  110. lastError == null ? "" : rootCause(lastError));
  111. nextReconnectAt = System.currentTimeMillis() + RECONNECT_COOLDOWN_MS;
  112. return false;
  113. }
  114. /**
  115. * 确保可用连接:半开连接探测失败时强制断开再重连。
  116. */
  117. public static synchronized boolean ensureConnected() {
  118. if (connected && MesClient.s7Connector != null) {
  119. try {
  120. // 心跳探测只读1字节,减轻通讯负担
  121. byte[] probe = MesClient.s7Connector.read(DaveArea.DB, DB9051, 1, 0);
  122. if (probe != null && probe.length >= 1) {
  123. return true;
  124. }
  125. log.info("[PLC连接] 探测读取异常,准备重连");
  126. } catch (Exception e) {
  127. log.info("[PLC连接] 连接已失效,准备重连: {}", rootCause(e));
  128. }
  129. disconnect();
  130. }
  131. if (System.currentTimeMillis() < nextReconnectAt) {
  132. return false;
  133. }
  134. return connect();
  135. }
  136. private static synchronized void markBroken(String action, Exception e) {
  137. log.info("[PLC] {} 失败,释放连接: {}", action, rootCause(e));
  138. disconnect();
  139. nextReconnectAt = System.currentTimeMillis() + RECONNECT_COOLDOWN_MS;
  140. }
  141. private static String rootCause(Throwable e) {
  142. if (e == null) {
  143. return "";
  144. }
  145. Throwable cur = e;
  146. while (cur.getCause() != null && cur.getCause() != cur) {
  147. cur = cur.getCause();
  148. }
  149. if (cur == e) {
  150. return e.getClass().getSimpleName() + ": " + e.getMessage();
  151. }
  152. return e.getClass().getSimpleName() + "(" + e.getMessage() + ") <- "
  153. + cur.getClass().getSimpleName() + ": " + cur.getMessage();
  154. }
  155. // 监听状态
  156. public static void getDeviceState(byte[] db9051){
  157. try {
  158. if (MesClient.s7Connector == null || db9051 == null) {
  159. return;
  160. }
  161. if (MesClient.tjFlag == 3) {
  162. if (MesClient.tjStatus == 0) {
  163. MesClient.tjStatus = 1;
  164. log.info("[流程] 焊接完成,复位允许启动并提交MES总结果 OK,工件={}",
  165. MesClient.product_sn.getText());
  166. sendAllowStart(false);
  167. Boolean ret = DataUtil.sendQuality(
  168. MesClient.nettyClient, MesClient.product_sn.getText(), "OK", MesClient.user20);
  169. if (ret) {
  170. log.info("[MES写] MQDW 提交成功,工件={}", MesClient.product_sn.getText());
  171. MesClient.resetScanA();
  172. MesClient.scan_type = 1;
  173. MesClient.scanBarcode();
  174. MesClient.setMenuStatus("结果提交成功,请扫下一件", 0);
  175. } else {
  176. log.warn("[MES写] MQDW 提交失败,工件={},等待重试", MesClient.product_sn.getText());
  177. MesClient.tjStatus = 0;
  178. MesClient.setMenuStatus(MesClient.tjFlagTextErr, -1);
  179. }
  180. }
  181. } else if (isWaitingProcess()) {
  182. if (isWeldingComplete(db9051)) {
  183. log.info("[流程] 检测到焊接完成 DB9051.DBX36.0=true,tjFlag: {} -> 3", MesClient.tjFlag);
  184. MesClient.tjFlag = 3;
  185. MesClient.tjStatus = 0;
  186. MesClient.status_menu.setText(MesClient.tjFlagText3);
  187. }
  188. } else if (MesClient.tjFlag == 0) {
  189. if (getAllowStart()) {
  190. log.info("[流程] 未扫码状态检测到允许启动仍为true,执行复位");
  191. sendAllowStart(false);
  192. }
  193. }
  194. }catch (Exception e){
  195. log.error("[流程异常] getDeviceState", e);
  196. }
  197. }
  198. /** @deprecated 请使用 pollDeviceCycle */
  199. public static void getDeviceState(){
  200. byte[] db9051 = readDb9051Raw(true);
  201. if (db9051 != null) {
  202. getDeviceState(db9051);
  203. }
  204. }
  205. /**
  206. * 实时刷新 DB9051 主轴转速、进给速度、压力值到界面
  207. */
  208. public static void updateRealtimeDisplay(byte[] data) {
  209. try {
  210. if (data == null || MesClient.spindleSpeedLabel == null) {
  211. return;
  212. }
  213. final String spindle = formatS7Val(subBytes(data, 0, 4));
  214. final String feed = formatS7Val(subBytes(data, 4, 4));
  215. final String pressure = formatS7Val(subBytes(data, 12, 4));
  216. saveFswParamIfProcessing(spindle, feed, pressure);
  217. SwingUtilities.invokeLater(() ->
  218. MesClient.updatePlcDisplay(spindle, feed, pressure));
  219. } catch (Exception e) {
  220. log.error("[PLC异常] updateRealtimeDisplay", e);
  221. }
  222. }
  223. /** @deprecated 请使用 pollDeviceCycle */
  224. public static void updateRealtimeDisplay() {
  225. byte[] data = readDb9051Raw(true);
  226. updateRealtimeDisplay(data);
  227. }
  228. private static byte[] readDb9051Raw(boolean withLog) {
  229. try {
  230. if (!ensureConnected()) {
  231. if (withLog) {
  232. log.warn("[PLC读] DB9051 失败:S7未连接");
  233. }
  234. return null;
  235. }
  236. byte[] data = MesClient.s7Connector.read(DaveArea.DB, DB9051, DB9051_LEN, 0);
  237. if (withLog) {
  238. log.info(formatDb9051Log(data));
  239. }
  240. return data;
  241. } catch (Exception e) {
  242. log.error("[PLC读] DB9051 异常", e);
  243. markBroken("读DB9051", e);
  244. return null;
  245. }
  246. }
  247. static String formatDb9051Log(byte[] data) {
  248. if (data == null) {
  249. return "[PLC读] DB9051 数据为空";
  250. }
  251. return String.format(
  252. "[PLC读] DB9051 | 主轴转速=%s(HEX:%s) 进给速度=%s(HEX:%s) 压力值=%s(HEX:%s) | " +
  253. "焊接长度1=%s 2=%s 3=%s 4=%s 5=%s | 焊接完成(DBX36.0)=%s 出站OK(DBB37)=%d | tjFlag=%d work_status=%d",
  254. formatS7Val(subBytes(data, 0, 4)), bytesToHex(subBytes(data, 0, 4)),
  255. formatS7Val(subBytes(data, 4, 4)), bytesToHex(subBytes(data, 4, 4)),
  256. formatS7Val(subBytes(data, 12, 4)), bytesToHex(subBytes(data, 12, 4)),
  257. formatS7Val(subBytes(data, 16, 4)),
  258. formatS7Val(subBytes(data, 20, 4)),
  259. formatS7Val(subBytes(data, 24, 4)),
  260. formatS7Val(subBytes(data, 28, 4)),
  261. formatS7Val(subBytes(data, 32, 4)),
  262. isWeldingComplete(data),
  263. data[37] & 0xFF,
  264. MesClient.tjFlag,
  265. MesClient.work_status
  266. );
  267. }
  268. /** 等待加工中(质量合格、尚未收到焊接完成),不含 tjFlag=3 提交阶段 */
  269. private static boolean isWaitingProcess() {
  270. return MesClient.tjFlag == 1;
  271. }
  272. private static boolean isMeaningfulParam(String value) {
  273. if (value == null || value.trim().isEmpty() || "--".equals(value.trim())) {
  274. return false;
  275. }
  276. try {
  277. float v = Float.parseFloat(value.trim());
  278. return !Float.isNaN(v) && !Float.isInfinite(v) && Math.abs(v) > 1e-4f;
  279. } catch (Exception e) {
  280. return false;
  281. }
  282. }
  283. private static void saveFswParamIfProcessing(String spindleSpeed, String feedRate, String pressure) {
  284. if (!isWaitingProcess()) {
  285. return;
  286. }
  287. String sn = MesClient.product_sn.getText();
  288. if (sn == null || sn.trim().isEmpty()) {
  289. return;
  290. }
  291. // 设备未启动前三个参数多为0,等任一非0再开始存,避免无用数据
  292. if (!MesClient.paramRecordingStarted) {
  293. if (isMeaningfulParam(spindleSpeed) || isMeaningfulParam(feedRate) || isMeaningfulParam(pressure)) {
  294. MesClient.paramRecordingStarted = true;
  295. log.info("[过程参数] 检测到有效数据,开始缓存 sn={}", sn.trim());
  296. } else {
  297. return;
  298. }
  299. }
  300. MesClient.getUser();
  301. boolean saved = JdbcUtils.insertFswParam(
  302. MesClient.mes_gw,
  303. MesClient.mes_line_sn,
  304. sn.trim(),
  305. spindleSpeed,
  306. feedRate,
  307. pressure,
  308. MesClient.user20.trim()
  309. );
  310. if (saved) {
  311. log.debug("[过程参数] 本地缓存 sn={} 主轴={} 进给={} 压力={}",
  312. sn.trim(), spindleSpeed, feedRate, pressure);
  313. }
  314. }
  315. /** DB9051.DBX36.0 焊接完成(BOOL) */
  316. private static boolean isWeldingComplete(byte[] data) {
  317. if (data.length <= 36) {
  318. return false;
  319. }
  320. return (data[36] & (1 << 0)) != 0;
  321. }
  322. private static byte[] subBytes(byte[] src, int offset, int length) {
  323. byte[] dest = new byte[length];
  324. System.arraycopy(src, offset, dest, 0, length);
  325. return dest;
  326. }
  327. //屏蔽mes交互 如果设备禁止启动 则启动设备
  328. public static void getDeviceShield(){
  329. try {
  330. if (!getAllowStart()) {
  331. log.info("[MES屏蔽] 强制发送允许启动 DB9052.DBX0.0=true");
  332. sendAllowStart(true);
  333. }
  334. }catch (Exception e){
  335. log.error("[MES屏蔽异常] getDeviceShield", e);
  336. }
  337. }
  338. // 监听急停
  339. public static void checkStop(){
  340. try {
  341. if (!ensureConnected()) {
  342. return;
  343. }
  344. byte[] bytes = MesClient.s7Connector.read(DaveArea.DB, DB9052, 1, 0);
  345. log.info(formatDb9052Log(bytes[0], "读-急停检测"));
  346. int jtstatus = getBit(bytes[0], 7);
  347. log.info("[PLC读] DB9052.DBX0.7 急停状态={}", jtstatus);
  348. if(jtstatus == 0){
  349. switchEnable(0);
  350. }
  351. if(MesClient.tjFlag == 2 && jtstatus == 0){
  352. MesClient.tjResult = "NG";
  353. MesClient.finish_ok_bt.setEnabled(true);
  354. MesClient.finish_ng_bt.setEnabled(true);
  355. }
  356. }catch (Exception e){
  357. log.error("[PLC异常] checkStop", e);
  358. markBroken("checkStop", e);
  359. }
  360. }
  361. public static int getBit(byte b, int n) {
  362. String binaryString = Integer.toBinaryString(b & 0xFF);
  363. binaryString = String.format("%8s", binaryString).replace(' ', '0');
  364. return binaryString.charAt(n) == '1' ? 1 : 0;
  365. }
  366. private static String formatDb9052Log(byte b, String action) {
  367. boolean allowStart = (b & (1 << 0)) != 0;
  368. return String.format("[PLC%s] DB9052.DBB0=0x%02X DBX0.0(允许启动)=%s DBX0.7(急停)=%d",
  369. action, b & 0xFF, allowStart, getBit(b, 7));
  370. }
  371. // 转大端排序
  372. private static byte[] xtob( byte[] bytes){
  373. byte[] reversedBytes = new byte[bytes.length];
  374. System.arraycopy(bytes, 0, reversedBytes, 0, bytes.length);
  375. for (int i = 0; i < bytes.length / 2; i++) {
  376. byte temp = reversedBytes[i];
  377. reversedBytes[i] = reversedBytes[reversedBytes.length - 1 - i];
  378. reversedBytes[reversedBytes.length - 1 - i] = temp;
  379. }
  380. return reversedBytes;
  381. }
  382. private static String bytesToHex(byte[] bytes) {
  383. StringBuilder sb = new StringBuilder();
  384. for (byte b : bytes) {
  385. sb.append(String.format("%02X", b & 0xFF));
  386. }
  387. return sb.toString();
  388. }
  389. // 格式化 S7 REAL(大端 IEEE754,不做字节反转)
  390. private static float parseS7Real(byte[] val) {
  391. if (val == null || val.length < 4) {
  392. return 0f;
  393. }
  394. int bits = ((val[0] & 0xFF) << 24)
  395. | ((val[1] & 0xFF) << 16)
  396. | ((val[2] & 0xFF) << 8)
  397. | (val[3] & 0xFF);
  398. return Float.intBitsToFloat(bits);
  399. }
  400. private static String formatS7Val(byte[] val){
  401. float floatValue = parseS7Real(val);
  402. if (Float.isNaN(floatValue) || Float.isInfinite(floatValue)) {
  403. return "0.0";
  404. }
  405. DecimalFormat df = new DecimalFormat("#.#");
  406. df.setMaximumFractionDigits(1);
  407. return df.format(floatValue);
  408. }
  409. // 禁止/启用启动
  410. public static Boolean switchEnable(int state){
  411. try{
  412. log.info("[PLC写] switchEnable state={} -> DB9052.DBX0.0={}", state, state == 1);
  413. sendAllowStart(state == 1);
  414. return true;
  415. }catch (Exception e){
  416. log.error("[PLC异常] switchEnable", e);
  417. return false;
  418. }
  419. }
  420. // 获取 DB9052.DBX0.0 允许启动状态
  421. public static boolean getAllowStart() {
  422. try {
  423. if (!ensureConnected()) {
  424. return false;
  425. }
  426. byte[] data = MesClient.s7Connector.read(DaveArea.DB, DB9052, 1, 0);
  427. log.info(formatDb9052Log(data[0], "读-允许启动"));
  428. return (data[0] & (1 << 0)) != 0;
  429. } catch (Exception e) {
  430. log.error("[PLC读] DB9052 允许启动 异常", e);
  431. markBroken("读允许启动", e);
  432. return false;
  433. }
  434. }
  435. // 获取允许运行状态
  436. public static Integer getSwitchEnable(){
  437. try{
  438. if (!ensureConnected()) {
  439. return 0;
  440. }
  441. byte[] data = MesClient.s7Connector.read(DaveArea.DB, DB9052, 1, 0);
  442. log.info(formatDb9052Log(data[0], "读-整字节"));
  443. return new BigInteger(xtob(data)).intValue();
  444. }catch (Exception e){
  445. log.error("[PLC读] DB9052 整字节 异常", e);
  446. markBroken("读DB9052", e);
  447. return 0;
  448. }
  449. }
  450. /** DB9052.DBX0.0 允许启动信号:true=允许,false=禁止/复位 */
  451. public static boolean sendAllowStart(boolean allow) {
  452. log.info("[PLC写] 请求设置 DB9052.DBX0.0 允许启动={}", allow);
  453. return writeBit(DB9052, 0, 0, allow);
  454. }
  455. /**
  456. * 兼容旧接口:true=禁止启动,false=允许启动
  457. */
  458. public static boolean sendStartSignal(boolean forbidStart) {
  459. return sendAllowStart(!forbidStart);
  460. }
  461. public static boolean writeBit(int dbNumber, int byteIndex, int bitIndex, boolean value) {
  462. try {
  463. if (!ensureConnected()) {
  464. log.warn("[PLC写] DB{}.DBX{}.{} 失败:S7未连接", dbNumber, byteIndex, bitIndex);
  465. return false;
  466. }
  467. byte[] data = MesClient.s7Connector.read(DaveArea.DB, dbNumber, 1, byteIndex);
  468. byte oldByte = data[0];
  469. boolean oldBit = (oldByte & (1 << bitIndex)) != 0;
  470. byte b = oldByte;
  471. if (value) {
  472. b = (byte)(b | (1 << bitIndex));
  473. } else {
  474. b = (byte)(b & ~(1 << bitIndex));
  475. }
  476. byte[] toWrite = new byte[]{b};
  477. MesClient.s7Connector.write(DaveArea.DB, dbNumber, byteIndex, toWrite);
  478. log.info("[PLC写] DB{}.DBX{}.{} {} -> {} | DBB{}: 0x{} -> 0x{}",
  479. dbNumber, byteIndex, bitIndex, oldBit, value,
  480. byteIndex,
  481. String.format("%02X", oldByte & 0xFF),
  482. String.format("%02X", b & 0xFF));
  483. if (dbNumber == DB9052 && byteIndex == 0 && bitIndex == 0) {
  484. log.info("[PLC写] DB9052.DBX0.0 允许启动信号已设为 {}", value);
  485. }
  486. return true;
  487. } catch (Exception e) {
  488. log.error("[PLC写] DB{}.DBX{}.{} 异常", dbNumber, byteIndex, bitIndex, e);
  489. markBroken("写位", e);
  490. return false;
  491. }
  492. }
  493. // 获取运行的程序名
  494. public static String getProgramName(S7PLC s7PLC){
  495. String programName = "";
  496. try{
  497. programName = s7PLC.readProgramName();
  498. log.info("[PLC读] 当前程序名={}", programName);
  499. }catch (Exception e){
  500. log.error("[PLC读] 程序名异常", e);
  501. programName = "";
  502. }
  503. return programName;
  504. }
  505. }