当MES遇上PLC——SOAP篇(上)(含调试程序)
前 言:
前段时间,有客户在网上看到了我们边缘计算模块产品,找到了我们,跟我们描述了他们目前遇到的问题:
某汽车零部件制造厂在进行智能工厂的升级改造,工单派发和生产顺序指定由MES系统完成,西门子1200 PLC负责生产控制系统。但是,MES系统只能提供SOAP协议给PLC。看到这里,大多数PLC工程师可能就有点懵了,这啥玩意啊?这玩意不是咱干的活呀!
确实,这不属于工控行业的协议,这是IT界的。
那么,何为SOAP呢?简单来说,SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。或者更简单地说:SOAP 是用于访问网络服务的协议,它独立于平台、独立于语言,用于在因特网传输消息的格式。你瞅瞅,这玩意就是一种能够跨平台发送消息的东西。
了解了这个协议,我们就有办法去搞定它,将它和PLC建立连接,让MES系统的数据,流畅地传输到PLC中。此时北京伟联科技有限公司发布的边缘计算模块(WL-320E-M)产品便担负起了这个重任。
在后期的沟通了解中,按照用户的设计要求,需要先将MES系统排序好的工单数据通过SOAP协议请求回来后记录到数据库中,然后由PLC按照生产的节奏从数据库中获取工单数据。同时,需要利用边缘计算模块,将MES系统正在排序的工单实时传输给PLC,供触摸屏和上位机实时显示。
针对用户的需求,设计的网络拓扑图如下:
在MySQL数据库内创建表
设计思路:
定时请求SOAP数据流程图
PLC请求数据库工单数据程序流程图
1. 边缘计算模块内配置
在本项目中,需要用到连接PLC和连接数据库节点,该节点配置具有全局属性,在整个边缘计算模块环境内都可以调用。该节点无需单独从节点区域拖拽,双击任意s7-in或者s7-out节点,点击右边编辑按钮即可编辑该节点。
S7-endpoint节点属性内
Transport:连接到PLC的通讯协议类型,选择Ethernet(ISO-ON-TCP)即可。
Address:PLC的IP地址及通讯协议端口号
Mode:选择通讯的模式,S7-200型号的PLC选择TSAP,除此之外其余的都选择RACK/SLOT
Rack:PLC的CPU位置,可在博图或者Step7软件里面硬件组态内看到
Name:自定义名称
Variable内为连接PLC的变量地址及在边缘计算模块内使用的变量名。
需要注意的是,边缘计算模块使用的是s7协议与西门子PLC建立连接,因此,需要将DB块属性内将“优化的块访问”选项去掉。
以及,在PLC属性内,将防护与安全选项里面的 允许PUTGET访问。
Variable选项内变量可以导出为CSV文件使用Excel编辑后再导入。
PLC地址编写方式参考节点帮助内容或WL-320E-M使用手册。
数据库连接节点mysql
其中,Host为运行MySQL数据库的计算机IP地址
Port端口默认为3306
User用户名为提前设置好的MESUser
Password:为提前设置好的密码
Database:为要连接MySQL内数据库名称
其余默认。
请求触发时间控制
本项目中,对SOAP接口采用定时触发的方式控制边缘计算模块对soap接口的访问频率。默认以5分钟为周期。
5分周期设定
整个流程初始触发条件为1秒周期,但是在定时请求任务号函数内,规定,在每小时的0分30秒,5分30秒,10分30秒,15分30秒等这样的时刻下触发后面的请求动作。在程序中,使用当前分钟数除以5取余数的方法判断当前时刻是否为计划的时刻。
判断当前时间分钟数除以5取余数为0 并且 当前秒为30时,触发后面动作。
var sta = msg.payload var CurMin,CurSec if( sta == "OK" || sta == "connected") //判断数据库连接是否正常 { CurMin = Number(getCurrentDate(1)) CurSec = Number(getCurrentDate(2)) if((CurMin % 5 == 0) && (CurSec == 30) ) { msg.payload = getCurrentDate(3) return msg; } } else { //只有判断到数据库连接正常后才输出,否则无输出 } function getCurrentDate(format) //获取当前日期时间函数 { var now = new Date(); var year = now.getFullYear(); //得到年份 var month = now.getMonth();//得到月份 var date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var hour = now.getHours();//得到小时 var minu = now.getMinutes();//得到分钟 var sec = now.getSeconds();//得到秒 month = month + 1; if (month < 10) month = "0" + month; if (date < 10) date = "0" + date; if (hour < 10) hour = "0" + hour; if (minu < 10) minu = "0" + minu; if (sec < 10) sec = "0" + sec; var time = ""; //精确到天 if(format==1){ //参数为1时返回分钟数 time = minu; } //精确到分 else if(format==2){ //参数为2时返回秒数 time = sec; } else if(format==3){ //参数为3时返回完整时刻 time = year + month + date+ hour + minu + sec; } return time; }
2. 从MES SOAP接口请求数据
在边缘计算模块中,需要使用 Simple SOAP节点来实现 SOAP XML方式得数据访问,再配合其他XML/JSON/JS对象/Function等数据处理节点,共同实现客户需要得功能。
定时请求SOAP数据
此处主要实现功能有:
(1) 按照5分钟的时间周期,输出触发SOAP的连接信号,触发该连接去获取MES系统对应接口的数据。
(2) 将从MES接口获取到的数据进行分类判断,正常值、空值、连接异常值。对应写入到数据库表内作为记录。
(3) 将连接异常信号发送给对应的PLC变量。
(4) 每次请求连接SOAP之前都需要判断边缘计算模块与数据库机器的连接状态,如果正常,则继续请求,如果异常,则不发出请求。
需要使用到的节点有
Inject插入(1秒周期触发):用于产生1秒周期脉冲信号 。
Change设定消息(获取全局):用于获取当前数据库连接状态 。
Function函数(定时请求任务号):
else { msg.payload = "EmptyData" return [null,msg,null]; } } else if(str2.fault) { msg.payload = str2 return [null,null,msg]; } else //除此之外,啥都不干 { }
JSON节点(JSON):用于将返回的JSON格式转换为JS对象格式。
Function函数(写入总表):用于将从MES获取回来的数据按照数据库表结构写入到对应的数据库表中。
function函数(写入未生产订单表):用于将获取到的数据在写入总表的同时,写入到数据库未生产订单表内。
var SQLStr1,SQLStr2 //定义SQL语句 var OrderData //定义数组获取接受到的数据 var OrderNum //定义变量,记录获取到数组元素个数 var SQLValueData1,SQLValueData2,SQLValueData3 //定义SQL语句内Value个数,即插入多行数据 OrderData= msg.payload OrderNum = OrderData.length SQLValueData3 = " " SQLStr1 = "insert into Getallorder(RecordTime,taskID,SerialNo,Category,PartNo,SeqOrderNo,SeqOrderSn,AssemblyLine,CustPartNo,PublishTime,ExpectedArrivalTime,RackNo,FlexTime,OrderStatus) Values" for(i=0;i<orderdata.length;i++) ="" 循环信息,将获取到的订单组合成多行插入语句<="" span=""> { SQLValueData1 = "('" +getCurrentDate(2) + "','"+ OrderData[i].taskID + "','" + OrderData[i].SerialNo + "','" + OrderData[i].Category + "','" + OrderData[i].PartNo+ "','" + OrderData[i].SeqOrderNo + "','" SQLValueData2 = OrderData[i].SeqOrderSn + "','" + OrderData[i].AssemblyLine + "','" + OrderData[i].CustPartNo + "','" + OrderData[i].PublishTime + "','" + OrderData[i].ExpectedArrivalTime+ "','" + OrderData[i].RackNo + "','"+ OrderData[i].FlexTime + "','" + "No')" SQLValueData3 =SQLValueData3 + SQLValueData1 + SQLValueData2 + "," } SQLStr2 = SQLStr1 + SQLValueData3 SQLStr2 = SQLStr2.slice(0,SQLStr2.length-1) //去掉最后一个字符 //调用getCurrentDate(1) 会返回当前日期 格式 2020-01-01 //调用getCurrentDate(2) 会返回当前日期时间 格式 2020-01-01 01:01:01 function getCurrentDate(format) //获取当前日期时间函数 { var now = new Date(); var year = now.getFullYear(); //得到年份 var month = now.getMonth();//得到月份 var date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var hour = now.getHours();//得到小时 var minu = now.getMinutes();//得到分钟 var sec = now.getSeconds();//得到秒 month = month + 1; if (month < 10) month = "0" + month; if (date < 10) date = "0" + date; if (hour < 10) hour = "0" + hour; if (minu < 10) minu = "0" + minu; if (sec < 10) sec = "0" + sec; var time = ""; //精确到天 if(format==1){ time = year + "-" + month + "-" + date; } //精确到分 else if(format==2){ time = year + "-" + month + "-" + date+ " " + hour + ":" + minu + ":" + sec; } return time; } msg.payload = SQLStr2 return msg;
function函数(空值记录):用于编写SQL语句,记录SOAP返回的空值和故障值写入到数据内。
var SQLStr //定义SQL语句 var GetData GetData= msg.payload if(GetData == "EmptyData") { SQLStr = "update GetallOrderEvent set ResponseTime = '" + getCurrentDate(2) + "',ResponseStatus = 'ConnGood,EmptyData' where ID = (select ID from (select ID from GetSeqOrderEvent order by ID desc limit 1 ) as a )" msg.payload = SQLStr return msg; } else if(GetData.fault) { SQLStr = "update GetallOrderEvent set ResponseTime = '" + getCurrentDate(2) + "',ResponseStatus = '" +GetData.fault[0].faultcode[0]._ + "' where ID = (select ID from (select ID from GetallOrderEvent order by ID desc limit 1 ) as a )" msg.payload = SQLStr return msg; } else //除此之外,啥都不输出 { } //调用getCurrentDate(1) 会返回当前日期 格式 2020-01-01 //调用getCurrentDate(2) 会返回当前日期时间 格式 2020-01-01 01:01:01 function getCurrentDate(format) //获取当前日期时间函数 { var now = new Date(); var year = now.getFullYear(); //得到年份 var month = now.getMonth();//得到月份 var date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var hour = now.getHours();//得到小时 var minu = now.getMinutes();//得到分钟 var sec = now.getSeconds();//得到秒 month = month + 1; if (month < 10) month = "0" + month; if (date < 10) date = "0" + date; if (hour < 10) hour = "0" + hour; if (minu < 10) minu = "0" + minu; if (sec < 10) sec = "0" + sec; var time = ""; //精确到天 if(format==1){ time = year + "-" + month + "-" + date; } //精确到分 else if(format==2){ time = year + "-" + month + "-" + date+ " " + hour + ":" + minu + ":" + sec; } return time; } msg.payload = SQLStr return msg;
Delay延时节点(限制1msg/s):用于限制数据流,此处设置为1秒1条信息流通过,为了避免信息流拥挤。
Change设定节点(设定SQL语句):用于配合后面的MySQL数据库连接节点使用,设定上一节点的信息传输到下一节点的topic属性内。
Mysql节点(MySQL):用于连接MYSQL数据库,执行前面编写的SQL语句。
点击上面Databas后面的小铅笔(编辑)按钮后,设置MySQL数据库的连接参数。
以下为辅助节点,用于报警,状态获取,手动触发等功能。
Status(状态)请求状态:用于获取SOAP连接节点的状态,将其连接状态信息通过后面节点记录到数据库内。
Function函数(响应事件记录):用于将SOAP连接节点的状态信息编写为SQL语句,记录到数据库内。
var SQLStr //定义SQL语句 var objStatus = new Object() //定义对象存储节点状态 if("text" in msg.status) //如果上一节点状态对象内包含text属性,表示有故障存在 { objStatus.text = msg.status["text"] //获取故障信息 objStatus.name = msg.status["source"]["name"] SQLStr = "update GetallOrderEvent set ResponseTime = '" + getCurrentDate(2) + "',ResponseStatus = '" + objStatus.text + "',ResponseNode ='" + objStatus.name + "' where ID = (select ID from (select ID from GetallOrderEvent order by ID desc limit 1 ) as a )" msg.payload = SQLStr return [msg,null]; //第一个出口输出故障信息 } else //如果上一节点状态对象内不包含text属性,表示无故障存在,响应正常 { objStatus.text = "ConnectGood" objStatus.name = msg.status["source"]["name"] SQLStr = "update GetallOrderEvent set ResponseTime = '" + getCurrentDate(2) + "',ResponseStatus = '" + objStatus.text + "',ResponseNode ='" + objStatus.name + "' where ID = (select ID from (select ID from GetSeqOrderEvent order by ID desc limit 1 ) as a )" msg.payload = SQLStr return [null,msg]; //第二个出口输出正常响应 } //调用getCurrentDate(1) 会返回当前日期 格式 2020-01-01 //调用getCurrentDate(2) 会返回当前日期时间 格式 2020-01-01 01:01:01 function getCurrentDate(format) //获取当前日期时间函数 { var now = new Date(); var year = now.getFullYear(); //得到年份 var month = now.getMonth();//得到月份 var date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var hour = now.getHours();//得到小时 var minu = now.getMinutes();//得到分钟 var sec = now.getSeconds();//得到秒 month = month + 1; if (month < 10) month = "0" + month; if (date < 10) date = "0" + date; if (hour < 10) hour = "0" + hour; if (minu < 10) minu = "0" + minu; if (sec < 10) sec = "0" + sec; var time = ""; //精确到天 if(format==1){ time = year + "-" + month + "-" + date; } //精确到分 else if(format==2){ time = year + "-" + month + "-" + date+ " " + hour + ":" + minu + ":" + sec; } return time; }
Function函数(请求事件记录):用于记录发起SOAP连接的请求时间,将其记录到数据库。
var SQLStr //定义SQL语句 //记录请求事件时间 SQLStr = "insert into GetallOrderEvent(RequestTime) Values ('" + getCurrentDate(2) + "')" //调用getCurrentDate(1) 会返回当前日期 格式 2020-01-01 //调用getCurrentDate(2) 会返回当前日期时间 格式 2020-01-01 01:01:01 function getCurrentDate(format) //获取当前日期时间函数 { var now = new Date(); var year = now.getFullYear(); //得到年份 var month = now.getMonth();//得到月份 var date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var hour = now.getHours();//得到小时 var minu = now.getMinutes();//得到分钟 var sec = now.getSeconds();//得到秒 month = month + 1; if (month < 10) month = "0" + month; if (date < 10) date = "0" + date; if (hour < 10) hour = "0" + hour; if (minu < 10) minu = "0" + minu; if (sec < 10) sec = "0" + sec; var time = ""; //精确到天 if(format==1){ time = year + "-" + month + "-" + date; } //精确到分 else if(format==2){ time = year + "-" + month + "-" + date+ " " + hour + ":" + minu + ":" + sec; } return time; } msg.payload = SQLStr return msg;
function函数(报警变量输出):用于将报警SOAP请求响应的报警信息转换为对应的数字信号传输给PLC。
function函数(报警变量复位):用于将报警SOAP请求响应正常后,将报警信息转换为对应的复位数字信号传输给PLC。
S7-out西门子PLC写入节点(MES请求异常报警):用于连接到西门子PLC并且执行变量值写入动作。
Status状态节点(MySQL连接状态):用于获取MySQL数据库连接状态。
Change设定(设定到全局):用于将获取到的MysQL状态值设定到一个全局的变量。
Function函数(数据库连接异常输出):用于判断当前数据库状态值,如果不是状态,都认为异常,写到PLC内对应报警变量。
var sta = msg.status.text if( sta == "OK" || sta == "connected") //如果状态正常,就复位报警变量 { msg.payload = 0 //函数返回0 return msg; } else { msg.payload = 1 //函数返回1 //如果状态不正常,就触发报警变量 return msg; }
S7-out西门子PLC变量写入节点(数据库连接异常报警):用于将上一节点编写的异常报警信号写入到PLC内对应的变量上。
Function函数(检测PLC信号):用于判断PLC内变量值为1时才允许触发后面的程序(模拟一种上升沿信号)。
Change设定(获取全局):获取数据库连接异常信号,如果异常,则阻止手动请求信号继续执行。
Function函数(手动请求任务号):用于编写手动请求时,SOAP连接的任务号,规定以“999”结尾的任务号为手动请求回来的。
var sta = msg.payload if( sta == "OK" || sta == "connected") //判断数据库连接是否正常 { msg.payload = getCurrentDate(2) + "999" //后缀为999的任务号为手动请求 return msg; } function getCurrentDate(format) //获取当前日期时间函数 { var now = new Date(); var year = now.getFullYear(); //得到年份 var month = now.getMonth();//得到月份 var date = now.getDate();//得到日期 var day = now.getDay();//得到周几 var hour = now.getHours();//得到小时 var minu = now.getMinutes();//得到分钟 var sec = now.getSeconds();//得到秒 month = month + 1; if (month < 10) month = "0" + month; if (date < 10) date = "0" + date; if (hour < 10) hour = "0" + hour; if (minu < 10) minu = "0" + minu; if (sec < 10) sec = "0" + sec; var time = ""; //精确到天 if(format==1){ time = year + "-" + month + "-" + date; } //精确到分 else if(format==2){ time = year + month + date+ hour + minu + sec; } return time; }
Delay延时(延迟2秒):接受到手动请求信号2秒后,将该信号复位。
未完待续
李大拿家的王小拿
2022年8月