视频演示链接:https://www.bilibili/video/av97314651?pop_share=1
前提条件:
1、电脑需要与PLC能通讯上;
2、手机要与电脑能通讯上(比如手机和电脑都在同一个局域网下或同一个WiFi下)。
主要思路:
1、利用S7实现上位机对西门子PLC数据的读写功能;
2、利用Socket实现上位机服务器与手机客户端的通讯,将上位机读取的PLC数据发送给手机客户端,以及将手机写入的信号写入到PLC。

服务器窗体简单画面如下:

服务器端的完整代码如下:

using S7.Net;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace 读写库位信息
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Control.CheckForIllegalCrossThreadCalls = false;//取消跨线程的访问
            try
            {
                //创建一个负责监听IP地址跟端口号的socket
                Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPAddress ip = IPAddress.Any;
                IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(text_Port.Text));//创建端口号对象
                socketWatch.Bind(point);//绑定端口号,其中端口号包含IP地址信息
                ShowMsg(DateTime.Now.ToString() + "  " + "监听成功");
                AddLogText(DateTime.Now.ToString() + "  " + "监听成功");
                socketWatch.Listen(10);//设置监听队列,在同一时间点内所能连接的最多客户量,如果超过了10个,则排队等待
                Thread th = new Thread(Listen);//创建新线程运行Listen()函数
                th.IsBackground = true;//将线程设置为后台线程
                th.Start(socketWatch);//把socketWatch传到线程的Start()函数,标记该线程可以被CPU执行了
            }

            catch
            { }
        }
        Socket socketsend;//创建跟客户端通讯的socket
        Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();
        string ip;
        /// <summary>
        /// 等待客户端的连接,并创建一个与之通信用个Socket(通过循环等待客户端连接,否则线程会一直停在Accept()函数处等待)
        /// </summary>
        /// <param name="o"></param>
        void Listen(object o)//被线程所执行的函数,如果有参数,必须是object型
        {
            Socket socketWatch = o as Socket;//把Listen()函数的object型的参数强转为Socket型            
            try
            {
                while (true)
                {
                    socketsend = socketWatch.Accept();
                    dicSocket.Add(socketsend.RemoteEndPoint.ToString(), socketsend);//获取远程客户端的IP地址和端口号
                    connected_Client.Items.Add(socketsend.RemoteEndPoint.ToString());
                    ShowMsg(DateTime.Now.ToString() + "\r\n" + socketsend.RemoteEndPoint.ToString() + " : " + "连接成功");
                    AddLogText(DateTime.Now.ToString() + "  " + socketsend.RemoteEndPoint.ToString() + " : " + "连接成功" );

                    byte[] buffer = System.Text.Encoding.UTF8.GetBytes(socketsend.RemoteEndPoint.ToString());
                    ip = socketsend.RemoteEndPoint.ToString();                   

                    Thread th = new Thread(Recieve);//开启一个新的线程不停地接收客户端发送过来的消息
                    th.IsBackground = true;//将线程设置为后台线程,这样主线程关闭时,新线程也会关闭(前台线程:只有所有的前台线程都关闭才能完成程序关闭;后台线程:只要所有的前台程序结束,后台线程自动结束)
                    th.Start(socketsend); //把socketsend传到线程的Start()函数,标记该线程可以被CPU执行了
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message.ToString()); //抛出异常信息
            }
        }
        
        /// <summary>
        /// 服务器端不停地接收客户端发送过来的消息
        /// </summary>
        /// <param name="o"></param>
        void Recieve(object o)
        {
            Socket socketSend = o as Socket;
            while (true)
            {
                try
                {
                    byte[] buffer = new byte[1024 * 1024 * 2];//创建一个2M的字节数组型变量(1024byte=1kb,1024*1024byte=1024*1kb=1M)
                    int r = socketSend.Receive(buffer);//将Receive()函数返回的int型的实际接收到的字节数赋值给r
                    if (r == 0)
                    {
                        break;//如果接收到的数据为空,则跳出循环
                    }
                    string str = Encoding.UTF8.GetString(buffer, 0, r);//将字节数组buffer[]中的0到r位解码为字符串,并赋值给str  
                    string[] sArray = str.Split('_'); //将字符串str按“_”分割,并存到字符串数组sArray中。
                    ShowMsg(DateTime.Now.ToString() + "\r\n" + socketSend.RemoteEndPoint + " : " + str);//调用ShowMsg()函数,将远程客户端的IP地址、端口号及接收到的信息显示到txtLog文本框
                    AddLogText(DateTime.Now.ToString() + "  " + socketSend.RemoteEndPoint + " : " + str );

                    string readOrWrite = sArray[0];//取字符串数组索引号为0的元素,判断是读还是写,0为读,1为写
                    string storage = sArray[1];//取字符串数组索引号为1的元素,获取库位号
                    int storageNumble = Convert.ToInt32(storage.Substring(2, storage.Length - 2));//截取库位号,并将库位号转换为整数型变量,存入storageNumble中
                    int DBAddress = (storageNumble - 1) * 2;
                    string[] signal1 = { "0", "0", "0", "0", "0", "0", "0", "0" };//定义用于存储读到的DB20中的数据的字符串数组
                    string[] signal2 = { "0", "0", "0", "0" };//定义用于存储读到的DB21中的数据的字符串数组
                    if (readOrWrite == "0")  //如果读到的数据为0,表示需要读取DB20和DB21中的数据
                    {                                       
                        for (int i = 0; i < 8; i++)//读DB20的数据,该数据为AGV发送给库位的信号
                        {
                            string readAddress1 = "DB20.DBX" + DBAddress.ToString() + "." + i.ToString();
                            bool test1 = (bool)plc.Read(readAddress1);                           
                            signal1[i] = test1.ToString();                                                                           
                        }

                        for (int i = 0; i < 4; i++)//读DB21的数据,该数据为AGV接收库位发送的信号
                        {                            
                            string readAddress2 = "DB21.DBX" + DBAddress.ToString() + "." + i.ToString();                            
                            bool test2 = (bool)plc.Read(readAddress2);                            
                            signal2[i] = test2.ToString();                            
                        }
                        string readResult = signal1[0];//新建字符串变量,用于存储读到的所有PLC信号
                        for (int i = 1; i < 8; i++)
                        {                            
                            readResult = readResult + "_" + signal1[i];//将在DB20数据块中读到的数据存放到readResult字符串中                            
                        }

                        for (int i = 0; i < 4; i++)
                        {                           
                            readResult = readResult + "_" + signal2[i]; //将在DB21数据块中读到的数据存放到readResult字符串中
                        }
                        dicSocket[ip].Send(System.Text.Encoding.UTF8.GetBytes(readResult));
                        ShowMsg(readResult);
                        AddLogText(readResult);
                    }
                    if (readOrWrite == "1") //如果读到的数据为1,表示需要在DB20中写入数据
                    {                        
                        string[] writeData = sArray[2].Split('-');//取接收到的客户端发过送的字符串数组索引号为2的元素,获取写入的数据,并将写入按“-”分割后存入writeData中
                        for (int i = 0; i < 8; i++)//读DB20的数据,该数据为AGV发送给库位的信号
                        {                                                     
                            string writeAddress = "DB20.DBX" + DBAddress.ToString() + "." + i.ToString();
                            plc.Write(writeAddress, bool.Parse(writeData[i])); //将写入的数据转换为bool量,并写入到相应的DB块中。                                                                         
                        }
                    }
                    
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message.ToString()+"(请检查是否连接PLC!)"); //抛出异常信息
                }
            }
        }
        /// <summary>
        /// 将字符串追加到文本框txtLog中
        /// </summary>
        /// <param name="v">追加的字符串数据</param>
        private void ShowMsg(string v)
        {
            txtLog.AppendText(v + "\r\n");
        }

        string strPath = System.Environment.CurrentDirectory + "/";//获取和设置当前目录(即该进程从中启动的目录)的完全限定路径。
        string fileName = DateTime.Now.ToLongDateString().ToString() + ".txt"; //获取系统日期,并将"日期.txt"赋值给文件名变量fileName 
        
        /// <summary>
        /// 将客户端的操作数据追加到文本日志文件中
        /// </summary>
        /// <param name="a">客户端的操作数据字符串</param>
        private void AddLogText(string a)
        {                        
            File.AppendAllText(strPath + fileName, a + "\r\n");  //将参数a的值追加到特定路径的fileName文本中,如果fileName不存在,则创建新文件后再将a追加到文本文件中。        
        }


        static string Plc_ip = "0.0.0.0";
        public Plc plc = new Plc(CpuType.S71200, Plc_ip, 0, 1);  //定义新的PLC对象  

        private void Btn_connect_Click(object sender, EventArgs e)
        {
            try
            {
                Plc_ip = txt_IP.Text;//将输入的IP地址赋值给Plc_ip
                plc = new Plc(CpuType.S71200, Plc_ip, 0, 1);//重新实例化plc对象
                plc.Open();//连接PLC
                if (plc.IsConnected)
                {
                    MessageBox.Show("连接成功!", "连接提示", MessageBoxButtons.OK);
                }
                else
                {
                    MessageBox.Show("连接失败!", "连接提示", MessageBoxButtons.OK);
                }
            }

            catch (Exception ex)
            {
                MessageBox.Show(ex.Message.ToString() + "(请检查PLC的IP地址是否正确!)");
            }

        }

        private void Btn_disconnect_Click(object sender, EventArgs e)
        {
            plc.Close();//断开PLC
        }

        private void Btn_Clear_Click(object sender, EventArgs e)
        {
            txtLog.Text = null;//清空消息框
        }

        private void Btn_LogView_Click(object sender, EventArgs e)
        {
            System.Diagnostics.Process.Start(strPath + fileName);//查看日志
        }
    }          
}


