最近一直在学习MODBUS通讯,于是想着写一个简单的上位机程序和PLC通讯。因为没有老师,很多东西都是自己瞎摸索的,做下来感觉还是有很大收获。
逻辑上其实很简单,最开始用WPF编写如下界面:
![3ddac71b1268ad646902819738cab750.png](https://i-blog.csdnimg.cn/blog_migrate/6aeeabfea10d8d64e6f2d05bfe7e3a5e.jpeg)
左边三个LED键我想设置为切换开关,所以用的ToggleButton控件。并且在按下之后将“开启”改为“关闭”。右下角的光强设置按钮用的Button控件,并实现Click事件。
状态指示灯这里我暂时没写,怕自己造出来的轮子太丑。
光强实际值用的三个TextBlock控件,光强设置值用了三个TextBox控件。
运行之后的效果如下:
![0a8d6ac46e17865b01bffe6e7a9c7453.png](https://i-blog.csdnimg.cn/blog_migrate/aef6ef2c323811aa40c248c87ed1e293.jpeg)
考虑到通讯时COM端口的不确定性,所以写了一个设置串口参数的类,并且用一个XML文档去配置串口参数。
XML文档如下:
![826b4daec3e00eca8fc86c8809784958.png](https://i-blog.csdnimg.cn/blog_migrate/2a1e514e44dd4f87335b20dba9706ac0.png)
逻辑很简单,当打开软件的时候,自动读取XML文档中的配置参数。
然后是串口参数类public static class SerialPortValue,之所以定义为静态类是因为想把它当成全局变量用。
定义了一个默认构造函数去读取XML的值:
static SerialPortValue()
{
//下载XML文档
XDocument xDocument = XDocument.Load("SerialPortValue.xml");
//找出根节点
XElement root = xDocument.Element("serialPortValue");
//给属性赋值
device = root.Element("device").Value.ToString();
parity = Convert.ToChar(root.Element("parity").Value);
baud = int.Parse(root.Element("baud").Value);
data_bit = int.Parse(root.Element("data_bit").Value);
stop_bit = int.Parse(root.Element("stop_bit").Value);
slaveID = int.Parse(root.Element("slaveID").Value);
}
这些属性都是在该类里面定义的:
![354fd12457d650e6adfd7b7cedb3fd80.png](https://i-blog.csdnimg.cn/blog_migrate/5ca12aa4a6a61fb4139d506afdb3dcec.png)
然后是定义了一个数据类public class UVLED_Data,用于与PLC进行数据交互的。逻辑是,PLC只与该类进行交互,UI控件绑定到该类的对应属性值上即可。
这里有一点需要注意,关于绑定静态属性还需要定义以下事件,否则控件的值只会更新一次:EventHandler<PropertyChangedEventArgs>
具体如下:
![a13ffa16bc14568972f33efffaea85af.png](https://i-blog.csdnimg.cn/blog_migrate/c6eebaab6394cc02bf80a5dc3a76bf7a.jpeg)
然后将其绑定到控件即可。
最后是定义一个实时刷新数据的函数void Updata(),将这个函数放在另一个线程中。在这个函数里面,不停重复读取和写入数据,这样当UI或者PLC的某个属性值变了之后,都能及时做出响应。代码片段如下:
public MainWindow()
{
InitializeComponent();
//定义线程
Thread LogThread = new Thread(new ThreadStart(Updata));
//设置线程为后台线程,那样进程里就不会有未关闭的程序了
LogThread.IsBackground = true;
LogThread.Start();//开起线程
}
//在另一个线程中不断刷新数据
void Updata()
{
while (true)
{
int rc;
//创建一个RTU容器
string device = SerialPortValue.Device;
int slaveID = SerialPortValue.SlaveID;
IntPtr ctx = libmodbus.modbus_new_rtu(device, SerialPortValue.Baud, SerialPortValue.Parity, SerialPortValue.Data_bit, SerialPortValue.Stop_bit);
//设置从站地址
rc = libmodbus.modbus_set_slave(ctx, slaveID);
if (rc == -1)
{
MessageBox.Show("ERROR: modbus_set_slave");
}
//打开连接
rc = libmodbus.modbus_connect(ctx);
if (rc == -1)
{
MessageBox.Show("ERROR: modbus_connect");
}
//写入Y0-Y2
rc = libmodbus.modbus_write_bit(ctx, 64512, UVLED_Data.Button_Status_LED1);
rc = libmodbus.modbus_write_bit(ctx, 64513, UVLED_Data.Button_Status_LED2);
rc = libmodbus.modbus_write_bit(ctx, 64514, UVLED_Data.Button_Status_LED3);
//写入PLC D600-D602
rc = libmodbus.modbus_write_register(ctx, 600, UVLED_Data.Set_Val_LED1);
rc = libmodbus.modbus_write_register(ctx, 601, UVLED_Data.Set_Val_LED2);
rc = libmodbus.modbus_write_register(ctx, 602, UVLED_Data.Set_Val_LED3);
//读取PLC D600-D602
rc = libmodbus.modbus_read_registers(ctx, 600, 3, read_Registers);
if (rc == -1)
{
MessageBox.Show("ERROR: modbus_read_registers");
}
UVLED_Data.Current_Val_LED1 = read_Registers[0];
UVLED_Data.Current_Val_LED2 = read_Registers[1];
UVLED_Data.Current_Val_LED3 = read_Registers[2];
libmodbus.modbus_close(ctx);
libmodbus.modbus_free(ctx);
System.Threading.Thread.Sleep(10);//10ms刷新一次
}
}
在控件中只需要写逻辑代码,而不涉及到读写数据操作。
以上纯属个人的想法,不足之处还请大神指点。
分割线
想了想还是把状态灯也添上去算了。。。
从网上找了两个状态灯的图片,把图片扣出来并存为PNG格式。关于PS怎么用,这里就不讲了。
如下是效果图:
![66e6f431f5fbee8b0d85c60683d7351c.png](https://i-blog.csdnimg.cn/blog_migrate/fbc8977868919c25f955d42317319335.jpeg)
点击按钮,指示灯可以切换亮灭的状态,实际就是两张Image切换。代码如下:
Image_LED1.Source = new BitmapImage(new Uri("Resources/绿色.png", UriKind.Relative));
Image_LED1.Source = new BitmapImage(new Uri("Resources/灰绿.png", UriKind.Relative));
另外,关于水机的状态,可能是一个输入信号。可以在Update函数中读取位状态,通过不同的状态去切换指示灯状态。