1、A-1E报文回顾
具体细节请看:
C#上位机与三菱PLC的通信03--MC协议之A-1E报文解析
C#上位机与三菱PLC的通信04--MC协议之A-1E报文测试
2、为何要开发自己的通讯库
前面使用了第3方的通讯库实现了与三菱PLC的通讯,实现了数据的读写,对于通讯库,我们只要引用并调用相关的方法即可实现目的,为什么别人可以封装通讯库dll文件,自己能不能做到?当然可以,但写一个通讯库需要非凡的技术,需要考虑的东西很多,比如扩展性,通用性,等等之类的。通过封装通讯库达到更高的层次,想想,别人使用自己的东西,说明自己牛XXXX啊,大师就是这样锻造出来的,接下来马上安排,鸿鹄之志从小事做起,振兴工业自动化,匹夫有责。
3、空谈误国,实干兴邦
1、创建vs项目
2、添加类库项目
3、创建目录及基础类
AreaCode.cs代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mitsubishi.Communication.MC.Mitsubishi.Base
{
/// <summary>
/// 存储区枚举
/// </summary>
public enum AreaCode
{
D = 0xA8,
X = 0x9C,
Y = 0x9D,
M = 0x90,
R = 0xAF,
S = 0x98,
TS = 0xC1,
CN = 0xC5
}
}
MelsecBase.cs代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Mitsubishi.Communication.MC.Mitsubishi.Base
{
/// <summary>
/// mc协议基类
/// </summary>
public class MelsecBase
{
/// <summary>
/// plc的ip地址
/// </summary>
public string _ip;
/// <summary>
/// plc的端口号
/// </summary>
public int _port;
/// <summary>
/// socket对象
/// </summary>
public Socket socket = null;
/// <summary>
/// 超时事件
/// </summary>
ManualResetEvent TimeoutObject = new ManualResetEvent(false);
/// <summary>
/// 连接状态
/// </summary>
bool connectState = false;
/// <summary>
/// 构造方法
/// </summary>
/// <param name="ip"></param>
/// <param name="port"></param>
public MelsecBase(string ip, short port)
{
_ip = ip;
_port = port;
// 初始化一个通信对象
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
/// <summary>
/// 连接PLC
/// </summary>
/// <param name="timeout">超时时间</param>
/// <returns></returns>
public Result Connect(int timeout = 50)
{
TimeoutObject.Reset();
Result result = new Result();
try
{
if (socket == null)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
int count = 0;
while (count < timeout)
{
if (!(!socket.Connected || (socket.Poll(200, SelectMode.SelectRead) && (socket.Available == 0))))
{
return result;
}
try
{
socket?.Close();
socket.Dispose();
socket = null;
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//异步连接
socket.BeginConnect(_ip, _port, callback =>
{
connectState = false;
var cbSocket = callback.AsyncState as Socket;
if (cbSocket != null)
{
connectState = cbSocket.Connected;
if (cbSocket.Connected)
{
cbSocket.EndConnect(callback);
}
}
TimeoutObject.Set();
}, socket);
TimeoutObject.WaitOne(2000, false);
if (!connectState)
{
throw new Exception();
}
else
{
break;
}
}
catch (SocketException ex)
{
if (ex.ErrorCode == 10060)
{
throw new Exception(ex.Message);
}
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
count++;
}
}
if (socket == null || !socket.Connected || ((socket.Poll(200, SelectMode.SelectRead) && (socket.Available == 0))))
{
throw new Exception("网络连接失败");
}
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
/// <summary>
/// 构建开始地址
/// </summary>
/// <param name="areaCode">存储区</param>
/// <param name="startAddr">开始地址</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public List<byte> StartToBytes(AreaCode areaCode, string startAddr)
{
List<byte> startBytes = new List<byte>();
if (areaCode == AreaCode.X || areaCode == AreaCode.Y)
{
string str = startAddr.ToString().PadLeft(8, '0');
for (int i = str.Length - 2; i >= 0; i -= 2)
{
string v = str[i].ToString() + str[i + 1].ToString();
startBytes.Add(Convert.ToByte(v, 16));
}
}
else
{
int addr = 0;
if (!int.TryParse(startAddr, out addr))
{
throw new Exception("软元件地址不支持!");
}
startBytes.Add((byte)(addr % 256));
startBytes.Add((byte)(addr / 256 % 256));
startBytes.Add((byte)(addr / 256 / 256 % 256));
startBytes.Add((byte)(addr / 256 / 256 / 256 % 256));
}
return startBytes;
}
/// <summary>
/// 发送报文
/// </summary>
/// <param name="reqBytes">字节集合</param>
/// <param name="count">字节长度</param>
/// <returns></returns>
public virtual List<byte> Send(List<byte> reqBytes, int count)
{
return null;
}
/// <summary>
/// 数据解析
/// </summary>
/// <typeparam name="T">读取的数据类型</typeparam>
/// <param name="datas">数据列表</param>
/// <param name="typeLen">类型长度</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public List<T> AnalysisDatas<T>(List<byte> datas, int typeLen)
{
List<T> resultDatas = new List<T>();
if (typeof(T) == typeof(bool))//bool类型
{
for (int i = 0; i < datas.Count; i++)
{
// 10 10 10 10 10
string binaryStr = Convert.ToString(datas[i], 2).PadLeft(8, '0');
dynamic state = binaryStr.Substring(0, 4) == "0001";
resultDatas.Add(state);
state = binaryStr.Substring(4) == "0001";
resultDatas.Add(state);
}
}
else//其他类型:ushort,short,float
{
for (int i = 0; i < datas.Count;)
{
List<byte> valueByte = new List<byte>();
for (int sit = 0; sit < typeLen * 2; sit++)
{
valueByte.Add(datas[i++]);
}
Type tBitConverter = typeof(BitConverter);
MethodInfo method = tBitConverter.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(mi => mi.ReturnType == typeof(T)) as MethodInfo;
if (method == null)
{
throw new Exception("未找到匹配的数据类型转换方法");
}
resultDatas.Add((T)method?.Invoke(tBitConverter, new object[] { valueByte.ToArray(), 0 }));
}
}
return resultDatas;
}
/// <summary>
/// 计算长度
/// </summary>
/// <typeparam name="T">读取的数据类型</typeparam>
/// <returns></returns>
public int CalculatLength<T>()
{
int typeLen = 1;
if (!typeof(T).Equals(typeof(bool)))
{
typeLen = Marshal.SizeOf<T>() / 2;// 每一个数据需要多少个寄存器
}
return typeLen;
}
/// <summary>
/// 获取数据的字节列表
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="values">数据列表</param>
/// <returns></returns>
public List<byte> GetDataBytes<T>(List<T> values)
{
List<byte> datas = new List<byte>();
int count = values.Count;
if (typeof(T) == typeof(bool))//bool类型的数据
{
dynamic value = false;
// 添加一个填充数据,保存一个完整字节
if (values.Count % 2 > 0)
{
values.Add(value);
}
for (int i = 0; i < values.Count; i += 2)
{
byte valueByte = 0;
if (bool.Parse(values[i].ToString()))
{
valueByte |= 16;
}
if (bool.Parse(values[i + 1].ToString()))
{
valueByte |= 1;
}
datas.Add(valueByte);
}
}
else //其他类型:float,short,int16
{
for (int i = 0; i < values.Count; i++)
{
dynamic value = values[i];
datas.AddRange(BitConverter.GetBytes(value)); // MC不需要字节的颠倒
}
}
return datas;
}
}
}
Result.cs代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mitsubishi.Communication.MC.Mitsubishi.Base
{
/// <summary>
/// 结果类
/// </summary>
/// <typeparam name="T"></typeparam>
public class Result<T>
{
/// <summary>
/// 状态
/// </summary>
public bool IsSuccessed { get; set; }
/// <summary>
/// 对应的消息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 数据列表
/// </summary>
public List<T> Datas { get; set; }
public Result() : this(true, "OK") { }
public Result(bool state, string msg) : this(state, msg, new List<T>()) { }
public Result(bool state, string msg, List<T> datas)
{
this.IsSuccessed = state; Message = msg; Datas = datas;
}
}
public class Result : Result<bool> { }
}
确保上面的三个类文件编译成功,继续干
4、编写核心的通信类A1E.cs
A1E.cs完整代码:
using Mitsubishi.Communication.MC.Mitsubishi.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Mitsubishi.Communication.MC.Mitsubishi
{
/// <summary>
/// A1E报文通讯库
/// </summary>
public class A1E : MelsecBase
{
/// <summary>
/// 构造方法
/// </summary>
/// <param name="ip"></param>
/// <param name="port"></param>
public A1E(string ip, short port) : base(ip, port)
{
}
#region 读取数据
/// <summary>
/// 读取数据
/// </summary>
/// <typeparam name="T">读取的数据类型</typeparam>
/// <param name="address">开始地址</param>
/// <param name="count">读取长度</param>
/// <returns></returns>
public Result<T> Read<T>(string address, short count)
{
AreaCode areaCode;
string start;
(areaCode, start) = this.AnalysisAddress(address);
return Read<T>(areaCode, start, count);
}
/// <summary>
/// 读取数据
/// </summary>
/// <typeparam name="T">读取的数据类型</typeparam>
/// <param name="areaCode">存储区代码</param>
/// <param name="startAddr">开始地址</param>
/// <param name="count">读取长度</param>
/// <returns></returns>
public Result<T> Read<T>(AreaCode areaCode, string startAddr, short count)
{
Result<T> result = new Result<T>();
try
{
var connectState = this.Connect();
if (!connectState.IsSuccessed)
{
throw new Exception(connectState.Message);
}
//读取类型
byte readCode = (byte)(typeof(T) == typeof(bool) ? 0x00 : 0x01);
//起始地址
List<byte> startBytes = this.StartToBytes(areaCode, startAddr);
//存储区代码
List<byte> areaBytes = this.AreaToBytes(areaCode);
//读取长度
int typeLen = this.CalculatLength<T>();
//组装报文
List<byte> command = new List<byte> {
readCode,///读取类型
0xFF,0x0A,0x00,//0xFF指PLC编号,0x0A,0x00指超时时间,超时时间是以250ms为单位
startBytes[0],startBytes[1],startBytes[2],startBytes[3], // 起始地址,占4个字节
areaBytes[0],areaBytes[1], // 存储区,占2个字节
(byte)(typeLen*count%256),// 读取长度,低位
(byte)(typeLen*count/256%256) // 读取长度,高位
};
//计算响应报文的长度
int respLen = typeLen * 2 * count;
if (typeof(T) == typeof(bool))
{
respLen = (int)Math.Ceiling(typeLen * count * 1.0 / 2);
}
//发送报文
List<byte> respBytes = this.Send(command, respLen);
//数据解析
result.Datas = this.AnalysisDatas<T>(respBytes, typeLen);
}
catch (Exception ex)
{
result = new Result<T>(false, ex.Message);
}
return result;
}
#endregion
#region 写入数据
/// <summary>
/// 写数据
/// </summary>
/// <typeparam name="T">写入的数据类型</typeparam>
/// <param name="values">数据值列表</param>
/// <param name="addr">开始地址</param>
/// <returns></returns>
public Result Write<T>(List<T> values, string addr)
{
AreaCode areaCode; string start;
(areaCode, start) = this.AnalysisAddress(addr);
return this.Write<T>(values, areaCode, start);
}
/// <summary>
/// 写数据
/// </summary>
/// <typeparam name="T">写入的数据类型</typeparam>
/// <param name="values">数据值列表</param>
/// <param name="areaCode">存储区代码</param>
/// <param name="startAddr">开始地址</param>
/// <returns></returns>
public Result Write<T>(List<T> values, AreaCode areaCode, string startAddr)
{
Result result = new Result();
try
{
var connectState = this.Connect();
if (!connectState.IsSuccessed)
{
throw new Exception(connectState.Message);
}
// 写操作的类型 //0x00 批量位读取 //0x01 批量字读取 //0x02 批量位写入 //0x03 批量字写入 //0x04 随机位写入 //0x05 随机字写入
byte writeCode = (byte)(typeof(T) == typeof(bool) ? 0x02 : 0x03);
//开始地址
List<byte> startBytes = this.StartToBytes(areaCode, startAddr);
//存储区代码
List<byte> areaBytes = this.AreaToBytes(areaCode);
//构建数据的字节列表
int count = values.Count;
List<byte> datas = this.GetDataBytes<T>(values);
//判断写入的长度,如果是float类型则长度要扩大2倍
int length = count;//长度等于值的个数
if (typeof(T) == typeof(float))
{
length = length * 2;
}
//拼装报文
List<byte> command = new List<byte> {
writeCode,
0xFF, 0x0A, 0x00,//0xFF指PLC编号,0x0A,0x00指超时时间,超时时间是以250ms为单位
startBytes[0], startBytes[1], startBytes[2], startBytes[3], // 起始地址
areaBytes[0], areaBytes[1], // 存储区
//写入的长度的低位和高位
(byte)(length % 256),
(byte)(length / 256 % 256),
};
command.AddRange(datas);//写入的具体数据
//发送报文
socket.Send(command.ToArray());
// 判断写入的结果
byte[] respBytes = new byte[2];
socket.Receive(respBytes);
if (respBytes[0] != (writeCode |= 0x80))
{
throw new Exception("响应报文结构异常。" + respBytes[0].ToString());
}
if (respBytes[1] != 0x00)
{
throw new Exception("响应异常。" + respBytes[1].ToString());
}
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
#endregion
#region PLC启停,区别功能码 0x13,0x14
public Result Run()
{
return PlcState(0x13);
}
public Result Stop()
{
return PlcState(0x14);
}
private Result PlcState(byte commandCode)
{
Result result = new Result();
try
{
var connectState = this.Connect();
if (!connectState.IsSuccessed)
{
throw new Exception(connectState.Message);
}
List<byte> commandBytes = new List<byte>
{
commandCode,0xFF,0x0A,0x00
};
socket.Send(commandBytes.ToArray());
// 先判断响应状态
byte[] respBytes = new byte[2];
socket.Receive(respBytes);
if (respBytes[0] != (commandCode |= 0x80))
{
throw new Exception("响应报文结构异常。" + respBytes[0].ToString());
}
if (respBytes[1] != 0x00)
{
throw new Exception("响应异常。" + respBytes[1].ToString());
}
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
#endregion
#region 内部方法
/// <summary>
/// 构建存储区代码
/// </summary>
/// <param name="areaCode">存储区代码</param>
/// <returns></returns>
private List<byte> AreaToBytes(AreaCode areaCode)
{
List<byte> areaBytes = new List<byte>();
string areaStr = areaCode.ToString();
areaBytes.AddRange(Encoding.ASCII.GetBytes(areaStr));
if (areaBytes.Count == 1)
{
areaBytes.Add(0x20);
}
areaBytes.Reverse(); //字节反转
return areaBytes;
}
/// <summary>
/// 发送报文
/// </summary>
/// <param name="reqBytes">报文字节集合</param>
/// <param name="len">报文字节长度</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public override List<byte> Send(List<byte> reqBytes, int len)
{
socket.Send(reqBytes.ToArray()); //发送报文
// 先判断响应状态
byte[] respBytes = new byte[2];
socket.Receive(respBytes);
if (respBytes[0] != (reqBytes[0] |= 0x80))
{
throw new Exception("响应报文结构异常。" + respBytes[0].ToString());
}
if (respBytes[1] != 0x00)
{
throw new Exception("响应异常。" + respBytes[1].ToString());
}
respBytes = new byte[len];
socket.Receive(respBytes, 0, len, SocketFlags.None);
return new List<byte>(respBytes);
}
/// <summary>
/// 地址解析,输入的地址:X100 X1A0 M100 D100 TN10
/// </summary>
/// <param name="address">地址字符串</param>
/// <returns>返回元组</returns>
public Tuple<AreaCode, string> AnalysisAddress(string address)
{
// 取两个字符
string area = address.Substring(0, 2);
if (!new string[] { "TN", "TS", "CS", "CN" }.Contains(area))
{
area = address.Substring(0, 1);
}
string start = address.Substring(area.Length);
// 返回元组(一个对象,该对象包括编号和地址)
var obj = new Tuple<AreaCode, string>((AreaCode)Enum.Parse(typeof(AreaCode), area), start);
return obj;
}
#endregion
}
}
确保项目编译成功,可以进行下一步
4、测试通讯库
1、添加项目引用
2、启动MC服务器
3、利用通讯库读写数据
1 读取X区100开始的4个bool数据
2、读取D区100开始的5个float数据
3、读取M区200开始的2个short数据
4、写入M区200开始的2个short数据
5、写入D区200开始的5个float数据
4、完整代码
using Mitsubishi.Communication.MC.Mitsubishi;
using Mitsubishi.Communication.MC.Mitsubishi.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Mitsubishi.Communication.Test
{
internal class Program
{
static void Main(string[] args)
{
MCLibTestA1E();
Console.WriteLine("执行完成!");
Console.ReadKey();
}
/// <summary>
/// 测试A-1E通讯库
/// </summary>
static void MCLibTestA1E()
{
A1E a1E = new A1E("192.168.1.7", 6000);
#region 读数据
Console.WriteLine("读取X区100开始的4个bool数据");
var result1 = a1E.Read<bool>(AreaCode.X, "100", 4);
if (result1.IsSuccessed)
{
result1.Datas.ForEach(d => Console.WriteLine(d));
}
else
{
Console.WriteLine(result1.Message);
}
Console.WriteLine("读取D区200开始的5个float数据");
var result2 = a1E.Read<float>(AreaCode.D, "200", 5);
if (result2.IsSuccessed)
{
result2.Datas.ForEach(d => Console.WriteLine(d));
}
else
{
Console.WriteLine(result2.Message);
}
Console.WriteLine("读取M区200开始的2个short数据");
var result3 = a1E.Read<short>(AreaCode.M, "200", 2);
if (result3.IsSuccessed)
{
result3.Datas.ForEach(d => Console.WriteLine(d));
}
else
{
Console.WriteLine(result3.Message);
}
#endregion
#region 写数据
Console.WriteLine("写入M区200开始的2个short数据");
var result4 = a1E.Write<short>(new List<short> { 61, 72 }, "M200");
if (result4.IsSuccessed)
{
Console.WriteLine(result4.Message);
}
Console.WriteLine("写入D区200开始的5个float数据");
var result5 = a1E.Write<float>(new List<float> { 3.2f, -2.5f, 0, 35, -98 }, "D200");
if (result5.IsSuccessed)
{
Console.WriteLine(result5.Message);
}
#endregion
}
}
}
5、小结
原创真的不容易,走过路过不要错过,点赞关注收藏又圈粉,共同致富。
原创真的不容易,走过路过不要错过,点赞关注收藏又圈粉,共同致富。
原创真的不容易,走过路过不要错过,点赞关注收藏又圈粉,共同致富