Ver código fonte

锯切设备定时获取

zhanghe 4 meses atrás
pai
commit
1f263242ea

+ 15 - 0
src/main/java/com/jeesite/modules/mes/dao/MesSawDao.java

@@ -0,0 +1,15 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesSaw;
+
+/**
+ * 锯切数据DAO接口
+ * @author system
+ * @version 2026-06-11
+ */
+@MyBatisDao
+public interface MesSawDao extends CrudDao<MesSaw> {
+
+}

+ 91 - 0
src/main/java/com/jeesite/modules/mes/service/MesSawService.java

@@ -0,0 +1,91 @@
+package com.jeesite.modules.mes.service;
+
+import java.util.List;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
+import com.jeesite.common.config.Global;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.common.service.ServiceException;
+import com.jeesite.common.utils.excel.ExcelImport;
+import com.jeesite.common.validator.ValidatorUtils;
+import com.jeesite.modules.mes.dao.MesSawDao;
+import com.jeesite.modules.mes.entity.MesSaw;
+
+/**
+ * 锯切数据Service
+ * @author system
+ * @version 2026-06-11
+ */
+@Service
+public class MesSawService extends CrudService<MesSawDao, MesSaw> {
+
+	@Override
+	public MesSaw get(MesSaw e) { return super.get(e); }
+
+	@Override
+	public Page<MesSaw> findPage(MesSaw e) { return super.findPage(e); }
+
+	@Override
+	public List<MesSaw> findList(MesSaw e) { return super.findList(e); }
+
+	@Override
+	@Transactional
+	public void save(MesSaw e) { super.save(e); }
+
+	@Override
+	@Transactional
+	public void updateStatus(MesSaw e) { super.updateStatus(e); }
+
+	@Override
+	@Transactional
+	public void delete(MesSaw e) { super.delete(e); }
+
+	@Transactional
+	public String importData(MultipartFile file) {
+		if (file == null) throw new ServiceException(text("请选择导入的数据文件!"));
+		int successNum = 0; int failureNum = 0;
+		StringBuilder successMsg = new StringBuilder();
+		StringBuilder failureMsg = new StringBuilder();
+		try (ExcelImport ei = new ExcelImport(file, 2, 0)) {
+			List<MesSaw> list = ei.getDataList(MesSaw.class);
+			for (MesSaw item : list) {
+				try {
+					ValidatorUtils.validateWithException(item);
+					this.save(item);
+					successNum++;
+					successMsg.append("<br/>").append(successNum).append("、编号 ").append(item.getId()).append(" 导入成功");
+				} catch (Exception ex) {
+					failureNum++;
+					String msg = "<br/>" + failureNum + "、编号 " + item.getId() + " 导入失败:";
+					if (ex instanceof ConstraintViolationException) {
+						for (ConstraintViolation<?> v : ((ConstraintViolationException) ex).getConstraintViolations()) {
+							msg += Global.getText(v.getMessage()) + " (" + v.getPropertyPath() + ")";
+						}
+					} else {
+						msg += ex.getMessage();
+					}
+					failureMsg.append(msg);
+					logger.error(msg, ex);
+				}
+			}
+		} catch (Exception ex) {
+			logger.error(ex.getMessage(), ex);
+			failureMsg.append(ex.getMessage());
+			return failureMsg.toString();
+		}
+		if (failureNum > 0) {
+			failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+			throw new ServiceException(failureMsg.toString());
+		} else {
+			successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
+		}
+		return successMsg.toString();
+	}
+}

+ 162 - 0
src/main/java/com/jeesite/modules/scheduling/SawScheduling.java

