本篇博客主要记录C#上位机开发的相关内容。
在公司的实践项目为橡胶缠绕机控制系统的设计与开发,下位机主要采用的是Rockwell的PLC和三个伺服电机以及海康威视的3D线激光扫描仪。
基于OPC服务器通信 C#开发主要是用于检测使用OPC服务器对PLC进行数据读写的时间差方便与基于Ethernet/IP协议自主编写的通信库读取PLC数据进行比较。
开发平台:Visual Studio
语言:C#
以下是OPC客户端的源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using OPCAutomation; using System.Diagnostics; namespace OPCtest4 { public partial class Form1 : Form { OPCServer KepServer; OPCGroups KepGroups; OPCGroup KepGroup; OPCItems KepItems; OPCItem KepItem; bool opc_connected = false;//连接状态 int itmHandleClient = 0;//客户端的句柄,句柄即控件名称,如“张三”,用来识别是哪个具体的对象,此处可理解为每个节点的编号 int itmHandleServer = 0;//服务器的句柄 Stopwatch stopwatch = new Stopwatch(); public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { GetLocalServer(); } /// <summary> /// 获取本地的OPC服务器名称 /// </summary> public void GetLocalServer() { IPHostEntry host = Dns.GetHostEntry("LAPTOP-U84ALVBN"); var strHostName = host.HostName; try { KepServer = new OPCServer(); object serverList = KepServer.GetOPCServers(strHostName); foreach (string turn in (Array)serverList) { cmbServerName.Items.Add(turn); } cmbServerName.SelectedIndex = 0; btnConnServer.Enabled = true; } catch (Exception err) { MessageBox.Show("枚举本地OPC服务器出错:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } /// <summary> /// "连接"按钮点击事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BtnConnServer_Click(object sender, EventArgs e) { try { if (!ConnectRemoteServer(txtRemoteServerIP.Text, cmbServerName.Text)) { return; } btnSetGroupPro.Enabled = true; opc_connected = true; GetServerInfo(); RecurBrowse(KepServer.CreateBrowser()); if (!CreateGroup()) { return; } } catch (Exception err) { MessageBox.Show("初始化出错:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } /// <summary> /// 连接服务器 /// </summary> /// <param name="remoteServerIP">服务器IP</param> /// <param name="remoteServerName">服务器名称</param> /// <returns></returns> public bool ConnectRemoteServer(string remoteServerIP, string remoteServerName) { try { KepServer.Connect(remoteServerName, remoteServerIP); if (KepServer.ServerState == (int)OPCServerState.OPCRunning) { tsslServerState.Text = "已连接到-" + KepServer.ServerName + " "; } else { //这里你可以根据返回的状态来自定义显示信息,请查看自动化接口API文档 tsslServerState.Text = "状态:" + KepServer.ServerState.ToString() + " "; } } catch (Exception err) { MessageBox.Show("连接远程服务器出现错误:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } return true; } /// <summary> /// 获取服务器信息,并显示在窗体状态栏上 /// </summary> public void GetServerInfo() { tsslServerStartTime.Text = "开始时间:" + KepServer.StartTime.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss"); tsslversion.Text = "版本:" + KepServer.MajorVersion.ToString() + "." + KepServer.MinorVersion.ToString() + "." + KepServer.BuildNumber.ToString(); } /// <summary> /// 展开树枝和叶子 /// </summary> /// <param name="oPCBrowser">opc浏览器</param> public void RecurBrowse(OPCBrowser oPCBrowser) { //展开分支 oPCBrowser.ShowBranches(); //展开叶子 oPCBrowser.ShowLeafs(true); foreach (object turn in oPCBrowser) { listBox1.Items.Add(turn.ToString()); } } /// <summary> /// 创建组,将本地组和服务器上的组对应 /// </summary> /// <returns></returns> public bool CreateGroup() { try { KepGroups = KepServer.OPCGroups;//将服务端的组集合复制到本地 KepGroup = KepGroups.Add("S");//添加一个组 SetGroupProperty();//设置组属性 KepItems = KepGroup.OPCItems;//将组里的节点集合复制到本地节点集合 KepGroup.DataChange += KepGroup_DataChange; KepGroup.AsyncWriteComplete += KepGroup_AsyncWriteComplete; } catch (Exception err) { MessageBox.Show("创建组出现错误:" + err.Message, "提示信息", MessageBoxButtons.OK, MessageBoxIcon.Warning); return false; } return true; } /// <summary> /// 设置组的属性,从对应的控件里获取 /// </summary> public void SetGroupProperty() { KepServer.OPCGroups.DefaultGroupIsActive = Convert.ToBoolean(txtGroupIsActive.Text);//激活组 KepServer.OPCGroups.DefaultGroupDeadband = Convert.ToInt32(txtGroupDeadband.Text);// 死区值,设为0时,服务器端该组内任何数据变化都通知组 KepGroup.UpdateRate = Convert.ToInt32(txtUpdateRate.Text);//服务器向客户程序提交数据变化的刷新速率 KepGroup.IsActive = Convert.ToBoolean(txtIsActive.Text);//组的激活状态标志 KepGroup.IsSubscribed = Convert.ToBoolean(txtIsSubscribed.Text);//是否订阅数据 } /// <summary> /// 异步写方法 /// </summary> /// <param name="TransactionID">处理ID</param> /// <param name="NumItems">项个数</param> /// <param name="ClientHandles">OPC客户端的句柄</param> /// <param name="Errors">错误个数</param> private void KepGroup_AsyncWriteComplete(int TransactionID, int NumItems, ref Array ClientHandles, ref Array Errors) { lblState.Text = ""; for (int i = 1; i <= NumItems; i++) { lblState.Text += "Tran:" + TransactionID.ToString() + " CH:" + ClientHandles.GetValue(i).ToString() + " Error:" + Errors.GetValue(i).ToString(); } stopwatch.Stop(); lblState.Text += " 耗时:" + stopwatch.ElapsedMilliseconds.ToString() + "ms"; } /// <summary> /// 数据订阅方法 /// </summary> /// <param name="TransactionID">处理ID</param> /// <param name="NumItems">项个数</param> /// <param name="ClientHandles">OPC客户端的句柄</param> /// <param name="ItemValues">节点的值</param> /// <param name="Qualities">节点的质量</param> /// <param name="TimeStamps">时间戳</param> private void KepGroup_DataChange(int TransactionID, int NumItems, ref Array ClientHandles, ref Array ItemValues, ref Array Qualities, ref Array TimeStamps) { for (int i = 1; i <= NumItems; i++)//下标一定要从1开始,NumItems参数是每次事件触发时Group中实际发生数据变化的Item的数量,而不是整个Group里的Items { this.txtTagValue.Text = ItemValues.GetValue(i).ToString(); this.txtQualities.Text = Qualities.GetValue(i).ToString(); this.txtTimeStamps.Text = TimeStamps.GetValue(i).ToString(); } } /// <summary> /// 选择列表时触发的事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void ListBox1_SelectedIndexChanged(object sender, EventArgs e) { try { if (itmHandleClient != 0) { this.txtTagValue.Text = ""; this.txtQualities.Text = ""; this.txtTimeStamps.Text = ""; Array Errors; OPCItem bItem = KepItems.GetOPCItem(itmHandleServer); //注:OPC中以1为数组的基数 int[] temp = new int[2] { 0, bItem.ServerHandle }; Array serverHandle = (Array)temp; //移除上一次选择的项 KepItems.Remove(KepItems.Count, ref serverHandle, out Errors); itmHandleClient = 1;//节点编号为1 KepItem = KepItems.AddItem(listBox1.SelectedItem.ToString(), itmHandleClient);//第一个参数为ItemID,第二个参数为节点编号,节点编号可自定义 itmHandleServer = KepItem.ServerHandle;//获取该节点的服务器句柄 } else { itmHandleClient = 1;//节点编号为1 KepItem = KepItems.AddItem(listBox1.SelectedItem.ToString(), itmHandleClient);//第一个参数为ItemID,第二个参数为节点编号,节点编号可自定义 itmHandleServer = KepItem.ServerHandle;//获取该节点的服务器句柄 } } catch (Exception err) { //没有任何权限的项,都是OPC服务器保留的系统项,此处可不做处理。 itmHandleClient = 0; txtTagValue.Text = "Error ox"; txtQualities.Text = "Error ox"; txtTimeStamps.Text = "Error ox"; MessageBox.Show("此项为系统保留项:" + err.Message, "提示信息"); } } /// <summary> /// 设置组属性的按钮点击事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BtnSetGroupPro_Click(object sender, EventArgs e) { SetGroupProperty(); } /// <summary> /// “写入”按钮点击事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BtnWrite_Click(object sender, EventArgs e) { OPCItem bItem = KepItems.GetOPCItem(itmHandleServer); int[] temp = new int[2] { 0, bItem.ServerHandle }; Array serverHandles = (Array)temp; object[] valueTemp = new object[2] { "", txtWriteTagValue.Text }; Array values = (Array)valueTemp; Array Errors; int cancelID; stopwatch.Restart(); KepGroup.AsyncWrite(1, ref serverHandles, ref values, out Errors, 2009, out cancelID); //KepItem.Write(txtWriteTagValue.Text);//这句也可以写入,但并不触发写入事件 lblState.Text = "正在写入..."; GC.Collect(); } /// <summary> /// 关闭窗口事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (!opc_connected) { return; } if (KepGroup != null) { KepGroup.DataChange -= new DIOPCGroupEvent_DataChangeEventHandler(KepGroup_DataChange); } if (KepServer != null) { KepServer.Disconnect(); KepServer = null; } opc_connected = false; } private void txtTimeStamps_TextChanged(object sender, EventArgs e) { } private void cmbServerName_SelectedIndexChanged(object sender, EventArgs e) { } private void lblState_Click(object sender, EventArgs e) { } } }
在OPC协议中采用的是客户端和服务器的模式,日常学习使用的服务器可以是Kepserver,在实际使用中,客户端是通信服务的发起方,用来读取数据。
OPC协议主要分为DA、A&E、HDA、UA四个规范,日常使用访问数据主要使用的是DA协议,现在UA的使用也开始增多。
OPC服务主要有三类对象,OPC server对象、OPC group对象、OPC item对象,每一类对象都包含一系列的接口。
OPC Server对象主要是功能创建和管理OPC Group对象、管理服务器内部的状态信息。
OPC Group对象主要管理该对象的内容状态信息、创建和管理Item对象以及服务器内部的实时数据的存取服务(同步与异步);通常分为私有组和公有组。公有组有多个客户共享、私有组只属于某个客户,大多数的服务器均未实现公有组。
OPC Item对象主要用来描述实时数据,一个Item对象不能单独被OPC客户端访问,所有的对象的访问必须通过OPC Group访问。
以上所有与OPC相关的对象及其调用都来自OPCAutomation的库。
基于Ethernet/IP协议通信 相较于传统的基于OPC服务器的通信方式,虽然协议的集成度较高,但是其缺点在需要高速数据传输的工业应用场景缺还是延迟略微有些许高,为了解决这个问题,基于Ethernet/IP协议,准备使用C语言自主构建一个通信库。
开发平台:Visual Studio
开发语言:C
以下是通信库部分源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #ifdef _WIN32 #include <WinSock2.h> #endif #include <stdio.h> #include <stdlib.h> #include <windows.h> #pragma warning(disable : 4996) #define GET_RESULT(ret) { if (ret != 0) failed_count++;} #include "ab_cip.h" extern uint32 g_session; int main(int argc, char** argv) { #ifdef _WIN32 WSADATA wsa; if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { return -1; } #endif char* plc_ip = "192.168.1.2"; int plc_port = 44818; LARGE_INTEGER frequency, start, end; QueryPerformanceFrequency(&frequency); if (argc > 1) { plc_ip = argv[1]; plc_port = atoi(argv[2]); } int fd = -1; int slot = 0; bool ret = ab_cip_connect(plc_ip, plc_port, 0, &fd); printf("Connect to PLC: %s:%d, ret: %d\n", plc_ip, plc_port, ret); printf("session : %x\n", g_session); if (ret && fd > 0) { cip_error_code_e ret = CIP_ERROR_CODE_FAILED; const int TEST_COUNT = 50; const int TEST_SLEEP_TIME = 1000; int failed_count = 0; char address[50] = { 0 }; int i = 0; bool val = false; strcpy(address, "Angel"); int i_val = 0; QueryPerformanceCounter(&start); ret = ab_cip_write_int32(fd, address, 89); QueryPerformanceCounter(&end); ret = ab_cip_read_int32(fd, address, &i_val); printf("Read\t %s \tint32:\t %d\n", address, i_val); GET_RESULT(ret); double elapsedTime = (double)(end.QuadPart - start.QuadPart) / frequency.QuadPart; printf("写入操作耗时:%f 秒\n", elapsedTime); strcpy(address, "low1"); ret = ab_cip_write_bool(fd, address, 0); ret = ab_cip_read_bool(fd, address, &val); GET_RESULT(ret); printf("Read low bool: %d, ret: %d\n", val, ret); printf("All Failed count: %d\n", failed_count); ab_cip_disconnect(fd); system("pause"); } #ifdef _WIN32 WSACleanup(); #endif }
以上是用于测试通信延迟的例程,主要采用的是基于Ethernet/IP协议的通信方式,整个通信均是使用底层报文的格式进行数据的读写和采集。
两种通信方式对比 两种通信方式的对比,主要是采用读写相同字节的数据所消耗的时间,在实际测试中可以看出因为自建通信库不用通过OPC服务器的转发,数据量较小时差异略微领先,但是在工业现场数据量庞大的情况,所节省的时间就较为可观了。