安卓手机客户端画面如下:

安卓手机客户端是利用E4A中文安卓编程软件制作的APP,完整代码如下:
登录页面代码:

变量 账号 为 文本型
变量 密码 为 文本型		
	
事件 主窗口.创建完毕()
    保存窗口("窗口1",创建 窗口1)
结束 事件
		
事件 按钮登录.被单击()
	账号 = 编辑框账号.内容
	密码 = 密码编辑框1.内容
	如果 账号 = "admin" 且 密码 = "123" 则
		切换窗口(读取窗口("窗口1"))
	否则
	    弹出提示("账号或密码错误,请重新输入!")
	结束 如果


	
结束 事件

事件 按钮取消.被单击()
	结束程序()
结束 事件

主页面代码

事件 窗口1.创建完毕()
	单选列表框1.背景颜色 = 白色	
	单选列表框1.清空项目()	
	变量 计次 为 整数型
	计次=1
	判断循环首 计次< 105
		单选列表框1.添加项目("库位" & 计次)
		单选列表框1.置项目状态(计次,)		
		计次 = 计次 + 1
	判断循环尾
	
结束 事件

事件 按钮连接.被单击()
	客户1.连接服务器(IP.内容,到整数(PORT.内容),3000)  '连接服务器	
结束 事件

事件 按钮断开.被单击()
	客户1.断开连接()		
