tt99166 发表于 2023-4-20 13:09:06

利用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]
查看完整版本: 利用Codesys+Node.js+Mysql构建CNC数字车间工业物联网