一、源码结构
二、运行效果
三、源码解析
PLC批量读写+点对点更新+数据类型处理
优点:根据数据类型,判定监听的地址范围(40120_int 监听两个word:40120 40121;40130_long 监听四个word:40130 40131 40132 40133),添加到UI字典中,PLC批量读取,判定数据变化,查找控件集合,点对点更新,效率高
实现流程:
1. 读取配置文件及创建变量信息(点位名称,地址,数据类型(bool/short/int/float/long/double))
2. 自定义控件绑定参数,用UI字典存储,通过属性get方式,如果是bool类型,直接取Bool字典的点位数据;如果是Word类型,根据数据类型拼装Word字典中的word数据,得到对应数据类型的点位数据;通过set方式,加入到写队列。
using PLCBind.CustomControls; using PLCBind.Service; using PLCBind.Util; using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace PLCBind { public partial class MainForm : Form { public MainForm() { InitializeComponent(); } private void MainForm_Load(object sender, EventArgs e) { CommonMethods.LoadVar();// 读取配置文件及创建变量信息(点位名称,地址,类型) PLCService.Init(); // 读任务&写任务,数据有变化时事件广播通知(自定义控件预先绑定事件) BindControlParamModel();// 为按钮绑定参数 } /// <summary> /// 为控件绑定参数 /// </summary> private void BindControlParamModel() { InitControlTag(); SetControlParamModel(); } /// <summary> /// 绑定控件变量 /// </summary> void InitControlTag() { // 左皮带 lblYX1.Tag = ucBeltLeft1.Tag = "00101";// 启动 lblZS1.Tag = "40101";// 转速 lblDY1.Tag = "40120";// 电压 lblDL1.Tag = "40130";// 电流 } /// <summary> /// 赋值控件参数 /// </summary> void SetControlParamModel() { foreach (Control item in this.pnlMain.Controls) { if (item is ITransferUI objItem) { var address = item.Tag.ToString(); var common = CommonMethods.HomeVariables.Where(obj => obj.PLCAddress == address).FirstOrDefault(); if (common != null) { objItem.ParamModel = common; List<string> lstAddress = null; switch (common.DataType) { case DataType.Bool: lstAddress = PLCService.RangeAddress(common.PLCAddress, 0); break; case DataType.Short: lstAddress = PLCService.RangeAddress(common.PLCAddress, 0); break; case DataType.Int: case DataType.Float: lstAddress = PLCService.RangeAddress(common.PLCAddress, 2);// 40120 监听两个word:40120 40121 break; case DataType.Long: case DataType.Double: lstAddress = PLCService.RangeAddress(common.PLCAddress, 4);// 40130 监听四个word:40130 40131 40132 40133 break; } foreach (var range in lstAddress) { CommonMethods.AddControl(CommonMethods.DicHomeControl, range, item); } } } } } private void tabControl1_SelectedIndexChanged(object sender, EventArgs e) { var index = tabControl1.SelectedIndex; switch (index) { case 0: this.ucParameter1.RemoveParams(); break; case 1: // 参数设置 this.pnlSet.Controls.Clear(); this.pnlSet.Controls.Add(ucParameter1); this.ucParameter1.ListParams = CommonMethods.SetVariables.Where(s => s.Group == "顺序启动参数").ToList(); break; } } } }
using PLCBind.Service; namespace PLCBind.UIForm { public class BaseParams { /// <summary> /// 描述 /// </summary> public string Description { get; set; } /// <summary> /// PLC地址, 多个输入时,用";"分隔开 /// </summary> public string PLCAddress { get; set; } /// <summary> /// 数据类型 /// </summary> public DataType DataType { get; set; } /// <summary> /// 数据分组 /// </summary> public string Group { get; set; } /// <summary> /// 单位 /// </summary> public string Unit { get; set; } /// <summary> /// 设置与获取PLC值 /// </summary> public object PLCValue { get { object obj = null; switch (DataType) { case DataType.Bool: obj = PLCService.GetBool(PLCAddress); break; case DataType.Short: obj = PLCService.GetShort(PLCAddress); break; case DataType.Int: obj = PLCService.GetInt(PLCAddress); break; case DataType.Float: obj = PLCService.GetFloat(PLCAddress); break; case DataType.Long: obj = PLCService.GetLong(PLCAddress); break; case DataType.Double: obj = PLCService.GetDouble(PLCAddress); break; } return obj; } set { PLCService.AddWriteVariable(PLCAddress, value, DataType); } } } }
3. 异步任务处理:读任务&写任务,将读到的数据存到Data字典中,判断数据是否有发生变化,如果数据有变化,通过UI字典获取控件集合,调用更新方法
using HslCommunication; using HslCommunication.Core; using HslCommunication.ModBus; using PLCBind.CustomControls; using PLCBind.Util; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; using System.Windows.Forms; using UtilHelper; namespace PLCBind.Service { public class PLCService { public static ConcurrentDictionary<string, bool> DicBoolData = new ConcurrentDictionary<string, bool>(); public static ConcurrentDictionary<string, Word> DicWordData = new ConcurrentDictionary<string, Word>(); public static ConcurrentDictionary<string, Word> DicWordChange = new ConcurrentDictionary<string, Word>(); // static ModbusTcpNet client = null; static IByteTransform byteTransform; static ConcurrentQueue<PLCModel> queueWrite = new ConcurrentQueue<PLCModel>(); // UI通知 static void NoticeUI(string address, ConcurrentDictionary<string, List<Control>> dicControl) { dicControl.TryGetValue(address, out List<Control> lstControl); if (null != lstControl) { foreach (var item in lstControl) { if (item is ITransferUI objItem) { objItem.NoticeChange(); } } } } /// <summary> /// 事件触发 /// </summary> public static void DataChange(string address) { Task.Run(() => NoticeUI(address, CommonMethods.DicHomeControl)); Task.Run(() => NoticeUI(address, CommonMethods.DicSetControl)); } /// <summary> /// 自定义控件内接收到数据变化事件,根据传入address,以及DataType查询监听地址所需要的监听范围(40120_int 监听两个word:40120 40121;40130_long 监听四个word:40130 40131 40132 40133),判断是否属于本控件监听 /// </summary> public static List<string> RangeAddress(string address, int length) { List<string> lstaddress = new List<string>(); if (0 == length) { lstaddress.Add(address); } else { for (int i = 0; i < length; i++) { lstaddress.Add(FillAddress((DataHelper.Obj2Int(address) + i).ToString())); } } return lstaddress; } /// <summary> /// 读取时,按位补充0 /// </summary> public static string FillAddress(string val, int length = 5) { return val.PadLeft(length, '0'); } /// <summary> /// 写入时,格式化地址,如:40101 -> 101 /// </summary> public static string FormatAddress(string val) { if (val.Length < 5) return val; return val.Substring(1, val.Length - 1); } /// <summary> /// 初始化plc通信,开启读写任务 /// </summary> public static void Init() { client = new ModbusTcpNet(CommonMethods.PLCConfig.HostAddress, CommonMethods.PLCConfig.PortNumber); client.AddressStartWithZero = false; client.DataFormat = DataFormat.CDAB; byteTransform = client.ByteTransform; TskPlcRead(); TskPlcWrite(); } /// <summary> /// 获取bool(bool类型) /// </summary> /// <param name="address"></param> /// <returns></returns> public static bool GetBool(string address) { try { bool exist = DicBoolData.TryGetValue(address, out var value);// 字典存储 if (!exist) { Logger.Info($"[Error] PLCService,GetBool,errmsg:查无点位数据({address})"); } return value; } catch (Exception ex) { Logger.Info("[Error] PLCService,GetBool,errmsg:" + ex.Message); } return false; } /// <summary> /// 获取word(1个word,2个字节) /// </summary> static Word GetAddressWord(string address, int add) { address = FillAddress((Convert.ToInt32(address) + add).ToString()); bool exist = DicWordData.TryGetValue(address, out var value); if (!exist) { Logger.Info($"[Error] PLCService,GetAddressWord,errmsg:查无点位数据({address})"); } return value; } /// <summary> /// 拼接字节(多个word) /// </summary> static byte[] JoinAddressWord(string address, DataType datatype) { byte[] ret = null; switch (datatype) { case DataType.Short: { var buff = GetAddressWord(address, 0); ret = new byte[2] { buff.Byte1, buff.Byte2 }; } break; case DataType.Int: case DataType.Float: { var buff1 = GetAddressWord(address, 0); var buff2 = GetAddressWord(address, 1); ret = new byte[4] { buff1.Byte1, buff1.Byte2, buff2.Byte1, buff2.Byte2 }; } break; case DataType.Long: case DataType.Double: { var buff1 = GetAddressWord(address, 0); var buff2 = GetAddressWord(address, 1); var buff3 = GetAddressWord(address, 2); var buff4 = GetAddressWord(address, 3); ret = new byte[8] { buff1.Byte1, buff1.Byte2, buff2.Byte1, buff2.Byte2, buff3.Byte1, buff3.Byte2, buff4.Byte1, buff4.Byte2 }; } break; } return ret; } public static ushort GetShort(string address) { try { var buff = JoinAddressWord(address, DataType.Short); return byteTransform.TransUInt16(buff, 0); } catch (Exception ex) { Logger.Info("[Error] PLCService,GetShort,errmsg:" + ex.Message); } return 0; } public static uint GetInt(string address) { try { var buff = JoinAddressWord(address, DataType.Int); return byteTransform.TransUInt32(buff, 0); } catch (Exception ex) { Logger.Info("[Error] PLCService,GetInt,errmsg:" + ex.Message); } return 0; } public static float GetFloat(string address) { try { var buff = JoinAddressWord(address, DataType.Float); return byteTransform.TransSingle(buff, 0); } catch (Exception ex) { Logger.Info("[Error] PLCService,GetFloat,errmsg:" + ex.Message); } return 0; } public static ulong GetLong(string address) { try { var buff = JoinAddressWord(address, DataType.Long); return byteTransform.TransUInt64(buff, 0); } catch (Exception ex) { Logger.Info("[Error] PLCService,GetLong,errmsg:" + ex.Message); } return 0; } public static double GetDouble(string address) { try { var buff = JoinAddressWord(address, DataType.Double); return byteTransform.TransDouble(buff, 0); } catch (Exception ex) { Logger.Info("[Error] PLCService,GetDouble,errmsg:" + ex.Message); } return 0; } /// <summary> /// 定时读取 /// </summary> static void TskPlcRead() { Task.Factory.StartNew(async () => { var start_c = CommonMethods.PLCConfig.ReadStart_Coil; var start_h = CommonMethods.PLCConfig.ReadStart_Holding; bool[] temp_c = null; bool init_c = false; byte[] temp_h = null; bool init_h = false; while (!CommonMethods.CTS.IsCancellationRequested) { try { DicWordChange.Clear(); var array_c = (await client.ReadBoolAsync(start_c, (ushort)CommonMethods.PLCConfig.ReadCount_Coil)).Content; var array_h = (await client.ReadAsync(start_h, (ushort)(CommonMethods.PLCConfig.ReadCount_Holding * 2))).Content;// ushort占两个字节 if (null != array_c) { // bool类型只占1位,数据有变化直接通知 if (null == temp_c) { init_c = true; temp_c = new bool[array_c.Length]; } CheckBoolChange("0", start_c, temp_c, array_c, init_c); init_c = false; Array.Copy(array_c, temp_c, array_c.Length); } if (null != array_h) { // word类型数据位(2,4,8),所以要先读取全部的数据,再通知变化 if (null == temp_h) { init_h = true; temp_h = new byte[array_h.Length]; } CheckWordChange("4", start_h, temp_h, array_h, init_h); init_h = false; Array.Copy(array_h, temp_h, array_h.Length); if (DicWordChange.Count > 0) { foreach (var item in DicWordChange) { DataChange(item.Key); } } } } catch (Exception ex) { Logger.Info("[Error] PLCMgr,TskPlcRead,errmsg" + ex.Message); } await Task.Delay(100); } }, TaskCreationOptions.LongRunning); } /// <summary> /// 检查数据是否有变化(bool类型) /// </summary> public static void CheckBoolChange(string flg, string start, bool[] oldbuffer, bool[] newbuffer, bool init) { for (int i = 0; i < newbuffer.Length; i++) { // 00101 string address = flg + FillAddress((i + Convert.ToInt32(start)).ToString(), 4); bool value = newbuffer[i]; DicBoolData.AddOrUpdate1(address, value); if (init || oldbuffer[i] != value) { DataChange(address); } } } /// <summary> /// 检查数据是否有变化(word类型) /// </summary> public static void CheckWordChange(string flg, string start, byte[] oldbuffer, byte[] newbuffer, bool init) { int index = 0; for (int i = 0; i < newbuffer.Length; i = i + 2) { // 40101 string address = flg + FillAddress((index + Convert.ToInt32(start)).ToString(), 4); index++; byte byte1 = newbuffer[i]; byte byte2 = newbuffer[i + 1]; Word buff = new Word() { Byte1 = byte1, Byte2 = byte2 }; DicWordData.AddOrUpdate1(address, buff); if (init || (oldbuffer[i] != byte1 || oldbuffer[i + 1] != byte2)) { DicWordChange.AddOrUpdate1(address, buff); } } } /// <summary> /// 添加写入值 /// </summary> public static void AddWriteVariable(string address, object value, DataType datatype) { queueWrite.Enqueue(new PLCModel() { Address = address, Value = value, PLCDataType = datatype });//加载值进队列 } /// <summary> /// 定时写入 /// </summary> static void TskPlcWrite() { Task.Factory.StartNew(async () => { while (!CommonMethods.CTS.IsCancellationRequested) { try { if (!queueWrite.IsEmpty) { PLCModel model = null; OperateResult result = null; queueWrite.TryDequeue(out model); var dataype = model.PLCDataType; switch (dataype) { case DataType.Bool: result = await client.WriteAsync(FormatAddress(model.Address), Convert.ToBoolean(model.Value)); break; case DataType.Short: result = await client.WriteAsync(FormatAddress(model.Address), Convert.ToUInt16(model.Value)); break; case DataType.Int: result = await client.WriteAsync(FormatAddress(model.Address), Convert.ToUInt32(model.Value)); break; case DataType.Float: result = await client.WriteAsync(FormatAddress(model.Address), Convert.ToSingle(model.Value)); break; case DataType.Long: result = await client.WriteAsync(FormatAddress(model.Address), Convert.ToUInt64(model.Value)); break; case DataType.Double: result = await client.WriteAsync(FormatAddress(model.Address), Convert.ToDouble(model.Value)); break; } if (!result.IsSuccess) { Logger.Info("[Error] PLCMgr,TskPlcWrite,errmsg:写入失败," + result.Message); } } } catch (Exception ex) { Logger.Info("[Error] PLCMgr,TskPlcWrite,errmsg:" + ex.Message); } await Task.Delay(100); } }, TaskCreationOptions.LongRunning); } } }
4. 主界面控件都是静态加载,参数设置的控件是动态加载(点击进入,动态加载变量并监听;离开,移除不监听)
using PLCBind.Util; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; namespace PLCBind.UIForm { public partial class ucParameter : UserControl { public ucParameter() { InitializeComponent(); } /// <summary> /// 参数集合 /// </summary> public object ListParams { set { RemoveParams(); if (value is List<BaseParams> Parameters) { AddParams(Parameters); } } } /// <summary> /// 移除参数 /// </summary> public void RemoveParams() { foreach (Control item in this.tableLayoutPanel1.Controls) { if (item is ucTextSetting ctrText) { CommonMethods.RemoveControl(CommonMethods.DicSetControl, ctrText.Address);// 移除集合 } } this.tableLayoutPanel1.Controls.Clear(); // 移除控件 } /// <summary> /// 添加参数 /// </summary> void AddParams(List<BaseParams> objParams) { var pamramCount = objParams.Count; var pamrammIndex = 0; for (int columnIndex = 0; columnIndex < tableLayoutPanel1.ColumnCount; columnIndex++) { for (int rowIndex = 0; rowIndex < tableLayoutPanel1.RowCount; rowIndex++) { if (pamramCount > pamrammIndex) { var common = objParams[pamrammIndex]; var address = common.PLCAddress; ucTextSetting ucLbText = new ucTextSetting(); ucLbText.Anchor = ((((AnchorStyles.Top | AnchorStyles.Bottom) | AnchorStyles.Left))); ucLbText.ParamModel = common; ucLbText.Address = address; CommonMethods.AddControl(CommonMethods.DicSetControl, address, ucLbText);// 添加集合 tableLayoutPanel1.Controls.Add(ucLbText, columnIndex, rowIndex);// 添加控件 pamrammIndex++; } } } } private void TableLayoutPanel1_CellPaint(object sender, TableLayoutCellPaintEventArgs e) { if (e.Row % 2 == 1) e.Graphics.FillRectangle(Brushes.White, e.CellBounds); else e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(192, 224, 248)), e.CellBounds); } } }
注意事项:
1. 字典类型
Data字典:ConcurrentDictionary<string, bool> DicBoolData;ConcurrentDictionary<string, Word> DicWordData;Word:byte1,byte2
UI字典:ConcurrentDictionary<string, List<Control>> DicHomeControl;ConcurrentDictionary<string, List<Control>> DicSetControl
2. bool类型只占1位,数据有变化直接通知
3. word类型数据位(short:2,int/float:4,long/double:8),所以要先读取全部的数据,再通知变化
4. 自定义控件继承ITransferUI类(属性:ParamModel,方法:NoticeChange),赋值属性ParamModel,其中PLCValue get:通过不同的数据类型,获取字典中的word数据,并拼接合成相应的数据类型;set:传入地址(写入时格式化地址,如:40101->101)及类型