结束 事件

事件 客户1.连接断开()
	弹出提示("连接已断开!")
结束 事件

    变量 是否连接服务器 为 逻辑型
事件 客户1.连接完毕(连接结果 为 逻辑型)
	是否连接服务器 = 连接结果
	如果 连接结果 = 真 则
		弹出提示("连接成功!")		
	否则 
		信息框("连接提示","连接服务器失败!请检查IP地址或端口号是否正确!","确定")
	结束 如果
结束 事件

	变量 库位号 为 文本型= "0"	
事件 单选列表框1.表项被单击(项目索引 为 整数型)	
	库位号=单选列表框1.取项目内容(项目索引)   	
	客户1.发送数据(文本到字节(0 & "_" & 库位号,"UTF-8")) '将库位号发送给服务器	
结束 事件
   
事件 客户1.收到数据(数据 为 字节型())
	变量 接收到的数据 为 文本型
	变量 解析数据 为 文本型(12)  '定义一个字符串数组,存放接收并解析后的数据
	接收到的数据 =  字节到文本(数据,"UTF-8")  '收到服务器发来的字节集数据,转换成文本	
	解析数据 = 分割文本(接收到的数据 ,"_")	
	如果 解析数据(0)="True" 则 
		拉空料.选中 = 真
	否则	
		拉空料.选中 = 假
	结束 如果
	
	如果 解析数据(1)="True" 则 
		送满料.选中=真
	否则	
		送满料.选中=假
	结束 如果
	
	如果 解析数据(2)="True" 则 
		申请进入.选中=真 
	否则	
		申请进入.选中=假
	结束 如果
	
	如果 解析数据(3)="True" 则 
		进入中.选中=真
	否则	
		进入中.选中=假
	结束 如果
	
	如果 解析数据(4)="True" 则 
		到达.选中=真
	否则	
		到达.选中=假
	结束 如果
	
	如果 解析数据(5)="True" 则 
		申请离开.选中=真
	否则	
		申请离开.选中=假
	结束 如果
	
	如果 解析数据(6)="True" 则 
		离开中.选中=真
	否则	
		离开中.选中=假
	结束 如果
	
	如果 解析数据(7)="True" 则 
		离开成功.选中=真
	否则	
		离开成功.选中=假
	结束 如果
	
	如果 解析数据(8)="True" 则 
		拉空料1.选中=真
	否则	
		拉空料1.选中=假
	结束 如果
	
	如果 解析数据(9)="True" 则 
		送满料1.选中=真
	否则	
		送满料1.选中=假
	结束 如果
	
	如果 解析数据(10)="True" 则 
		允许进入.选中=真
	否则	
		允许进入.选中=假
	结束 如果
	
	如果 解析数据(11)="True" 则 
		允许离开.选中=真
	否则	
		允许离开.选中=假
	结束 如果
	
	