@@ -0,0 +1,162 @@
+package com.jeesite.modules.scheduling;
+
+import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+import com.jeesite.common.config.Global;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.mes.entity.MesSaw;
+import com.jeesite.modules.mes.service.MesSawService;
+import com.jeesite.modules.mes.service.MesDeviceAlarmService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * 半自动锯切站数据采集定时任务(S7 协议,DB28)
+ * @author system
+ * @version 2026-06-11
+ */
+@Configuration
+@EnableScheduling
+public class SawScheduling {
+
+    private static final Logger logger = LoggerFactory.getLogger(SawScheduling.class);
+
+    private static final String DEFAULT_PLC_IP = "10.0.20.120";
+    private static final String DEVICE_SN = "JQ_OP140_01";
+    private static final String STATE_PENDING = "0"; // 待处理
+    private static final String STATE_HANDLED = "1"; // 已处理
+
+    @Autowired
+    private MesSawService mesSawService;
+
+    @Autowired
+    private MesDeviceAlarmService mesDeviceAlarmService;
+
+    /**
+     * 每 30 秒采集一次
+     */
+//    @Scheduled(fixedRate = 30000)
+    public void collect() {
+        String enabled = Global.getConfig("mes.saw.enabled");
+        if (StringUtils.isNotBlank(enabled) && !"true".equalsIgnoreCase(enabled.trim())) {
+            return;
+        }
+        String ip = Global.getConfig("mes.saw.plc.ip");
+        if (StringUtils.isBlank(ip)) {
+            ip = DEFAULT_PLC_IP;
+        }
+
+        S7PLC s7PLC = null;
+        try {
+            s7PLC = new S7PLC(EPlcType.S1200, ip);
+
+            MesSaw e = new MesSaw();
+            // ===== UINT(DB28.0/2/4/6)=====
+            e.setSeq1Takt(String.valueOf(s7PLC.readUInt16("DB28.0")));
+            e.setSeq2Takt(String.valueOf(s7PLC.readUInt16("DB28.2")));
+            e.setSawSpeed(String.valueOf(s7PLC.readUInt16("DB28.4")));
+            e.setMillSpeed(String.valueOf(s7PLC.readUInt16("DB28.6")));
+
+            // ===== BOOL(DB28.8.x / 9.x / 10.x)=====
+            boolean servoAlarm        = s7PLC.readBoolean("DB28.8.0");
+            boolean inverterFault     = s7PLC.readBoolean("DB28.8.1");
+            boolean inverterOverload  = s7PLC.readBoolean("DB28.8.2");
+            boolean oilMistLow        = s7PLC.readBoolean("DB28.8.3");
+            boolean chipConveyorTrip  = s7PLC.readBoolean("DB28.8.4");
+            boolean hydraulicTrip     = s7PLC.readBoolean("DB28.8.5");
+            boolean waterCoolerStop   = s7PLC.readBoolean("DB28.8.6");
+            boolean manualCameraAlarm = s7PLC.readBoolean("DB28.8.7");
+            boolean sawCameraAlarm    = s7PLC.readBoolean("DB28.9.0");
+            boolean cyl1Retract       = s7PLC.readBoolean("DB28.9.1");
+            boolean cyl1Extend        = s7PLC.readBoolean("DB28.9.2");
+            boolean cyl2Retract       = s7PLC.readBoolean("DB28.9.3");
+            boolean cyl2Extend        = s7PLC.readBoolean("DB28.9.4");
+            boolean cyl3Retract       = s7PLC.readBoolean("DB28.9.5");
+            boolean cyl3Extend        = s7PLC.readBoolean("DB28.9.6");
+            boolean cyl4Retract       = s7PLC.readBoolean("DB28.9.7");
+            boolean cyl4Extend        = s7PLC.readBoolean("DB28.10.0");
+            boolean estopCabinet      = s7PLC.readBoolean("DB28.10.1");
+            boolean estopManual       = s7PLC.readBoolean("DB28.10.2");
+            boolean spindleSpeedAlarm = s7PLC.readBoolean("DB28.10.3");
+
+//            e.setServoAlarm(bs(servoAlarm));
+//            e.setInverterFault(bs(inverterFault));
+//            e.setInverterOverload(bs(inverterOverload));
+//            e.setOilMistLow(bs(oilMistLow));
+//            e.setChipConveyorTrip(bs(chipConveyorTrip));
+//            e.setHydraulicTrip(bs(hydraulicTrip));
+//            e.setWaterCoolerStop(bs(waterCoolerStop));
+//            e.setManualCameraAlarm(bs(manualCameraAlarm));
+//            e.setSawCameraAlarm(bs(sawCameraAlarm));
+//            e.setCyl1RetractFault(bs(cyl1Retract));
+//            e.setCyl1ExtendFault(bs(cyl1Extend));
+//            e.setCyl2RetractFault(bs(cyl2Retract));
+//            e.setCyl2ExtendFault(bs(cyl2Extend));
+//            e.setCyl3RetractFault(bs(cyl3Retract));
+//            e.setCyl3ExtendFault(bs(cyl3Extend));
+//            e.setCyl4RetractFault(bs(cyl4Retract));
+//            e.setCyl4ExtendFault(bs(cyl4Extend));
+//            e.setEstopCabinet(bs(estopCabinet));
+//            e.setEstopManual(bs(estopManual));
+//            e.setSpindleSpeedAlarm(bs(spindleSpeedAlarm));
+//
+            mesSawService.save(e);
+
+            // ===== 报警联动:True->待处理(0),False->已处理(1) =====
+            // 报警类型名称需与 mes_device_alarm 表中 type 一致
+            updateAlarm("伺服报警", servoAlarm);
+            updateAlarm("变频器异常(A1/C1)", inverterFault);
+            updateAlarm("变频器过负载OL", inverterOverload);
+            updateAlarm("油雾器液位低", oilMistLow);
+            updateAlarm("排屑机跳匝", chipConveyorTrip);
+            updateAlarm("液压站跳匝", hydraulicTrip);
+            updateAlarm("水冷机停止中", waterCoolerStop);
+            updateAlarm("人工位安全相机检测报警", manualCameraAlarm);
+            updateAlarm("锯切站内安全相机检测报警", sawCameraAlarm);
+            updateAlarm("转台定位气缸1缩回检测故障", cyl1Retract);
+            updateAlarm("转台定位气缸1伸出检测故障", cyl1Extend);
+            updateAlarm("转台定位气缸2缩回检测故障", cyl2Retract);
+            updateAlarm("转台定位气缸2伸出检测故障", cyl2Extend);
+            updateAlarm("转台定位气缸3缩回检测故障", cyl3Retract);
+            updateAlarm("转台定位气缸3伸出检测故障", cyl3Extend);
+            updateAlarm("转台定位气缸4缩回检测故障", cyl4Retract);
+            updateAlarm("转台定位气缸4伸出检测故障", cyl4Extend);
+            updateAlarm("急停(电柜上)", estopCabinet);
+            updateAlarm("人工位急停", estopManual);
+            updateAlarm("主轴转速未达到报警", spindleSpeedAlarm);
+
+            logger.info("锯切数据采集成功[{}] 一序节拍:{} 二序节拍:{} 锯切转速:{} 铣削转速:{}",
+                    ip, e.getSeq1Takt(), e.getSeq2Takt(), e.getSawSpeed(), e.getMillSpeed());
+        } catch (Exception ex) {
+            logger.error("锯切数据采集失败[{}]:{}", ip, ex.getMessage());
+        } finally {
+            if (s7PLC != null) {
+                try {
+                    s7PLC.close();
+                } catch (Exception ignore) {
+                }
+            }
+        }
+    }
+
+    /** boolean 转存储字符串:true->1,false->0 */
+    private String bs(boolean b) {
+        return b ? "1" : "0";
+    }
+
+    /**
+     * 报警联动:信号为 true 时置待处理(0),false 时置已处理(1)
+     */
+    private void updateAlarm(String type, boolean alarmOn) {
+        try {
+            mesDeviceAlarmService.updateStateByDeviceSnAndType(
+                    DEVICE_SN, type, alarmOn ? STATE_PENDING : STATE_HANDLED);
+        } catch (Exception e) {
+            logger.warn("更新报警[{}-{}]状态失败:{}", DEVICE_SN, type, e.getMessage());
+        }
+    }
+}

