一、源码结构


二、运行效果




三、源码解析
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)及类型


