结束 事件

变量 写入数据 为 文本型
事件 按钮写入.被单击()
	变量 是否拉空料 为 文本型
	变量 是否送满料 为 文本型
	变量 是否申请进入 为 文本型
	变量 是否在进入中 为 文本型	
	变量 是否到达 为 文本型
	变量 是否申请离开 为 文本型
	变量 是否在离开中 为 文本型
	变量 是否离开成功 为 文本型
	如果 拉空料.选中=真 则 
		是否拉空料 = "True"  
	否则	
		是否拉空料 = "False"
	结束 如果
	
	如果 送满料.选中=真 则 
		是否送满料 = "True"
	否则	
		是否送满料 = "False"
	结束 如果
	
	如果 申请进入.选中=真 则 
		是否申请进入 = "True"
	否则	
		是否申请进入 = "False"
	结束 如果
	
	如果 进入中.选中=真 则 
		是否在进入中 = "True"
	否则	
		是否在进入中 = "False"
	结束 如果
	
	如果 到达.选中=真 则 
		是否到达 = "True"
	否则	
		是否到达 ="False"
	结束 如果
	
	如果 申请离开.选中=真 则 
		是否申请离开 = "True"
	否则	
		是否申请离开 = "False"
	结束 如果
	
	如果 离开中.选中=真 则 
		是否在离开中 = "True"
	否则	
		是否在离开中 = "False"
	结束 如果
	
	如果 离开成功.选中=真 则 
		是否离开成功 = "True"
	否则	
		是否离开成功 =  "False"
	结束 如果	
	
	
	变量 结果 为 整数型
	
	写入数据 = 是否拉空料 & "-" & 是否送满料  & "-" &   是否申请进入  & "-" &  是否在进入中   & "-" &   是否到达   & "-" &  是否申请离开   & "-" &   是否在离开中  & "-" &   是否离开成功  
	
	如果 是否连接服务器 = 假 则
		弹出提示("请连接服务器!")		
	否则 
		
		如果 库位号 = "0" 则 
		弹出提示("请选择库位号!")
	    否则
	        结果 = 信息框2("写入确认","是否写入" & 库位号 & "信号!","确定","取消")
	        如果 结果 = 0 则		
	            客户1.发送数据(文本到字节("1" & "_" & 库位号 & "_" & 写入数据 ,"UTF-8")) '将xxx发送给服务器
		        弹出提示(库位号 & "信号已写入!" )	
	        否则 
		        弹出提示(库位号 & "信号未写入!")		
	        结束 如果	
	    结束 如果
		
		
		
	结束 如果
	
	

	
结束 事件


事件 按钮刷新.被单击()
	
	如果 是否连接服务器 = 假 则
		弹出提示("请连接服务器!")		
	否则 
		
		如果 库位号 = "0" 则 
		   弹出提示("请选择库位号!")
	    否则	
		   客户1.发送数据(文本到字节( 0 & "_" & 库位号 ,"UTF-8")) '将xxx发送给服务器
	    结束 如果					
		
	结束 如果	
	
结束 事件

事件 按钮退出.被单击()
	结束程序()
结束 事件

更多推荐

C#用Socket和S7.net实现安卓手机APP读写西门子PLC数据(安卓APP使用的E4A中文编程软件)