+ 7 - 0
src/main/resources/mappings/modules/mes/MesSawDao.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeesite.modules.mes.dao.MesSawDao">
+
+	<!-- 使用 JeeSite 内置 SQL 构建,无需手写 CRUD -->
+
+</mapper>

+ 123 - 0
src/main/resources/views/modules/mes/mesSawBoard.html

@@ -0,0 +1,123 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>锯切数据实时看板</title>
+	<script src="${ctxStatic}/jquery-1.11.3.min.js"></script>
+	<style>
+		* { margin: 0; padding: 0; box-sizing: border-box; }
+		html, body { width: 100%; height: 100%; }
+		body {
+			font-family: "Microsoft YaHei", sans-serif;
+			background: #04122b;
+			background-image: radial-gradient(circle at 50% 0%, #0a2350 0%, #04122b 60%);
+			color: #d6e6ff;
+			overflow: auto;
+		}
+		.bd-header {
+			height: 70px; display: flex; align-items: center; justify-content: center;
+			position: relative; border-bottom: 1px solid rgba(0,160,255,0.25);
+		}
+		.bd-title { font-size: 26px; letter-spacing: 4px; color: #5ad8ff; font-weight: bold; text-shadow: 0 0 12px rgba(0,180,255,0.6); }
+		.bd-time { position: absolute; right: 24px; font-size: 14px; color: #8fb6e6; }
+		.bd-status { position: absolute; left: 24px; font-size: 14px; }
+		.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }
+		.dot-on { background: #25e29a; box-shadow: 0 0 8px #25e29a; }
+		.dot-off { background: #ff5a6a; box-shadow: 0 0 8px #ff5a6a; }
+
+		.cards { display: flex; flex-wrap: wrap; gap: 24px; padding: 40px; justify-content: center; }
+		.card {
+			flex: 1 1 380px; max-width: 460px; min-width: 320px; height: 240px;
+			border-radius: 14px;
+			background: linear-gradient(160deg, rgba(13,45,95,0.9), rgba(8,26,56,0.9));
+			border: 1px solid rgba(0,160,255,0.35);
+			box-shadow: 0 0 24px rgba(0,100,220,0.18) inset;
+			display: flex; flex-direction: column; align-items: center; justify-content: center;
+			position: relative; overflow: hidden;
+		}
+		.card::before {
+			content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
+			background: linear-gradient(90deg, transparent, #2fa8ff, transparent);
+		}
+		.card-label { font-size: 22px; color: #9cc4f0; letter-spacing: 2px; margin-bottom: 18px; }
+		.card-value { font-size: 70px; font-weight: bold; color: #41e6c3; line-height: 1; text-shadow: 0 0 18px rgba(65,230,195,0.5); }
+		.card-unit { font-size: 22px; color: #7fd6ff; margin-left: 8px; }
+		.card-1 .card-value { color: #ffb454; text-shadow: 0 0 18px rgba(255,180,84,0.5); }
+		.card-2 .card-value { color: #41e6c3; }
+		.card-3 .card-value { color: #5ab8ff; }
+		.card-4 .card-value { color: #ff7a9c; text-shadow: 0 0 18px rgba(255,122,156,0.5); }
+		.bd-foot { text-align: center; color: #6f93c4; font-size: 13px; padding-bottom: 20px; }
+	</style>
+</head>
+<body>
+	<div class="bd-header">
+		<span class="bd-status"><i id="connDot" class="dot dot-off"></i><span id="connText">未连接</span></span>
+		<span class="bd-title">锯切数据实时监控看板</span>
+		<span class="bd-time" id="nowTime"></span>
+	</div>
+
+	<div class="cards">
+		<div class="card card-1">
+			<div class="card-label">一序节拍</div>
+			<div><span class="card-value" id="seq1Takt">--</span><span class="card-unit">秒</span></div>
+		</div>
+		<div class="card card-2">
+			<div class="card-label">二序节拍</div>
+			<div><span class="card-value" id="seq2Takt">--</span><span class="card-unit">秒</span></div>
+		</div>
+		<div class="card card-3">
+			<div class="card-label">锯切转速</div>
+			<div><span class="card-value" id="sawSpeed">--</span><span class="card-unit">r/min</span></div>
+		</div>
+		<div class="card card-4">
+			<div class="card-label">铣削转速</div>
+			<div><span class="card-value" id="millSpeed">--</span><span class="card-unit">r/min</span></div>
+		</div>
+	</div>
+
+	<div class="bd-foot">最近更新时间:<span id="dataTime">--</span> | 每 5 秒自动刷新</div>
+</body>
+</html>
+<script>
+	var CTX = "${ctx}";
+	function pad(n){ return n < 10 ? '0'+n : ''+n; }
+	function updateClock(){
+		var d = new Date();
+		document.getElementById('nowTime').textContent =
+			d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+' '+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());
+	}
+	function setConn(ok){
+		var dot = document.getElementById('connDot');
+		var txt = document.getElementById('connText');
+		if (ok) { dot.className='dot dot-on'; txt.textContent='采集正常'; }
+		else    { dot.className='dot dot-off'; txt.textContent='采集异常 / 无数据'; }
+	}
+	function setVal(id, val){
+		var el = document.getElementById(id);
+		if (el) el.textContent = (val===null||val===undefined||val==='') ? '--' : val;
+	}
+	function loadData(){
+		$.ajax({
+			url: CTX + '/mes/mesSaw/latestData',
+			type: 'POST',
+			dataType: 'json',
+			success: function(data){
+				if (!data || !data.id){ setConn(false); return; }
+				setVal('seq1Takt', data.seq1Takt);
+				setVal('seq2Takt', data.seq2Takt);
+				setVal('sawSpeed', data.sawSpeed);
+				setVal('millSpeed', data.millSpeed);
+				setVal('dataTime', data.createDate || '--');
+				setConn(true);
+			},
+			error: function(){ setConn(false); }
+		});
+	}
+	$(function(){
+		updateClock();
+		setInterval(updateClock, 1000);
+		loadData();
+		setInterval(loadData, 5000);
+	});
+</script>

+ 42 - 0
src/main/resources/views/modules/mes/mesSawForm.html

@@ -0,0 +1,42 @@
+<% layout('/layouts/default.html', {title: '锯切数据详情', libs: ['validate']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header with-border">
+			<div class="box-title">
+				<i class="fa icon-note"></i> ${text('锯切数据详情')}
+			</div>
+		</div>
+		<#form:form id="inputForm" model="${mesSaw}" action="${ctx}/mes/mesSaw/save" method="post" class="form-horizontal">
+			<div class="box-body">
+				<div class="form-unit">${text('基本信息')}</div>
+				<#form:hidden path="id"/>
+				<div class="row">
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('一序节拍(秒)')}:</label>
+						<div class="col-sm-8"><#form:input path="seq1Takt" maxlength="64" class="form-control" readonly="true"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('二序节拍(秒)')}:</label>
+						<div class="col-sm-8"><#form:input path="seq2Takt" maxlength="64" class="form-control" readonly="true"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('锯切转速(r/min)')}:</label>
+						<div class="col-sm-8"><#form:input path="sawSpeed" maxlength="64" class="form-control" readonly="true"/></div>
+					</div></div>
+					<div class="col-xs-6"><div class="form-group">
+						<label class="control-label col-sm-4">${text('铣削转速(r/min)')}:</label>
+						<div class="col-sm-8"><#form:input path="millSpeed" maxlength="64" class="form-control" readonly="true"/></div>
+					</div></div>
+				</div>
+			</div>
+			<div class="box-footer">
+				<div class="row">
+					<div class="col-sm-offset-2 col-sm-10">
+						<button type="button" class="btn btn-sm btn-default" id="btnCancel" onclick="js.closeCurrentTabPage()"><i class="fa fa-reply-all"></i> ${text('关 闭')}</button>
+					</div>
+				</div>
+			</div>
+		</#form:form>
+	</div>
+</div>
+<% } %>

+ 43 - 0
src/main/resources/views/modules/mes/mesSawList.html

@@ -0,0 +1,43 @@
+<% layout('/layouts/default.html', {title: '锯切数据管理', libs: ['dataGrid']}){ %>
+<div class="main-content">
+	<div class="box box-main">
+		<div class="box-header">
+			<div class="box-title">
+				<i class="fa icon-notebook"></i> ${text('锯切数据管理')}
+			</div>
+			<div class="box-tools pull-right">
+				<a href="#" class="btn btn-default" id="btnSearch" title="${text('查询')}"><i class="fa fa-filter"></i> ${text('查询')}</a>
+				<a href="${ctx}/mes/mesSaw/board" class="btn btn-info btnTool" data-title="${text('实时数据看板')}" title="${text('实时数据看板')}"><i class="fa fa-dashboard"></i> ${text('实时看板')}</a>
+				<a href="#" class="btn btn-default" id="btnExport"><i class="glyphicon glyphicon-export"></i> 导出</a>
+				<a href="#" class="btn btn-default" id="btnSetting" title="${text('设置')}"><i class="fa fa-navicon"></i></a>
+			</div>
+		</div>
+		<div class="box-body">
+			<#form:form id="searchForm" model="${mesSaw}" action="${ctx}/mes/mesSaw/listData" method="post" class="form-inline hide"
+					data-page-no="${parameter.pageNo}" data-page-size="${parameter.pageSize}" data-order-by="${parameter.orderBy}">
+				<div class="form-group">
+					<button type="submit" class="btn btn-primary btn-sm">${text('查询')}</button>
+					<button type="reset" class="btn btn-default btn-sm">${text('重置')}</button>
+				</div>
+			</#form:form>
+			<table id="dataGrid"></table>
+			<div id="dataGridPage"></div>
+		</div>
+	</div>
+</div>
+<% } %>
+<script>
+$('#dataGrid').dataGrid({
+	searchForm: $("#searchForm"),
+	columnModel: [
+		{header:'${text("采集时间")}', name:'createDate', index:'a.create_date', width:180, align:"center", frozen:true},
+		{header:'${text("一序节拍(秒)")}', name:'seq1Takt', index:'a.seq1_takt', width:160, align:"center"},
+		{header:'${text("二序节拍(秒)")}', name:'seq2Takt', index:'a.seq2_takt', width:160, align:"center"},
+		{header:'${text("锯切转速(r/min)")}', name:'sawSpeed', index:'a.saw_speed', width:170, align:"center"},
+		{header:'${text("铣削转速(r/min)")}', name:'millSpeed', index:'a.mill_speed', width:170, align:"center"}
+	]
+});
+$('#btnExport').click(function(){
+	js.ajaxSubmitForm($('#searchForm'), { url:'${ctx}/mes/mesSaw/exportData', downloadFile:true });
+});
+</script>