利用Codesys+Node.js+Mysql构建CNC数字车间工业物联网
一、概述
本项目的应用场景是CNC加工车间。
1,通过Codesys定时主动连接并且以http-post的方式上传状态信息到web服务器(Node.js+Express),而用户通过PC、手机和平板的浏览器访问web服务器获取设备状态信息。
2,Codesys在有需要时候主动连接并且以Tcp-Mysql报文的方式操作数据库(Mysql),获取生产需要的资料(G加工代码)。
3,用户经过PC、手机和平板的浏览器访问web服务器来灵活管理保存于数据库的生产资料,实现生产调度。
通过本项目展示 工业物联网项目的设备上云的基本框架,实现 设备远程数字化监控、灵活有效的生产调度这两个工业物联网的关键能力。
二、代码展示
2.1Codesys代码
2.1.1架构
2.1.2代码
本项目的Codesys程序沿用了我之前博文的《WPF上位机+Codesys的CNC》和《在Codesys利用Socket操作Mysql》,因此Servo、CNC和Mysql的代码,就不再展示。本文只展示其中的 http-post部分和Mysql的应用部分。
2.1.2.1 WebUpInfo
//作者:AlongWU
TYPE WebUpInfo :
STRUCT
STR_EquipId :STRING(30); //设备ID
R_V_X :REAL; //X位置
R_V_Y :REAL; //Y位置
R_V_Z :REAL; //Z位置
I_LineNo :DINT; //G代码的当前执行行号
W_V_State :WORD; //程序状态
END_STRUCT
END_TYPE
2.1.2.2 Http_PostPacket_FB
//作者:AlongWU
FUNCTION_BLOCK Http_PostPacket_FB
VAR CONSTANT
abyEmpty :ARRAY OF BYTE; //复位数组
END_VAR
VAR_INPUT
STR_Url :STRING(30); //post报文的url字段
STR_Host :STRING(30); //post报文的Host字段
Data :ARRAYOF BYTE; //post报文 数据字节数组
DataLen :INT; //post报文 数据字节数组的字节数
END_VAR
VAR_OUTPUT
Packet :ARRAY OF BYTE; //输出post报文
PacketSize :UINT; //post报文的字节数
END_VAR
VAR
HeaderLen :INT; //post报文header的字节数
STR_HttpHeader :STRING(200); //post报文Header
END_VAR
//构建报文header,$0a 是换行符 $0d 是回车符
STR_HttpHeader:=CONCAT('POST ',STR_Url);
STR_HttpHeader:=CONCAT(STR_HttpHeader,' HTTP/1.1$0d$0aAccept: */*$0d$0aAccept-Language: zh-CN$0d$0ahost:');
STR_HttpHeader:=CONCAT(STR_HttpHeader,STR_Host);
STR_HttpHeader:=CONCAT(STR_HttpHeader,'$0d$0aContent-TYPE: application/x-www-form-urlencoded$0d$0aConnection: close$0d$0aContent-Length: ');
STR_HttpHeader:=CONCAT(STR_HttpHeader,INT_TO_STRING(DataLen));
STR_HttpHeader:=CONCAT(STR_HttpHeader,'$0d$0a$0d$0a');
//Header的字节数
HeaderLen :=LEN(STR_HttpHeader);
//复位字节数组
Packet :=abyEmpty;
//header复制到 Packet
MEM.MemMove(pSource:= ADR(STR_HttpHeader), pDestination:= ADR(Packet), uiNumberOfBytes:=INT_TO_UINT(HeaderLen));
//data复制到 Packet
MEM.MemMove(pSource:= ADR(Data), pDestination:= ADR(Packet)+INT_TO_UINT(HeaderLen), uiNumberOfBytes:=INT_TO_UINT(DataLen));
//packet的总字节数
PacketSize :=INT_TO_UINT(HeaderLen + DataLen);
2.1.2.3 TcpUpInfoClient
//作者:AlongWU
PROGRAM TcpUpInfoClient
VAR CONSTANT
abyEmpty :ARRAY OF BYTE; //复位数组
END_VAR
VAR
TCP_ClientFB :NBS.TCP_Client; //TCP客户端连接实例
TCP_ReadFB :NBS.TCP_Read; //TCP接收实例
TCP_WriteFB :NBS.TCP_Write; //TCP发送实例
ServerIp :NBS.IP_ADDR:=(sAddr:='192.168.0.132'); //Tcp服务器地址
ServerPort :UINT:=8100; //Tcp服务器port
Enable_F_TRIG :F_TRIG; //通信使能下降沿
Enable_R_TRIG :R_TRIG; //通信使能上升沿
Recei_R_TRIG :R_TRIG; //接收上升沿
Close_R_TRIG :R_TRIG; //关闭连接上升沿
Send_R_TRIG :R_TRIG; //发送上升沿
WaitCount :UINT:=0; //空闲计时
TryConnect :UINT; //尝试连接次数
STR_Url :STRING(30); //post报文的url字段
STR_Host :STRING(30); //post报文的host字段
ConnectActive_R_TRIG :R_TRIG; //TCP连接成功上升沿
W_state :WORD; //程序状态值
UpInfo :WebUpInfo; //web上传数据的结构体
Datalen :INT; //post报文data的字节数
PostPacketFB :Http_PostPacket_FB; //构建post报文功能块的实例
UpInfoFB :WebUpInfoData_FB; //构建post报文的data的功能块的实例
UpInfoFB_Size :INT; //构建post报文的data的功能块的字节数
UpInfoFB_Data :ARRAY OF BYTE; //构建post报文的data的功能块的输出字节数组
END_VAR
Enable_F_TRIG(CLK:=GVL.B_TcpUpInfo_Enable, Q=> ); //使能下降沿
Enable_R_TRIG(CLK:=GVL.B_TcpUpInfo_Enable, Q=> ); //使能上升沿
Recei_R_TRIG(CLK:=TCP_ReadFB.xReady, Q=> ); //接收上升沿
Send_R_TRIG(CLK:=(GVL.B_TcpUpInfo_Send), Q=> ); //发送上升沿
Close_R_TRIG(CLK:=GVL.B_TcpUpInfo_Close, Q=> ); //关闭信号上升沿
IF Enable_F_TRIG.Q THEN
//使能下降沿,复位状态
GVL.B_TcpUpInfo_Send :=FALSE;
GVL.TcpUpInfo_abyRx :=abyEmpty;
END_IF
IF Enable_R_TRIG.Q THEN
//使能上升沿,复位状态
GVL.B_TcpUpInfo_OK :=FALSE;
GVL.B_TcpUpInfo_FF :=FALSE;
TryConnect:=0;
WaitCount :=0;
END_IF
IF Send_R_TRIG.Q THEN
//发送上升沿,复位状态
GVL.B_TcpUpInfo_OK :=FALSE;
GVL.B_TcpUpInfo_FF :=FALSE;
WaitCount :=0;
(*
Http报文 例子
POST /PlcUpInfoHTTP/1.1
Accept: */*
Accept-Language: zh-CN
host: localhost:8100
Content-Type: application/x-www-form-urlencoded
Content-Length: 54
Connection:close
EquipId=E00001&V_X=10&V_Y=20&V_Z=30&LineNo=1&V_State=1
*)
//结构体赋值
UpInfo.STR_EquipId := GVL.STR_EquipID;
UpInfo.R_V_X :=LREAL_TO_REAL(GVL.X_Pos);
UpInfo.R_V_Y :=LREAL_TO_REAL(GVL.Y_Pos);
UpInfo.R_V_Z :=LREAL_TO_REAL(GVL.Z_Pos);
UpInfo.I_LineNo :=DINT_TO_INT(GVL.D_LineNo);
W_state.0 :=GVL.Interpolator.bBusy;
W_state.1 :=GVL.Interpolator.bDone;
UpInfo.W_V_State :=W_state;
//打包POST报文的 数据区
UpInfoFB(UpInfo:=UpInfo , Data=>UpInfoFB_Data , DataSize=>UpInfoFB_Size );
STR_Url:='/PlcUpInfo';
STR_Host:=CONCAT(ServerIp.sAddr,':');
STR_Host:=CONCAT(STR_Host,UINT_TO_STRING(ServerPort));
//打包POST报文
PostPacketFB(
STR_Url:= STR_Url,
STR_Host:= STR_Host,
Data:= UpInfoFB_Data,
DataLen:= UpInfoFB_Size,
Packet=> GVL.TcpUpInfo_abyTx,
PacketSize=> GVL.TcpUpInfo_WriteSize);
END_IF
//TCP客户端连接功能块
TCP_ClientFB(
xEnable:= GVL.B_TcpUpInfo_Enable,
xDone=> ,
xBusy=> ,
xError=> ,
udiTimeOut:= 0,
ipAddr:= ServerIp,
uiPort:= ServerPort,
eError=> ,
xActive=> ,
hConnection=> );
//TCP客户端接收功能块
TCP_ReadFB(
xEnable:= TCP_ClientFB.xActive,
xDone=> ,
xBusy=> ,
xError=> ,
hConnection:= TCP_ClientFB.hConnection,
szSize:= 1000,
pData:= ADR(GVL.TcpUpInfo_abyRx),
eError=> ,
xReady=> ,
szCount=> );
//TCP客户端发送功能块
TCP_WriteFB(
xExecute:= GVL.B_TcpUpInfo_Send,
udiTimeOut:= 100,
xDone=> ,
xBusy=> ,
xError=> ,
hConnection:= TCP_ClientFB.hConnection,
szSize:= GVL.TcpUpInfo_WriteSize,
pData:= ADR(GVL.TcpUpInfo_abyTx),
eError=> );
//TCP连接成功上升沿
ConnectActive_R_TRIG(CLK:= TCP_ClientFB.xActive, Q=> );
IF GVL.B_TcpUpInfo_Enable THEN
IF ConnectActive_R_TRIG.Q THEN
//TCP上传动作使能后,成功建立连接后,启动发送动作
GVL.B_TcpUpInfo_Send := TRUE;
END_IF
END_IF
//发送完成
IF TCP_WriteFB.xDone THEN
GVL.B_TcpUpInfo_Send :=FALSE;
END_IF
//发送后,web服务返回结果,断开连接
IF GVL.B_TcpUpInfo_Enable THEN
WaitCount := WaitCount +1;
IF (WaitCount >5 AND GVL.B_TcpUpInfo_Close =FALSE) OR TCP_ReadFB.xReady THEN
GVL.B_TcpUpInfo_Close := TRUE;
END_IF
END_IF
IF Close_R_TRIG.Q THEN
//关闭信号上升沿,复位状态
GVL.B_TcpUpInfo_Close := FALSE;
GVL.B_TcpUpInfo_Enable:=FALSE;
GVL.B_TcpUpInfo_Send :=FALSE;
END_IF
2.1.2.4 WebUpInfoData_FB
//作者:AlongWU
FUNCTION_BLOCK WebUpInfoData_FB
VAR_INPUT
UpInfo :WebUpInfo; //输入上传数据结构体
END_VAR
VAR_OUTPUT
Data :ARRAYOF BYTE; //输出结果字节数组
DataSize :INT; //输出结果字节数组的字节数
END_VAR
VAR
STR_Data :STRING(255);
END_VAR
//构建post报文的data
STR_Data:=CONCAT('EquipId=',UpInfo.STR_EquipId);
STR_Data:=CONCAT(STR_Data,'&V_X=');
STR_Data:=CONCAT(STR_Data,Real2Str(UpInfo.R_V_X,3));
STR_Data:=CONCAT(STR_Data,'&V_Y=');
STR_Data:=CONCAT(STR_Data,Real2Str(UpInfo.R_V_Y,3));
STR_Data:=CONCAT(STR_Data,'&V_Z=');
STR_Data:=CONCAT(STR_Data,Real2Str(UpInfo.R_V_Z,3));
STR_Data:=CONCAT(STR_Data,'&LineNo=');
STR_Data:=CONCAT(STR_Data,DINT_TO_STRING(UpInfo.I_LineNo));
STR_Data:=CONCAT(STR_Data,'&V_State=');
STR_Data:=CONCAT(STR_Data,WORD_TO_STRING(UpInfo.W_V_State));
//data的字节数量
DataSize :=LEN(STR_Data);
//字符串转字节数组
MEM.MemMove(pSource:= ADR(STR_Data), pDestination:= ADR(Data), uiNumberOfBytes:=INT_TO_UINT(DataSize));
2.1.2.5 MysqlGetNewGcode
//作者:AlongWU
PROGRAM MysqlGetNewGcode
VAR CONSTANT
EmptyResult :ARRAY OF BYTE; //复位字节数组
END_VAR
VAR
QueryStr :STRING(100); //查询命令字符串
QueryBytes :ARRAY OF BYTE; //查询命令buffer
MysqlCmdPack :MysqlQueryCmdPack_FB; //Mysql指令实例
MysqlFirstRowInfo :MysqlRowInfo; //Mysql首行数据结构体
MysqlFirstRowDecode :MysqlFirstRowData_FB; //Mysql解析回复表格信息并提取首行数据
GcodeBytes :ARRAY OF BYTE; //查询命令buffer
tmpUint :UINT;
tmpAddr :UINT;
TxtWrite :TxtWrite_FB; //txt写入功能块实例
TxtTruncate :TxtTruncate_FB; //txt清空功能块实例
GcodeSize :UINT; //NextGcode字节数
END_VAR
CASE GVL.I_GetNextCodeStep OF
0:
QueryBytes := EmptyResult; //指令buffer复位
IF GVL.B_Mysql_Inited THEN
GVL.I_GetNextCodeStep := 10; //已连接数据库
ELSE
GVL.I_GetNextCodeStep := GVL.I_GetNextCodeStep +1; //未连接数据库,下一步
END_IF
1:
GVL.B_Mysql_Enable :=TRUE;
GVL.I_GetNextCodeStep := GVL.I_GetNextCodeStep +1; //下一步
2:
IF GVL.B_Mysql_Inited THEN
GVL.I_GetNextCodeStep := 10; //已连接数据库
END_IF
IF GVL.B_Mysql_AuthFalure THEN //连接失败,推出登录
GVL.I_GetNextCodeStep := 99;
END_IF
10:
GVL.B_Mysql_Result :=FALSE; //复位接收解析标志
// '要用$27来转义,特殊的符号,用$+16进制的acsii值
QueryStr :='SELECT `NextGcode` FROM `equiptable` WHERE `STATE`=1 AND `EquipId`=$27';
QueryStr := CONCAT(QueryStr,GVL.STR_EquipID);
QueryStr := CONCAT(QueryStr,'$27 LIMIT 1');
MEM.MemMove(pSource:= ADR(QueryStr), pDestination:= ADR(QueryBytes), uiNumberOfBytes:=INT_TO_UINT(LEN(QueryStr)));
//构建cmd
MysqlCmdPack(Cmd:=3 , InputBytes:= QueryBytes, Result=> GVL.Mysql_abyTx, ResultSize=>GVL.Mysql_WriteSize );
GVL.B_Mysql_Send :=TRUE;
GVL.I_GetNextCodeStep := GVL.I_GetNextCodeStep +1; //下一步
11:
IF GVL.B_Mysql_FF OR GVL.B_Mysql_FE THEN
GVL.I_GetNextCodeStep:=99; //数据库错误,退出程序
RETURN;
END_IF
IF GVL.B_Mysql_Result THEN
MysqlFirstRowDecode(InputRxBytes:=GVL.Mysql_abyRx , FirstRowInfo=>MysqlFirstRowInfo ); //解析row数据
IF MysqlFirstRowInfo.IsNull THEN
//回复为空,即该EquipID没有数据
//NOTHING
GVL.I_GetNextCodeStep:=99;
ELSE
//回复为真,该EquipID有G代码资料
TxtTruncate(); //重置cnc文件
GVL.I_GetNextCodeStep:=GVL.I_GetNextCodeStep +1;
END_IF
END_IF
12:
//读取mysql回复数据
GcodeSize :=MysqlFirstRowInfo.RowData;
MEM.MemMove(pSource:= ADR(MysqlFirstRowInfo.RowData)+1, pDestination:= ADR(GcodeBytes), uiNumberOfBytes:=GcodeSize);
//写入并更新txt文件
TxtWrite(Buffer:=ADR(GcodeBytes), BufferLen:= GcodeSize);
GVL.I_GetNextCodeStep:=GVL.I_GetNextCodeStep +1;
13:
// 更新ActiveGcode
QueryStr :='UPDATE `equiptable` SET `ActiveGcode`=$27';
tmpUint :=INT_TO_UINT(LEN(QueryStr));
MEM.MemMove(pSource:= ADR(QueryStr), pDestination:= ADR(QueryBytes), uiNumberOfBytes:=tmpUint);
tmpAddr := tmpUint;
//Gcode复制到命令中
MEM.MemMove(pSource:= ADR(GcodeBytes), pDestination:= ADR(QueryBytes)+tmpAddr, uiNumberOfBytes:=GcodeSize);
tmpAddr := tmpAddr + GcodeSize;
QueryStr :='$27 WHERE `EquipId`=$27';
QueryStr := CONCAT(QueryStr,GVL.STR_EquipID);
QueryStr := CONCAT(QueryStr,'$27');
MEM.MemMove(pSource:= ADR(QueryStr), pDestination:= ADR(QueryBytes)+tmpAddr, uiNumberOfBytes:=INT_TO_UINT(LEN(QueryStr)));
//构建cmd
MysqlCmdPack(Cmd:=3 , InputBytes:= QueryBytes, Result=> GVL.Mysql_abyTx, ResultSize=>GVL.Mysql_WriteSize );
GVL.B_Mysql_Send :=TRUE;
GVL.I_GetNextCodeStep := GVL.I_GetNextCodeStep +1; //下一步
14:
IF GVL.B_Mysql_FF OR GVL.B_Mysql_FE ORGVL.B_Mysql_OK THEN
GVL.I_GetNextCodeStep:=99;
END_IF
99:
GVL.I_GetNextCodeStep:=100;
ELSE
GVL.B_GetNextCode:=FALSE;
GVL.B_GetNextCodeTask:=FALSE;
END_CASE
//mysql错误处理
IF GVL.B_Mysql_AuthFalure THEN
GVL.B_GetNextCode := FALSE;
GVL.B_GetNextCodeTask:=FALSE;
END_IF
2.1.2.6 PLC_PRG
//作者:AlongWu
PROGRAM PLC_PRG
VAR
ReadFileTask_F_TRIG :F_TRIG; //读取cnc文件标志下降沿
Cnc_Start_R_TRIG :R_TRIG; //cnc执行标志上升沿
B_MysqlGetGCode_R_TRIG :R_TRIG; //Mysql更新G代码上升沿
UpBlink :INT:=0; //http post的延时计数
END_VAR
//cnc执行标志上升沿
Cnc_Start_R_TRIG(CLK:= GVL.B_Cnc_Start, Q=> );
IF Cnc_Start_R_TRIG.Q THEN
//读取cnc文件标志和读取文件Task的触发标志,置TRUE。
GVL.B_ReadFile:=TRUE;
GVL.B_ReadFileTask:=TRUE;
END_IF
//读取txt文件标志下降沿
ReadFileTask_F_TRIG(CLK:= GVL.B_ReadFileTask, Q=> );
IF ReadFileTask_F_TRIG.Q THEN
//读取cnc并解析后,启动插补器
GVL.B_Cnc_Ipo:= TRUE;
GVL.B_Cnc_Start :=FALSE;
END_IF
//通过Mysql获取 NextGCode
B_MysqlGetGCode_R_TRIG(CLK:=GVL.B_GetNextCode , Q=> );
IF B_MysqlGetGCode_R_TRIG.Q THEN
//启动Mysql获取NextGCode
GVL.I_GetNextCodeStep := 0;
GVL.B_GetNextCodeTask :=TRUE;
GVL.B_Mysql_AuthFalure :=FALSE;
END_IF
(*http post 上传数据*)
IF UpBlink < 2 THEN
UpBlink :=UpBlink+1;
ELSE
//每2个周期上传一次
GVL.B_TcpUpInfo_Enable:= TRUE;
UpBlink:=0;
END_IF
2.2服务器端Node.js代码
//------------const end------------
const express = require('express');
const mysql = require('mysql');
const bodyParser = require('body-parser');
// 创建web服务器
const app = express();
//mysql connection
const connection = mysql.createConnection({
host : 'localhost',
user : 'testNode',
password : 'testNode',
database : 'testcnc'
});
//------------const end--------------
//------------class start------------
class PLC {
constructor(EquipId, Name,TouchCount,V_X,V_Y,V_Z,V_LineNo,V_State,ActGcode,NextGcode,sqlState) {
this.EquipId = EquipId
this.Name = Name
this.V_X = V_X
this.V_Y = V_Y
this.V_Z = V_Z
this.V_LineNo = V_LineNo
this.V_State = V_State
this.TouchCount = TouchCount
this.ActGcode = ActGcode
this.NextGcode = NextGcode
this.sqlState = sqlState
}
}
//------------class end--------------
//------------object start------------
//plc变量值 Json对象
var tmpPlcVars =
{
EquipId:'',
Name:'',
X:0,
Y:0,
Z:0,
LineNo:0,
TouchCount:0,
State:0
};
var tmpGcodeVars =
{
EquipId:'',
ActGcode:'',
NextGcode:'',
res:0
};
//------------object end------------
//------------var start-------------
//PLC的class数组
var PLC_ArrList = new Array();
// 创建 application/x-www-form-urlencoded 编码解析
var urlencodedParser = bodyParser.urlencoded({ extended: false })
//------------var end---------------
//------------ function start-------
//查询mysql设备信息表
functionQueryEquipList()
{
connection.query('SELECT * FROM equiptable WHERE STATE=1', function (error, results) {
if(error){
console.log(' - ',error.message);
return;
}
let rowResult;
let tmpPlc;
let n,h,i,j;
let TmpPlcList;
h = results.length;
n = PLC_ArrList.length;
/*
1,查询mysql数据库,获取当前能用的equiplist。
2,查询结果跟当前的PLC_ArrList进行,数据更新并且 mysql数据库标注为可用的,更新PLC_ArrList项的sqlState;如果有新增,push添加。
3,将PLC_ArrList项的sqlState为0 的剔除,并重新建立PLC_ArrList。
*/
if(h > 0)
{
//全部plc的sqlstate置0
for(i=0;i < n;i++ )
{
PLC_ArrList.sqlState = 0;
}
//如果PLC_ArrList的项在mysql仍然有记录,则sqlstate置1
for ( i=0; i<h; i++)
{
rowResult = results;
for( j=0;j < n;j++ )
{
if(rowResult['EquipId'] == PLC_ArrList.EquipId)
{
//找到PLC_ArrList的位置,更新ActiveGcode和NextGcode。
PLC_ArrList.ActiveGcode = rowResult['ActiveGcode'];
PLC_ArrList.NextGcode = rowResult['NextGcode'];
PLC_ArrList.sqlState = 1;
break;
}
}
//PLC_ArrList没有记录,push新增
if(j == n)
{
tmpPlc = new PLC(rowResult['EquipId'],rowResult['Name'],0,0,0,0,0,0,rowResult['ActiveGcode'],rowResult['NextGcode'],1);
PLC_ArrList.push(tmpPlc);
}
}
//更新 PLC_ArrList的数量
n = PLC_ArrList.length;
TmpPlcList = new Array();
//把 PLC_ArrList数组 复制到 TmpPlcList
for (i=0; i<n; i++)
{
TmpPlcList.push(PLC_ArrList.pop());
}
//PLC_ArrList复位
PLC_ArrList = new Array();
for (i=0; i<n; i++)
{
tmpPlc = TmpPlcList.pop();
if(tmpPlc.sqlState == 1)
{
//如果 sqlState=1,重新压回 PLC_ArrList
PLC_ArrList.push(tmpPlc);
}
}
}
});
}
//更新mysql设备的G代码数据项
function MysqlUpdateGCode(equipid,newcode)
{
connection.query("UPDATE equiptable SET NextGcode='"+newcode+"' WHERE EquipId='"+equipid+"'", function (error, results) {
if(error){
console.log(' - ',error.message);
return;
}
});
}
//构建express(web服务器)
function InitExpress()
{
// 启动服务器
app.listen(8100, () => {
console.log('express server running at http://127.0.0.1')
});
//use方法,静态路由
app.use( express.static('page'));
//State指令的处理
app.get('/State', (req, res) => {
let id = req.query.EquipId;
n = PLC_ArrList.length;
if( n >0)
{
for(i=0;i<n;i++)
{
if(PLC_ArrList.EquipId == id)
{
break;
}
}
if(n == i)
{
//设备未登记
tmpPlcVars.EquipId ='';
tmpPlcVars.Name ='';
tmpPlcVars.X = 0;
tmpPlcVars.Y = 0;
tmpPlcVars.Z = 0;
tmpPlcVars.LineNo = 0;
tmpPlcVars.State = 0;
tmpPlcVars.TouchCount = 0;
}
else
{
//设备已登记
//更新Plc数组的信息
tmpPlcVars.EquipId= PLC_ArrList.EquipId;
tmpPlcVars.Name= PLC_ArrList.Name;
tmpPlcVars.X =PLC_ArrList.V_X;
tmpPlcVars.Y =PLC_ArrList.V_Y;
tmpPlcVars.Z =PLC_ArrList.V_Z;
tmpPlcVars.LineNo = PLC_ArrList.LineNo;
tmpPlcVars.State = PLC_ArrList.V_State;
tmpPlcVars.TouchCount = PLC_ArrList.TouchCount;
}
}
//回复用户浏览器
res.send(JSON.stringify(tmpPlcVars));
});
//Fresh G Code指令的处理
app.get('/FreshGode', urlencodedParser, function (req, res) {
let id = req.query.EquipId;
let i,n;
n = PLC_ArrList.length;
for(i=0;i<n;i++)
{
if(PLC_ArrList.EquipId == id)
{
break;
}
}
if(i<n)
{
//如果所查找的EquipId在PLC_ArrList数组内,则赋值tmpGcodeVars。
tmpGcodeVars.EquipId = id;
tmpGcodeVars.ActGcode = PLC_ArrList.ActGcode;
tmpGcodeVars.NextGcode = PLC_ArrList.NextGcode;
tmpGcodeVars.res = 1;
}
else
{
//如果所查找的EquipId不在PLC_ArrList数组内,则回复查询失败。
tmpGcodeVars.EquipId = "";
tmpGcodeVars.ActGcode = "";
tmpGcodeVars.NextGcode ="";
tmpGcodeVars.res = 0;
}
//回复用户浏览器
res.send(JSON.stringify(tmpGcodeVars));
})
//Plc上传数据 PlcUpInfo指令的处理
app.post('/PlcUpInfo', urlencodedParser, function (req, res) {
let id = req.body.EquipId;
let i,n;
n = PLC_ArrList.length;
if( n >0)
{
for(i=0;i<n;i++)
{
if(PLC_ArrList.EquipId == id)
{
break;
}
}
if(n == i)
{
//设备未登记
//nothing
console.log('no equip');
}
else
{
//设备已登记
//更新Plc数组的信息
PLC_ArrList.V_X = req.body.V_X;
PLC_ArrList.V_Y = req.body.V_Y;
PLC_ArrList.V_Z = req.body.V_Z;
PLC_ArrList.LineNo = req.body.LineNo;
PLC_ArrList.V_State = req.body.V_State;
PLC_ArrList.TouchCount = PLC_ArrList.TouchCount + 1;
if(PLC_ArrList.TouchCount > 10000)
{
PLC_ArrList.TouchCount =0;
}
}
}
res.end();//结束进程
})
//web导入新Gcode
app.post('/Newgcode', urlencodedParser, function (req, res) {
let id = req.body.EquipId;
let i,n;
n = PLC_ArrList.length;
if( n >0)
{
for(i=0;i<n;i++)
{
if(PLC_ArrList.EquipId == id)
{
break;
}
}
if(n == i)
{
//设备未登记
res.send('EquipId:'+id+" 不可用。");
}
else
{
//设备已登记
MysqlUpdateGCode(id,req.body.gc);
res.send('新G代码已更新');
}
}
})
}
//定时循环任务函数
function CircleTask1()
{
QueryEquipList(); //查询mysql,获取最新equiplist
}
//------------ function end----------
//------------Main function start----
function MainFunction (){
InitExpress(); //建立Web服务器 express
console.log('已建立Express');
//建立mysql长连接
connection.connect(function(err,data){
if(err)
{
throw err
return; //mysql不可用,退出程序
}else
{
//连接成功
console.log('已连接mysql');
}});
QueryEquipList(); //第一次查询mysql,获取最新equiplist
setInterval(CircleTask1,2000); //启动定时任务1,周期2s
console.log('已启动定时循环任务');
};
MainFunction(); //启动MainFunction
//------------Main function end----
2.3客户端Html+原生js
<html>
<head>
<style type="text/css">
.mbutton {
height: 45px;
width: 100px;
}
.mtable{
border:1px;
border-style: solid;
width: 850px;
height: 350px;
border-collapse: collapse;
}
.mtextarea{
width: 250px;
height: 400px;
overflow-y: auto;
}
.codeSp
{
background-color: lightgray;
}
.mtr{
height: 50px;
}
</style>
<script type="text/javascript">
//------------var start------------
var GcodeTxt ;
var TmpActGcode='';
var T_CircleQuery,T_ReplyCheck;
var LostConnect =0;
var EquipId = 'E00001';
var TouchCount = 0,LastTouchCount = 0;
var SendLock=0;
var PlcOnline=0;
var codeArr;
//delay函数
const delay = ms => new Promise((resolve, reject) => setTimeout(resolve, ms))
//------------var end------------
//主函数
function MainFuncion()
{
console.log("启动循环查询");
var t=0;
//构建定时循环任务
var CircleAsyncTask = async() =>{
while(LostConnect == 0)
{
if(t<10)
{
//每0.2s查询一次状态
QueryFunc();
t++;
}
else
{
//每2s查询一次g代码
GetGodeFunc();
t=0;
if(LastTouchCount != TouchCount)
{
document.getElementById("onLine").textContent ='在线中';
PlcOnline=1;
}
else
{
document.getElementById("onLine").textContent ='未连接';
PlcOnline=0;
}
//更新TouchCount
LastTouchCount = TouchCount;
}
//异步循环
await delay(200)
}
console.log("结束循环查询");
};
//启动定时循环任务
CircleAsyncTask();
}
//G代码上传函数
function TxtUpload(input) {
//支持chrome IE10
if (window.FileReader) {
var reader = new FileReader();
var file = input.files;
//构建reader
reader.onload = function() {
GcodeTxt = this.result;
//把Txt文件的G代码发送到服务器
SendGodeFunc();
}
reader.readAsText(file);
}
}
//发送G代码至服务器的函数
function SendGodeFunc()
{
let xmlhttp;
let postCode;
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
//复位Ajax响应计时
clearTimeout(T_ReplyCheck);
SendLock=0;
}
}
xmlhttp.open("post","/Newgcode",true);
xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
postCode ="EquipId="+EquipId+"&gc="+GcodeTxt;
while(SendLock==1)
{
//wait
}
xmlhttp.send(postCode);
SendLock=1;
//启动Ajax响应计时
T_ReplyCheck =setTimeout("AjaxRelyFalure()", 100);
}
//查询当前设备状态的函数
function QueryFunc()
{
let xmlhttp;
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
//复位Ajax响应计时
clearTimeout(T_ReplyCheck);
SendLock =0;
let PlcObj = JSON.parse(xmlhttp.responseText);
document.getElementById("Plc_X").textContent = PlcObj.X;
document.getElementById("Plc_Y").textContent = PlcObj.Y;
document.getElementById("Plc_Z").textContent = PlcObj.Z;
if(parseInt(PlcObj.State) == 0)
{
document.getElementById("Plc_State").textContent = "待机";
}
else
{
document.getElementById("Plc_State").textContent = "加工中";
}
TouchCount = parseInt(PlcObj.TouchCount);
let lineNo = parseInt(PlcObj.LineNo);
if(lineNo ==-1 || PlcOnline == 0)
{
document.getElementById("ActGcode").innerHTML =TmpActGcode;
}
else
{
let n = codeArr.length;
let i;
let code='';
if(n>0)
{
for(i=0;i<n;i++)
{
if(i == lineNo)
{
code = code + "<span class='codeSp'>"+codeArr+'</span><br>';
}
else
{
code = code + codeArr+'<br>';
}
}
document.getElementById("ActGcode").innerHTML =code;
}
}
}
}
xmlhttp.open("get","/State?EquipId="+EquipId,true);
xmlhttp.setRequestHeader("Content-Type", "text/html");
while(SendLock==1)
{
//wait
}
xmlhttp.send();
SendLock = 1;
//启动Ajax响应计时
T_ReplyCheck =setTimeout("AjaxRelyFalure()", 100);
}
//查询当前设备G代码的函数
function GetGodeFunc()
{
let xmlhttp;
if (window.XMLHttpRequest)
{// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else
{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
//复位Ajax响应计时
clearTimeout(T_ReplyCheck);
SendLock = 0;
let GcodeObj = JSON.parse(xmlhttp.responseText);
if(GcodeObj.res == 1)
{
codeArr = (GcodeObj.ActGcode).split('\r\n');
TmpActGcode = (GcodeObj.ActGcode).replace(/\r\n/g,'<br>');
document.getElementById("NextGcode").innerHTML = (GcodeObj.NextGcode).replace(/\r\n/g,'<br>');
}
else
{
document.getElementById("ActGcode").innerHTML ='';
document.getElementById("NextGcode").innerHTML= '';
}
}
}
xmlhttp.open("get","/FreshGode?EquipId="+EquipId,true);
xmlhttp.setRequestHeader("Content-Type", "text/html");
while(SendLock==1)
{
//wait
}
xmlhttp.send();
SendLock = 1;
//启动Ajax响应计时
T_ReplyCheck =setTimeout("AjaxRelyFalure()", 100);
}
//ajax接收超时报警函数
function AjaxRelyFalure()
{
SendLock=0;
LostConnect = 1 ;
alert("服务器连接失败,请刷新页面");
}
//页面加载完成后,启动主函数
window.onload = function(){
//页面加载即执行函数
MainFuncion();
}
</script>
</head>
<body>
<h2>NodeJs+Codesys</h2>
<table class="mtable" cellspacing="0">
<tr>
<td width="250">
<table cellspacing="0">
<tr>
<td><h3>设备执行G代码</h3></td>
</tr>
<tr>
<td><div id="ActGcode" class="mtextarea"></div></td>
</tr>
</table>
</td>
<td width="250">
<table cellspacing="0">
<tr>
<td><h3>设备下一个G代码</h3></td>
</tr>
<tr>
<td><div id="NextGcode" class="mtextarea"></div></td>
</tr>
</table>
</td>
<td width="350">
<table>
<tr class="mtr">
<td width="90px">
<h3>X</h3>
</td>
<td>
<h2 id="Plc_X"></h2>
</td>
</tr>
<tr class="mtr">
<td >
<h3>Y</h3>
</td>
<td>
<h2 id="Plc_Y"></h2>
</td>
</tr>
<tr class="mtr">
<td >
<h3>Z</h3>
</td>
<td>
<h2 id="Plc_Z"></h2>
</td>
</tr>
<tr class="mtr">
<td >
设备状态
</td>
<td>
<span id="Plc_State"></span>
</td>
</tr>
<tr class="mtr">
<td >
在线状态
</td>
<td>
<span id="onLine"></span>
</td>
</tr>
<tr class="mtr">
<td valign="middle">
导入
</td>
<td >
<input type="file" />
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
三、总结
本项目通过CNC的G代码的联网数据库调用和管理,展示了Codesys+Node.js+Mysql的工业物联网应用的基本构架和实现。项目实现的功能虽然比较简单,但基本体现了工业物联网的特性和应用。
Codesys通过http报文与web系统交互,意味着web服务器软件不限于Node.js+express,还可以PHP+apache 、 Asp.net+iis 等等全部web服务器系统。前端不限于原生js,还可以Vue、React等等的全部SPA的框架。
Codesys通过Tcp报文与数据库系统交互,意味着数据库不限于Mysql,还可以在codesys开发相应数据库的操作子程序来使用Oracle、SQLServer等等。
基于Codesys的socket能力,可以十分灵活和便捷地对接服务器软件(Web, ERP,MES等等),实现设备的联网,进而实现设备数字化管理和使用。
页:
[1]