前 言:
前段时间,有客户在网上看到了我们边缘计算模块产品,找到了我们,跟我们描述了他们目前遇到的问题:
某汽车零部件制造厂在进行智能工厂的升级改造,工单派发和生产顺序指定由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时,触发后面动作。
代码语言:javascript复制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函数(定时请求任务号):
代码语言:javascript复制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;
}
Template模板(请求XML方法):用于编写SOAP接口连接主体参数 。
XML(XML与JS对象格式互转):格式转换,用于将XML与JS对象格式互相转换。
Simple-SOAP SOAP请求:用于设定连接参数,接口信息和连接SOAP接口 。
Base URL:
http://xxxxxxxx/MESService/BaseService.svc
Action: http://tempuri.org/IServiceBase/xxxxxxx
Function函数(判断是否为空):用于判断SOAP请求返回值是否为空值、故障值、正常值。
代码语言:javascript复制var str0,str1,str2
str2 = msg.payload["envelope"]["body"]["0"]
if(str2.getseqorderresponse) //判断请求返回数据是否为正常数据
{
str0 = msg.payload["envelope"]["body"]["0"]["getseqorderresponse"]["0"]["getseqorderresult"]["0"]
if(str0.length >10)
{
str1 = msg.payload["envelope"]["body"]["0"]["getseqorderresponse"]["0"]["getseqorderresult"]["0"]
msg.payload = str1
return [msg,null,null];
}
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函数(写入未生产订单表):用于将获取到的数据在写入总表的同时,写入到数据库未生产订单表内。
代码语言:javascript复制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返回的空值和故障值写入到数据内。
代码语言:javascript复制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语句,记录到数据库内。
代码语言:javascript复制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连接的请求时间,将其记录到数据库。
代码语言:javascript复制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内对应报警变量。
代码语言:javascript复制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”结尾的任务号为手动请求回来的。
代码语言:javascript复制 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月