初始化上传

This commit is contained in:
2025-08-26 08:37:44 +08:00
commit 31d81b91b6
448 changed files with 80981 additions and 0 deletions

View File

@@ -0,0 +1,587 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace EIP调试.EIP
{
/// <summary>
/// CIP通信工具类
/// </summary>
public class CIPHelper
{
/// <summary>
/// IP地址
/// </summary>
public string IpAddress { get; set; }
/// <summary>
/// 通信端口号
/// </summary>
public int Port { get; set; }
public bool IsConnected
{
get
{
if (client == null)
return false;
return client.Connected;
}
}
private Socket client;
public int SessionHandle = 0;
/// <summary>
/// 默认无参构造
/// </summary>
public CIPHelper()
{
}
/// <summary>
/// 构造方法传递通信IP与端口
/// </summary>
public CIPHelper(string ip, int port)
{
this.IpAddress = ip;
this.Port = port;
}
/// <summary>
/// 建立连接
/// </summary>
public void Connect()
{
//连接
try
{
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
client.Connect(new IPEndPoint(IPAddress.Parse(IpAddress), Port));
}
catch (Exception ex)
{
}
}
/// <summary>
/// 注册会话
/// </summary>
public bool RegisterSession()
{
//注册会话
if (client != null && client.Connected)
{
byte[] registerSessionBytes = { (byte)0x65, (byte)0x00, (byte)0x04, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00 };
client.Send(registerSessionBytes);
Thread.Sleep(20);
byte[] rev = new byte[1024];
try
{
int length = client.Receive(rev);
byte[] data = new byte[length];
Array.Copy(rev, 0, data, 0, length);
if (data[0] == 0x65)
return GetSessionHandle(data);
}
catch
{
return false;
}
}
return false;
}
/// <summary>
/// 读取标签值
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
public object ReadTag(string tag)
{
if (client == null)
return null;
if (!client.Connected)
return null;
if (SessionHandle == 0)
return null;
byte[] d = GetReadTagBytes(tag);
client.Send(d);
Thread.Sleep(20);
byte[] rev = new byte[1024];
try
{
int length = client.Receive(rev);
byte[] data = new byte[length];
Array.Copy(rev, 0, data, 0, length);
if (data[0] == 0x6f)
return ReadTagVal(data);
return null;
}
catch
{
return null;
}
}
public bool WriteTag(string tag, object value)
{
if (client == null)
return false;
if (!client.Connected)
return false;
if (SessionHandle == 0)
return false;
byte[] d = GetWriteTagBytes(tag, value);
client.Send(d);
Thread.Sleep(20);
byte[] rev = new byte[1024];
try
{
int length = client.Receive(rev);
byte[] data = new byte[length];
Array.Copy(rev, 0, data, 0, length);
if (data[0] == 0x6f)
return WriteTagVal(data);
return false;
}
catch
{
return false;
}
}
/// <summary>
/// 注销会话
/// </summary>
/// <returns></returns>
public bool UnRegisterSession()
{
if (client == null)
return false;
if (!client.Connected)
return false;
if (SessionHandle == 0)
return false;
List<byte> UnregisterSession = new List<byte>();
UnregisterSession.AddRange(new List<byte> { (byte)0x66, (byte)0x00, (byte)0x00, (byte)0x00 });
UnregisterSession.AddRange(BitConverter.GetBytes(SessionHandle));
UnregisterSession.AddRange(new List<byte> { (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00 });
try
{
client.Send(UnregisterSession.ToArray());
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 断开连接
/// </summary>
public void DisConnect()
{
if (client == null)
return;
//断开
try
{
if (IsConnected)
{
client.Close();
}
client = null;
SessionHandle = 0;
}
catch (Exception ex)
{
client = null;
SessionHandle = 0;
}
}
#region Private
/// <summary>
/// 解析写标签数据
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
private bool WriteTagVal(byte[] data)
{
int byte_index = 0;
short var1 = BitConverter.ToInt16(data, 0);//命令码
short var2 = BitConverter.ToInt16(data, 2);//去除Hearder后的长度
if ((var2 + 24) != data.Length)//24是Hearder封装头的长度 封装头长度+后面报文长度 应该等于总长度 否则报文错误
{
return false;
}
int sessionHandle = BitConverter.ToInt32(data, 4);
if (sessionHandle != this.SessionHandle)
{
//如果会话句柄不一致 返回
return false;
}
int state = BitConverter.ToInt32(data, 8);
if (state != 0)
{
//会话状态不正确
return false;
}
//00 00 00 00 01 00 02 00 00 00 00 00 B2 00 08 00 CC 00 00 00 C1 00 00 00
//发送方描述
byte_index = 12; //8
//选项 4字节
byte_index = 20;
//接口句柄 4字节
byte_index = 24;
//超时 2字节
byte_index = 28;
//项数 2字节
byte_index = 30;
//连接的地址项 2字节
byte_index = 32;
//连接地址项长度 2字节
byte_index = 34;
//未连接数据项 2字节
byte_index = 36;
//连接长度 2字节
byte_index = 38;
//数据开始
byte_index = 40;
for (int i = byte_index; i < data.Length; i++)
{
if (data[i] != 0xCD)
continue;
//填充字节 1字节
if (BitConverter.ToInt16(data, i + 2) != 0x00)
{
return true;
}
return false;
}
return false;
}
/// <summary>
/// 解析读标签数据
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
private object ReadTagVal(byte[] data)
{
int byte_index = 0;
short var1 = BitConverter.ToInt16(data, 0);//命令码
short var2 = BitConverter.ToInt16(data, 2);//去除Hearder后的长度
if ((var2 + 24) != data.Length)//24是Hearder封装头的长度 封装头长度+后面报文长度 应该等于总长度 否则报文错误
{
return null;
}
int sessionHandle = BitConverter.ToInt32(data, 4);
if (sessionHandle != this.SessionHandle)
{
//如果会话句柄不一致 返回
return null;
}
int state = BitConverter.ToInt32(data, 8);
if (state != 0)
{
//会话状态不正确
return null;
}
//发送方描述
byte_index = 12; //8
//选项 4字节
byte_index = 20;
//接口句柄 4字节
byte_index = 24;
//超时 2字节
byte_index = 28;
//项数 2字节
byte_index = 30;
//连接的地址项 2字节
byte_index = 32;
//连接地址项长度 2字节
byte_index = 34;
//未连接数据项 2字节
byte_index = 36;
//连接长度 2字节
byte_index = 38;
//数据开始
byte_index = 40;
for (int i = byte_index; i < data.Length; i++)
{
if (data[i] != 0xCC)
continue;
//服务标识 0xCC
//填充字节 1字节
if (BitConverter.ToInt16(data, i + 2) != 0x00) // data[var4] != 0x00 || data[var4 + 1] != 0x00)
{
return null;
}
//数据类型
short dataType = BitConverter.ToInt16(data, i + 4);
if (dataType == 0x00C1)//BOOL
{
bool value = Convert.ToBoolean(((short)data[i + 6] & 0x00ff) | ((short)data[i + 7] << 8));
return value;
}
if (dataType == 0x00D1)//byte
{
byte value = data[i + 6];
return value;
}
if (dataType == 0x00C3)//int
{
short value = BitConverter.ToInt16(data, i + 6);// ((short)data[i+6] & 0x00ff) | ((short)data[i+7] << 8);
return value;
}
if (dataType == 0x00C4)//long型
{
int value = BitConverter.ToInt32(data, i + 6);
return value;
}
if (dataType == 0x00CA)//float
{
float value = BitConverter.ToSingle(data, i + 6);
return value;
}
}
return null;
}
/// <summary>
/// 写标签报文数据
/// </summary>
/// <param name="tag"></param>
/// <param name="value"></param>
/// <returns></returns>
private byte[] GetWriteTagBytes(string tag, object value)
{
byte[] TagStringToAscii;
byte[] Var4 = Encoding.Default.GetBytes(tag);
if (Var4.Length % 2 == 0)
{
TagStringToAscii = new byte[Var4.Length];
for (int i = 0; i < Var4.Length; i++)
{
TagStringToAscii[i] = Var4[i];
}
}
else
{
TagStringToAscii = new byte[Var4.Length + 1];
for (int i = 0; i < Var4.Length; i++)
{
TagStringToAscii[i] = Var4[i];
}
TagStringToAscii[Var4.Length] = 0x00;
}
//CIP协议数据 =================================================
List<byte> Cip_Msg = new List<byte>();
Cip_Msg.Add((byte)0x4D);//服务标识
Cip_Msg.Add((byte)((TagStringToAscii.Length + 2) / 2));//CIP长度多少字 从标识开始 大读取标签长度结束
Cip_Msg.Add((byte)0x91);//固定
Cip_Msg.Add((byte)TagStringToAscii.Length);//PLC标签长度 多少个字节
Cip_Msg.AddRange(TagStringToAscii);//添加标签
//根据类型
if (value is bool)
{
Cip_Msg.AddRange(new byte[] { 0xC1, 0x00 });
if ((bool)value)
{
Cip_Msg.AddRange(new byte[] { 0x01, 0x00 });
}
else
{
Cip_Msg.AddRange(new byte[] { 0x00, 0x00 });
}
}
else if (value is byte)
{
Cip_Msg.AddRange(new byte[] { 0xD1, 0x00 });
Cip_Msg.Add((byte)value);
}
else if (value is short)
{
Cip_Msg.AddRange(new byte[] { 0xC3, 0x00 });
Cip_Msg.AddRange(BitConverter.GetBytes((short)value));
}
else if (value is int)
{
Cip_Msg.AddRange(new byte[] { 0xC4, 0x00 });
Cip_Msg.AddRange(BitConverter.GetBytes((int)value));
}
else if (value is float)
{
Cip_Msg.AddRange(new byte[] { 0xCA, 0x00 });
Cip_Msg.AddRange(BitConverter.GetBytes((float)value));
}
List<byte> Slot = new List<byte> { (byte)0x01, (byte)0x00, (byte)0x01, (byte)0x00 }; //槽号
List<byte> Cip_Msg0 = new List<byte>();
Cip_Msg0.Add((byte)0x52);//命令
Cip_Msg0.Add((byte)0X02);//请求路径长度
Cip_Msg0.AddRange(new byte[] { 0x20, 0x06, 0x24, 0x01 });//默认请求路径
Cip_Msg0.AddRange(new byte[] { 0x0A, 0xF0 });//默认超时
Cip_Msg0.AddRange(BitConverter.GetBytes((short)Cip_Msg.Count)); //后面报文长度 不包含PLC槽号
Cip_Msg0.AddRange(Cip_Msg);//添加CIP报文
List<byte> C_S_D = new List<byte>();
C_S_D.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 });//接口句柄 CIP
C_S_D.AddRange(new byte[] { 0x01, 0x00 });//超时时间 默认
C_S_D.AddRange(new byte[] { 0x02, 0x00 });//项数 默认
C_S_D.AddRange(new byte[] { 0x00, 0x00 });//空地址项 默认
C_S_D.AddRange(new byte[] { 0x00, 0x00 });//空地址长度 默认
C_S_D.AddRange(new byte[] { 0xB2, 0x00 });//未连接项 默认
C_S_D.AddRange(BitConverter.GetBytes((short)(Cip_Msg0.Count + Slot.Count)));//CIP报文包的长度 包括槽号
List<byte> Header = new List<byte>();
Header.AddRange(new byte[] { 0x6F, 0x00 });//命令码
Header.AddRange(BitConverter.GetBytes((short)(Cip_Msg0.Count + C_S_D.Count + Slot.Count)));//后面报文的长度 特定命令数据 和 CIP报文长度 和PLC槽号
Header.AddRange(BitConverter.GetBytes(SessionHandle));//添加会话句柄
Header.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 });//添加状态
Header.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); //发送方描述
Header.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 });//选项默认
List<byte> EtherNet_IP_CIP_MSG = new List<byte>();//单标签读取完整报文
EtherNet_IP_CIP_MSG.AddRange(Header);//添加Header 封装头
EtherNet_IP_CIP_MSG.AddRange(C_S_D);//添加特定命令数据
EtherNet_IP_CIP_MSG.AddRange(Cip_Msg0);//添加CIP报文
EtherNet_IP_CIP_MSG.AddRange(Slot);//添加槽号
return EtherNet_IP_CIP_MSG.ToArray();
}
/// <summary>
/// 读标签报文数据
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
private byte[] GetReadTagBytes(string tag)
{
byte[] TagStringToAscii;
byte[] Var4 = Encoding.Default.GetBytes(tag);
if (Var4.Length % 2 == 0)
{
TagStringToAscii = new byte[Var4.Length];
for (int i = 0; i < Var4.Length; i++)
{
TagStringToAscii[i] = Var4[i];
}
}
else
{
TagStringToAscii = new byte[Var4.Length + 1];
for (int i = 0; i < Var4.Length; i++)
{
TagStringToAscii[i] = Var4[i];
}
TagStringToAscii[Var4.Length] = 0x00;
}
//CIP协议数据 =================================================
List<byte> Cip_Msg = new List<byte>();
Cip_Msg.Add((byte)0x4C);//服务标识
Cip_Msg.Add((byte)((TagStringToAscii.Length + 2) / 2));//CIP长度多少字 从标识开始 大读取标签长度结束
Cip_Msg.Add((byte)0x91);//固定
Cip_Msg.Add((byte)TagStringToAscii.Length);//PLC标签长度 多少个字节
Cip_Msg.AddRange(TagStringToAscii);//添加标签
Cip_Msg.Add((byte)0x01);
Cip_Msg.Add((byte)0x00); // 读取长度
List<byte> Slot = new List<byte> { (byte)0x01, (byte)0x00, (byte)0x01, (byte)0x00 }; //槽号
List<byte> Cip_Msg0 = new List<byte>();
Cip_Msg0.Add((byte)0x52);//命令
Cip_Msg0.Add((byte)0X02);//请求路径长度
Cip_Msg0.AddRange(new byte[] { 0x20, 0x06, 0x24, 0x01 });//默认请求路径
Cip_Msg0.AddRange(new byte[] { 0x0A, 0xF0 });//默认超时
Cip_Msg0.AddRange(BitConverter.GetBytes((short)Cip_Msg.Count)); //后面报文长度 不包含PLC槽号
Cip_Msg0.AddRange(Cip_Msg);//添加CIP报文
List<byte> C_S_D = new List<byte>();
C_S_D.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 });//接口句柄 CIP
C_S_D.AddRange(new byte[] { 0x01, 0x00 });//超时时间 默认
C_S_D.AddRange(new byte[] { 0x02, 0x00 });//项数 默认
C_S_D.AddRange(new byte[] { 0x00, 0x00 });//空地址项 默认
C_S_D.AddRange(new byte[] { 0x00, 0x00 });//空地址长度 默认
C_S_D.AddRange(new byte[] { 0xB2, 0x00 });//未连接项 默认
C_S_D.AddRange(BitConverter.GetBytes((short)(Cip_Msg0.Count + Slot.Count)));//CIP报文包的长度 包括槽号
List<byte> Header = new List<byte>();
Header.Add((byte)0x6F); Header.Add((byte)0x00);//命令码
Header.AddRange(BitConverter.GetBytes((short)(Cip_Msg0.Count + C_S_D.Count + Slot.Count)));//后面报文的长度 特定命令数据 和 CIP报文长度 和PLC槽号
Header.AddRange(BitConverter.GetBytes(SessionHandle));//添加会话句柄
Header.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 });//添加状态
Header.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); //发送方描述
Header.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x00 });//选项默认
List<byte> EtherNet_IP_CIP_MSG = new List<byte>();//单标签读取完整报文
EtherNet_IP_CIP_MSG.AddRange(Header);//添加Header 封装头
EtherNet_IP_CIP_MSG.AddRange(C_S_D);//添加特定命令数据
EtherNet_IP_CIP_MSG.AddRange(Cip_Msg0);//添加CIP报文
EtherNet_IP_CIP_MSG.AddRange(Slot);//添加槽号
return EtherNet_IP_CIP_MSG.ToArray();
}
/// <summary>
/// 注册会话结果解析
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
private bool GetSessionHandle(byte[] data)
{
//short sessionLength = BitConverter.ToInt16(data, 2);// (Convert.ToInt16(data[3]) * 256) | Convert.ToInt16(data[2]);
//byte[] var1 = new byte[4];
//for (int i = 0; i < 4; i++)
//{
// var1[i] = data[sessionLength + 4 + i];
//}
int state = BitConverter.ToInt32(data, 8);
if (state == 0)//判断报文状态
{
SessionHandle = BitConverter.ToInt32(data, 4);
//SessionHandle = new byte[sessionLength];
//for (int i = 0; i < sessionLength; i++)
//{
// SessionHandle[i] = data[4 + i];
//}
return true;
}
else
{
return false;
}
}
#endregion
}
}

View File

@@ -0,0 +1,601 @@
using System;
using System.Collections.Generic;
using System.Drawing.Printing;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace .Utility.Network
{
public class FtpHelper
{
private string ip;
private int port = 21;
private string ftpUserId;
private string ftpPassword;
private Encoding encoding;
private string FtpUrl
{
get
{
return $"ftp://{ip}:{port}/";
}
}
public FtpHelper(string ip) : this(ip, 21) { }
public FtpHelper(string ip, int port) : this(ip, 21, Encoding.GetEncoding("GB2312")) { }
/// <summary>
/// 匿名
/// </summary>
/// <param name="ip"></param>
public FtpHelper(string ip, Encoding encoding) : this(ip, 21, encoding) { }
public FtpHelper(string ip, int port, Encoding encoding) : this(ip, port, "anonymous", "", encoding) { }
public FtpHelper(string ip, string userName, string password) : this(ip, 21, userName, password) { }
public FtpHelper(string ip, int port, string userName, string password) : this(ip, port, userName, password, Encoding.UTF8) { }
/// <summary>
/// 指定用户名密码
/// </summary>
/// <param name="ip"></param>
/// <param name="userName"></param>
/// <param name="password"></param>
public FtpHelper(string ip, string userName, string password, Encoding encoding) : this(ip, 21, userName, password, encoding) { }
/// <summary>
/// 指定用户名密码
/// </summary>
/// <param name="ip"></param>
/// <param name="userName"></param>
/// <param name="password"></param>
public FtpHelper(string ip, int port, string userName, string password, Encoding encoding)
{
this.ip = ip;
this.port = port;
this.ftpUserId = userName;
this.ftpPassword = password;
this.encoding = encoding;
}
/// <summary>
/// 获得文件的最后更新时间
/// </summary>
/// <param name="ftpPath"></param>
/// <param name="ftpUserId"></param>
/// <param name="ftpPassword"></param>
/// <param name="relativeFilePath"></param>
/// <returns></returns>
public DateTime GetFileLastModified(string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.UseBinary = true;
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = WebRequestMethods.Ftp.GetDateTimestamp;
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
DateTime dt = response.LastModified;
response.Close();
response = null;
request = null;
return dt;
}
catch (Exception ex)
{
Console.WriteLine("FtpUtil.GetFileLastModified 获得文件的最后更新时间 错误");
throw ex;
}
}
/// <summary>
/// 列出ftp目录下的所有文件
/// </summary>
/// <param name="ftpPath"></param>
/// <param name="ftpUserId"></param>
/// <param name="ftpPassword"></param>
/// <returns></returns>
public List<FtpFileInfo> GetList(string ftpPath)
{
List<string> fileList = GetFileDetail(ftpPath);
List<FtpFileInfo> list = new List<FtpFileInfo>();
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = WebRequestMethods.Ftp.ListDirectory;
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
MemoryStream stream = new MemoryStream();
byte[] bArr = new byte[1024 * 1024];
int size = responseStream.Read(bArr, 0, (int)bArr.Length);
while (size > 0)
{
stream.Write(bArr, 0, size);
size = responseStream.Read(bArr, 0, (int)bArr.Length);
}
stream.Seek(0, SeekOrigin.Begin);
responseStream.Close();
string str = encoding.GetString(stream.ToArray());
foreach (string line in str.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
{
if (line == "." || line == "..")
continue;
string detail = fileList.Where(it => it.EndsWith(line)).FirstOrDefault();
if (detail == null || detail.Length == 0)
{
continue;
}
if (detail.ToUpper().Contains("<DIR>") || detail.ToUpper().Contains("DR"))
{
list.Add(new FtpFileInfo { IsDirectory = true, Name = line });
}
else
{
list.Add(new FtpFileInfo { IsDirectory = false, Name = line });
}
}
return list.OrderByDescending(it => it.IsDirectory).ToList();
}
catch (Exception ex)
{
Console.WriteLine("FtpUtil.GetFile 列出ftp目录下的所有文件 错误");
throw ex;
}
}
public List<string> GetFileDetail(string ftpPath)
{
List<string> list = new List<string>();
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = WebRequestMethods.Ftp.ListDirectoryDetails;
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
MemoryStream stream = new MemoryStream();
byte[] bArr = new byte[1024 * 1024];
int size = responseStream.Read(bArr, 0, (int)bArr.Length);
while (size > 0)
{
stream.Write(bArr, 0, size);
size = responseStream.Read(bArr, 0, (int)bArr.Length);
}
stream.Seek(0, SeekOrigin.Begin);
responseStream.Close();
string str = encoding.GetString(stream.ToArray());
foreach (string line in str.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
{
list.Add(line);
}
return list;
}
catch (Exception ex)
{
Console.WriteLine("FtpUtil.GetFile 列出ftp目录下的所有文件 错误");
throw ex;
}
}
/// <summary>
/// 判断是否是文件
/// </summary>
/// <param name="ftpPath"></param>
/// <param name="ftpUserId"></param>
/// <param name="ftpPassword"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public bool IsFileExist(string ftpPath, string fileName)
{
List<FtpFileInfo> list = GetList(ftpPath);
if (list.Any(it => !it.IsDirectory && it.Name == fileName))
{
return true;
}
return false;
}
#region
/// <summary>
/// 文件夹是否存在
/// </summary>
/// <param name="ftpPath"></param>
/// <param name="folderName"></param>
/// <returns></returns>
public bool FolderExists(string ftpPath, string folderName)
{
List<FtpFileInfo> list = GetList(ftpPath);
if (list.Any(it => it.IsDirectory && it.Name == folderName))
{
return true;
}
return false;
}
#endregion
#region
/// <summary>
/// 创建文件夹
/// </summary>
/// <param name="ftpPath">ftp路径</param>
/// <param name="folderName">文件夹名称</param>
public bool CreateFolder(string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = "MKD";
((FtpWebResponse)request.GetResponse()).Close();
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
#region
/// <summary>
/// 文件是否存在
/// </summary>
/// <param name="ftpPath">ftp路径</param>
/// <param name="ftpUserId">用户名</param>
/// <param name="ftpPassword">密码</param>
/// <param name="relativeFilePath">文件相对路径</param>
public bool FileExists(string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
string folderName = Path.GetDirectoryName(ftpPath);
string fileName = Path.GetFileName(ftpPath);
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = "LIST";
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
MemoryStream stream = new MemoryStream();
byte[] bArr = new byte[1024 * 1024];
int size = responseStream.Read(bArr, 0, (int)bArr.Length);
while (size > 0)
{
stream.Write(bArr, 0, size);
size = responseStream.Read(bArr, 0, (int)bArr.Length);
}
stream.Seek(0, SeekOrigin.Begin);
responseStream.Close();
string str = encoding.GetString(stream.ToArray());
foreach (string line in str.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries))
{
if (!line.ToUpper().Contains("DIR") && !line.ToUpper().Contains("DR"))
{
int pos = line.LastIndexOf(" ");
string strFileName = line.Substring(pos).Trim();
if (strFileName == fileName)
{
return true;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("FtpUtil.FileExists 判断文件是否存在 错误");
throw ex;
}
return false;
}
#endregion
#region
/// <summary>
/// 下载文件
/// </summary>
/// <param name="ftpPath">ftp路径</param>
/// <param name="ftpUserId">用户名</param>
/// <param name="ftpPassword">密码</param>
/// <param name="relativeFilePath">文件相对路径</param>
public MemoryStream DownloadFile(string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = "RETR";
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
MemoryStream stream = new MemoryStream();
byte[] bArr = new byte[1024 * 1024];
int size = responseStream.Read(bArr, 0, (int)bArr.Length);
while (size > 0)
{
stream.Write(bArr, 0, size);
size = responseStream.Read(bArr, 0, (int)bArr.Length);
}
stream.Seek(0, SeekOrigin.Begin);
responseStream.Close();
return stream;
}
catch (Exception ex)
{
Console.WriteLine("FtpUtil.DownloadFile 下载文件 错误");
throw ex;
}
}
/// <summary>
/// 下载文件
/// </summary>
/// <param name="ftpPath">ftp路径</param>
/// <param name="ftpUserId">用户名</param>
/// <param name="ftpPassword">密码</param>
/// <param name="relativeFilePath">文件相对路径</param>
public void DownloadFile(string ftpPath, string localFile)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = "RETR";
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
FileStream stream = new FileStream(localFile, FileMode.OpenOrCreate, FileAccess.Write);
byte[] bArr = new byte[1024 * 1024];
int size = responseStream.Read(bArr, 0, (int)bArr.Length);
while (size > 0)
{
stream.Write(bArr, 0, size);
size = responseStream.Read(bArr, 0, (int)bArr.Length);
}
stream.Seek(0, SeekOrigin.Begin);
responseStream.Close();
stream.Dispose();
}
catch (Exception ex)
{
Console.WriteLine("FtpUtil.DownloadFile 下载文件 错误");
throw ex;
}
}
#endregion
#region
/// <summary>
/// 异步下载文件
/// </summary>
/// <param name="ftpPath">ftp路径</param>
public async Task<MemoryStream> DownloadFileAsync(string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = "RETR";
FtpWebResponse response = (FtpWebResponse)(await request.GetResponseAsync());
Stream responseStream = response.GetResponseStream();
MemoryStream stream = new MemoryStream();
byte[] bArr = new byte[1024 * 1024];
int size = await responseStream.ReadAsync(bArr, 0, (int)bArr.Length);
while (size > 0)
{
stream.Write(bArr, 0, size);
size = await responseStream.ReadAsync(bArr, 0, (int)bArr.Length);
}
stream.Seek(0, SeekOrigin.Begin);
responseStream.Close();
return stream;
}
catch (Exception ex)
{
Console.WriteLine("FtpUtil.DownloadFileAsync 异步下载文件 错误");
throw ex;
}
}
#endregion
#region
/// <summary>
/// 上传文件
/// </summary>
/// <param name="ftpPath">ftp路径</param>
/// <param name="relativeFilePath">文件相对路径</param>
/// <param name="bArr">文件内容</param>
public bool UploadFile(string ftpPath, byte[] bArr)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = "STOR";
Stream postStream = request.GetRequestStream();
int pageSize = 1024 * 1024;
int index = 0;
while (index < bArr.Length)
{
if (bArr.Length - index > pageSize)
{
postStream.Write(bArr, index, pageSize);
index += pageSize;
}
else
{
postStream.Write(bArr, index, bArr.Length - index);
index = index + (bArr.Length - index);
}
}
postStream.Close();
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
StreamReader sr = new StreamReader(responseStream, Encoding.UTF8);
string result = sr.ReadToEnd();
responseStream.Close();
return true;
}
catch (Exception ex)
{
return false;
}
}
/// <summary>
/// 上传文件
/// </summary>
/// <param name="localFilePath">本地文件</param>
/// <param name="ftpPath">远端路径</param>
/// <returns></returns>
public bool UploadFile(string localFilePath, string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
byte[] bytes = new byte[1024 * 1024];
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = "STOR";
FileStream stream = new FileStream(localFilePath, FileMode.Open);
Stream postStream = request.GetRequestStream();
while (true)
{
int count = stream.Read(bytes, 0, bytes.Length);
if (count == 0)
break;
postStream.Write(bytes, 0, count);
}
postStream.Close();
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();
StreamReader sr = new StreamReader(responseStream, Encoding.UTF8);
string result = sr.ReadToEnd();
responseStream.Close();
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
#region
/// <summary>
/// 删除文件
/// </summary>
/// <param name="ftpPath">ftp路径</param>
/// <param name="ftpUserId">用户名</param>
/// <param name="ftpPassword">密码</param>
/// <param name="relativeFilePath">文件相对路径</param>
public bool DeleteFile(string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = WebRequestMethods.Ftp.DeleteFile;
((FtpWebResponse)request.GetResponse()).Close();
return true;
}
catch (Exception ex)
{
throw ex;
}
}
public bool Rename(string ftpPath, string newName)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = WebRequestMethods.Ftp.Rename;
request.RenameTo = newName;
((FtpWebResponse)request.GetResponse()).Close();
return true;
}
catch (Exception ex)
{
throw ex;
}
}
public bool DeleteDirectory(string ftpPath)
{
try
{
string url = Uri.EscapeUriString(FtpUrl + ftpPath);
if (ftpPath.EndsWith("/"))
ftpPath = ftpPath.Substring(0, ftpPath.Length - 1);
FtpWebRequest request = null;
request = (FtpWebRequest)WebRequest.Create(new Uri(url));
request.Credentials = new NetworkCredential(ftpUserId, ftpPassword);
request.Method = WebRequestMethods.Ftp.RemoveDirectory;
((FtpWebResponse)request.GetResponse()).Close();
return true;
}
catch (Exception ex)
{
throw ex;
}
}
#endregion
}
public class FtpFileInfo
{
public bool IsDirectory { get; set; }
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,321 @@
using MES.Utility.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace .Utility.Network
{
public class HttpUtils
{
public static bool DoGetFile(string url, string filePath, int? timeout)
{
var task = DoGetFileAsync(url, filePath);
if (timeout != null)
{
if (!task.Wait(timeout.Value))
{
return false;
}
}
return task.Result;
}
public static byte[] DoGetFile(string url, int? timeout)
{
var task = DoGetFileAsync(url);
if (timeout != null)
{
if (!task.Wait(timeout.Value))
{
return null;
}
}
return task.Result;
}
public static async Task<bool> DoGetFileAsync(string url, string filePath)
{
try
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(url);
var fileStream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write);
await response.Content.CopyToAsync(fileStream);
fileStream.Close();
return true;
}
catch
{
return false;
}
}
public static async Task<byte[]> DoGetFileAsync(string url)
{
try
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(url);
MemoryStream memoryStream = new MemoryStream();
return await response.Content.ReadAsByteArrayAsync();
}
catch
{
return null;
}
}
/// <summary>
/// 通过Get请求数据
/// </summary>
/// <param name="url"></param>
/// <param name="parametersDict"></param>
/// <param name="timeout"></param>
/// <returns></returns>
public static string DoGet(string url, Dictionary<string, string> parametersDict, int? timeout)
{
var task = DoGetAsync(url, parametersDict);
if (timeout != null)
{
if (!task.Wait(timeout.Value))
{
return null;
}
}
return task.Result;
}
/// <summary>
/// 通过Get请求数据
/// </summary>
/// <param name="url"></param>
/// <param name="parametersDict"></param>
/// <returns></returns>
public static async Task<string> DoGetAsync(string url, Dictionary<string, string> parametersDict)
{
try
{
if (!url.Contains("?"))
url = url + "?";
foreach (string key in parametersDict.Keys)
{
url = url + key + "=" + parametersDict[key] + "&";
}
url = url.Substring(0, url.Length - 1);
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(url);
string str= await response.Content.ReadAsStringAsync();
return str;
}
catch
{
return string.Empty;
}
}
/// <summary>
/// 通过Post上传文件multipart/form-data
/// </summary>
/// <param name="url"></param>
/// <param name="parms"></param>
/// <param name="fileParms"></param>
/// <param name="timeout"></param>
/// <returns></returns>
public static string DoPostFile(string url, Dictionary<string, string> parms, Dictionary<string, string> fileParms, int? timeout)
{
var task = DoPostFileAsync(url, parms, fileParms);
if (timeout != null)
{
if (!task.Wait(timeout.Value))
{
return null;
}
}
return task.Result;
}
/// <summary>
/// 通过Post上传文件multipart/form-data
/// </summary>
/// <param name="url"></param>
/// <param name="parms"></param>
/// <param name="fileParms"></param>
/// <returns></returns>
public static async Task<string> DoPostFileAsync(string url, Dictionary<string, string> parms, Dictionary<string, string> fileParms)
{
try
{
var httpClient = new HttpClient();
var content = new MultipartFormDataContent();
if (parms != null)
{
foreach (KeyValuePair<string, string> keyValue in parms)
{
content.Add(new StringContent(keyValue.Value), keyValue.Key);
}
}
if (fileParms != null)
{
foreach (KeyValuePair<string, string> keyValue in fileParms)
{
string filePath = keyValue.Value;
string fileName = Path.GetFileName(filePath);
if (!File.Exists(filePath))
{
continue;
}
content.Add(new ByteArrayContent(File.ReadAllBytes(filePath)), keyValue.Key, fileName);
}
}
var response = await httpClient.PostAsync(url, content);
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
return string.Empty;
}
}
/// <summary>
/// 通过Post请求数据 application/x-www-form-urlencoded
/// </summary>
/// <param name="url"></param>
/// <param name="parms"></param>
/// <param name="timeout"></param>
/// <returns></returns>
public static string DoPostForm(string url, Dictionary<string, string> parms, int? timeout)
{
var task = DoPostFormAsync(url, parms);
if (timeout != null)
{
if (!task.Wait(timeout.Value))
{
return null;
}
}
return task.Result;
}
/// <summary>
/// 通过Post请求数据 application/x-www-form-urlencoded
/// </summary>
/// <param name="url"></param>
/// <param name="parms"></param>
/// <returns></returns>
public static async Task<string> DoPostFormAsync(string url, Dictionary<string, string> parms)
{
try
{
var httpClient = new HttpClient();
FormUrlEncodedContent formData;
if (parms != null)
{
formData = new FormUrlEncodedContent(parms);
}
else
{
formData = new FormUrlEncodedContent(new Dictionary<string, string>());
}
var response = await httpClient.PostAsync(url, formData);
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
return string.Empty;
}
}
/// <summary>
/// 通过Post请求数据 application/x-www-form-urlencoded
/// </summary>
/// <param name="url"></param>
/// <param name="json"></param>
/// <param name="timeout"></param>
/// <returns></returns>
public static string DoPostJson(string url, string json, int? timeout)
{
var task = DoPostJsonAsync(url, json);
if (timeout != null)
{
if (!task.Wait(timeout.Value))
{
return null;
}
}
return task.Result;
}
/// <summary>
/// 通过Post请求数据 application/x-www-form-urlencoded
/// </summary>
/// <param name="url"></param>
/// <param name="json"></param>
/// <returns></returns>
public static async Task<string> DoPostJsonAsync(string url, string json)
{
try
{
var httpClient = new HttpClient();
StringContent jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(url, jsonContent);
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
return string.Empty;
}
}
/// <summary>
/// 通过Post请求数据 application/x-www-form-urlencoded
/// </summary>
/// <param name="url"></param>
/// <param name="data"></param>
/// <param name="timeout"></param>
/// <returns></returns>
public static string DoPostData(string url, object data, int? timeout)
{
var task = DoPostDataAsync(url, data);
if (timeout != null)
{
if (!task.Wait(timeout.Value))
{
return null;
}
}
return task.Result;
}
/// <summary>
/// 通过Post请求数据 application/x-www-form-urlencoded
/// </summary>
/// <param name="url"></param>
/// <param name="data"></param>
/// <returns></returns>
public static async Task<string> DoPostDataAsync(string url, object obj)
{
try
{
var httpClient = new HttpClient();
StringContent jsonContent = new StringContent(obj.ToJson(), Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(url, jsonContent);
return await response.Content.ReadAsStringAsync();
}
catch (Exception ex)
{
return string.Empty;
}
}
}
}

View File

@@ -0,0 +1,234 @@
using McProtocol.Mitsubishi;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace .Utility.Network.Mitsubishi
{
internal class MC3EServer
{
public delegate void DataChanged(int address);
public delegate void LogChanged(string message);
public event DataChanged DataChangedEvent;
public event LogChanged LogChangedEvent;
private TcpListener _listener;
private bool _isRunning;
public static ushort[] _dataStorage = new ushort[65535]; // 初始化数据存储区; // 65535个地址数据类型为ushort
public MC3EServer(int port)
{
_listener = new TcpListener(IPAddress.Any, port);
}
public void Start()
{
new Thread(() =>
{
_isRunning = true;
_listener.Start();
LogChangedEvent?.Invoke("MC-3E Protocol Server started...");
while (_isRunning)
{
try
{
TcpClient client = _listener.AcceptTcpClient();
Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClient));
clientThread.Start(client);
}
catch (SocketException)
{
continue;
}
catch (ThreadInterruptedException)
{
break;
}
}
}).Start();
}
public void Stop()
{
_isRunning = false;
_listener.Stop();
}
private void HandleClient(object obj)
{
TcpClient client = (TcpClient)obj;
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
{
LogChangedEvent?.Invoke($"Received {bytesRead} bytes from client.");
// 解析MC-3E协议请求
byte[] response = ProcessMc3ERequest(buffer, bytesRead, out bool write, out int address);
// 发送响应
stream.Write(response, 0, response.Length);
if (write) Task.Run(() => DataChangedEvent?.Invoke(address));
}
}
catch (Exception ex)
{
LogChangedEvent?.Invoke(ex.Message);
}
client.Close();
}
private byte[] ProcessMc3ERequest(byte[] request, int length, out bool write, out int address)
{
// MC-3E协议帧格式示例
// 子头 (2字节) + 网络编号 (1字节) + PLC编号 (1字节) + 请求目标模块IO编号 (2字节) + 请求目标模块站号 (1字节)
// + 请求数据长度 (2字节) + 请求数据 (N字节)
// 解析请求数据
if (length < 8)
{
LogChangedEvent?.Invoke($"Invalid request length.");
write = false;
address = 0;
return BuildErrorResponse();
}
McFrame mcFrame = new McFrame();
mcFrame.Frame = BitConverter.ToUInt16(request, 0); // 假设前两字节为帧类型 0x0050 80
mcFrame.NetworkNumber = request[2]; // 假设第3字节为网络编号
mcFrame.PcNumber = request[3]; // 假设第4字节为PLC编号
mcFrame.IoNumber = BitConverter.ToUInt16(request, 4); // 假设第5-6字节为模块IO编号
mcFrame.ChannelNumber = request[6]; // 假设第7字节为模块站号
int dataLength = BitConverter.ToUInt16(request, 7); // 假设第8-9字节为数据长度
mcFrame.CpuTimer = BitConverter.ToUInt16(request, 9); // 假设第10-11字节为CPU监视定时器
int mainCommand = BitConverter.ToUInt16(request, 11); // 假设第12-13字节为主命令
int subCommand = BitConverter.ToUInt16(request, 13); // 假设第14-15字节为子命令
mcFrame.Address = request[17] << 16 | request[16] << 8 | request[15];
mcFrame.Type = (PlcDeviceType)request[18];
mcFrame.Size = BitConverter.ToUInt16(request, 19);
byte[] data = new byte[mcFrame.Size * 2];
for (int i = 0; i < data.Length; i++)
{
data[i] = request[21 + i];
}
mcFrame.Data = data;
// 处理读请求
if (mainCommand == 0x0401) //读命令
{
LogChangedEvent?.Invoke($"Read request: Address={mcFrame.Address},Type={mcFrame.Type} Count={mcFrame.Size}");
write = false;
address = 0;
return HandleReadRequest(mcFrame);
}
// 处理写请求
else if (mainCommand == 0x1401) // 写命令
{
LogChangedEvent?.Invoke($"Write request: Address={mcFrame.Address},Type={mcFrame.Type} Count={mcFrame.Size}");
write = true;
address = mcFrame.Address;
return HandleWriteRequest(mcFrame); ;
}
else
{
LogChangedEvent?.Invoke("Unknown command.");
write = false;
address = 0;
return BuildErrorResponse();
}
}
class McFrame
{
public ushort Frame { get; set; } // 头部
public byte NetworkNumber { get; set; } // 网络号码
public byte PcNumber { get; set; } // 电脑号码
public ushort IoNumber { get; set; } // 请求单元 I/O 编号/O番号
public byte ChannelNumber { get; set; } // 请求单位站号
public ushort CpuTimer { get; set; } // 中央处理器监控计时器
public int Address { get; set; } // 地址
public PlcDeviceType Type { get; set; } // 类型
public uint Size { get; set; } // 大小
public byte[] Data { get; set; }
}
private byte[] HandleReadRequest(McFrame mcFrame)
{
// 读取数据
List<byte> responseData = new List<byte>(); // 每个ushort占2字节
for (int i = 0; i < mcFrame.Size; i++)
{
byte[] valueBytes = BitConverter.GetBytes(_dataStorage[mcFrame.Address + i]);
responseData.AddRange(valueBytes);
}
// 构建响应帧
return BuildSuccessResponse(mcFrame, responseData.ToArray());
}
private byte[] HandleWriteRequest(McFrame mcFrame)
{
for (int i = 0; i < mcFrame.Size; i++)
{
_dataStorage[mcFrame.Address + i] = BitConverter.ToUInt16(mcFrame.Data, i * 2);
}
LogChangedEvent?.Invoke($"Write successful: Address={mcFrame.Address}, Length={mcFrame.Size}");
return BuildSuccessResponse(mcFrame, new byte[0]); // 写入成功,返回空响应
}
private byte[] BuildSuccessResponse(McFrame mcFrame, byte[] responseData)
{
// 构建成功响应帧
List<byte> response = new List<byte>();
mcFrame.Frame = 0x00D0;
response.AddRange(BitConverter.GetBytes(mcFrame.Frame));//0 1
response.Add(mcFrame.NetworkNumber); //2
response.Add(mcFrame.PcNumber); //3
response.AddRange(BitConverter.GetBytes(mcFrame.IoNumber)); //4 5
response.Add(mcFrame.ChannelNumber);//6
//response.AddRange(BitConverter.GetBytes(mcFrame.CpuTimer));
//数据长度
response.AddRange(BitConverter.GetBytes((short)(responseData.Length + 2)));//7 8
response.AddRange(BitConverter.GetBytes((short)0));//9 10结束符号
response.AddRange(responseData);
return response.ToArray();
}
private byte[] BuildErrorResponse()
{
// 构建错误响应帧
byte[] response = new byte[8];
// 子头 (2字节)
response[0] = 0xD0;
response[1] = 0x00;
// 网络编号 (1字节)
response[2] = 0x00;
// PLC编号 (1字节)
response[3] = 0xFF;
// 模块IO编号 (2字节)
response[4] = 0x00;
response[5] = 0x00;
// 模块站号 (1字节)
response[6] = 0x00;
// 响应数据长度 (2字节)
response[7] = 0x00;
response[8] = 0x00;
return response;
}
}
}

View File

@@ -0,0 +1,512 @@
using McProtocol.Mitsubishi;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace McProtocol
{
/// <summary>
/// MC协议通信帮助类
/// </summary>
public class McHelper : IDisposable
{
private McProtocolTcp plc;
/// <summary>
/// 构造方法
/// </summary>
/// <param name="ip"></param>
/// <param name="port"></param>
public McHelper(string ip, int port)
{
plc = new McProtocolTcp(ip, port, McFrame.MC3E);
}
public McHelper(string ip, int port, McFrame mcFrame)
{
plc = new McProtocolTcp(ip, port, mcFrame);
}
/// <summary>
/// 建立连接
/// </summary>
/// <returns></returns>
public bool Connect()
{
try
{
plc.Open();
return true;
}
catch (Exception ex)
{
return false;
}
}
/// <summary>
/// 退出后销毁
/// </summary>
public void Dispose()
{
plc.Dispose();
}
#region Int16读写操作
/// <summary>
/// 读取Int16数据
/// </summary>
/// <param name="address">地址:例如 D10</param>
/// <param name="value">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadInt16(string address, out short value)
{
short[] values = new short[1];
bool flag = ReadInts16(address, 1, out values);
if (!flag)
{
value = 0;
return false;
}
value = values[0];
return true;
}
/// <summary>
/// 读取Int16数据
/// </summary>
/// <param name="address">地址:例如 D10</param>
/// <param name="count">读取长度</param>
/// <param name="values">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadInts16(string address, int count, out short[] values)
{
try
{
//地址解析
values = plc.ReadInt16(address, count);
return true;
}
catch (Exception ex)
{
values = null;
return false;
}
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="value">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteInt16(string address, short value)
{
short[] values = new short[1];
values[0] = value;
return WriteInts16(address, values);
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="values">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteInts16(string address, short[] values)
{
try
{
plc.WriteInt16(address, values);
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
#region UInt16读写操作
/// <summary>
/// 读取UInt16数据
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="value">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadUInt16(string address, out ushort value)
{
ushort[] values = new ushort[1];
bool flag = ReadUInts16(address, 1, out values);
if (!flag)
{
value = 0;
return false;
}
value = values[0];
return true;
}
/// <summary>
/// 读取UInt16数据
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="count">读取数量</param>
/// <param name="values">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadUInts16(string address, int count, out ushort[] values)
{
try
{
values = plc.ReadUInt16(address, count);
return true;
}
catch (Exception ex)
{
values = null;
return false;
}
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="value">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteUInt16(string address, ushort value)
{
ushort[] values = new ushort[1];
values[0] = value;
return WriteUInts16(address, values);
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="values">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteUInts16(string address, ushort[] values)
{
try
{
plc.WriteUInt16(address, values);
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
#region Int32读写操作
/// <summary>
/// 读取Int32
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="value">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadInt32(string address, out int value)
{
int[] values;
bool flag = ReadInts32(address, 1, out values);
if (!flag)
{
value = 0;
return false;
}
value = values[0];
return true;
}
/// <summary>
/// 读取Int32
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="count">读取数量</param>
/// <param name="values">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadInts32(string address, int count, out int[] values)
{
try
{
//地址解析
values = plc.ReadInt32(address, count);
return true;
}
catch (Exception ex)
{
values = null;
return false;
}
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="value">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteInt32(string address, int value)
{
int[] values = new int[1];
values[0] = value;
return WriteInts32(address, values);
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="values">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteInts32(string address, int[] values)
{
try
{
plc.WriteInt32(address, values);
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
#region UInt32读写操作
/// <summary>
/// 读取UInt32
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="value">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadUInt32(string address, out uint value)
{
uint[] values = new uint[1];
bool flag = ReadUInts32(address, 1, out values);
if (!flag)
{
value = 0;
return false;
}
value = values[0];
return true;
}
/// <summary>
/// 读取UInt32
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="count">读取数量</param>
/// <param name="values">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadUInts32(string address, int count, out uint[] values)
{
try
{
//地址解析
values = plc.ReadUInt32(address, count);
return true;
}
catch (Exception ex)
{
values = null;
return false;
}
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="value">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteUInt32(string address, uint value)
{
uint[] values = new uint[1];
values[0] = value;
return WriteUInts32(address, values);
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="values">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteUInts32(string address, uint[] values)
{
try
{
plc.WriteUInt32(address, values);
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
#region Float读写操作
/// <summary>
/// 读取Float
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="value">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadFloat(string address, out float value)
{
float[] values;
bool flag = ReadFloats(address, 1, out values);
if (!flag)
{
value = 0;
return false;
}
value = values[0];
return true;
}
/// <summary>
/// 读取Float
/// </summary>
/// <param name="address">例如 D10</param>
/// <param name="count">读取数量</param>
/// <param name="values">返回数据</param>
/// <returns>读取成功还是失败</returns>
public bool ReadFloats(string address, int count, out float[] values)
{
try
{
//地址解析
values = plc.ReadFloat(address, count);
return true;
}
catch (Exception ex)
{
values = null;
return false;
}
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="value">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteFloat(string address, float value)
{
float[] values = new float[1];
values[0] = value;
return WriteFloats(address, values);
}
/// <summary>
/// 写入数据
/// </summary>
/// <param name="address">地址D100</param>
/// <param name="values">写入数据</param>
/// <returns>写入成功还是失败</returns>
public bool WriteFloats(string address, float[] values)
{
try
{
plc.WriteFloat(address, values);
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
#region String读写操作
/// <summary>
/// 读取字符串
/// </summary>
/// <param name="address">地址,例如:D3200</param>
/// <param name="length">此处为地址长度例50表示字符串长度100</param>
/// <param name="str">读到的字符串</param>
/// <returns>返回读取成功还是失败</returns>
public bool ReadString(string address, int length, out string str)
{
try
{
ushort[] value = new ushort[length];
byte[] values = plc.ReadDeviceBlock(address, length, value);
List<byte> bytes = new List<byte>();
foreach (var item in value)
{
bytes.AddRange(BitConverter.GetBytes(item));
}
str = Encoding.GetEncoding("GB2312").GetString(bytes.ToArray());
return true;
}
catch (Exception ex)
{
str = string.Empty;
return false;
}
}
/// <summary>
/// 写入字符串
/// </summary>
/// <param name="address">地址,例如:D3200</param>
/// <param name="length">此处为地址长度例50表示字符串长度100</param>
/// <param name="str">写入的字符串</param>
/// <returns>返回写入成功还是失败</returns>
public bool WriteString(string address, int length, string str)
{
try
{
byte[] Bytes = new byte[length * 2];
byte[] bytes = Encoding.GetEncoding("GB2312").GetBytes(str);
if (bytes.Length > Bytes.Length)
{
for (int i = 0; i < Bytes.Length; i++)
{
Bytes[i] = bytes[i];
}
}
else
{
for (int i = 0; i < bytes.Length; i++)
{
Bytes[i] = bytes[i];
}
}
ushort[] value = new ushort[length];
for (int i = 0; i < length; i++)
{
value[i] = BitConverter.ToUInt16(Bytes, i * 2);
}
plc.WriteDeviceBlock(address, value);
return true;
}
catch (Exception ex)
{
return false;
}
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
/*
Copyright (c) 2018-2020 Rossmann-Engineering
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software
and associated documentation files (the "Software"),
to deal in the Software without restriction,
including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission
notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Runtime.Serialization;
namespace EasyModbus.Exceptions
{
/// <summary>
/// Exception to be thrown if serial port is not opened
/// </summary>
public class SerialPortNotOpenedException : ModbusException
{
public SerialPortNotOpenedException()
: base()
{
}
public SerialPortNotOpenedException(string message)
: base(message)
{
}
public SerialPortNotOpenedException(string message, Exception innerException)
: base(message, innerException)
{
}
protected SerialPortNotOpenedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// Exception to be thrown if Connection to Modbus device failed
/// </summary>
public class ConnectionException : ModbusException
{
public ConnectionException()
: base()
{
}
public ConnectionException(string message)
: base(message)
{
}
public ConnectionException(string message, Exception innerException)
: base(message, innerException)
{
}
protected ConnectionException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// Exception to be thrown if Modbus Server returns error code "Function code not supported"
/// </summary>
public class FunctionCodeNotSupportedException : ModbusException
{
public FunctionCodeNotSupportedException()
: base()
{
}
public FunctionCodeNotSupportedException(string message)
: base(message)
{
}
public FunctionCodeNotSupportedException(string message, Exception innerException)
: base(message, innerException)
{
}
protected FunctionCodeNotSupportedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// Exception to be thrown if Modbus Server returns error code "quantity invalid"
/// </summary>
public class QuantityInvalidException : ModbusException
{
public QuantityInvalidException()
: base()
{
}
public QuantityInvalidException(string message)
: base(message)
{
}
public QuantityInvalidException(string message, Exception innerException)
: base(message, innerException)
{
}
protected QuantityInvalidException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// Exception to be thrown if Modbus Server returns error code "starting adddress and quantity invalid"
/// </summary>
public class StartingAddressInvalidException : ModbusException
{
public StartingAddressInvalidException()
: base()
{
}
public StartingAddressInvalidException(string message)
: base(message)
{
}
public StartingAddressInvalidException(string message, Exception innerException)
: base(message, innerException)
{
}
protected StartingAddressInvalidException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// Exception to be thrown if Modbus Server returns error code "Function Code not executed (0x04)"
/// </summary>
public class ModbusException : Exception
{
public ModbusException()
: base()
{
}
public ModbusException(string message)
: base(message)
{
}
public ModbusException(string message, Exception innerException)
: base(message, innerException)
{
}
protected ModbusException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
/// <summary>
/// Exception to be thrown if CRC Check failed
/// </summary>
public class CRCCheckFailedException : ModbusException
{
public CRCCheckFailedException()
: base()
{
}
public CRCCheckFailedException(string message)
: base(message)
{
}
public CRCCheckFailedException(string message, Exception innerException)
: base(message, innerException)
{
}
protected CRCCheckFailedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
/*
Copyright (c) 2018-2020 Rossmann-Engineering
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software
and associated documentation files (the "Software"),
to deal in the Software without restriction,
including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission
notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Text;
namespace EasyModbus
{
/// <summary>
/// Store Log-Data in a File
/// </summary>
public sealed class StoreLogData
{
private String filename = null;
private static volatile StoreLogData instance;
private static object syncObject = new Object();
/// <summary>
/// Private constructor; Ensures the access of the class only via "instance"
/// </summary>
private StoreLogData()
{
}
/// <summary>
/// Returns the instance of the class (singleton)
/// </summary>
/// <returns>instance (Singleton)</returns>
public static StoreLogData Instance
{
get
{
if (instance == null)
{
lock (syncObject)
{
if (instance == null)
instance = new StoreLogData();
}
}
return instance;
}
}
/// <summary>
/// Store message in Log-File
/// </summary>
/// <param name="message">Message to append to the Log-File</param>
public void Store(String message)
{
if (this.filename == null)
return;
using (System.IO.StreamWriter file =
new System.IO.StreamWriter(Filename, true))
{
file.WriteLine(message);
}
}
/// <summary>
/// Store message in Log-File including Timestamp
/// </summary>
/// <param name="message">Message to append to the Log-File</param>
/// <param name="timestamp">Timestamp to add to the same Row</param>
public void Store(String message, DateTime timestamp)
{
try
{
using (System.IO.StreamWriter file =
new System.IO.StreamWriter(Filename, true))
{
file.WriteLine(timestamp.ToString("dd.MM.yyyy H:mm:ss.ff ") + message);
}
}
catch (Exception e)
{
}
}
/// <summary>
/// Gets or Sets the Filename to Store Strings in a File
/// </summary>
public string Filename
{
get
{
return filename;
}
set
{
filename = value;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
using System;
namespace SharpModbus
{
public class ModbusF01ReadCoils : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly ushort count;
public byte Code { get { return 1; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public ushort Count { get { return count; } }
public int RequestLength { get { return 6; } }
public int ResponseLength { get { return 3 + ModbusUtils.BytesForBools(count); } }
public ModbusF01ReadCoils(byte slave, ushort address, ushort count)
{
this.slave = slave;
this.address = address;
this.count = count;
}
public void FillRequest(byte[] request, int offset)
{
request[offset + 0] = slave;
request[offset + 1] = 1;
request[offset + 2] = ModbusUtils.High(address);
request[offset + 3] = ModbusUtils.Low(address);
request[offset + 4] = ModbusUtils.High(count);
request[offset + 5] = ModbusUtils.Low(count);
}
public object ParseResponse(byte[] response, int offset)
{
var bytes = ModbusUtils.BytesForBools(count);
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 1, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 2], bytes, "Bytes mismatch got {0} expected {1}");
return ModbusUtils.DecodeBools(response, offset + 3, count);
}
public object ApplyTo(IModbusModel model)
{
return model.getDOs(slave, address, count);
}
public void FillResponse(byte[] response, int offset, object value)
{
var bytes = ModbusUtils.BytesForBools(count);
response[offset + 0] = slave;
response[offset + 1] = 1;
response[offset + 2] = bytes;
var data = ModbusUtils.EncodeBools(value as bool[]);
ModbusUtils.Copy(data, 0, response, offset + 3, bytes);
}
public override string ToString()
{
return string.Format("[ModbusF01ReadCoils Slave={0}, Address={1}, Count={2}]", slave, address, count);
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
namespace SharpModbus
{
public class ModbusF02ReadInputs : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly ushort count;
public byte Code { get { return 2; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public ushort Count { get { return count; } }
public int RequestLength { get { return 6; } }
public int ResponseLength { get { return 3 + ModbusUtils.BytesForBools(count); } }
public ModbusF02ReadInputs(byte slave, ushort address, ushort count)
{
this.slave = slave;
this.address = address;
this.count = count;
}
public void FillRequest(byte[] request, int offset)
{
request[offset + 0] = slave;
request[offset + 1] = 2;
request[offset + 2] = ModbusUtils.High(address);
request[offset + 3] = ModbusUtils.Low(address);
request[offset + 4] = ModbusUtils.High(count);
request[offset + 5] = ModbusUtils.Low(count);
}
public object ParseResponse(byte[] response, int offset)
{
var bytes = ModbusUtils.BytesForBools(count);
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 2, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 2], bytes, "Bytes mismatch got {0} expected {1}");
return ModbusUtils.DecodeBools(response, offset + 3, count);
}
public object ApplyTo(IModbusModel model)
{
return model.getDIs(slave, address, count);
}
public void FillResponse(byte[] response, int offset, object value)
{
var bytes = ModbusUtils.BytesForBools(count);
response[offset + 0] = slave;
response[offset + 1] = 2;
response[offset + 2] = bytes;
var data = ModbusUtils.EncodeBools(value as bool[]);
ModbusUtils.Copy(data, 0, response, offset + 3, bytes);
}
public override string ToString()
{
return string.Format("[ModbusF02ReadInputs Slave={0}, Address={1}, Count={2}]", slave, address, count);
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
namespace SharpModbus
{
public class ModbusF03ReadHoldingRegisters : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly ushort count;
public byte Code { get { return 3; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public ushort Count { get { return count; } }
public int RequestLength { get { return 6; } }
public int ResponseLength { get { return 3 + ModbusUtils.BytesForWords(count); } }
public ModbusF03ReadHoldingRegisters(byte slave, ushort address, ushort count)
{
this.slave = slave;
this.address = address;
this.count = count;
}
public void FillRequest(byte[] request, int offset)
{
request[offset + 0] = slave;
request[offset + 1] = 3;
request[offset + 2] = ModbusUtils.High(address);
request[offset + 3] = ModbusUtils.Low(address);
request[offset + 4] = ModbusUtils.High(count);
request[offset + 5] = ModbusUtils.Low(count);
}
public object ParseResponse(byte[] response, int offset)
{
var bytes = ModbusUtils.BytesForWords(count);
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 3, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 2], bytes, "Bytes mismatch got {0} expected {1}");
return ModbusUtils.DecodeWords(response, offset + 3, count);
}
public object ApplyTo(IModbusModel model)
{
return model.getWOs(slave, address, count);
}
public void FillResponse(byte[] response, int offset, object value)
{
var bytes = ModbusUtils.BytesForWords(count);
response[offset + 0] = slave;
response[offset + 1] = 3;
response[offset + 2] = bytes;
var data = ModbusUtils.EncodeWords((ushort[])value);
ModbusUtils.Copy(data, 0, response, offset + 3, bytes);
}
public override string ToString()
{
return string.Format("[ModbusF03ReadHoldingRegisters Slave={0}, Address={1}, Count={2}]", slave, address, count);
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
namespace SharpModbus
{
public class ModbusF04ReadInputRegisters : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly ushort count;
public byte Code { get { return 4; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public ushort Count { get { return count; } }
public int RequestLength { get { return 6; } }
public int ResponseLength { get { return 3 + ModbusUtils.BytesForWords(count); } }
public ModbusF04ReadInputRegisters(byte slave, ushort address, ushort count)
{
this.slave = slave;
this.address = address;
this.count = count;
}
public void FillRequest(byte[] request, int offset)
{
request[offset + 0] = slave;
request[offset + 1] = 4;
request[offset + 2] = ModbusUtils.High(address);
request[offset + 3] = ModbusUtils.Low(address);
request[offset + 4] = ModbusUtils.High(count);
request[offset + 5] = ModbusUtils.Low(count);
}
public object ParseResponse(byte[] response, int offset)
{
var bytes = ModbusUtils.BytesForWords(count);
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 4, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 2], bytes, "Bytes mismatch got {0} expected {1}");
return ModbusUtils.DecodeWords(response, offset + 3, count);
}
public object ApplyTo(IModbusModel model)
{
return model.getWIs(slave, address, count);
}
public void FillResponse(byte[] response, int offset, object value)
{
var bytes = ModbusUtils.BytesForWords(count);
response[offset + 0] = slave;
response[offset + 1] = 4;
response[offset + 2] = bytes;
var data = ModbusUtils.EncodeWords(value as ushort[]);
ModbusUtils.Copy(data, 0, response, offset + 3, bytes);
}
public override string ToString()
{
return string.Format("[ModbusF04ReadInputRegisters Slave={0}, Address={1}, Count={2}]", slave, address, count);
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
namespace SharpModbus
{
public class ModbusF05WriteCoil : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly bool value;
public byte Code { get { return 5; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public bool Value { get { return value; } }
public int RequestLength { get { return 6; } }
public int ResponseLength { get { return 6; } }
public ModbusF05WriteCoil(byte slave, ushort address, bool state)
{
this.slave = slave;
this.address = address;
this.value = state;
}
public void FillRequest(byte[] request, int offset)
{
request[offset + 0] = slave;
request[offset + 1] = 5;
request[offset + 2] = ModbusUtils.High(address);
request[offset + 3] = ModbusUtils.Low(address);
request[offset + 4] = ModbusUtils.EncodeBool(value);
request[offset + 5] = 0;
}
public object ParseResponse(byte[] response, int offset)
{
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 5, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 2), address, "Address mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 4], ModbusUtils.EncodeBool(value), "Value mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 5], 0, "Pad mismatch {0} expected:{1}");
return null;
}
public object ApplyTo(IModbusModel model)
{
model.setDO(slave, address, value);
return null;
}
public void FillResponse(byte[] response, int offset, object value)
{
FillRequest(response, offset);
}
public override string ToString()
{
return string.Format("[ModbusF05WriteCoil Slave={0}, Address={1}, Value={2}]", slave, address, value);
}
}
}

View File

@@ -0,0 +1,60 @@
using System;
namespace SharpModbus
{
public class ModbusF06WriteRegister : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly ushort value;
public byte Code { get { return 6; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public ushort Value { get { return value; } }
public int RequestLength { get { return 6; } }
public int ResponseLength { get { return 6; } }
public ModbusF06WriteRegister(byte slave, ushort address, ushort value)
{
this.slave = slave;
this.address = address;
this.value = value;
}
public void FillRequest(byte[] request, int offset)
{
request[offset + 0] = slave;
request[offset + 1] = 6;
request[offset + 2] = ModbusUtils.High(address);
request[offset + 3] = ModbusUtils.Low(address);
request[offset + 4] = ModbusUtils.High(value);
request[offset + 5] = ModbusUtils.Low(value);
}
public object ParseResponse(byte[] response, int offset)
{
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 6, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 2), address, "Address mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 4), value, "Value mismatch got {0} expected {1}");
return null;
}
public object ApplyTo(IModbusModel model)
{
model.setWO(slave, address, value);
return null;
}
public void FillResponse(byte[] response, int offset, object value)
{
FillRequest(response, offset);
}
public override string ToString()
{
return string.Format("[ModbusF06WriteRegister Slave={0}, Address={1}, Value={2}]", slave, address, value);
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
namespace SharpModbus
{
public class ModbusF15WriteCoils : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly bool[] values;
public byte Code { get { return 15; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public bool[] Values { get { return ModbusUtils.Clone(values); } }
public int RequestLength { get { return 7 + ModbusUtils.BytesForBools(values.Length); } }
public int ResponseLength { get { return 6; } }
public ModbusF15WriteCoils(byte slave, ushort address, bool[] values)
{
this.slave = slave;
this.address = address;
this.values = values;
}
public void FillRequest(byte[] request, int offset)
{
FillResponse(request, offset, null);
var bytes = ModbusUtils.EncodeBools(values);
request[offset + 6] = (byte)bytes.Length;
ModbusUtils.Copy(bytes, 0, request, offset + 7, bytes.Length);
}
public object ParseResponse(byte[] response, int offset)
{
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 15, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 2), address, "Address mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 4), values.Length, "Coil count mismatch got {0} expected {1}");
return null;
}
public object ApplyTo(IModbusModel model)
{
model.setDOs(slave, address, values);
return null;
}
public void FillResponse(byte[] response, int offset, object value)
{
response[offset + 0] = slave;
response[offset + 1] = 15;
response[offset + 2] = ModbusUtils.High(address);
response[offset + 3] = ModbusUtils.Low(address);
response[offset + 4] = ModbusUtils.High(values.Length);
response[offset + 5] = ModbusUtils.Low(values.Length);
}
public override string ToString()
{
return string.Format("[ModbusF15WriteCoils Slave={0}, Address={1}, Values={2}]", slave, address, values);
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
namespace SharpModbus
{
public class ModbusF16WriteRegisters : IModbusCommand
{
private readonly byte slave;
private readonly ushort address;
private readonly ushort[] values;
public byte Code { get { return 16; } }
public byte Slave { get { return slave; } }
public ushort Address { get { return address; } }
public ushort[] Values { get { return ModbusUtils.Clone(values); } }
public int RequestLength { get { return 7 + ModbusUtils.BytesForWords(values.Length); } }
public int ResponseLength { get { return 6; } }
public ModbusF16WriteRegisters(byte slave, ushort address, ushort[] values)
{
this.slave = slave;
this.address = address;
this.values = values;
}
public void FillRequest(byte[] request, int offset)
{
FillResponse(request, offset, null);
var bytes = ModbusUtils.EncodeWords(values);
request[offset + 6] = (byte)bytes.Length;
ModbusUtils.Copy(bytes, 0, request, offset + 7, bytes.Length);
}
public object ParseResponse(byte[] response, int offset)
{
Tools.AssertEqual(response[offset + 0], slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(response[offset + 1], 16, "Function mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 2), address, "Address mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 4), values.Length, "Register count mismatch got {0} expected {1}");
return null;
}
public object ApplyTo(IModbusModel model)
{
model.setWOs(slave, address, values);
return null;
}
public void FillResponse(byte[] response, int offset, object value)
{
response[offset + 0] = slave;
response[offset + 1] = 16;
response[offset + 2] = ModbusUtils.High(address);
response[offset + 3] = ModbusUtils.Low(address);
response[offset + 4] = ModbusUtils.High(values.Length);
response[offset + 5] = ModbusUtils.Low(values.Length);
}
public override string ToString()
{
return string.Format("[ModbusF16WriteRegisters Slave={0}, Address={1}, Values={2}]", slave, address, values);
}
}
}

View File

@@ -0,0 +1,18 @@

using System;
namespace SharpModbus
{
public interface IModbusCommand
{
byte Code { get; }
byte Slave { get; }
ushort Address { get; }
int RequestLength { get; }
int ResponseLength { get; }
void FillRequest(byte[] request, int offset);
object ParseResponse(byte[] response, int offset);
object ApplyTo(IModbusModel model);
void FillResponse(byte[] response, int offset, object value);
}
}

View File

@@ -0,0 +1,39 @@
using System;
namespace SharpModbus
{
public interface IModbusModel
{
void setDI(byte slave, ushort address, bool value);
void setDIs(byte slave, ushort address, bool[] values);
bool getDI(byte slave, ushort address);
bool[] getDIs(byte slave, ushort address, int count);
void setDO(byte slave, ushort address, bool value);
void setDOs(byte slave, ushort address, bool[] values);
bool getDO(byte slave, ushort address);
bool[] getDOs(byte slave, ushort address, int count);
void setWI(byte slave, ushort address, ushort value);
void setWIs(byte slave, ushort address, ushort[] values);
ushort getWI(byte slave, ushort address);
ushort[] getWIs(byte slave, ushort address, int count);
void setWO(byte slave, ushort address, ushort value);
void setWOs(byte slave, ushort address, ushort[] values);
ushort getWO(byte slave, ushort address);
ushort[] getWOs(byte slave, ushort address, int count);
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace SharpModbus
{
public interface IModbusProtocol
{
IModbusWrapper Wrap(IModbusCommand wrapped);
IModbusWrapper Parse(byte[] request, int offset);
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace SharpModbus
{
public interface IModbusScanner
{
void Append(byte[] data, int offset, int count);
IModbusWrapper Scan();
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace SharpModbus
{
public interface IModbusStream : IDisposable
{
void Write(byte[] data);
int Read(byte[] data);
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace SharpModbus
{
public interface IModbusWrapper : IModbusCommand
{
IModbusCommand Wrapped { get; }
byte[] GetException(byte code);
void CheckException(byte[] respose, int count);
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace SharpModbus
{
public class ModbusException : Exception
{
private readonly byte code;
public byte Code { get { return code; } }
public ModbusException(byte code) :
base(string.Format("Modbus exception {0}", code))
{
this.code = code;
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using SharpSerial;
namespace SharpModbus
{
public class ModbusIsolatedStream : IModbusStream
{
private readonly Action<char, byte[], int> monitor;
private readonly SerialProcess serialProcess;
private readonly int timeout;
public ModbusIsolatedStream(object settings, int timeout, Action<char, byte[], int> monitor = null)
{
this.serialProcess = new SerialProcess(settings);
this.timeout = timeout;
this.monitor = monitor;
}
public void Dispose()
{
Tools.Dispose(serialProcess);
}
public void Write(byte[] data)
{
if (monitor != null) monitor('>', data, data.Length);
serialProcess.Write(data);
}
public int Read(byte[] data)
{
var response = serialProcess.Read(data.Length, -1, timeout);
var count = response.Length;
for (var i = 0; i < count; i++) data[i] = response[i];
if (monitor != null) monitor('<', data, count);
return count;
}
}
}

View File

@@ -0,0 +1,132 @@
using MES.Utility.Core;
using System;
using System.Linq;
namespace SharpModbus
{
public class ModbusMaster : IDisposable
{
public delegate void OnSendedData(byte[] bytes, string hexString);
public event OnSendedData OnSended;
public delegate void OnRecivedData(byte[] bytes, string hexString);
public event OnRecivedData OnRecived;
public static ModbusMaster RTU(SerialSettings settings, int timeout = 400)
{
var stream = new ModbusSerialStream(settings, timeout);
var protocol = new ModbusRTUProtocol();
return new ModbusMaster(stream, protocol);
}
public static ModbusMaster IsolatedRTU(SerialSettings settings, int timeout = 400)
{
var stream = new ModbusIsolatedStream(settings, timeout);
var protocol = new ModbusRTUProtocol();
return new ModbusMaster(stream, protocol);
}
public static ModbusMaster TCP(string ip, int port, int timeout = 400)
{
var socket = Tools.ConnectWithTimeout(ip, port, timeout);
var stream = new ModbusSocketStream(socket, timeout);
var protocol = new ModbusTCPProtocol();
return new ModbusMaster(stream, protocol);
}
private readonly IModbusProtocol protocol;
private readonly IModbusStream stream;
public ModbusMaster(IModbusStream stream, IModbusProtocol protocol)
{
this.stream = stream;
this.protocol = protocol;
}
public void Dispose()
{
Tools.Dispose(stream);
}
public bool ReadCoil(byte slave, ushort address)
{
return ReadCoils(slave, address, 1)[0]; //there is no code for single read
}
public bool ReadInput(byte slave, ushort address)
{
return ReadInputs(slave, address, 1)[0]; //there is no code for single read
}
public ushort ReadInputRegister(byte slave, ushort address)
{
return ReadInputRegisters(slave, address, 1)[0]; //there is no code for single read
}
public ushort ReadHoldingRegister(byte slave, ushort address)
{
return ReadHoldingRegisters(slave, address, 1)[0]; //there is no code for single read
}
public bool[] ReadCoils(byte slave, ushort address, ushort count)
{
return Execute(new ModbusF01ReadCoils(slave, address, count)) as bool[];
}
public bool[] ReadInputs(byte slave, ushort address, ushort count)
{
return Execute(new ModbusF02ReadInputs(slave, address, count)) as bool[];
}
public ushort[] ReadInputRegisters(byte slave, ushort address, ushort count)
{
return Execute(new ModbusF04ReadInputRegisters(slave, address, count)) as ushort[];
}
public ushort[] ReadHoldingRegisters(byte slave, ushort address, ushort count)
{
return Execute(new ModbusF03ReadHoldingRegisters(slave, address, count)) as ushort[];
}
public void WriteCoil(byte slave, ushort address, bool value)
{
Execute(new ModbusF05WriteCoil(slave, address, value));
}
public void WriteRegister(byte slave, ushort address, ushort value)
{
Execute(new ModbusF06WriteRegister(slave, address, value));
}
public void WriteCoils(byte slave, ushort address, params bool[] values)
{
Execute(new ModbusF15WriteCoils(slave, address, values));
}
public void WriteRegisters(byte slave, ushort address, params ushort[] values)
{
Execute(new ModbusF16WriteRegisters(slave, address, values));
}
private object Execute(IModbusCommand cmd)
{
var wrapper = protocol.Wrap(cmd);
var request = new byte[wrapper.RequestLength];
var response = new byte[wrapper.ResponseLength];
wrapper.FillRequest(request, 0);
OnSended?.Invoke(request, DataHexString(request));
stream.Write(request);
var count = stream.Read(response);
OnRecived?.Invoke(response, DataHexString(response));
if (count < response.Length) wrapper.CheckException(response, count);
return wrapper.ParseResponse(response, 0);
}
private string DataHexString(byte[] bytes)
{
if (bytes == null)
return string.Empty;
return bytes.Select(it => Convert.ToString(it, 16).PadLeft(2, '0').ToUpper()).ToList().GetStrArray(" ");
}
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
namespace SharpModbus
{
public enum ModbusIoType
{
DI,
DO,
WO,
WI
}
public class ModbusModel : IModbusModel
{
private readonly IDictionary<string, bool> digitals = new Dictionary<string, bool>();
private readonly IDictionary<string, ushort> words = new Dictionary<string, ushort>();
public void setDI(byte slave, ushort address, bool value)
{
var key = Key(ModbusIoType.DI, slave, address);
digitals[key] = value;
}
public void setDIs(byte slave, ushort address, bool[] values)
{
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.DI, slave, address + i);
digitals[key] = values[i];
}
}
public bool getDI(byte slave, ushort address)
{
var key = Key(ModbusIoType.DI, slave, address);
return digitals[key];
}
public bool[] getDIs(byte slave, ushort address, int count)
{
var values = new bool[count];
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.DI, slave, address + i);
values[i] = digitals[key];
}
return values;
}
public void setDO(byte slave, ushort address, bool value)
{
var key = Key(ModbusIoType.DO, slave, address);
digitals[key] = value;
}
public void setDOs(byte slave, ushort address, bool[] values)
{
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.DO, slave, address + i);
digitals[key] = values[i];
}
}
public bool getDO(byte slave, ushort address)
{
var key = Key(ModbusIoType.DO, slave, address);
return digitals[key];
}
public bool[] getDOs(byte slave, ushort address, int count)
{
var values = new bool[count];
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.DO, slave, address + i);
values[i] = digitals[key];
}
return values;
}
public void setWI(byte slave, ushort address, ushort value)
{
var key = Key(ModbusIoType.WI, slave, address);
words[key] = value;
}
public void setWIs(byte slave, ushort address, ushort[] values)
{
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.WI, slave, address + i);
words[key] = values[i];
}
}
public ushort getWI(byte slave, ushort address)
{
var key = Key(ModbusIoType.WI, slave, address);
return words[key];
}
public ushort[] getWIs(byte slave, ushort address, int count)
{
var values = new ushort[count];
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.WI, slave, address + i);
values[i] = words[key];
}
return values;
}
public void setWO(byte slave, ushort address, ushort value)
{
var key = Key(ModbusIoType.WO, slave, address);
words[key] = value;
}
public void setWOs(byte slave, ushort address, ushort[] values)
{
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.WO, slave, address + i);
words[key] = values[i];
}
}
public ushort getWO(byte slave, ushort address)
{
var key = Key(ModbusIoType.WO, slave, address);
return words[key];
}
public ushort[] getWOs(byte slave, ushort address, int count)
{
var values = new ushort[count];
for (var i = 0; i < values.Length; i++)
{
var key = Key(ModbusIoType.WO, slave, address + i);
values[i] = words[key];
}
return values;
}
private string Key(ModbusIoType type, byte slave, int address)
{
return string.Format("{0},{1},{2}", slave, type, address);
}
}
}

View File

@@ -0,0 +1,90 @@
using System;
namespace SharpModbus
{
public static class ModbusParser
{
public static IModbusCommand Parse(byte[] request, int offset)
{
var slave = request[offset + 0];
var code = request[offset + 1];
var address = ModbusUtils.GetUShort(request, offset + 2);
switch (code)
{
case 1:
return Parse01(slave, code, address, request, offset);
case 2:
return Parse02(slave, code, address, request, offset);
case 3:
return Parse03(slave, code, address, request, offset);
case 4:
return Parse04(slave, code, address, request, offset);
case 5:
return Parse05(slave, code, address, request, offset);
case 6:
return Parse06(slave, code, address, request, offset);
case 15:
return Parse15(slave, code, address, request, offset);
case 16:
return Parse16(slave, code, address, request, offset);
}
throw Tools.Make("Unsupported function code {0}", code);
}
private static IModbusCommand Parse01(byte slave, byte code, ushort address, byte[] request, int offset)
{
var count = ModbusUtils.GetUShort(request, offset + 4);
return new ModbusF01ReadCoils(slave, address, count);
}
private static IModbusCommand Parse02(byte slave, byte code, ushort address, byte[] request, int offset)
{
var count = ModbusUtils.GetUShort(request, offset + 4);
return new ModbusF02ReadInputs(slave, address, count);
}
private static IModbusCommand Parse03(byte slave, byte code, ushort address, byte[] request, int offset)
{
var count = ModbusUtils.GetUShort(request, offset + 4);
return new ModbusF03ReadHoldingRegisters(slave, address, count);
}
private static IModbusCommand Parse04(byte slave, byte code, ushort address, byte[] request, int offset)
{
var count = ModbusUtils.GetUShort(request, offset + 4);
return new ModbusF04ReadInputRegisters(slave, address, count);
}
private static IModbusCommand Parse05(byte slave, byte code, ushort address, byte[] request, int offset)
{
var value = ModbusUtils.DecodeBool(request[offset + 4]);
var zero = request[offset + 5];
Tools.AssertEqual(zero, 0, "Zero mismatch got {0} expected {1}");
return new ModbusF05WriteCoil(slave, address, value);
}
private static IModbusCommand Parse06(byte slave, byte code, ushort address, byte[] request, int offset)
{
var value = ModbusUtils.GetUShort(request, offset + 4);
return new ModbusF06WriteRegister(slave, address, value);
}
private static IModbusCommand Parse15(byte slave, byte code, ushort address, byte[] request, int offset)
{
var count = ModbusUtils.GetUShort(request, offset + 4);
var values = ModbusUtils.DecodeBools(request, offset + 7, count);
var bytes = request[offset + 6];
Tools.AssertEqual(ModbusUtils.BytesForBools(count), bytes, "Byte count mismatch got {0} expected {1}");
return new ModbusF15WriteCoils(slave, address, values);
}
private static IModbusCommand Parse16(byte slave, byte code, ushort address, byte[] request, int offset)
{
var count = ModbusUtils.GetUShort(request, offset + 4);
var values = ModbusUtils.DecodeWords(request, offset + 7, count);
var bytes = request[offset + 6];
Tools.AssertEqual(ModbusUtils.BytesForWords(count), bytes, "Byte count mismatch got {0} expected {1}");
return new ModbusF16WriteRegisters(slave, address, values);
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
namespace SharpModbus
{
public class ModbusRTUProtocol : IModbusProtocol
{
public IModbusWrapper Wrap(IModbusCommand wrapped)
{
return new ModbusRTUWrapper(wrapped);
}
public IModbusWrapper Parse(byte[] request, int offset)
{
var wrapped = ModbusParser.Parse(request, offset);
var crc = ModbusUtils.CRC16(request, offset, wrapped.RequestLength);
Tools.AssertEqual(crc, ModbusUtils.GetUShortLittleEndian(request, offset + wrapped.RequestLength),
"CRC mismatch {0:X4} expected {1:X4}");
return new ModbusRTUWrapper(wrapped);
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
namespace SharpModbus
{
public class ModbusRTUScanner : IModbusScanner
{
private readonly ModbusRTUProtocol protocol = new ModbusRTUProtocol();
private readonly List<byte> buffer = new List<byte>();
public void Append(byte[] data, int offset, int count)
{
for (var i = 0; i < count; i++) buffer.Add(data[offset + i]);
}
public IModbusWrapper Scan()
{
//01,02,03,04,05,06 have 6 + 2(CRC)
//15,16 have 6 + 1(len) + len + 2(CRC)
if (buffer.Count >= 8)
{
var code = buffer[1];
CheckCode(code);
var length = 8;
if (HasBytesAt6(code)) length += 1 + buffer[6];
if (buffer.Count >= length)
{
var request = buffer.GetRange(0, length).ToArray();
buffer.RemoveRange(0, length);
return protocol.Parse(request, 0);
}
}
return null; //not enough data to parse
}
bool HasBytesAt6(byte code)
{
return "15,16".Contains(code.ToString("00"));
}
void CheckCode(byte code)
{
var valid = "01,02,03,04,05,06,15,16".Contains(code.ToString("00"));
if (!valid) Tools.Throw("Unsupported code {0}", code);
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
namespace SharpModbus
{
public class ModbusRTUWrapper : IModbusWrapper
{
private readonly IModbusCommand wrapped;
public byte Code { get { return wrapped.Code; } }
public byte Slave { get { return wrapped.Slave; } }
public ushort Address { get { return wrapped.Address; } }
public IModbusCommand Wrapped { get { return wrapped; } }
public int RequestLength { get { return wrapped.RequestLength + 2; } }
public int ResponseLength { get { return wrapped.ResponseLength + 2; } }
public ModbusRTUWrapper(IModbusCommand wrapped)
{
this.wrapped = wrapped;
}
public void FillRequest(byte[] request, int offset)
{
wrapped.FillRequest(request, offset);
var crc = ModbusUtils.CRC16(request, offset, wrapped.RequestLength);
request[offset + wrapped.RequestLength + 0] = ModbusUtils.Low(crc);
request[offset + wrapped.RequestLength + 1] = ModbusUtils.High(crc);
}
public object ParseResponse(byte[] response, int offset)
{
var crc1 = ModbusUtils.CRC16(response, offset, wrapped.ResponseLength);
//crc is little endian page 13 http://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
var crc2 = ModbusUtils.GetUShortLittleEndian(response, offset + wrapped.ResponseLength);
Tools.AssertEqual(crc2, crc1, "CRC mismatch got {0:X4} expected {1:X4}");
return wrapped.ParseResponse(response, offset);
}
public object ApplyTo(IModbusModel model)
{
return wrapped.ApplyTo(model);
}
public void FillResponse(byte[] response, int offset, object value)
{
wrapped.FillResponse(response, offset, value);
var crc = ModbusUtils.CRC16(response, offset, wrapped.ResponseLength);
response[offset + wrapped.ResponseLength + 0] = ModbusUtils.Low(crc);
response[offset + wrapped.ResponseLength + 1] = ModbusUtils.High(crc);
}
public byte[] GetException(byte code)
{
var exception = new byte[5];
exception[0] = wrapped.Slave;
exception[1] = (byte)(wrapped.Code | 0x80);
exception[2] = code;
var crc = ModbusUtils.CRC16(exception, 0, 3);
exception[3] = ModbusUtils.Low(crc);
exception[4] = ModbusUtils.High(crc);
return exception;
}
public void CheckException(byte[] response, int count)
{
if (count < 5) Tools.Throw("Partial packet exception got {0} expected >= {1}", count, 5);
var offset = 0;
var code = response[offset + 1];
if ((code & 0x80) != 0)
{
Tools.AssertEqual(response[offset + 0], wrapped.Slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(code & 0x7F, wrapped.Code, "Code mismatch got {0} expected {1}");
var crc1 = ModbusUtils.CRC16(response, offset, 3);
//crc is little endian page 13 http://modbus.org/docs/Modbus_over_serial_line_V1_02.pdf
var crc2 = ModbusUtils.GetUShortLittleEndian(response, offset + 3);
Tools.AssertEqual(crc2, crc1, "CRC mismatch got {0:X4} expected {1:X4}");
throw new ModbusException(response[offset + 2]);
}
}
public override string ToString()
{
return string.Format("[ModbusRTUWrapper Wrapped={0}]", wrapped);
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using SharpSerial;
namespace SharpModbus
{
public class ModbusSerialStream : IModbusStream
{
private readonly Action<char, byte[], int> monitor;
private readonly SerialDevice serialDevice;
private readonly int timeout;
public ModbusSerialStream(SerialSettings settings, int timeout, Action<char, byte[], int> monitor = null)
{
this.serialDevice = new SerialDevice(settings);
this.timeout = timeout;
this.monitor = monitor;
}
public void Dispose()
{
Tools.Dispose(serialDevice);
}
public void Write(byte[] data)
{
if (monitor != null) monitor('>', data, data.Length);
serialDevice.Write(data);
}
public int Read(byte[] data)
{
var response = serialDevice.Read(data.Length, -1, timeout);
var count = response.Length;
for (var i = 0; i < count; i++) data[i] = response[i];
if (monitor != null) monitor('<', data, count);
return count;
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Threading;
using System.Net.Sockets;
namespace SharpModbus
{
public class ModbusSocketStream : IModbusStream
{
private readonly TcpClient socket;
private readonly Action<char, byte[], int> monitor;
public ModbusSocketStream(TcpClient socket, int timeout, Action<char, byte[], int> monitor = null)
{
socket.ReceiveTimeout = timeout;
socket.SendTimeout = timeout;
this.monitor = monitor;
this.socket = socket;
}
public void Dispose()
{
Tools.Dispose(socket);
}
public void Write(byte[] data)
{
//implicit discard to allow retry after timeout as per #5
while (socket.Available > 0) socket.GetStream().ReadByte();
if (monitor != null) monitor('>', data, data.Length);
socket.GetStream().Write(data, 0, data.Length);
}
public int Read(byte[] data)
{
var count = 0;
var dl = DateTime.Now.AddMilliseconds(socket.ReceiveTimeout);
while (count < data.Length)
{
var available = socket.Available;
if (available == 0)
{
if (DateTime.Now > dl) break;
Thread.Sleep(1);
}
else
{
var size = (int)Math.Min(available, data.Length - count);
count += socket.GetStream().Read(data, count, size);
dl = DateTime.Now; //should come in single packet
}
}
if (monitor != null) monitor('<', data, count);
return count;
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
namespace SharpModbus
{
public class ModbusTCPProtocol : IModbusProtocol
{
private ushort transactionId = 0;
public ushort TransactionId
{
get { return transactionId; }
set { transactionId = value; }
}
public IModbusWrapper Wrap(IModbusCommand wrapped)
{
return new ModbusTCPWrapper(wrapped, transactionId++);
}
public IModbusWrapper Parse(byte[] request, int offset)
{
var wrapped = ModbusParser.Parse(request, offset + 6);
Tools.AssertEqual(wrapped.RequestLength, ModbusUtils.GetUShort(request, offset + 4),
"RequestLength mismatch got {0} expected {1}");
var transaction = ModbusUtils.GetUShort(request, offset);
return new ModbusTCPWrapper(wrapped, transaction);
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace SharpModbus
{
public class ModbusTCPScanner : IModbusScanner
{
private readonly ModbusTCPProtocol protocol = new ModbusTCPProtocol();
private readonly List<byte> buffer = new List<byte>();
public void Append(byte[] data, int offset, int count)
{
for (var i = 0; i < count; i++) buffer.Add(data[offset + i]);
}
public IModbusWrapper Scan()
{
if (buffer.Count >= 6)
{
var length = ModbusUtils.GetUShort(buffer[4], buffer[5]);
if (buffer.Count >= 6 + length)
{
var request = buffer.GetRange(0, 6 + length).ToArray();
buffer.RemoveRange(0, 6 + length);
return protocol.Parse(request, 0);
}
}
return null; //not enough data to parse
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
namespace SharpModbus
{
public class ModbusTCPWrapper : IModbusWrapper
{
private readonly IModbusCommand wrapped;
private readonly ushort transactionId;
public byte Code { get { return wrapped.Code; } }
public byte Slave { get { return wrapped.Slave; } }
public ushort Address { get { return wrapped.Address; } }
public IModbusCommand Wrapped { get { return wrapped; } }
public ushort TransactionId { get { return transactionId; } }
public int RequestLength { get { return wrapped.RequestLength + 6; } }
public int ResponseLength { get { return wrapped.ResponseLength + 6; } }
public ModbusTCPWrapper(IModbusCommand wrapped, ushort transactionId)
{
this.wrapped = wrapped;
this.transactionId = transactionId;
}
public void FillRequest(byte[] request, int offset)
{
request[offset + 0] = ModbusUtils.High(transactionId);
request[offset + 1] = ModbusUtils.Low(transactionId);
request[offset + 2] = 0;
request[offset + 3] = 0;
request[offset + 4] = ModbusUtils.High(wrapped.RequestLength);
request[offset + 5] = ModbusUtils.Low(wrapped.RequestLength);
wrapped.FillRequest(request, offset + 6);
}
public object ParseResponse(byte[] response, int offset)
{
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 0), transactionId, "TransactionId mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 2), 0, "Zero mismatch got {0} expected {1}");
Tools.AssertEqual(ModbusUtils.GetUShort(response, offset + 4), wrapped.ResponseLength, "Length mismatch got {0} expected {1}");
return wrapped.ParseResponse(response, offset + 6);
}
public object ApplyTo(IModbusModel model)
{
return wrapped.ApplyTo(model);
}
public void FillResponse(byte[] response, int offset, object value)
{
response[offset + 0] = ModbusUtils.High(transactionId);
response[offset + 1] = ModbusUtils.Low(transactionId);
response[offset + 2] = 0;
response[offset + 3] = 0;
response[offset + 4] = ModbusUtils.High(wrapped.ResponseLength);
response[offset + 5] = ModbusUtils.Low(wrapped.ResponseLength);
wrapped.FillResponse(response, offset + 6, value);
}
public byte[] GetException(byte code)
{
var exception = new byte[9];
exception[0] = ModbusUtils.High(transactionId);
exception[1] = ModbusUtils.Low(transactionId);
exception[2] = 0;
exception[3] = 0;
exception[4] = ModbusUtils.High(3);
exception[5] = ModbusUtils.Low(3);
exception[6 + 0] = wrapped.Slave;
exception[6 + 1] = (byte)(wrapped.Code | 0x80);
exception[6 + 2] = code;
return exception;
}
public void CheckException(byte[] response, int count)
{
if (count < 9) Tools.Throw("Partial packet exception got {0} expected >= {1}", count, 9);
var offset = 6;
var code = response[offset + 1];
if ((code & 0x80) != 0)
{
Tools.AssertEqual(response[offset + 0], wrapped.Slave, "Slave mismatch got {0} expected {1}");
Tools.AssertEqual(code & 0x7F, wrapped.Code, "Code mismatch got {0} expected {1}");
throw new ModbusException(response[offset + 2]);
}
}
public override string ToString()
{
return string.Format("[ModbusTCPWrapper Wrapped={0}, TransactionId={1}]", wrapped, transactionId);
}
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Net.Sockets;
namespace SharpModbus
{
public class SerialSettings : SharpSerial.SerialSettings { }
public static class Tools
{
public static void AssertEqual(int a, int b, string format)
{
if (a != b) Tools.Throw(format, a, b);
}
public static TcpClient ConnectWithTimeout(string host, int port, int timeout)
{
var socket = new TcpClient();
try
{
var result = socket.BeginConnect(host, port, null, null);
var connected = result.AsyncWaitHandle.WaitOne(timeout, true);
if (!connected) Tools.Throw("Timeout connecting to {0}:{1}", host, port);
socket.EndConnect(result);
return socket;
}
catch (Exception ex)
{
Tools.Dispose(socket);
throw ex;
}
}
public static void Dispose(IDisposable disposable)
{
try { if (disposable != null) disposable.Dispose(); }
catch (Exception) { }
}
public static Exception Make(string format, params object[] args)
{
var message = format;
if (args.Length > 0) message = string.Format(format, args);
return new Exception(message);
}
public static void Throw(string format, params object[] args)
{
var message = format;
if (args.Length > 0) message = string.Format(format, args);
throw new Exception(message);
}
public static void Throw(Exception inner, string format, params object[] args)
{
var message = format;
if (args.Length > 0) message = string.Format(format, args);
throw new Exception(message, inner);
}
}
}

View File

@@ -0,0 +1,169 @@
using System;
namespace SharpModbus
{
public static class ModbusUtils
{
public static ushort CRC16(byte[] bytes, int offset, int count)
{
ushort crc = 0xFFFF;
for (int pos = 0; pos < count; pos++)
{
crc ^= (ushort)bytes[pos + offset];
for (int i = 8; i > 0; i--)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= 0xA001;
}
else
crc >>= 1;
}
}
return crc;
}
public static byte EncodeBool(bool value)
{
return (byte)(value ? 0xFF : 0x00);
}
public static bool DecodeBool(byte value)
{
return (value != 0x00);
}
public static byte[] EncodeBools(bool[] bools)
{
var count = BytesForBools(bools.Length);
var bytes = new byte[count];
for (var i = 0; i < count; i++)
{
bytes[i] = 0;
}
for (var i = 0; i < bools.Length; i++)
{
var v = bools[i];
if (v)
{
var bi = i / 8;
bytes[bi] |= (byte)(1 << (i % 8));
}
}
return bytes;
}
public static byte[] EncodeWords(ushort[] words)
{
var count = 2 * words.Length;
var bytes = new byte[count];
for (var i = 0; i < count; i++)
{
bytes[i] = 0;
}
for (var i = 0; i < words.Length; i++)
{
bytes[2 * i + 0] = (byte)((words[i] >> 8) & 0xff);
bytes[2 * i + 1] = (byte)((words[i] >> 0) & 0xff);
}
return bytes;
}
public static bool[] DecodeBools(byte[] packet, int offset, ushort count)
{
var bools = new bool[count];
var bytes = BytesForBools(count);
for (var i = 0; i < bytes; i++)
{
var bits = count >= 8 ? 8 : count % 8;
var b = packet[offset + i];
ByteToBools(b, bools, bools.Length - count, bits);
count -= (ushort)bits;
}
return bools;
}
public static ushort[] DecodeWords(byte[] packet, int offset, ushort count)
{
var results = new ushort[count];
for (int i = 0; i < count; i++)
{
results[i] = (ushort)(packet[offset + 2 * i] << 8 | packet[offset + 2 * i + 1]);
}
return results;
}
private static void ByteToBools(byte b, bool[] bools, int offset, int count)
{
for (int i = 0; i < count; i++)
bools[offset + i] = ((b >> i) & 0x01) == 1;
}
public static byte BytesForWords(int count)
{
return (byte)(2 * count);
}
public static byte BytesForBools(int count)
{
return (byte)(count == 0 ? 0 : (count - 1) / 8 + 1);
}
public static byte High(int value)
{
return (byte)((value >> 8) & 0xff);
}
public static byte Low(int value)
{
return (byte)((value >> 0) & 0xff);
}
public static ushort GetUShort(byte bh, byte bl)
{
return (ushort)(
((bh << 8) & 0xFF00)
| (bl & 0xff)
);
}
public static ushort GetUShort(byte[] bytes, int offset)
{
return (ushort)(
((bytes[offset + 0] << 8) & 0xFF00)
| (bytes[offset + 1] & 0xff)
);
}
public static ushort GetUShortLittleEndian(byte[] bytes, int offset)
{
return (ushort)(
((bytes[offset + 1] << 8) & 0xFF00)
| (bytes[offset + 0] & 0xff)
);
}
public static void Copy(byte[] src, int srcOffset, byte[] dst, int dstOffset, int count)
{
for (var i = 0; i < count; i++)
dst[dstOffset + i] = src[srcOffset + i];
}
public static bool[] Clone(bool[] values)
{
var clone = new bool[values.Length];
for (var i = 0; i < values.Length; i++)
clone[i] = values[i];
return clone;
}
public static ushort[] Clone(ushort[] values)
{
var clone = new ushort[values.Length];
for (var i = 0; i < values.Length; i++)
clone[i] = values[i];
return clone;
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Diagnostics;
namespace SharpSerial
{
public static class Stdio
{
private static bool traceTimed = false;
private static bool traceEnabled = false;
private static readonly object locker = new object();
public static string ReadLine() => Console.ReadLine();
public static void WriteLine(string format, params object[] args)
{
lock (locker)
{
Console.Out.WriteLine(format, args);
Console.Out.Flush();
}
}
[Conditional("DEBUG")]
public static void Trace(string format, params object[] args)
{
if (traceEnabled)
{
lock (locker)
{
if (traceTimed)
{
Console.Error.Write(DateTime.Now.ToString("HH:mm:ss.fff"));
Console.Error.Write(" ");
}
Console.Error.WriteLine(format, args);
Console.Error.Flush();
}
}
}
public static void EnableTrace(bool enable, bool timed)
{
traceTimed = timed;
traceEnabled = enable;
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
using System.Text;
namespace SharpSerial
{
public static class Tools
{
public static void SetupGlobalCatcher()
{
AppDomain.CurrentDomain.UnhandledException += ExceptionHandler;
}
public static void ExceptionHandler(object sender, UnhandledExceptionEventArgs args)
{
ExceptionHandler(args.ExceptionObject as Exception);
}
public static void ExceptionHandler(Exception ex)
{
Try(() => Stdio.Trace("!{0}", ex.ToString()));
Try(() => Stdio.WriteLine("!{0}", ex.ToString()));
Environment.Exit(1);
}
public static string StringHex(byte[] data)
{
var sb = new StringBuilder();
sb.Append("<");
foreach (var b in data) sb.Append(b.ToString("X2"));
return sb.ToString();
}
public static byte[] ParseHex(string text)
{
Assert(text.Length % 2 == 1, "Odd length expected for {0}:{1}", text.Length, text);
var bytes = new byte[text.Length / 2];
for (var i = 0; i < bytes.Length; i++)
{
var b2 = text.Substring(1 + i * 2, 2);
bytes[i] = Convert.ToByte(b2, 16);
}
return bytes;
}
public static void SetProperty(object target, string line)
{
var parts = line.Split(new char[] { '=' });
if (parts.Length != 2) throw Make("Expected 2 parts in {0}", Readable(line));
var propertyName = parts[0];
var propertyValue = parts[1];
var property = target.GetType().GetProperty(propertyName);
if (property == null) throw Make("Property not found {0}", Readable(propertyName));
var value = FromString(property.PropertyType, propertyValue);
property.SetValue(target, value, null);
}
public static object FromString(Type type, string text)
{
if (type.IsEnum) return Enum.Parse(type, text);
return Convert.ChangeType(text, type);
}
public static void Try(Action action)
{
try
{
action.Invoke();
}
catch (Exception)
{
//no clear use case for cleanup exception
}
}
public static Exception Make(string format, params object[] args)
{
var line = format;
if (args.Length > 0) line = string.Format(format, args);
return new Exception(line);
}
public static string Readable(string text)
{
var sb = new StringBuilder();
foreach (var c in text)
{
if (Char.IsControl(c)) sb.Append(((int)c).ToString("X2"));
else if (Char.IsWhiteSpace(c)) sb.Append(((int)c).ToString("X2"));
else sb.Append(c);
}
return sb.ToString();
}
public static void Assert(bool condition, string format, params object[] args)
{
if (!condition) throw Make(format, args);
}
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using System.IO.Ports;
namespace SharpSerial
{
// com0com BytesToRead is UNRELIABLE
// Solution based on BaseStream and influenced by
// https://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport
// https://www.vgies.com/a-reliable-serial-port-in-c/
public class SerialDevice : ISerialStream, IDisposable
{
private readonly SerialPort serial;
private readonly List<byte> list;
private readonly Queue<byte> queue;
private readonly byte[] buffer;
public SerialDevice(object settings)
{
this.buffer = new byte[256];
this.list = new List<byte>(256);
this.queue = new Queue<byte>(256);
this.serial = new SerialPort();
//init serial port and launch async reader
SerialSettings.CopyProperties(settings, serial);
serial.Open();
//DiscardInBuffer not needed by FTDI and ignored by com0com
var stream = serial.BaseStream;
//unavailable after closed so pass it
stream.BeginRead(buffer, 0, buffer.Length, ReadCallback, stream);
}
public void Dispose()
{
Tools.Try(serial.Close);
Tools.Try(serial.Dispose);
}
public void Write(byte[] data)
{
var stream = serial.BaseStream;
stream.Write(data, 0, data.Length);
//always flush to allow sync by following read available
stream.Flush();
}
public byte[] Read(int size, int eop, int toms)
{
list.Clear();
var dl = DateTime.Now.AddMilliseconds(toms);
while (true)
{
int b = ReadByte();
if (b == -1)
{
//toms=0 should return immediately with available
if (DateTime.Now >= dl) break;
Thread.Sleep(1);
continue;
}
list.Add((byte)b);
if (eop >= 0 && b == eop) break;
if (size >= 0 && list.Count >= size) break;
dl = DateTime.Now.AddMilliseconds(toms);
}
return list.ToArray();
}
private int ReadByte()
{
lock (queue)
{
if (queue.Count == 0) return -1;
return queue.Dequeue();
}
}
private void ReadCallback(IAsyncResult ar)
{
Tools.Try(() =>
{
//try needed to avoid triggering the domain unhandled
//exception handler when used as standalone stream
var stream = ar.AsyncState as Stream;
int count = stream.EndRead(ar);
if (count > 0) //0 for closed stream
{
lock (queue) for (var i = 0; i < count; i++) queue.Enqueue(buffer[i]);
stream.BeginRead(buffer, 0, buffer.Length, ReadCallback, stream);
}
});
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace SharpSerial
{
public class SerialException : Exception
{
private readonly string trace;
public SerialException(string message, string trace) : base(message)
{
this.trace = trace;
}
public string Trace { get { return trace; } }
}
}

View File

@@ -0,0 +1,126 @@
using System;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Threading.Tasks;
namespace SharpSerial
{
//this class should not swallow exceptions outside dispose
public class SerialProcess : ISerialStream, IDisposable
{
private readonly Process process;
private readonly int pid;
public int Pid { get { return pid; } }
public SerialProcess(object settings)
{
var ss = new SerialSettings(settings);
var args = new StringBuilder();
foreach (var p in ss.GetType().GetProperties())
{
if (args.Length > 0) args.Append(" ");
args.AppendFormat("{0}={1}", p.Name, p.GetValue(ss, null).ToString());
}
process = new Process();
process.StartInfo = new ProcessStartInfo()
{
FileName = typeof(SerialProcess).Assembly.Location,
Arguments = args.ToString(),
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = false,
};
EnableStandardError(process.StartInfo);
process.Start();
pid = process.Id;
ForwardStandardError(process.StandardError);
}
public void Dispose()
{
Tools.Try(() =>
{
process.StandardInput.Close();
process.WaitForExit(200);
});
Tools.Try(process.Kill);
Tools.Try(process.Dispose);
}
public void Write(byte[] data)
{
WriteHex(data);
var line = ReadLine();
Tools.Assert(line == "<ok", "Unexpected write response {0}", line);
}
public byte[] Read(int size, int eop, int toms)
{
WriteLine("$r,{0},{1},{2}", size, eop, toms);
return ParseHex(ReadLine());
}
private string ReadLine()
{
var line = process.StandardOutput.ReadLine();
if (line == null) throw new EndOfStreamException("Serial process EOF");
if (line.StartsWith("!"))
{
var trace = process.StandardOutput.ReadToEnd();
throw new SerialException(line.Substring(1), trace);
}
return line;
}
private void WriteLine(string format, params object[] args)
{
process.StandardInput.WriteLine(format, args);
process.StandardInput.Flush();
}
private void WriteHex(byte[] data)
{
var sb = new StringBuilder();
sb.Append(">");
foreach (var b in data) sb.Append(b.ToString("X2"));
WriteLine(sb.ToString());
}
private byte[] ParseHex(string text)
{
Tools.Assert(text.StartsWith("<"), "First char < expected for {0}:{1}", text.Length, text);
Tools.Assert(text.Length % 2 == 1, "Odd length expected for {0}:{1}", text.Length, text);
var bytes = new byte[text.Length / 2];
for (var i = 0; i < bytes.Length; i++)
{
var b2 = text.Substring(1 + i * 2, 2);
bytes[i] = Convert.ToByte(b2, 16);
}
return bytes;
}
[Conditional("DEBUG")]
private void EnableStandardError(ProcessStartInfo psi)
{
psi.RedirectStandardError = true;
}
[Conditional("DEBUG")]
private void ForwardStandardError(StreamReader reader)
{
Task.Factory.StartNew(() =>
{
var line = reader.ReadLine();
while (line != null)
{
Stdio.Trace(line);
line = reader.ReadLine();
}
});
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.IO.Ports;
using System.Globalization;
using System.ComponentModel;
using System.Text.RegularExpressions;
namespace SharpSerial
{
public class SerialSettings
{
public SerialSettings() => CopyProperties(new SerialPort(), this);
public SerialSettings(string portName) => CopyProperties(new SerialPort(portName), this);
public SerialSettings(object source) => CopyProperties(source, this);
public void CopyFrom(object source) => CopyProperties(source, this);
public void CopyTo(object target) => CopyProperties(this, target);
[TypeConverter(typeof(PortNameConverter))]
public string PortName { get; set; }
[TypeConverter(typeof(BaudRateConverter))]
public int BaudRate { get; set; }
public int DataBits { get; set; }
public Parity Parity { get; set; }
public StopBits StopBits { get; set; }
public Handshake Handshake { get; set; }
public static void CopyProperties(Object source, Object target)
{
CopyProperty(source, target, "PortName");
CopyProperty(source, target, "BaudRate");
CopyProperty(source, target, "DataBits");
CopyProperty(source, target, "Parity");
CopyProperty(source, target, "StopBits");
CopyProperty(source, target, "Handshake");
}
static void CopyProperty(Object source, Object target, string name)
{
var propertySource = source.GetType().GetProperty(name);
var propertyTarget = target.GetType().GetProperty(name);
propertyTarget.SetValue(target, propertySource.GetValue(source, null), null);
}
}
public class PortNameConverter : TypeConverter
{
//windows only?
private readonly Regex re = new Regex(@"[^a-zA-Z0-9_]");
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return true;
}
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
return new StandardValuesCollection(SerialPort.GetPortNames());
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
{
return false;
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return (sourceType == typeof(string));
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (re.IsMatch(value.ToString())) throw Tools.Make("Invalid chars");
return value;
}
public override object ConvertTo(ITypeDescriptorContext context,
CultureInfo culture, object value, Type destinationType)
{
return value;
}
}
public class BaudRateConverter : TypeConverter
{
public readonly static int[] BaudRates = new int[] {
110,
300,
600,
1200,
2400,
4800,
9600,
14400,
19200,
28800,
38400,
56000,
57600,
115200,
128000,
153600,
230400,
256000,
460800,
921600
};
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return true;
}
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
return new StandardValuesCollection(BaudRates);
}
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
{
return false;
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return (sourceType == typeof(string));
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
return int.Parse(value.ToString());
}
public override object ConvertTo(ITypeDescriptorContext context,
CultureInfo culture, object value, Type destinationType)
{
return value.ToString();
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
namespace SharpSerial
{
public interface ISerialStream : IDisposable
{
void Write(byte[] data);
byte[] Read(int size, int eop, int toms);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,476 @@
/* ========================================================================
* Copyright (c) 2005-2019 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Text;
using System.Collections.Generic;
using Opc.Ua;
using Opc.Ua.Client;
namespace OpcUaHelper
{
/// <summary>
/// Stores a type declaration retrieved from a server.
/// </summary>
public class TypeDeclaration
{
/// <summary>
/// The node if for the type.
/// </summary>
public NodeId NodeId;
/// <summary>
/// The fully inhierited list of instance declarations for the type.
/// </summary>
public List<InstanceDeclaration> Declarations;
}
/// <summary>
/// Stores an instance declaration fetched from the server.
/// </summary>
public class InstanceDeclaration
{
/// <summary>
/// The type that the declaration belongs to.
/// </summary>
public NodeId RootTypeId;
/// <summary>
/// The browse path to the instance declaration.
/// </summary>
public QualifiedNameCollection BrowsePath;
/// <summary>
/// The browse path to the instance declaration.
/// </summary>
public string BrowsePathDisplayText;
/// <summary>
/// A localized path to the instance declaration.
/// </summary>
public string DisplayPath;
/// <summary>
/// The node id for the instance declaration.
/// </summary>
public NodeId NodeId;
/// <summary>
/// The node class of the instance declaration.
/// </summary>
public NodeClass NodeClass;
/// <summary>
/// The browse name for the instance declaration.
/// </summary>
public QualifiedName BrowseName;
/// <summary>
/// The display name for the instance declaration.
/// </summary>
public string DisplayName;
/// <summary>
/// The description for the instance declaration.
/// </summary>
public string Description;
/// <summary>
/// The modelling rule for the instance declaration (i.e. Mandatory or Optional).
/// </summary>
public NodeId ModellingRule;
/// <summary>
/// The data type for the instance declaration.
/// </summary>
public NodeId DataType;
/// <summary>
/// The value rank for the instance declaration.
/// </summary>
public int ValueRank;
/// <summary>
/// The built-in type parent for the data type.
/// </summary>
public BuiltInType BuiltInType;
/// <summary>
/// A localized name for the data type.
/// </summary>
public string DataTypeDisplayText;
/// <summary>
/// An instance declaration that has been overridden by the current instance.
/// </summary>
public InstanceDeclaration OverriddenDeclaration;
}
/// <summary>
/// A field in a filter declaration.
/// </summary>
public class FilterDeclarationField
{
/// <summary>
/// Creates a new instance of a FilterDeclarationField.
/// </summary>
public FilterDeclarationField()
{
Selected = true;
DisplayInList = false;
FilterEnabled = false;
FilterOperator = FilterOperator.Equals;
FilterValue = Variant.Null;
InstanceDeclaration = null;
}
/// <summary>
/// Creates a new instance of a FilterDeclarationField.
/// </summary>
public FilterDeclarationField(InstanceDeclaration instanceDeclaration)
{
Selected = true;
DisplayInList = false;
FilterEnabled = false;
FilterOperator = FilterOperator.Equals;
FilterValue = Variant.Null;
InstanceDeclaration = instanceDeclaration;
}
/// <summary>
/// Creates a new instance of a FilterDeclarationField.
/// </summary>
public FilterDeclarationField(FilterDeclarationField field)
{
Selected = field.Selected;
DisplayInList = field.DisplayInList;
FilterEnabled = field.FilterEnabled;
FilterOperator = field.FilterOperator;
FilterValue = field.FilterValue;
InstanceDeclaration = field.InstanceDeclaration;
}
/// <summary>
/// Whether the field is returned as part of the event notification.
/// </summary>
public bool Selected;
/// <summary>
/// Whether the field is displayed in the list view.
/// </summary>
public bool DisplayInList;
/// <summary>
/// Whether the filter is enabled.
/// </summary>
public bool FilterEnabled;
/// <summary>
/// The filter operator to use in the where clause.
/// </summary>
public FilterOperator FilterOperator;
/// <summary>
/// The filter value to use in the where clause.
/// </summary>
public Variant FilterValue;
/// <summary>
/// The instance declaration associated with the field.
/// </summary>
public InstanceDeclaration InstanceDeclaration;
}
/// <summary>
/// A declararion of an event filter.
/// </summary>
public class FilterDeclaration
{
/// <summary>
/// Creates a new instance of a FilterDeclaration.
/// </summary>
public FilterDeclaration()
{
EventTypeId = Opc.Ua.ObjectTypeIds.BaseEventType;
Fields = new List<FilterDeclarationField>();
}
/// <summary>
/// Creates a new instance of a FilterDeclaration.
/// </summary>
public FilterDeclaration(TypeDeclaration eventType, FilterDeclaration template)
{
EventTypeId = eventType.NodeId;
Fields = new List<FilterDeclarationField>();
foreach (InstanceDeclaration instanceDeclaration in eventType.Declarations)
{
if (instanceDeclaration.NodeClass == NodeClass.Method)
{
continue;
}
if (NodeId.IsNull(instanceDeclaration.ModellingRule))
{
continue;
}
FilterDeclarationField element = new FilterDeclarationField(instanceDeclaration);
Fields.Add(element);
// set reasonable defaults.
if (template == null)
{
if (instanceDeclaration.RootTypeId == Opc.Ua.ObjectTypeIds.BaseEventType && instanceDeclaration.BrowseName != Opc.Ua.BrowseNames.EventId)
{
element.DisplayInList = true;
}
}
// preserve filter settings.
else
{
foreach (FilterDeclarationField field in template.Fields)
{
if (field.InstanceDeclaration.BrowsePathDisplayText == element.InstanceDeclaration.BrowsePathDisplayText)
{
element.DisplayInList = field.DisplayInList;
element.FilterEnabled = field.FilterEnabled;
element.FilterOperator = field.FilterOperator;
element.FilterValue = field.FilterValue;
break;
}
}
}
}
}
/// <summary>
/// Creates a new instance of a FilterDeclaration.
/// </summary>
public FilterDeclaration(FilterDeclaration declaration)
{
EventTypeId = declaration.EventTypeId;
Fields = new List<FilterDeclarationField>(declaration.Fields.Count);
for (int ii = 0; ii < declaration.Fields.Count; ii++)
{
Fields.Add(new FilterDeclarationField(declaration.Fields[ii]));
}
}
/// <summary>
/// Returns the event filter defined by the filter declaration.
/// </summary>
public EventFilter GetFilter()
{
EventFilter filter = new EventFilter();
filter.SelectClauses = GetSelectClause();
filter.WhereClause = GetWhereClause();
return filter;
}
/// <summary>
/// Adds a simple field to the declaration.
/// </summary>
public void AddSimpleField(QualifiedName browseName, BuiltInType dataType, bool displayInList)
{
AddSimpleField(new QualifiedName[] { browseName }, NodeClass.Variable, dataType, ValueRanks.Scalar, displayInList);
}
/// <summary>
/// Adds a simple field to the declaration.
/// </summary>
public void AddSimpleField(QualifiedName browseName, BuiltInType dataType, int valueRank, bool displayInList)
{
AddSimpleField(new QualifiedName[] { browseName }, NodeClass.Variable, dataType, valueRank, displayInList);
}
/// <summary>
/// Adds a simple field to the declaration.
/// </summary>
public void AddSimpleField(QualifiedName[] browseNames, BuiltInType dataType, int valueRank, bool displayInList)
{
AddSimpleField(browseNames, NodeClass.Variable, dataType, valueRank, displayInList);
}
/// <summary>
/// Adds a simple field to the declaration.
/// </summary>
public void AddSimpleField(QualifiedName[] browseNames, NodeClass nodeClass, BuiltInType dataType, int valueRank, bool displayInList)
{
FilterDeclarationField field = new FilterDeclarationField();
field.DisplayInList = displayInList;
field.InstanceDeclaration = new InstanceDeclaration();
field.InstanceDeclaration.NodeClass = nodeClass;
if (browseNames != null)
{
field.InstanceDeclaration.BrowseName = browseNames[browseNames.Length - 1];
field.InstanceDeclaration.BrowsePath = new QualifiedNameCollection();
StringBuilder path = new StringBuilder();
for (int ii = 0; ii < browseNames.Length; ii++)
{
if (path.Length > 0)
{
path.Append('/');
}
path.Append(browseNames[ii]);
field.InstanceDeclaration.BrowsePath.Add(browseNames[ii]);
}
field.InstanceDeclaration.BrowsePathDisplayText = path.ToString();
}
field.InstanceDeclaration.BuiltInType = dataType;
field.InstanceDeclaration.DataType = (uint)dataType;
field.InstanceDeclaration.ValueRank = valueRank;
field.InstanceDeclaration.DataTypeDisplayText = dataType.ToString();
if (valueRank >= 0)
{
field.InstanceDeclaration.DataTypeDisplayText += "[]";
}
field.InstanceDeclaration.DisplayName = field.InstanceDeclaration.BrowseName.Name;
field.InstanceDeclaration.DisplayPath = field.InstanceDeclaration.BrowsePathDisplayText;
field.InstanceDeclaration.RootTypeId = ObjectTypeIds.BaseEventType;
Fields.Add(field);
}
/// <summary>
/// Returns the select clause defined by the filter declaration.
/// </summary>
public SimpleAttributeOperandCollection GetSelectClause()
{
SimpleAttributeOperandCollection selectClause = new SimpleAttributeOperandCollection();
SimpleAttributeOperand operand = new SimpleAttributeOperand();
operand.TypeDefinitionId = Opc.Ua.ObjectTypeIds.BaseEventType;
operand.AttributeId = Attributes.NodeId;
selectClause.Add(operand);
foreach (FilterDeclarationField field in Fields)
{
if (field.Selected)
{
operand = new SimpleAttributeOperand();
operand.TypeDefinitionId = field.InstanceDeclaration.RootTypeId;
operand.AttributeId = (field.InstanceDeclaration.NodeClass == NodeClass.Object) ? Attributes.NodeId : Attributes.Value;
operand.BrowsePath = field.InstanceDeclaration.BrowsePath;
selectClause.Add(operand);
}
}
return selectClause;
}
/// <summary>
/// Returns the where clause defined by the filter declaration.
/// </summary>
public ContentFilter GetWhereClause()
{
ContentFilter whereClause = new ContentFilter();
ContentFilterElement element1 = whereClause.Push(FilterOperator.OfType, EventTypeId);
EventFilter filter = new EventFilter();
foreach (FilterDeclarationField field in Fields)
{
if (field.FilterEnabled)
{
SimpleAttributeOperand operand1 = new SimpleAttributeOperand();
operand1.TypeDefinitionId = field.InstanceDeclaration.RootTypeId;
operand1.AttributeId = (field.InstanceDeclaration.NodeClass == NodeClass.Object) ? Attributes.NodeId : Attributes.Value;
operand1.BrowsePath = field.InstanceDeclaration.BrowsePath;
LiteralOperand operand2 = new LiteralOperand();
operand2.Value = field.FilterValue;
ContentFilterElement element2 = whereClause.Push(field.FilterOperator, operand1, operand2);
element1 = whereClause.Push(FilterOperator.And, element1, element2);
}
}
return whereClause;
}
/// <summary>
/// Returns the value for the specified browse name.
/// </summary>
public T GetValue<T>(QualifiedName browseName, VariantCollection fields, T defaultValue)
{
if (fields == null || fields.Count == 0)
{
return defaultValue;
}
if (browseName == null)
{
browseName = QualifiedName.Null;
}
for (int ii = 0; ii < this.Fields.Count; ii++)
{
if (this.Fields[ii].InstanceDeclaration.BrowseName == browseName)
{
if (ii >= fields.Count + 1)
{
return defaultValue;
}
object value = fields[ii + 1].Value;
if (typeof(T).IsInstanceOfType(value))
{
return (T)value;
}
break;
}
}
return defaultValue;
}
/// <summary>
/// The type of event.
/// </summary>
public NodeId EventTypeId;
/// <summary>
/// The list of declarations for the fields.
/// </summary>
public List<FilterDeclarationField> Fields;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
using Opc.Ua;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpcUaHelper
{
/// <summary>
/// OPC UA的状态更新消息
/// </summary>
public class OpcUaStatusEventArgs
{
/// <summary>
/// 是否异常
/// </summary>
public bool Error { get; set; }
/// <summary>
/// 时间
/// </summary>
public DateTime Time { get; set; }
/// <summary>
/// 文本
/// </summary>
public string Text { get; set; }
/// <summary>
/// 转化为字符串
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Error ? "[异常]" : "[正常]" + Time.ToString(" yyyy-MM-dd HH:mm:ss ") + Text;
}
}
/// <summary>
/// 读取属性过程中用于描述的
/// </summary>
public class OpcNodeAttribute
{
/// <summary>
/// 属性的名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 属性的类型描述
/// </summary>
public string Type { get; set; }
/// <summary>
/// 操作结果状态描述
/// </summary>
public StatusCode StatusCode { get; set; }
/// <summary>
/// 属性的值,如果读取错误,返回文本描述
/// </summary>
public object Value { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OmronLib
{
public static class DataTools
{
/// <summary>
/// 将byte类型数据数组转换为16进制字符串形式
/// </summary>
/// <param name="hex"></param>
/// <param name="len"></param>
/// <returns></returns>
public static string ByteToHexString(byte[] hex, int len)
{
string returnstr = "";
for (int i = 0; i < len; i++)
{
returnstr += hex[i].ToString("X2");
}
return returnstr;
}
/// <summary>
/// 将一个长字符串转化为每个元素包含两个字符的字符数组
/// </summary>
/// <param name="src"></param>
/// <param name="def"></param>
/// <returns></returns>
public static string[] StrToStrArray(string src)
{
try
{
string[] res = new string[src.Length / 2];
for (int i = 0; i < src.Length / 2; i++)
{
res[i] = src.Substring(i * 2, 2);
}
return res;
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,960 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
namespace OmronLib
{
/*
*Copyright: Copyright (c) 2019
*Created on 2019-11-7
*Author: coder_li@outlook.com
*/
/// <summary>
/// 欧姆龙PLC使用FINS通讯在与PLC建立tcp连接后必须先进行读写准备获取到DA1和SA1
/// 这里默认读取200个D区地址从D5000开始到D5199读取的返回值都是高位在前低位在后
/// 写入时都是单个寄存器写入,写入的内容也是高位在前低位在后
/// </summary>
public class Omron
{
/// <summary>
/// PLCIP
/// </summary>
public IPAddress IPAddr { get; set; }
/// <summary>
/// PLC端口号
/// </summary>
public int Port { get; set; }
public int Timeout = 1000;
/// <summary>
/// 获取是否已经连接到PLC
/// </summary>
/// <returns></returns>
public bool IsConnected
{
get
{
if (PlcClient == null)
return false;
else
return PlcClient.Connected;
}
}
/// <summary>
/// PLC内存区域类型
/// </summary>
public enum AreaType
{
CIO_Bit = 0x30,
WR_Bit = 0x31,
HR_Bit = 0x32,
AR_Bit = 0x33,
DM_Bit = 0x02,
CIO_Word = 0xB0,
WR_Word = 0xB1,
HR_Word = 0xB2,
AR_Word = 0xB3,
DM_Word = 0x82
}
private readonly object locker = new object();
private TcpClient PlcClient = null;
private NetworkStream Stream = null;
private IPEndPoint PlcIP = null;
private byte DA1 = 0x00, SA1 = 0x00;
public Omron()
{ }
#region
/// <summary>
/// 写命令
/// </summary>
private readonly byte[] WriteCommand = new byte[] { 0x46, 0x49, 0x4e, 0x53, // F I N S
0x00, 0x00, 0x00, 0x1c, // 命令长度
0x00, 0x00, 0x00, 0x02, // 命令码
0x00, 0x00, 0x00, 0x00, // 错误码
0x80, 0x00, 0x02, 0x00, // ICF RSV GCT DNA
0x00, 0x00, 0x00, 0x00, // DA1 DA2 SNA SA1
0x00, 0xDA, // SA2 SID
0x01, 0x02, // FINS主命令 FINS从命令 01 02 写)
0x82, 0x00, 0x00, 0x00, // 寄存器控制位 开始字地址(从高到低) 开始位地址
0x00, 0x01 }; // 写入数量(从高到低) 后面都是写入的内容(可以加长)
/// <summary>
/// 读命令
/// </summary>
private readonly byte[] ReadCommand = new byte[] { 0x46, 0x49, 0x4e, 0x53, // F I N S
0x00, 0x00, 0x00, 0x1a, // 命令长度
0x00, 0x00, 0x00, 0x02, // 命令码
0x00, 0x00, 0x00, 0x00, // 错误码
0x80, 0x00, 0x02, 0x00, // ICF RSV GCT DNA ***************
0x00, 0x00, 0x00, 0x00, // DA1 DA2 SNA SA1 FINS Header
0x00, 0xDA, // SA2 SID ***************
0x01, 0x01, // FINS主命令 FINS从命令 01 01 读) ***************
0x82, 0x00, 0x00, 0x00, // 寄存器控制位 开始字地址(高,低) 开始位地址 FINS Command
0x00, 0x01 }; // 读取数量(高,低) ***************
/// <summary>
/// 获取节点地址的命令
/// </summary>
private readonly byte[] PrepareCmd = new byte[] { 0x46, 0x49, 0x4e, 0x53,
0x00, 0x00, 0x00, 0x0c,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00 };
#endregion
/// <summary>
/// 向PLC发送指令
/// </summary>
/// <param name="commands">指令</param>
/// <param name="info">报错信息</param>
/// <returns>是否发送成功</returns>
private bool DataSend(byte[] commands)
{
string info;
try
{
if (this.PlcClient != null && PlcClient.Connected)
{
//lock (locker)
{
if (Stream == null) Stream = PlcClient.GetStream();
Stream.Write(commands, 0, commands.Length);
Stream.Flush();
}
return true;
}
else
{
info = "连接已断开";
return false;
}
}
catch (Exception exp)
{
info = "发送指令到PLC失败 " + exp.Message;
return false;
}
}
/// <summary>
/// 从PLC读取数据
/// </summary>
/// <param name="isExtCmd"></param>
/// <param name="info">报错信息</param>
/// <returns></returns>
private string DataRead()
{
byte[] buffer = new byte[1024];
int bytestoread = 0;
string info;
try
{
//if (Stream.DataAvailable)
{
int start = Environment.TickCount;
do
{
bytestoread = Stream.Read(buffer, 0, buffer.Length);
if (Math.Abs(Environment.TickCount - start) >= 20000)
{
info = "PLC应答超时请确认PLC连接线路及cpu是否有报错";
break;
}
} while (bytestoread == 0);
if (buffer[11] == 3)
{
if (buffer[15] == 1)
{
info = "发送的命令不是有效的FINS命令";
return "";
}
else if (buffer[15] == 2)
{
info = "发送的命令长度超出范围!";
return "";
}
else if (buffer[15] == 3)
{
info = "PLC不支持该FINS命令";
return "";
}
}
if (buffer[25] == WriteCommand[25])
return DataTools.ByteToHexString(buffer, bytestoread);
else
{
info = "PLC返回错误返回值 " + buffer[0].ToString();
return "";
}
}
}
catch
{
return "";
}
}
/// <summary>
/// 判断是否成功写入命令到PLC
/// </summary>
/// <returns></returns>
private bool Write2PlcSuccess()
{
byte[] buffer = new byte[1024];
int bytestoread = 0;
string info;
int start = Environment.TickCount;
try
{
//lock (locker)
{
do
{
bytestoread = Stream.Read(buffer, 0, buffer.Length);
if (Math.Abs(Environment.TickCount - start) >= 1000)
{
info = "PLC应答超时请确认PLC连接线路及cpu是否有报错";
break;
}
} while (bytestoread == 0);
}
if (buffer[25] == WriteCommand[25]) return true;
else
{
info = "PLC返回错误返回值 " + buffer[1].ToString();
return false;
}
}
catch
{
return false;
}
}
/// <summary>
/// 发送连接字符串获取PLC返回的DA1和SA1值
/// </summary>
/// <returns></returns>
private bool ConnectPrepare()
{
try
{
Stream.Write(PrepareCmd, 0, PrepareCmd.Length);
byte[] res = new byte[24];
Stream.Read(res, 0, res.Length);
DA1 = res[23];
SA1 = res[19];
}
catch { return false; }
return true;
}
/// <summary>
/// 检查命令帧中的EndCode
/// </summary>
/// <param name="Main">主码</param>
/// <param name="Sub">副码</param>
/// <param name="info">错误信息</param>
/// <returns>指示程序是否可以继续进行</returns>
private bool CheckEndCode(byte Main, byte Sub, out string info)
{
info = "";
switch (Main)
{
case 0x00:
switch (Sub)
{
case 0x00: return true;//the only situation of success
case 0x01: info = "service canceled"; return false;
}
break;
case 0x01:
switch (Sub)
{
case 0x01: info = "local node not in network"; return false;
case 0x02: info = "token timeout"; return false;
case 0x03: info = "retries failed"; return false;
case 0x04: info = "too many send frames"; return false;
case 0x05: info = "node address range error"; return false;
case 0x06: info = "node address duplication"; return false;
}
break;
case 0x02:
switch (Sub)
{
case 0x01: info = "destination node not in network"; return false;
case 0x02: info = "unit missing"; return false;
case 0x03: info = "third node missing"; return false;
case 0x04: info = "destination node busy"; return false;
case 0x05: info = "response timeout"; return false;
}
break;
case 0x03:
switch (Sub)
{
case 0x01: info = "communications controller error"; return false;
case 0x02: info = "CPU unit error"; return false;
case 0x03: info = "controller error"; return false;
case 0x04: info = "unit number error"; return false;
}
break;
case 0x04:
switch (Sub)
{
case 0x01: info = "undefined command"; return false;
case 0x02: info = "not supported by model/version"; return false;
}
break;
case 0x05:
switch (Sub)
{
case 0x01: info = "destination address setting error"; return false;
case 0x02: info = "no routing tables"; return false;
case 0x03: info = "routing table error"; return false;
case 0x04: info = "too many relays"; return false;
}
break;
case 0x10:
switch (Sub)
{
case 0x01: info = "command too long"; return false;
case 0x02: info = "command too short"; return false;
case 0x03: info = "elements/data don't match"; return false;
case 0x04: info = "command format error"; return false;
case 0x05: info = "header error"; return false;
}
break;
case 0x11:
switch (Sub)
{
case 0x01: info = "area classification missing"; return false;
case 0x02: info = "access size error"; return false;
case 0x03: info = "address range error"; return false;
case 0x04: info = "address range exceeded"; return false;
case 0x06: info = "program missing"; return false;
case 0x09: info = "relational error"; return false;
case 0x0a: info = "duplicate data access"; return false;
case 0x0b: info = "response too long"; return false;
case 0x0c: info = "parameter error"; return false;
}
break;
case 0x20:
switch (Sub)
{
case 0x02: info = "protected"; return false;
case 0x03: info = "table missing"; return false;
case 0x04: info = "data missing"; return false;
case 0x05: info = "program missing"; return false;
case 0x06: info = "file missing"; return false;
case 0x07: info = "data mismatch"; return false;
}
break;
case 0x21:
switch (Sub)
{
case 0x01: info = "read-only"; return false;
case 0x02: info = "protected , cannot write data link table"; return false;
case 0x03: info = "cannot register"; return false;
case 0x05: info = "program missing"; return false;
case 0x06: info = "file missing"; return false;
case 0x07: info = "file name already exists"; return false;
case 0x08: info = "cannot change"; return false;
}
break;
case 0x22:
switch (Sub)
{
case 0x01: info = "not possible during execution"; return false;
case 0x02: info = "not possible while running"; return false;
case 0x03: info = "wrong PLC mode"; return false;
case 0x04: info = "wrong PLC mode"; return false;
case 0x05: info = "wrong PLC mode"; return false;
case 0x06: info = "wrong PLC mode"; return false;
case 0x07: info = "specified node not polling node"; return false;
case 0x08: info = "step cannot be executed"; return false;
}
break;
case 0x23:
switch (Sub)
{
case 0x01: info = "file device missing"; return false;
case 0x02: info = "memory missing"; return false;
case 0x03: info = "clock missing"; return false;
}
break;
case 0x24:
switch (Sub)
{ case 0x01: info = "table missing"; return false; }
break;
case 0x25:
switch (Sub)
{
case 0x02: info = "memory error"; return false;
case 0x03: info = "I/O setting error"; return false;
case 0x04: info = "too many I/O points"; return false;
case 0x05: info = "CPU bus error"; return false;
case 0x06: info = "I/O duplication"; return false;
case 0x07: info = "CPU bus error"; return false;
case 0x09: info = "SYSMAC BUS/2 error"; return false;
case 0x0a: info = "CPU bus unit error"; return false;
case 0x0d: info = "SYSMAC BUS No. duplication"; return false;
case 0x0f: info = "memory error"; return false;
case 0x10: info = "SYSMAC BUS terminator missing"; return false;
}
break;
case 0x26:
switch (Sub)
{
case 0x01: info = "no protection"; return false;
case 0x02: info = "incorrect password"; return false;
case 0x04: info = "protected"; return false;
case 0x05: info = "service already executing"; return false;
case 0x06: info = "service stopped"; return false;
case 0x07: info = "no execution right"; return false;
case 0x08: info = "settings required before execution"; return false;
case 0x09: info = "necessary items not set"; return false;
case 0x0a: info = "number already defined"; return false;
case 0x0b: info = "error will not clear"; return false;
}
break;
case 0x30:
switch (Sub)
{ case 0x01: info = "no access right"; return false; }
break;
case 0x40:
switch (Sub)
{ case 0x01: info = "service aborted"; return false; }
break;
}
info = "unknown exception";
return false;
}
/// <summary>
/// PLC初始化
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
private bool PlcInit()
{
bool flag = false;
PlcClient = new TcpClient();
IAsyncResult asyncResult = PlcClient.BeginConnect(IPAddr, Port, null, null);
if (!asyncResult.AsyncWaitHandle.WaitOne(Timeout))
{
return flag;
}
PlcClient.EndConnect(asyncResult);
if (PlcClient != null)
{
Stream = PlcClient.GetStream();
Stream.ReadTimeout = Timeout;
Stream.WriteTimeout = Timeout;
if (ConnectPrepare())
{
flag = true;
}
}
return flag;
}
/// <summary>
/// 连接PLC
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
public bool PlcConnect(out string info)
{
info = "";
try
{
if (PlcClient == null)
{
PlcInit();
}
if (!PlcClient.Connected) // 没连上的话重试一遍
{
PlcClient.Close();
PlcClient = null;
PlcInit();
}
return PlcClient.Connected;
}
catch (Exception exp)
{
info = "连接PLC失败\n" + exp.Message;
return false;
}
}
/// <summary>
/// 断开PLC连接
/// </summary>
/// <returns></returns>
public bool DisConnect()
{
if (this.PlcClient == null)
return true;
if (!this.PlcClient.Connected)
return true;
this.PlcClient.Close();
return true;
}
#region
/// <summary>
/// 读PLC字或位
/// 读位时从左到右是低位到高位
/// </summary>
/// <param name="type">PLC内存区类型</param>
/// <param name="startword">开始地址(字)</param>
/// <param name="startbit">开始地址(位)</param>
/// <param name="count">长度</param>
/// <param name="result">返回的字符数组,每个元素表示一个字</param>
/// <returns></returns>
public bool Read(AreaType type, int startword, int startbit, int count, out string[] result)
{
result = new string[0];
byte[] cmd = (byte[])ReadCommand.Clone();
cmd[20] = DA1; //连接时获取到的DA1
cmd[23] = SA1; //连接时获取到的SA1
// 内存类型
cmd[28] = (byte)type;
// 读取起始地址
byte[] bytesAddr = BitConverter.GetBytes((short)startword); //BitConverter转出来的Byte按从低位到高位排列
cmd[29] = bytesAddr[1];
cmd[30] = bytesAddr[0];
cmd[31] = (byte)startbit;
// 读取长度
byte[] bytesLength = BitConverter.GetBytes((short)count);
cmd[32] = bytesLength[1];
cmd[33] = bytesLength[0];
//开始读取
lock (locker)
{
if (DataSend(cmd))
{
string res = DataRead();
if (res.Length == 0)
return false;
result = DataTools.StrToStrArray(res.Substring(60));
return true;
}
}
return false;
}
/// <summary>
/// 读取DM区的字
/// </summary>
/// <param name="startword"></param>
/// <param name="count"></param>
/// <param name="result"></param>
/// <returns></returns>
public bool ReadDWords(int startword, int count, out string[] result)
{
string[] res;
bool isSucess = Read(AreaType.DM_Word, startword, 0, count, out res);
result = res;
return isSucess;
}
/// <summary>
/// 读取DM区的位
/// </summary>
/// <param name="startword"></param>
/// <param name="startbit"></param>
/// <param name="count"></param>
/// <param name="result"></param>
/// <returns></returns>
public bool ReadDBits(int startword, int startbit, int count, out string[] result)
{
string[] res;
bool isSucess = Read(AreaType.DM_Bit, startword, startbit, count, out res);
result = res;
return isSucess;
}
#endregion
#region
/// <summary>
/// 写PLC字
/// </summary>
/// <param name="type">地址类型</param>
/// <param name="startword">开始字地址</param>
/// <param name="count">字数</param>
/// <param name="paras">值</param>
/// <returns>写入成功与否</returns>
public bool WriteWords(AreaType type, int startword, int count, int[] paras)
{
byte[] hexadd = BitConverter.GetBytes(startword);
byte[] sendCmd = WriteCommand.Concat(new byte[count * 2]).ToArray(); // 这里扩容sendCmd数组不然会溢出
sendCmd[7] = (byte)(26 + count * 2);
sendCmd[20] = DA1;
sendCmd[23] = SA1;
// 内存类型
sendCmd[28] = (byte)type;
// 写入起始地址
byte[] bytesAddr = BitConverter.GetBytes((short)startword); //BitConverter转出来的Byte按从低位到高位排列
sendCmd[29] = bytesAddr[1];
sendCmd[30] = bytesAddr[0];
sendCmd[31] = 0;
// 写入长度
byte[] bytesLength = BitConverter.GetBytes((short)count);
sendCmd[32] = bytesLength[1];
sendCmd[33] = bytesLength[0];
byte[] hexPara;
for (int i = 0; i < count; i++)
{
hexPara = BitConverter.GetBytes(paras[i]);
sendCmd[34 + i * 2] = hexPara[1];
sendCmd[34 + i * 2 + 1] = hexPara[0];
}
lock (locker)
{
if (DataSend(sendCmd))
{
if (Write2PlcSuccess())
return true;
}
}
return false;
}
/// <summary>
/// 写PLC位
///
/// </summary>
/// <param name="type">地址类型</param>
/// <param name="startword">开始字地址</param>
/// <param name="startbit">开始位地址</param>
/// <param name="count">写入位数</param>
/// <param name="paras">值</param>
/// <returns>写入成功与否</returns>
public bool WriteBits(AreaType type, int startword, int startbit, int count, bool[] paras)
{
byte[] hexadd = BitConverter.GetBytes(startword);
byte[] sendCmd = WriteCommand.Concat(new byte[count]).ToArray(); // 这里扩容sendCmd数组不然会溢出
// 命令长度
sendCmd[7] = (byte)(26 + count);
sendCmd[20] = DA1;
sendCmd[23] = SA1;
// 内存类型
sendCmd[28] = (byte)type;
// 写入起始地址
byte[] bytesAddr = BitConverter.GetBytes((short)startword); //BitConverter转出来的Byte按从低位到高位排列
sendCmd[29] = bytesAddr[1];
sendCmd[30] = bytesAddr[0];
sendCmd[31] = (byte)startbit;
// 写入长度
byte[] bytesLength = BitConverter.GetBytes((short)count);
sendCmd[32] = bytesLength[1];
sendCmd[33] = bytesLength[0];
for (int i = 0; i < count; i++)
{
sendCmd[34 + i] = Convert.ToByte(paras[i]);
}
lock (locker)
{
if (DataSend(sendCmd))
{
if (Write2PlcSuccess())
return true;
}
}
return false;
}
/// <summary>
/// 写D字
/// </summary>
/// <param name="startword"></param>
/// <param name="count"></param>
/// <param name="paras"></param>
/// <returns></returns>
public bool WriteDWords(int startword, int count, int[] paras)
{
return WriteWords(AreaType.DM_Word, startword, count, paras);
}
/// <summary>
/// 写D位
/// </summary>
/// <param name="startword"></param>
/// <param name="startbit"></param>
/// <param name="count"></param>
/// <param name="paras"></param>
/// <returns></returns>
public bool WriteDBits(int startword, int startbit, int count, bool[] paras)
{
return WriteBits(AreaType.DM_Bit, startword, startbit, count, paras);
}
public bool ReadString(int address, int count, out string str)
{
try
{
string[] res;
bool isSucess = ReadDWords(address, count, out res);
if (!isSucess)
{
str = "";
return false;
}
byte[] bRes = res.Select(it => Convert.ToByte(it, 16)).ToArray();
List<int> list = new List<int>();
for (int i = 0; i < bRes.Length; i += 2)
{
list.Add((int)(bRes[i] << 8 | bRes[i + 1]));
}
str = GetQRCodeByData(list.ToArray());
return true;
}
catch (Exception ex)
{
str = ex.Message;
return false;
}
}
public bool ReadFloat(int address, out float value)
{
try
{
string[] res;
bool isSucess = ReadDWords(address, 2, out res);
if (!isSucess)
{
value = 0;
return false;
}
byte[] bRes = res.Select(it => Convert.ToByte(it, 16)).ToArray();
if (bRes.Length != 4)
{
value = 0;
return false;
}
int i = 0;
value = BitConverter.ToSingle(new byte[] { bRes[i + 1], bRes[i], bRes[i + 3], bRes[i + 2] }, 0);
return true;
}
catch (Exception ex)
{
value = 0;
return false;
}
}
public bool ReadFloats(int address, int count, out float[] values)
{
try
{
string[] res;
bool isSucess = ReadDWords(address, count * 2, out res);
if (!isSucess)
{
values = null;
return false;
}
byte[] bRes = res.Select(it => Convert.ToByte(it, 16)).ToArray();
if (bRes.Length != (count * 4))
{
values = null;
return false;
}
List<float> list = new List<float>();
for (int i = 0; i < bRes.Length; i += 4)
{
list.Add(BitConverter.ToSingle(new byte[] { bRes[i + 1], bRes[i], bRes[i + 3], bRes[i + 2] }, 0));
}
values = list.ToArray();
return true;
}
catch (Exception ex)
{
values = null;
return false;
}
}
public bool WriteFloat(int address, float[] values)
{
List<int> writeValues = new List<int>();
foreach (float f in values)
{
byte[] bytes = BitConverter.GetBytes(f);
writeValues.Add(BitConverter.ToInt32(new byte[] { bytes[0], bytes[1], 0, 0 }, 0));
writeValues.Add(BitConverter.ToInt32(new byte[] { bytes[2], bytes[3], 0, 0 }, 0));
}
return WriteWords(AreaType.DM_Word, address, values.Length * 2, writeValues.ToArray());
}
public bool ReadShort(int address, out short value)
{
try
{
string[] res;
bool isSucess = ReadDWords(address, 1, out res);
if (!isSucess)
{
value = 0;
return false;
}
byte[] bRes = res.Select(it => Convert.ToByte(it, 16)).ToArray();
if (bRes.Length != 2)
{
value = 0;
return false;
}
value = BitConverter.ToInt16(new byte[] { bRes[1], bRes[0] }, 0);
return true;
}
catch (Exception ex)
{
value = 0;
return false;
}
}
public bool ReadShorts(int address, int count, out short[] values)
{
try
{
string[] res;
bool isSucess = ReadDWords(address, count, out res);
if (!isSucess)
{
values = null;
return false;
}
byte[] bRes = res.Select(it => Convert.ToByte(it, 16)).ToArray();
if (bRes.Length != (count * 2))
{
values = null;
return false;
}
List<short> list = new List<short>();
for (int i = 0; i < bRes.Length; i += 2)
{
list.Add(BitConverter.ToInt16(new byte[] { bRes[i + 1], bRes[i] }, 0));
}
values = list.ToArray();
return true;
}
catch (Exception ex)
{
values = null;
return false;
}
}
public bool WriteShort(int address, short[] values)
{
List<int> writeValues = new List<int>();
foreach (short f in values)
{
writeValues.Add(f);
}
return WriteWords(AreaType.DM_Word, address, values.Length, writeValues.ToArray());
}
public bool WriteString(int address, string str, int count)
{
try
{
int[] aaa = GetDataByQRCode(str, count);
return WriteDWords(address, count, aaa);
}
catch (Exception ex)
{
return false;
}
}
private int[] GetDataByQRCode(string qrcode, int totalCount)
{
byte[] byteList = new byte[2 * totalCount];
byte[] bytes = Encoding.ASCII.GetBytes(qrcode);
for (int i = 0; i < byteList.Length; i++)
{
if (i >= bytes.Length)
{
break;
}
byteList[i] = bytes[i];
}
List<int> shortList = new List<int>();
for (int i = 0; i < (2 * totalCount); i += 2)
{
//将字节数组转为int存入list
shortList.Add(BitConverter.ToInt32(new byte[] { byteList[i], byteList[i + 1], 0x00, 0x00 }, 0));
}
return shortList.ToArray();
}
/// <summary>
/// 将PLC中获取到的数据转成string
/// </summary>
/// <param name="qrcode"></param>
/// <returns></returns>
private string GetQRCodeByData(int[] qrcode)
{
List<byte> list = new List<byte>();
for (int i = 0; i < qrcode.Length; i++)
{
//int中取两个字节存入list
list.AddRange(BitConverter.GetBytes(qrcode[i]).Take(2));
}
//去掉多余的/0和空格 转为string
string qrcodes = Encoding.ASCII.GetString(list.ToArray()).Trim('\0').Trim();
return qrcodes;
}
#endregion
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// COTP Protocol functions and types
/// </summary>
internal class COTP
{
public enum PduType : byte
{
Data = 0xf0,
ConnectionConfirmed = 0xd0
}
/// <summary>
/// Describes a COTP TPDU (Transport protocol data unit)
/// </summary>
public class TPDU
{
public TPKT TPkt { get; }
public byte HeaderLength;
public PduType PDUType;
public int TPDUNumber;
public byte[] Data;
public bool LastDataUnit;
public TPDU(TPKT tPKT)
{
TPkt = tPKT;
HeaderLength = tPKT.Data[0]; // Header length excluding this length byte
if (HeaderLength >= 2)
{
PDUType = (PduType)tPKT.Data[1];
if (PDUType == PduType.Data) //DT Data
{
var flags = tPKT.Data[2];
TPDUNumber = flags & 0x7F;
LastDataUnit = (flags & 0x80) > 0;
Data = new byte[tPKT.Data.Length - HeaderLength - 1]; // substract header length byte + header length.
Array.Copy(tPKT.Data, HeaderLength + 1, Data, 0, Data.Length);
return;
}
//TODO: Handle other PDUTypes
}
Data = new byte[0];
}
/// <summary>
/// Reads COTP TPDU (Transport protocol data unit) from the network stream
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="stream">The socket to read from</param>
/// <returns>COTP DPDU instance</returns>
public static async Task<TPDU> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var tpkt = await TPKT.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (tpkt.Length == 0)
{
throw new TPDUInvalidException("No protocol data received");
}
return new TPDU(tpkt);
}
public override string ToString()
{
return string.Format("Length: {0} PDUType: {1} TPDUNumber: {2} Last: {3} Segment Data: {4}",
HeaderLength,
PDUType,
TPDUNumber,
LastDataUnit,
BitConverter.ToString(Data)
);
}
}
/// <summary>
/// Describes a COTP TSDU (Transport service data unit). One TSDU consist of 1 ore more TPDUs
/// </summary>
public class TSDU
{
/// <summary>
/// Reads the full COTP TSDU (Transport service data unit)
/// See: https://tools.ietf.org/html/rfc905
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>Data in TSDU</returns>
public static async Task<byte[]> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
if (segment.LastDataUnit)
{
return segment.Data;
}
// More segments are expected, prepare a buffer to store all data
var buffer = new byte[segment.Data.Length];
Array.Copy(segment.Data, buffer, segment.Data.Length);
while (!segment.LastDataUnit)
{
segment = await TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
var previousLength = buffer.Length;
Array.Resize(ref buffer, buffer.Length + segment.Data.Length);
Array.Copy(segment.Data, 0, buffer, previousLength, segment.Data.Length);
}
return buffer;
}
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Net.Sockets;
namespace S7.Net
{
public static class TcpClientMixins
{
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Globalization;
namespace S7.Net
{
/// <summary>
/// Conversion methods to convert from Siemens numeric format to C# and back
/// </summary>
public static class Conversion
{
/// <summary>
/// Converts a binary string to Int32 value
/// </summary>
/// <param name="txt"></param>
/// <returns></returns>
public static int BinStringToInt32(this string txt)
{
int ret = 0;
for (int i = 0; i < txt.Length; i++)
{
ret = (ret << 1) | ((txt[i] == '1') ? 1 : 0);
}
return ret;
}
/// <summary>
/// Converts a binary string to a byte. Can return null.
/// </summary>
/// <param name="txt"></param>
/// <returns></returns>
public static byte? BinStringToByte(this string txt)
{
if (txt.Length == 8) return (byte)BinStringToInt32(txt);
return null;
}
/// <summary>
/// Converts the value to a binary string
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string ValToBinString(this object value)
{
int cnt = 0;
int cnt2 = 0;
int x = 0;
string txt = "";
long longValue = 0;
try
{
if (value.GetType().Name.IndexOf("[]") < 0)
{
// ist nur ein Wert
switch (value.GetType().Name)
{
case "Byte":
x = 7;
longValue = (long)((byte)value);
break;
case "Int16":
x = 15;
longValue = (long)((Int16)value);
break;
case "Int32":
x = 31;
longValue = (long)((Int32)value);
break;
case "Int64":
x = 63;
longValue = (long)((Int64)value);
break;
default:
throw new Exception();
}
for (cnt = x; cnt >= 0; cnt += -1)
{
if (((Int64)longValue & (Int64)Math.Pow(2, cnt)) > 0)
txt += "1";
else
txt += "0";
}
}
else
{
// ist ein Array
switch (value.GetType().Name)
{
case "Byte[]":
x = 7;
byte[] ByteArr = (byte[])value;
for (cnt2 = 0; cnt2 <= ByteArr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((ByteArr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
case "Int16[]":
x = 15;
Int16[] Int16Arr = (Int16[])value;
for (cnt2 = 0; cnt2 <= Int16Arr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((Int16Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
case "Int32[]":
x = 31;
Int32[] Int32Arr = (Int32[])value;
for (cnt2 = 0; cnt2 <= Int32Arr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((Int32Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
case "Int64[]":
x = 63;
byte[] Int64Arr = (byte[])value;
for (cnt2 = 0; cnt2 <= Int64Arr.Length - 1; cnt2++)
{
for (cnt = x; cnt >= 0; cnt += -1)
if ((Int64Arr[cnt2] & (byte)Math.Pow(2, cnt)) > 0) txt += "1"; else txt += "0";
}
break;
default:
throw new Exception();
}
}
return txt;
}
catch
{
return "";
}
}
/// <summary>
/// Helper to get a bit value given a byte and the bit index.
/// Example: DB1.DBX0.5 -> var bytes = ReadBytes(DB1.DBW0); bool bit = bytes[0].SelectBit(5);
/// </summary>
/// <param name="data"></param>
/// <param name="bitPosition"></param>
/// <returns></returns>
public static bool SelectBit(this byte data, int bitPosition)
{
int mask = 1 << bitPosition;
int result = data & mask;
return (result != 0);
}
/// <summary>
/// Converts from ushort value to short value; it's used to retrieve negative values from words
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static short ConvertToShort(this ushort input)
{
short output;
output = short.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from short value to ushort value; it's used to pass negative values to DWs
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static ushort ConvertToUshort(this short input)
{
ushort output;
output = ushort.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from UInt32 value to Int32 value; it's used to retrieve negative values from DBDs
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static Int32 ConvertToInt(this uint input)
{
int output;
output = int.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from Int32 value to UInt32 value; it's used to pass negative values to DBDs
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static UInt32 ConvertToUInt(this int input)
{
uint output;
output = uint.Parse(input.ToString("X"), NumberStyles.HexNumber);
return output;
}
/// <summary>
/// Converts from float to DWord (DBD)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static UInt32 ConvertToUInt(this float input)
{
uint output;
output = S7.Net.Types.DWord.FromByteArray(S7.Net.Types.Real.ToByteArray(input));
return output;
}
/// <summary>
/// Converts from DWord (DBD) to float
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static float ConvertToFloat(this uint input)
{
float output;
output = S7.Net.Types.Real.FromByteArray(S7.Net.Types.DWord.ToByteArray(input));
return output;
}
}
}

View File

@@ -0,0 +1,211 @@
namespace S7.Net
{
/// <summary>
/// Types of S7 cpu supported by the library
/// </summary>
public enum CpuType
{
/// <summary>
/// S7 200 cpu type
/// </summary>
S7200 = 0,
/// <summary>
/// Siemens Logo 0BA8
/// </summary>
Logo0BA8 = 1,
/// <summary>
/// S7 200 Smart
/// </summary>
S7200Smart = 2,
/// <summary>
/// S7 300 cpu type
/// </summary>
S7300 = 10,
/// <summary>
/// S7 400 cpu type
/// </summary>
S7400 = 20,
/// <summary>
/// S7 1200 cpu type
/// </summary>
S71200 = 30,
/// <summary>
/// S7 1500 cpu type
/// </summary>
S71500 = 40,
}
/// <summary>
/// Types of error code that can be set after a function is called
/// </summary>
public enum ErrorCode
{
/// <summary>
/// The function has been executed correctly
/// </summary>
NoError = 0,
/// <summary>
/// Wrong type of CPU error
/// </summary>
WrongCPU_Type = 1,
/// <summary>
/// Connection error
/// </summary>
ConnectionError = 2,
/// <summary>
/// Ip address not available
/// </summary>
IPAddressNotAvailable,
/// <summary>
/// Wrong format of the variable
/// </summary>
WrongVarFormat = 10,
/// <summary>
/// Wrong number of received bytes
/// </summary>
WrongNumberReceivedBytes = 11,
/// <summary>
/// Error on send data
/// </summary>
SendData = 20,
/// <summary>
/// Error on read data
/// </summary>
ReadData = 30,
/// <summary>
/// Error on write data
/// </summary>
WriteData = 50
}
/// <summary>
/// Types of memory area that can be read
/// </summary>
public enum DataType
{
/// <summary>
/// Input area memory
/// </summary>
Input = 129,
/// <summary>
/// Output area memory
/// </summary>
Output = 130,
/// <summary>
/// Merkers area memory (M0, M0.0, ...)
/// </summary>
Memory = 131,
/// <summary>
/// DB area memory (DB1, DB2, ...)
/// </summary>
DataBlock = 132,
/// <summary>
/// Timer area memory(T1, T2, ...)
/// </summary>
Timer = 29,
/// <summary>
/// Counter area memory (C1, C2, ...)
/// </summary>
Counter = 28
}
/// <summary>
/// Types
/// </summary>
public enum VarType
{
/// <summary>
/// S7 Bit variable type (bool)
/// </summary>
Bit,
/// <summary>
/// S7 Byte variable type (8 bits)
/// </summary>
Byte,
/// <summary>
/// S7 Word variable type (16 bits, 2 bytes)
/// </summary>
Word,
/// <summary>
/// S7 DWord variable type (32 bits, 4 bytes)
/// </summary>
DWord,
/// <summary>
/// S7 Int variable type (16 bits, 2 bytes)
/// </summary>
Int,
/// <summary>
/// DInt variable type (32 bits, 4 bytes)
/// </summary>
DInt,
/// <summary>
/// Real variable type (32 bits, 4 bytes)
/// </summary>
Real,
/// <summary>
/// LReal variable type (64 bits, 8 bytes)
/// </summary>
LReal,
/// <summary>
/// Char Array / C-String variable type (variable)
/// </summary>
String,
/// <summary>
/// S7 String variable type (variable)
/// </summary>
S7String,
/// <summary>
/// S7 WString variable type (variable)
/// </summary>
S7WString,
/// <summary>
/// Timer variable type
/// </summary>
Timer,
/// <summary>
/// Counter variable type
/// </summary>
Counter,
/// <summary>
/// DateTIme variable type
/// </summary>
DateTime,
/// <summary>
/// DateTimeLong variable type
/// </summary>
DateTimeLong
}
}

View File

@@ -0,0 +1,35 @@
using System.IO;
namespace S7.Net.Helper
{
internal static class MemoryStreamExtension
{
/// <summary>
/// Helper function to write to whole content of the given byte array to a memory stream.
///
/// Writes all bytes in value from 0 to value.Length to the memory stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="value"></param>
public static void Write(this MemoryStream stream, byte[] value)
{
stream.Write(value, 0, value.Length);
}
/// <summary>
/// Helper function to write the whole content of the given byte span to a memory stream.
/// </summary>
/// <param name="stream"></param>
/// <param name="value"></param>
//public static void Write(this MemoryStream stream, ReadOnlySpan<byte> value)
//{
// byte[] buffer = ArrayPool<byte>.Shared.Rent(value.Length);
// value.CopyTo(buffer);
// stream.Write(buffer, 0, value.Length);
// ArrayPool<byte>.Shared.Return(buffer);
//}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net.Internal
{
internal class TaskQueue
{
private static readonly object Sentinel = new object();
private Task prev = Task.FromResult(Sentinel);
public async Task<T> Enqueue<T>(Func<Task<T>> action)
{
var tcs = new TaskCompletionSource<object>();
await Interlocked.Exchange(ref prev, tcs.Task).ConfigureAwait(false);
try
{
return await action.Invoke().ConfigureAwait(false);
}
finally
{
tcs.SetResult(Sentinel);
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
namespace S7.Net
{
[Serializable]
public class InvalidDataException : Exception
{
public byte[] ReceivedData { get; }
public int ErrorIndex { get; }
public byte ExpectedValue { get; }
public InvalidDataException(string message, byte[] receivedData, int errorIndex, byte expectedValue)
: base(FormatMessage(message, receivedData, errorIndex, expectedValue))
{
ReceivedData = receivedData;
ErrorIndex = errorIndex;
ExpectedValue = expectedValue;
}
protected InvalidDataException(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
ReceivedData = (byte[])info.GetValue(nameof(ReceivedData), typeof(byte[]));
ErrorIndex = info.GetInt32(nameof(ErrorIndex));
ExpectedValue = info.GetByte(nameof(ExpectedValue));
}
private static string FormatMessage(string message, byte[] receivedData, int errorIndex, byte expectedValue)
{
if (errorIndex >= receivedData.Length)
throw new ArgumentOutOfRangeException(nameof(errorIndex),
$"{nameof(errorIndex)} {errorIndex} is outside the bounds of {nameof(receivedData)} with length {receivedData.Length}.");
return $"{message} Invalid data received. Expected '{expectedValue}' at index {errorIndex}, " +
$"but received {receivedData[errorIndex]}. See the {nameof(ReceivedData)} property " +
"for the full message received.";
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using S7.Net;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7Client : IDisposable
{
public S7Helper Plc;
public bool IsConnected;
public S7Client(CpuType cpu, string ip)
{
try
{
Plc = new S7Helper(cpu, ip);
IsConnected = true;
}
catch (Exception ex)
{
IsConnected = false;
}
}
public S7Read<T> Readable<T>() where T : class, new()
{
return new S7Read<T>(this);
}
public S7Write<T> Writeable<T>(T entity) where T : class, new()
{
return new S7Write<T>(this, entity);
}
public void Dispose()
{
try
{
if (Plc != null)
{
Plc.Dispose();
Plc = null;
}
}
catch { }
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MES.Utility.Network.S7netplus.ORM
{
public enum S7NodeType
{
[Description("可读可写")]
ReadAndWrite,
[Description("只读")]
ReadOnly,
[Description("只写")]
WriteOnly
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MES.Utility.Network.S7netplus.ORM;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7PLCAttribute : Attribute
{
public string Address { get; set; }
public int StrLength { get; set; }
public S7NodeType Type { get; set; }
public S7PLCAttribute() { }
public S7PLCAttribute(string address, S7NodeType type)
{
this.Address = address;
this.Type = type;
}
public S7PLCAttribute(string address, int strLength, S7NodeType type)
{
this.Address = address;
this.StrLength = strLength;
this.Type = type;
}
}
}

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7Read<T> where T : class, new()
{
private S7Client opcClient;
private Dictionary<string, PropertyInfo> fieldDict;
private Dictionary<string, int> stringLengthDict;
/// <summary>
/// 构造方法,传入参数
/// </summary>
/// <param name="opcClient"></param>
public S7Read(S7Client opcClient)
{
this.opcClient = opcClient;
//默认取出所有的只读或者可读可写的属性
GetNodeType(out fieldDict, out stringLengthDict);
}
/// <summary>
/// 获得所有具有可读属性的数据
/// </summary>
/// <returns></returns>
private void GetNodeType(out Dictionary<string, PropertyInfo> fieldDict, out Dictionary<string, int> stringLengthDict)
{
fieldDict = new Dictionary<string, PropertyInfo>();
stringLengthDict = new Dictionary<string, int>();
//获得对象T的属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
Object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.ReadOnly)
{
fieldDict.Add(myattr.Address, info);
stringLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
/// <summary>
/// 执行获得数据,映射成对象
/// </summary>
/// <param name="message">返回错误信息</param>
/// <returns></returns>
public bool Execute(out T t)
{
if (fieldDict.Count == 0)
{
t = default(T);
return false;
}
try
{
if (!opcClient.IsConnected)
{
t = default(T);
return false;
}
bool flag = false;
T entity = (T)Activator.CreateInstance(typeof(T));
//循环读取每一个
foreach (KeyValuePair<string, PropertyInfo> keyValue in fieldDict)
{
//判断当前keyValue是什么类型
if (keyValue.Value.PropertyType == typeof(ushort))
{
ushort value=0;
flag = opcClient.Plc.ReadInt16(keyValue.Key, out value);
if (!flag)
{
t = default(T);
return false;
}
keyValue.Value.SetValue(entity, value);
}
else if (keyValue.Value.PropertyType == typeof(int))
{
ushort value =0;
flag = opcClient.Plc.ReadInt16(keyValue.Key, out value);
if (!flag)
{
t = default(T);
return false;
}
keyValue.Value.SetValue(entity, value);
}
//else if (keyValue.Value.PropertyType == typeof(float))
//{
// float value;
// flag = opcClient.Plc.ReadReal(keyValue.Key, out value);
// if (!flag)
// {
// t = default(T);
// return false;
// }
// keyValue.Value.SetValue(entity, value);
//}
else if (keyValue.Value.PropertyType == typeof(string))
{
string value;
flag = opcClient.Plc.ReadString(keyValue.Key, stringLengthDict[keyValue.Key], out value);
if (!flag)
{
t = default(T);
return false;
}
keyValue.Value.SetValue(entity, value);
}
}
t = entity;
return true;
}
catch
{
t = default(T);
return false;
}
}
/// <summary>
/// 传入Lambda表达式指定读取的数据
/// </summary>
/// <param name="columns"></param>
/// <returns></returns>
public S7Read<T> ReadField(Expression<Func<T, object>> columns)
{
Dictionary<string, PropertyInfo> fieldDict = new Dictionary<string, PropertyInfo>();
Dictionary<string, int> stringLengthDict = new Dictionary<string, int>();
//取出要读的属性
string lambda = columns.Body.ToString();
int index1 = lambda.IndexOf('(');
int index2 = lambda.IndexOf(')');
string str = lambda.Substring(index1 + 1, index2 - index1 - 1);
List<string> keyList = str.Split(',').Select(it => it.Split('=')[0].Trim()).ToList();
fieldDict.Clear();
//获得对象T的属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
Object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.ReadOnly)
{
if (keyList.Contains(info.Name))
{
fieldDict.Add(myattr.Address, info);
stringLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
this.fieldDict = fieldDict;
this.stringLengthDict = stringLengthDict;
return this;
}
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace MES.Utility.Network.S7netplus.ORM
{
public class S7Write<T> where T : class, new()
{
private S7Client opcClient;
private T entity;
private Dictionary<string, PropertyInfo> dict;
private Dictionary<string, int> strLengthDict;
/// <summary>
/// 构造方法,传入参数
/// </summary>
/// <param name="opcClient"></param>
/// <param name="entity"></param>
public S7Write(S7Client opcClient, T entity)
{
this.opcClient = opcClient;
this.entity = entity;
//默认取出所有的只写或者可读可写的属性
GetAllKeyList(out dict, out strLengthDict);
}
/// <summary>
/// 获得所有具有可写属性的数据
/// </summary>
/// <returns></returns>
private void GetAllKeyList(out Dictionary<string, PropertyInfo> dict, out Dictionary<string, int> strLengthDict)
{
dict = new Dictionary<string, PropertyInfo>();
strLengthDict = new Dictionary<string, int>();
//获取entity对象的所有属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.WriteOnly)
{
dict.Add(myattr.Address, info);
strLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
/// <summary>
/// 执行写入数据将对象中的数据写入到OPC服务中
/// </summary>
/// <param name="message">返回写入过程中出现的错误信息</param>
/// <returns></returns>
public bool Execute()
{
if (dict.Count == 0)
{
return false;
}
if (!opcClient.IsConnected)
return false;
//循环写入
try
{
bool flag;
foreach (KeyValuePair<string, PropertyInfo> keyValue in dict)
{
//判断当前keyValue是什么类型
if (keyValue.Value.PropertyType == typeof(ushort))
{
flag = opcClient.Plc.WriteInt16(keyValue.Key, (ushort)keyValue.Value.GetValue(entity));
if (!flag)
{
return false;
}
}
else if (keyValue.Value.PropertyType == typeof(int))
{
flag = opcClient.Plc.WriteInt32(keyValue.Key, (int)keyValue.Value.GetValue(entity));
if (!flag)
{
return false;
}
}
else if (keyValue.Value.PropertyType == typeof(float))
{
flag = opcClient.Plc.WriteSingle(keyValue.Key, (float)keyValue.Value.GetValue(entity));
if (!flag)
{
return false;
}
}
else if (keyValue.Value.PropertyType == typeof(string))
{
flag = opcClient.Plc.WriteString(keyValue.Key, (string)keyValue.Value.GetValue(entity), strLengthDict[keyValue.Key]);
if (!flag)
{
return false;
}
}
}
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 传入Lambda表达式指定写入的数据
/// </summary>
/// <param name="columns"></param>
/// <returns></returns>
public S7Write<T> WriteField(Expression<Func<T, object>> columns)
{
Dictionary<string, PropertyInfo> dict = new Dictionary<string, PropertyInfo>();
Dictionary<string, int> strLengthDict = new Dictionary<string, int>();
string lambda = columns.Body.ToString();
int index1 = lambda.IndexOf('(');
int index2 = lambda.IndexOf(')');
string str = lambda.Substring(index1 + 1, index2 - index1 - 1);
List<string> keyList = str.Split(',').Select(it => it.Split('=')[0].Trim()).ToList();
//获取entity对象的所有属性
PropertyInfo[] properties = typeof(T).GetProperties();
//得到所有添加注解的属性
foreach (PropertyInfo info in properties)
{
//判断是否具有HttpController属性
Object[] attr = info.GetCustomAttributes(false);
if (attr.Length == 0)
{
continue;
}
//从注解数组中取第一个注解(一个属性可以包含多个注解)
S7PLCAttribute myattr = attr[0] as S7PLCAttribute;
if (myattr == null)
continue;
if (myattr.Type == S7NodeType.ReadAndWrite || myattr.Type == S7NodeType.WriteOnly)
{
string name = info.Name;
if (keyList.Contains(name))
{
dict.Add(myattr.Address, info);
strLengthDict.Add(myattr.Address, myattr.StrLength);
}
}
}
this.dict = dict;
this.strLengthDict = strLengthDict;
return this;
}
}
}

View File

@@ -0,0 +1,329 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using S7.Net.Internal;
using S7.Net.Protocol;
using S7.Net.Types;
namespace S7.Net
{
/// <summary>
/// Creates an instance of S7.Net driver
/// </summary>
public partial class Plc : IDisposable
{
/// <summary>
/// The default port for the S7 protocol.
/// </summary>
public const int DefaultPort = 102;
/// <summary>
/// The default timeout (in milliseconds) used for <see cref="P:ReadTimeout"/> and <see cref="P:WriteTimeout"/>.
/// </summary>
public const int DefaultTimeout = 10_000;
private readonly TaskQueue queue = new TaskQueue();
//TCP connection to device
private TcpClient tcpClient;
private NetworkStream _stream;
private int readTimeout = DefaultTimeout; // default no timeout
private int writeTimeout = DefaultTimeout; // default no timeout
/// <summary>
/// IP address of the PLC
/// </summary>
public string IP { get; }
/// <summary>
/// PORT Number of the PLC, default is 102
/// </summary>
public int Port { get; }
/// <summary>
/// The TSAP addresses used during the connection request.
/// </summary>
public TsapPair TsapPair { get; set; }
/// <summary>
/// CPU type of the PLC
/// </summary>
public CpuType CPU { get; }
/// <summary>
/// Rack of the PLC
/// </summary>
public Int16 Rack { get; }
/// <summary>
/// Slot of the CPU of the PLC
/// </summary>
public Int16 Slot { get; }
/// <summary>
/// Max PDU size this cpu supports
/// </summary>
public int MaxPDUSize { get; private set; }
/// <summary>Gets or sets the amount of time that a read operation blocks waiting for data from PLC.</summary>
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a read operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the read operation does not time out.</returns>
public int ReadTimeout
{
get => readTimeout;
set
{
readTimeout = value;
if (tcpClient != null) tcpClient.ReceiveTimeout = readTimeout;
}
}
/// <summary>Gets or sets the amount of time that a write operation blocks waiting for data to PLC. </summary>
/// <returns>A <see cref="T:System.Int32" /> that specifies the amount of time, in milliseconds, that will elapse before a write operation fails. The default value, <see cref="F:System.Threading.Timeout.Infinite" />, specifies that the write operation does not time out.</returns>
public int WriteTimeout
{
get => writeTimeout;
set
{
writeTimeout = value;
if (tcpClient != null) tcpClient.SendTimeout = writeTimeout;
}
}
/// <summary>
/// Gets a value indicating whether a connection to the PLC has been established.
/// </summary>
/// <remarks>
/// The <see cref="IsConnected"/> property gets the connection state of the Client socket as
/// of the last I/O operation. When it returns <c>false</c>, the Client socket was either
/// never connected, or is no longer connected.
///
/// <para>
/// Because the <see cref="IsConnected"/> property only reflects the state of the connection
/// as of the most recent operation, you should attempt to send or receive a message to
/// determine the current state. After the message send fails, this property no longer
/// returns <c>true</c>. Note that this behavior is by design. You cannot reliably test the
/// state of the connection because, in the time between the test and a send/receive, the
/// connection could have been lost. Your code should assume the socket is connected, and
/// gracefully handle failed transmissions.
/// </para>
/// </remarks>
public bool IsConnected => tcpClient?.Connected ?? false;
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
/// You need slot > 0 if you are connecting to external ethernet card (CP).
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
/// </summary>
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
/// If you use an external ethernet card, this must be set accordingly.</param>
public Plc(CpuType cpu, string ip, Int16 rack, Int16 slot)
: this(cpu, ip, DefaultPort, rack, slot)
{
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
/// You need slot > 0 if you are connecting to external ethernet card (CP).
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
/// </summary>
/// <param name="cpu">CpuType of the PLC (select from the enum)</param>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port number used for the connection, default 102.</param>
/// <param name="rack">rack of the PLC, usually it's 0, but check in the hardware configuration of Step7 or TIA portal</param>
/// <param name="slot">slot of the CPU of the PLC, usually it's 2 for S7300-S7400, 0 for S7-1200 and S7-1500.
/// If you use an external ethernet card, this must be set accordingly.</param>
public Plc(CpuType cpu, string ip, int port, Int16 rack, Int16 slot)
: this(ip, port, TsapPair.GetDefaultTsapPair(cpu, rack, slot))
{
if (!Enum.IsDefined(typeof(CpuType), cpu))
throw new ArgumentException(
$"The value of argument '{nameof(cpu)}' ({cpu}) is invalid for Enum type '{typeof(CpuType).Name}'.",
nameof(cpu));
CPU = cpu;
Rack = rack;
Slot = slot;
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections.
/// For S7-1200 and S7-1500, the default is rack = 0 and slot = 0.
/// You need slot > 0 if you are connecting to external ethernet card (CP).
/// For S7-300 and S7-400 the default is rack = 0 and slot = 2.
/// </summary>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
public Plc(string ip, TsapPair tsapPair) : this(ip, DefaultPort, tsapPair)
{
}
/// <summary>
/// Creates a PLC object with all the parameters needed for connections. Use this constructor
/// if you want to manually override the TSAP addresses used during the connection request.
/// </summary>
/// <param name="ip">Ip address of the PLC</param>
/// <param name="port">Port number used for the connection, default 102.</param>
/// <param name="tsapPair">The TSAP addresses used for the connection request.</param>
public Plc(string ip, int port, TsapPair tsapPair)
{
if (string.IsNullOrEmpty(ip))
throw new ArgumentException("IP address must valid.", nameof(ip));
IP = ip;
Port = port;
MaxPDUSize = 240;
TsapPair = tsapPair;
}
/// <summary>
/// Close connection to PLC
/// </summary>
public void Close()
{
if (tcpClient != null)
{
if (tcpClient.Connected) tcpClient.Close();
tcpClient = null; // Can not reuse TcpClient once connection gets closed.
}
}
private void AssertPduSizeForRead(ICollection<DataItem> dataItems)
{
// send request limit: 19 bytes of header data, 12 bytes of parameter data for each dataItem
var requiredRequestSize = 19 + dataItems.Count * 12;
if (requiredRequestSize > MaxPDUSize) throw new Exception($"Too many vars requested for read. Request size ({requiredRequestSize}) is larger than protocol limit ({MaxPDUSize}).");
// response limit: 14 bytes of header data, 4 bytes of result data for each dataItem and the actual data
var requiredResponseSize = GetDataLength(dataItems) + dataItems.Count * 4 + 14;
if (requiredResponseSize > MaxPDUSize) throw new Exception($"Too much data requested for read. Response size ({requiredResponseSize}) is larger than protocol limit ({MaxPDUSize}).");
}
private void AssertPduSizeForWrite(ICollection<DataItem> dataItems)
{
// 12 bytes of header data, 18 bytes of parameter data for each dataItem
if (dataItems.Count * 18 + 12 > MaxPDUSize) throw new Exception("Too many vars supplied for write");
// 12 bytes of header data, 16 bytes of data for each dataItem and the actual data
if (GetDataLength(dataItems) + dataItems.Count * 16 + 12 > MaxPDUSize)
throw new Exception("Too much data supplied for write");
}
private void ConfigureConnection()
{
if (tcpClient == null)
{
return;
}
tcpClient.ReceiveTimeout = ReadTimeout;
tcpClient.SendTimeout = WriteTimeout;
}
private int GetDataLength(IEnumerable<DataItem> dataItems)
{
// Odd length variables are 0-padded
return dataItems.Select(di => VarTypeToByteLength(di.VarType, di.Count))
.Sum(len => (len & 1) == 1 ? len + 1 : len);
}
private static void AssertReadResponse(byte[] s7Data, int dataLength)
{
var expectedLength = dataLength + 18;
PlcException NotEnoughBytes() =>
new PlcException(ErrorCode.WrongNumberReceivedBytes,
$"Received {s7Data.Length} bytes: '{BitConverter.ToString(s7Data)}', expected {expectedLength} bytes.")
;
if (s7Data == null)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes, "No s7Data received.");
if (s7Data.Length < 15) throw NotEnoughBytes();
ValidateResponseCode((ReadWriteErrorCode)s7Data[14]);
if (s7Data.Length < expectedLength) throw NotEnoughBytes();
}
internal static void ValidateResponseCode(ReadWriteErrorCode statusCode)
{
switch (statusCode)
{
case ReadWriteErrorCode.ObjectDoesNotExist:
throw new Exception("Received error from PLC: Object does not exist.");
case ReadWriteErrorCode.DataTypeInconsistent:
throw new Exception("Received error from PLC: Data type inconsistent.");
case ReadWriteErrorCode.DataTypeNotSupported:
throw new Exception("Received error from PLC: Data type not supported.");
case ReadWriteErrorCode.AccessingObjectNotAllowed:
throw new Exception("Received error from PLC: Accessing object not allowed.");
case ReadWriteErrorCode.AddressOutOfRange:
throw new Exception("Received error from PLC: Address out of range.");
case ReadWriteErrorCode.HardwareFault:
throw new Exception("Received error from PLC: Hardware fault.");
case ReadWriteErrorCode.Success:
break;
default:
throw new Exception($"Invalid response from PLC: statusCode={(byte)statusCode}.");
}
}
private Stream GetStreamIfAvailable()
{
if (_stream == null)
{
throw new PlcException(ErrorCode.ConnectionError, "Plc is not connected");
}
return _stream;
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
/// <summary>
/// Dispose Plc Object
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
Close();
}
// TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
// TODO: set large fields to null.
disposedValue = true;
}
}
// TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
// ~Plc() {
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
// Dispose(false);
// }
// This code added to correctly implement the disposable pattern.
void IDisposable.Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
// TODO: uncomment the following line if the finalizer is overridden above.
// GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@@ -0,0 +1,207 @@
namespace S7.Net
{
internal class PLCAddress
{
private DataType dataType;
private int dbNumber;
private int startByte;
private int bitNumber;
private VarType varType;
public DataType DataType
{
get => dataType;
set => dataType = value;
}
public int DbNumber
{
get => dbNumber;
set => dbNumber = value;
}
public int StartByte
{
get => startByte;
set => startByte = value;
}
public int BitNumber
{
get => bitNumber;
set => bitNumber = value;
}
public VarType VarType
{
get => varType;
set => varType = value;
}
public PLCAddress(string address)
{
Parse(address, out dataType, out dbNumber, out varType, out startByte, out bitNumber);
}
public static void Parse(string input, out DataType dataType, out int dbNumber, out VarType varType, out int address, out int bitNumber)
{
bitNumber = -1;
dbNumber = 0;
switch (input.Substring(0, 2))
{
case "DB":
string[] strings = input.Split(new char[] { '.' });
if (strings.Length < 2)
throw new InvalidAddressException("To few periods for DB address");
dataType = DataType.DataBlock;
dbNumber = int.Parse(strings[0].Substring(2));
address = int.Parse(strings[1].Substring(3));
string dbType = strings[1].Substring(0, 3);
switch (dbType)
{
case "DBB":
varType = VarType.Byte;
return;
case "DBW":
varType = VarType.Word;
return;
case "DBD":
varType = VarType.DWord;
return;
case "DBX":
bitNumber = int.Parse(strings[2]);
if (bitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
varType = VarType.Bit;
return;
default:
throw new InvalidAddressException();
}
case "IB":
case "EB":
// Input byte
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "IW":
case "EW":
// Input word
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "ID":
case "ED":
// Input double-word
dataType = DataType.Input;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
case "QB":
case "AB":
case "OB":
// Output byte
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "QW":
case "AW":
case "OW":
// Output word
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "QD":
case "AD":
case "OD":
// Output double-word
dataType = DataType.Output;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
case "MB":
// Memory byte
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Byte;
return;
case "MW":
// Memory word
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.Word;
return;
case "MD":
// Memory double-word
dataType = DataType.Memory;
dbNumber = 0;
address = int.Parse(input.Substring(2));
varType = VarType.DWord;
return;
default:
switch (input.Substring(0, 1))
{
case "E":
case "I":
// Input
dataType = DataType.Input;
varType = VarType.Bit;
break;
case "Q":
case "A":
case "O":
// Output
dataType = DataType.Output;
varType = VarType.Bit;
break;
case "M":
// Memory
dataType = DataType.Memory;
varType = VarType.Bit;
break;
case "T":
// Timer
dataType = DataType.Timer;
dbNumber = 0;
address = int.Parse(input.Substring(1));
varType = VarType.Timer;
return;
case "Z":
case "C":
// Counter
dataType = DataType.Counter;
dbNumber = 0;
address = int.Parse(input.Substring(1));
varType = VarType.Counter;
return;
default:
throw new InvalidAddressException(string.Format("{0} is not a valid address", input.Substring(0, 1)));
}
string txt2 = input.Substring(1);
if (txt2.IndexOf(".") == -1)
throw new InvalidAddressException("To few periods for DB address");
address = int.Parse(txt2.Substring(0, txt2.IndexOf(".")));
bitNumber = int.Parse(txt2.Substring(txt2.IndexOf(".") + 1));
if (bitNumber > 7)
throw new InvalidAddressException("Bit can only be 0-7");
return;
}
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Runtime.Serialization;
namespace S7.Net
{
public class WrongNumberOfBytesException : Exception
{
public WrongNumberOfBytesException() : base()
{
}
public WrongNumberOfBytesException(string message) : base(message)
{
}
public WrongNumberOfBytesException(string message, Exception innerException) : base(message, innerException)
{
}
protected WrongNumberOfBytesException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class InvalidAddressException : Exception
{
public InvalidAddressException() : base ()
{
}
public InvalidAddressException(string message) : base(message)
{
}
public InvalidAddressException(string message, Exception innerException) : base(message, innerException)
{
}
protected InvalidAddressException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class InvalidVariableTypeException : Exception
{
public InvalidVariableTypeException() : base()
{
}
public InvalidVariableTypeException(string message) : base(message)
{
}
public InvalidVariableTypeException(string message, Exception innerException) : base(message, innerException)
{
}
protected InvalidVariableTypeException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class TPKTInvalidException : Exception
{
public TPKTInvalidException() : base()
{
}
public TPKTInvalidException(string message) : base(message)
{
}
public TPKTInvalidException(string message, Exception innerException) : base(message, innerException)
{
}
protected TPKTInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
public class TPDUInvalidException : Exception
{
public TPDUInvalidException() : base()
{
}
public TPDUInvalidException(string message) : base(message)
{
}
public TPDUInvalidException(string message, Exception innerException) : base(message, innerException)
{
}
protected TPDUInvalidException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

View File

@@ -0,0 +1,265 @@
using S7.Net.Helper;
using S7.Net.Protocol.S7;
using S7.Net.Types;
using System.Collections.Generic;
using System.Linq;
using DateTime = S7.Net.Types.DateTime;
namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// Creates the header to read bytes from the PLC
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
private static void BuildHeaderPackage(System.IO.MemoryStream stream, int amount = 1)
{
//header size = 19 bytes
stream.Write(new byte[] { 0x03, 0x00 });
//complete package size
stream.Write(Int.ToByteArray((short)(19 + (12 * amount))));
stream.Write(new byte[] { 0x02, 0xf0, 0x80, 0x32, 0x01, 0x00, 0x00, 0x00, 0x00 });
//data part size
stream.Write(Word.ToByteArray((ushort)(2 + (amount * 12))));
stream.Write(new byte[] { 0x00, 0x00, 0x04 });
//amount of requests
stream.WriteByte((byte)amount);
}
/// <summary>
/// Create the bytes-package to request data from the PLC. You have to specify the memory type (dataType),
/// the address of the memory, the address of the byte and the bytes count.
/// </summary>
/// <param name="dataType">MemoryType (DB, Timer, Counter, etc.)</param>
/// <param name="db">Address of the memory to be read</param>
/// <param name="startByteAdr">Start address of the byte</param>
/// <param name="count">Number of bytes to be read</param>
/// <returns></returns>
private static void BuildReadDataRequestPackage(System.IO.MemoryStream stream, DataType dataType, int db, int startByteAdr, int count = 1)
{
//single data req = 12
stream.Write(new byte[] { 0x12, 0x0a, 0x10 });
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
stream.WriteByte((byte)dataType);
break;
default:
stream.WriteByte(0x02);
break;
}
stream.Write(Word.ToByteArray((ushort)(count)));
stream.Write(Word.ToByteArray((ushort)(db)));
stream.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
stream.WriteByte((byte)overflow);
switch (dataType)
{
case DataType.Timer:
case DataType.Counter:
stream.Write(Word.ToByteArray((ushort)(startByteAdr)));
break;
default:
stream.Write(Word.ToByteArray((ushort)((startByteAdr) * 8)));
break;
}
}
/// <summary>
/// Given a S7 variable type (Bool, Word, DWord, etc.), it converts the bytes in the appropriate C# format.
/// </summary>
/// <param name="varType"></param>
/// <param name="bytes"></param>
/// <param name="varCount"></param>
/// <param name="bitAdr"></param>
/// <returns></returns>
private object ParseBytes(VarType varType, byte[] bytes, int varCount, byte bitAdr = 0)
{
if (bytes == null || bytes.Length == 0)
return null;
switch (varType)
{
case VarType.Byte:
if (varCount == 1)
return bytes[0];
else
return bytes;
case VarType.Word:
if (varCount == 1)
return Word.FromByteArray(bytes);
else
return Word.ToArray(bytes);
case VarType.Int:
if (varCount == 1)
return Int.FromByteArray(bytes);
else
return Int.ToArray(bytes);
case VarType.DWord:
if (varCount == 1)
return DWord.FromByteArray(bytes);
else
return DWord.ToArray(bytes);
case VarType.DInt:
if (varCount == 1)
return DInt.FromByteArray(bytes);
else
return DInt.ToArray(bytes);
case VarType.Real:
if (varCount == 1)
return Types.Real.FromByteArray(bytes);
else
return Types.Real.ToArray(bytes);
case VarType.LReal:
if (varCount == 1)
return Types.LReal.FromByteArray(bytes);
else
return Types.LReal.ToArray(bytes);
case VarType.String:
return Types.String.FromByteArray(bytes);
case VarType.S7String:
return S7String.FromByteArray(bytes);
case VarType.S7WString:
return S7WString.FromByteArray(bytes);
case VarType.Timer:
if (varCount == 1)
return Timer.FromByteArray(bytes);
else
return Timer.ToArray(bytes);
case VarType.Counter:
if (varCount == 1)
return Counter.FromByteArray(bytes);
else
return Counter.ToArray(bytes);
case VarType.Bit:
if (varCount == 1)
{
if (bitAdr > 7)
return null;
else
return Bit.FromByte(bytes[0], bitAdr);
}
else
{
return Bit.ToBitArray(bytes, varCount);
}
case VarType.DateTime:
if (varCount == 1)
{
return DateTime.FromByteArray(bytes);
}
else
{
return DateTime.ToArray(bytes);
}
case VarType.DateTimeLong:
if (varCount == 1)
{
return DateTimeLong.FromByteArray(bytes);
}
else
{
return DateTimeLong.ToArray(bytes);
}
default:
return null;
}
}
/// <summary>
/// Given a S7 <see cref="VarType"/> (Bool, Word, DWord, etc.), it returns how many bytes to read.
/// </summary>
/// <param name="varType"></param>
/// <param name="varCount"></param>
/// <returns>Byte lenght of variable</returns>
internal static int VarTypeToByteLength(VarType varType, int varCount = 1)
{
switch (varType)
{
case VarType.Bit:
return (varCount + 7) / 8;
case VarType.Byte:
return (varCount < 1) ? 1 : varCount;
case VarType.String:
return varCount;
case VarType.S7String:
return ((varCount + 2) & 1) == 1 ? (varCount + 3) : (varCount + 2);
case VarType.S7WString:
return (varCount * 2) + 4;
case VarType.Word:
case VarType.Timer:
case VarType.Int:
case VarType.Counter:
return varCount * 2;
case VarType.DWord:
case VarType.DInt:
case VarType.Real:
return varCount * 4;
case VarType.LReal:
case VarType.DateTime:
return varCount * 8;
case VarType.DateTimeLong:
return varCount * 12;
default:
return 0;
}
}
private byte[] GetS7ConnectionSetup()
{
return new byte[] { 3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3,
3, 192 // Use 960 PDU size
};
}
private void ParseDataIntoDataItems(byte[] s7data, List<DataItem> dataItems)
{
int offset = 14;
foreach (var dataItem in dataItems)
{
// check for Return Code = Success
if (s7data[offset] != 0xff)
throw new PlcException(ErrorCode.WrongNumberReceivedBytes);
// to Data bytes
offset += 4;
int byteCnt = VarTypeToByteLength(dataItem.VarType, dataItem.Count);
dataItem.Value = ParseBytes(
dataItem.VarType,
s7data.Skip(offset).Take(byteCnt).ToArray(),
dataItem.Count,
dataItem.BitAdr
);
// next Item
offset += byteCnt;
// Always align to even offset
if (offset % 2 != 0)
offset++;
}
}
private static byte[] BuildReadRequestPackage(IList<DataItemAddress> dataItems)
{
int packageSize = 19 + (dataItems.Count * 12);
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package, dataItems.Count);
foreach (var dataItem in dataItems)
{
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAddress, dataItem.ByteLength);
}
return package.ToArray();
}
}
}

View File

@@ -0,0 +1,582 @@
using S7.Net.Types;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading.Tasks;
using S7.Net.Protocol;
using System.Threading;
using S7.Net.Protocol.S7;
namespace S7.Net
{
/// <summary>
/// Creates an instance of S7.Net driver
/// </summary>
public partial class Plc
{
/// <summary>
/// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
/// </summary>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that the cancellation will not affect opening the socket in any way and only affects data transfers for configuring the connection after the socket connection is successfully established.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous open operation.</returns>
public async Task OpenAsync(CancellationToken cancellationToken = default)
{
var stream = await ConnectAsync(cancellationToken).ConfigureAwait(false);
try
{
await queue.Enqueue(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await EstablishConnection(stream, cancellationToken).ConfigureAwait(false);
_stream = stream;
return default(object);
}).ConfigureAwait(false);
}
catch (Exception)
{
stream.Dispose();
throw;
}
}
private async Task<NetworkStream> ConnectAsync(CancellationToken cancellationToken)
{
tcpClient = new TcpClient();
ConfigureConnection();
#if NETFRAMEWORK
IAsyncResult asyncResult = tcpClient.BeginConnect(IP, Port, null, null);
if (!asyncResult.AsyncWaitHandle.WaitOne(1000))
{
throw new Exception("<22><><EFBFBD>ӳ<EFBFBD>ʱ");
}
tcpClient.EndConnect(asyncResult);
#else
await tcpClient.ConnectAsync(IP, Port, cancellationToken).ConfigureAwait(false);
#endif
return tcpClient.GetStream();
}
private async Task EstablishConnection(Stream stream, CancellationToken cancellationToken)
{
await RequestConnection(stream, cancellationToken).ConfigureAwait(false);
await SetupConnection(stream, cancellationToken).ConfigureAwait(false);
}
private async Task RequestConnection(Stream stream, CancellationToken cancellationToken)
{
var requestData = ConnectionRequest.GetCOTPConnectionRequest(TsapPair);
var response = await NoLockRequestTpduAsync(stream, requestData, cancellationToken).ConfigureAwait(false);
if (response.PDUType != COTP.PduType.ConnectionConfirmed)
{
throw new InvalidDataException("Connection request was denied", response.TPkt.Data, 1, 0x0d);
}
}
private async Task SetupConnection(Stream stream, CancellationToken cancellationToken)
{
var setupData = GetS7ConnectionSetup();
var s7data = await NoLockRequestTsduAsync(stream, setupData, 0, setupData.Length, cancellationToken)
.ConfigureAwait(false);
if (s7data.Length < 2)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
//Check for S7 Ack Data
if (s7data[1] != 0x03)
throw new InvalidDataException("Error reading Communication Setup response", s7data, 1, 0x03);
if (s7data.Length < 20)
throw new WrongNumberOfBytesException("Not enough data received in response to Communication Setup");
// TODO: check if this should not rather be UInt16.
MaxPDUSize = s7data[18] * 256 + s7data[19];
}
/// <summary>
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns the bytes in an array</returns>
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken cancellationToken = default)
{
var resultBytes = new byte[count];
int index = 0;
while (count > 0)
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18);
await ReadBytesWithSingleRequestAsync(dataType, db, startByteAdr + index, resultBytes, index, maxToRead, cancellationToken).ConfigureAwait(false);
count -= maxToRead;
index += maxToRead;
}
return resultBytes;
}
/// <summary>
/// Read and decode a certain number of bytes of the "VarType" provided.
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<object> ReadAsync(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0, CancellationToken cancellationToken = default)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = await ReadBytesAsync(dataType, db, startByteAdr, cntBytes, cancellationToken).ConfigureAwait(false);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
/// <summary>
/// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly.</returns>
public async Task<object> ReadAsync(string variable, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
return await ReadAsync(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct.
/// </summary>
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns a struct that must be cast.</returns>
public async Task<object> ReadStructAsync(Type structType, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
int numBytes = Types.Struct.GetStructSize(structType);
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
// and decode it
return Types.Struct.FromBytes(structType, resultBytes);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read.
/// </summary>
/// <typeparam name="T">The struct type</typeparam>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>Returns a nulable struct. If nothing was read null will be returned.</returns>
public async Task<T?> ReadStructAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : struct
{
return await ReadStructAsync(typeof(T), db, startByteAdr, cancellationToken).ConfigureAwait(false) as T?;
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>The number of read bytes</returns>
public async Task<Tuple<int, object>> ReadClassAsync(object sourceClass, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
int numBytes = (int)Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
}
// now read the package
var resultBytes = await ReadBytesAsync(DataType.DataBlock, db, startByteAdr, numBytes, cancellationToken).ConfigureAwait(false);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
return new Tuple<int, object>(resultBytes.Length, sourceClass);
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
/// type, the class needs a default constructor.
/// </summary>
/// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T> ReadClassAsync<T>(int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
{
return await ReadClassAsync(() => Activator.CreateInstance<T>(), db, startByteAdr, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
/// <param name="classFactory">Function to instantiate the class</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public async Task<T> ReadClassAsync<T>(Func<T> classFactory, int db, int startByteAdr = 0, CancellationToken cancellationToken = default) where T : class
{
var instance = classFactory();
var res = await ReadClassAsync(instance, db, startByteAdr, cancellationToken).ConfigureAwait(false);
int readBytes = res.Item1;
if (readBytes <= 0)
{
return null;
}
return (T)res.Item2;
}
/// <summary>
/// Reads multiple vars in a single request.
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
/// Values are stored in the property "Value" of the dataItem and are already converted.
/// If you don't want the conversion, just create a dataItem of bytes.
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
public async Task<List<DataItem>> ReadMultipleVarsAsync(List<DataItem> dataItems, CancellationToken cancellationToken = default)
{
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
AssertPduSizeForRead(dataItems);
try
{
var dataToSend = BuildReadRequestPackage(dataItems.Select(d => DataItem.GetDataItemAddress(d)).ToList());
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}
catch (SocketException socketException)
{
throw new PlcException(ErrorCode.ReadData, socketException);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
}
return dataItems;
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken = default)
{
int localIndex = 0;
int count = value.Length;
while (count > 0)
{
var maxToWrite = (int)Math.Min(count, MaxPDUSize - 35);
await WriteBytesWithASingleRequestAsync(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite, cancellationToken).ConfigureAwait(false);
count -= maxToWrite;
localIndex += maxToWrite;
}
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool value, CancellationToken cancellationToken = default)
{
if (bitAdr < 0 || bitAdr > 7)
throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
await WriteBitWithASingleRequestAsync(dataType, db, startByteAdr, bitAdr, value, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteBitAsync(DataType dataType, int db, int startByteAdr, int bitAdr, int value, CancellationToken cancellationToken = default)
{
if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value));
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, value == 1, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type.
/// You must specify the memory area type, memory are address, byte start address and bytes count.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteAsync(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1, CancellationToken cancellationToken = default)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool boolean)
{
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, boolean, cancellationToken).ConfigureAwait(false);
}
else if (value is int intValue)
{
if (intValue < 0 || intValue > 7)
throw new ArgumentOutOfRangeException(
string.Format(
"Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
bitAdr), nameof(bitAdr));
await WriteBitAsync(dataType, db, startByteAdr, bitAdr, intValue == 1, cancellationToken).ConfigureAwait(false);
}
else throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
}
else await WriteBytesAsync(dataType, db, startByteAdr, Serialization.SerializeValue(value), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteAsync(string variable, object value, CancellationToken cancellationToken = default)
{
var adr = new PLCAddress(variable);
await WriteAsync(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteStructAsync(object structValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
var bytes = Struct.ToBytes(structValue).ToList();
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes.ToArray(), cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is None.
/// Please note that cancellation is advisory/cooperative and will not lead to immediate cancellation in all cases.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteClassAsync(object classValue, int db, int startByteAdr = 0, CancellationToken cancellationToken = default)
{
byte[] bytes = new byte[(int)Class.GetClassSize(classValue)];
Types.Class.ToBytes(classValue, bytes);
await WriteBytesAsync(DataType.DataBlock, db, startByteAdr, bytes, cancellationToken).ConfigureAwait(false);
}
private async Task ReadBytesWithSingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var dataToSend = BuildReadRequestPackage(new[] { new DataItemAddress(dataType, db, startByteAdr, count) });
var s7data = await RequestTsduAsync(dataToSend, cancellationToken);
AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count);
}
/// <summary>
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
/// </summary>
/// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
/// <returns>Task that completes when response from PLC is parsed.</returns>
public async Task WriteAsync(params DataItem[] dataItems)
{
AssertPduSizeForWrite(dataItems);
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
var response = await RequestTsduAsync(message.Array, 0, length).ConfigureAwait(false);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
/// <summary>
/// Writes up to 200 bytes to the PLC. You must specify the memory area type, memory are address, byte start address and bytes count.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
private async Task WriteBytesWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count, CancellationToken cancellationToken)
{
try
{
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
private async Task WriteBitWithASingleRequestAsync(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue, CancellationToken cancellationToken)
{
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = await RequestTsduAsync(dataToSend, cancellationToken).ConfigureAwait(false);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
private Task<byte[]> RequestTsduAsync(byte[] requestData, CancellationToken cancellationToken = default) =>
RequestTsduAsync(requestData, 0, requestData.Length, cancellationToken);
private Task<byte[]> RequestTsduAsync(byte[] requestData, int offset, int length, CancellationToken cancellationToken = default)
{
var stream = GetStreamIfAvailable();
return queue.Enqueue(() =>
NoLockRequestTsduAsync(stream, requestData, offset, length, cancellationToken));
}
private async Task<COTP.TPDU> NoLockRequestTpduAsync(Stream stream, byte[] requestData,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (var closeOnCancellation = cancellationToken.Register(Close))
{
await stream.WriteAsync(requestData, 0, requestData.Length, cancellationToken).ConfigureAwait(false);
return await COTP.TPDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception exc)
{
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
{
Close();
}
throw;
}
}
private async Task<byte[]> NoLockRequestTsduAsync(Stream stream, byte[] requestData, int offset, int length,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using (var closeOnCancellation = cancellationToken.Register(Close))
{
await stream.WriteAsync(requestData, offset, length, cancellationToken).ConfigureAwait(false);
return await COTP.TSDU.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
}
}
catch (Exception exc)
{
if (exc is TPDUInvalidException || exc is TPKTInvalidException)
{
Close();
}
throw;
}
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
namespace S7.Net
{
[Serializable]
public class PlcException : Exception
{
public ErrorCode ErrorCode { get; }
public PlcException(ErrorCode errorCode) : this(errorCode, $"PLC communication failed with error '{errorCode}'.")
{
}
public PlcException(ErrorCode errorCode, Exception innerException) : this(errorCode, innerException.Message,
innerException)
{
}
public PlcException(ErrorCode errorCode, string message) : base(message)
{
ErrorCode = errorCode;
}
public PlcException(ErrorCode errorCode, string message, Exception inner) : base(message, inner)
{
ErrorCode = errorCode;
}
protected PlcException(System.Runtime.Serialization.SerializationInfo info,System.Runtime.Serialization.StreamingContext context) : base(info, context)
{
ErrorCode = (ErrorCode) info.GetInt32(nameof(ErrorCode));
}
}
}

View File

@@ -0,0 +1,585 @@
using S7.Net.Types;
using System;
using System.IO;
using System.Collections.Generic;
using S7.Net.Protocol;
using S7.Net.Helper;
using System.Net.Sockets;
using System.Threading;
//Implement synchronous methods here
namespace S7.Net
{
public partial class Plc
{
/// <summary>
/// Connects to the PLC and performs a COTP ConnectionRequest and S7 CommunicationSetup.
/// </summary>
public void Open(int timeout = 0)
{
try
{
if (timeout == 0)
{
OpenAsync().GetAwaiter().GetResult();
}
else
{
CancellationTokenSource cts = new CancellationTokenSource(timeout);
OpenAsync(cts.Token).GetAwaiter().GetResult();
}
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ConnectionError,
$"Couldn't establish the connection to {IP}.\nMessage: {exc.Message}", exc);
}
}
/// <summary>
/// Reads a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="count">Byte count, if you want to read 120 bytes, set this to 120.</param>
/// <returns>Returns the bytes in an array</returns>
public byte[] ReadBytes(DataType dataType, int db, int startByteAdr, int count)
{
//var result = new byte[count];
//ReadBytes(result, dataType, db, startByteAdr);
//return result;
var result = new byte[count];
int index = 0;
while (count > 0)
{
//This works up to MaxPDUSize-1 on SNAP7. But not MaxPDUSize-0.
var maxToRead = Math.Min(count, MaxPDUSize - 18);
ReadBytesWithSingleRequest(dataType, db, startByteAdr + index, result, index, maxToRead);
count -= maxToRead;
index += maxToRead;
}
return result;
}
private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, byte[] buffer, int offset, int count)
{
try
{
// first create the header
int packageSize = 19 + 12; // 19 header + 12 for 1 request
var package = new System.IO.MemoryStream(packageSize);
BuildHeaderPackage(package);
// package.Add(0x02); // datenart
BuildReadDataRequestPackage(package, dataType, db, startByteAdr, count);
var dataToSend = package.ToArray();
var s7data = RequestTsdu(dataToSend);
AssertReadResponse(s7data, count);
Array.Copy(s7data, 18, buffer, offset, count);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
}
}
/// <summary>
/// Read and decode a certain number of bytes of the "VarType" provided.
/// This can be used to read multiple consecutive variables of the same type (Word, DWord, Int, etc).
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="varType">Type of the variable/s that you are reading</param>
/// <param name="bitAdr">Address of bit. If you want to read DB1.DBX200.6, set 6 to this parameter.</param>
/// <param name="varCount"></param>
public object Read(DataType dataType, int db, int startByteAdr, VarType varType, int varCount, byte bitAdr = 0)
{
int cntBytes = VarTypeToByteLength(varType, varCount);
byte[] bytes = ReadBytes(dataType, db, startByteAdr, cntBytes);
return ParseBytes(varType, bytes, varCount, bitAdr);
}
/// <summary>
/// Reads a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <returns>Returns an object that contains the value. This object must be cast accordingly. If no data has been read, null will be returned</returns>
public object Read(string variable)
{
var adr = new PLCAddress(variable);
return Read(adr.DataType, adr.DbNumber, adr.StartByte, adr.VarType, 1, (byte)adr.BitNumber);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and return an object that can be casted to the struct.
/// </summary>
/// <param name="structType">Type of the struct to be readed (es.: TypeOf(MyStruct)).</param>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a struct that must be cast. If no data has been read, null will be returned</returns>
public object ReadStruct(Type structType, int db, int startByteAdr = 0)
{
int numBytes = Struct.GetStructSize(structType);
// now read the package
var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
return Struct.FromBytes(structType, resultBytes);
}
/// <summary>
/// Reads all the bytes needed to fill a struct in C#, starting from a certain address, and returns the struct or null if nothing was read.
/// </summary>
/// <typeparam name="T">The struct type</typeparam>
/// <param name="db">Address of the DB.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>Returns a nullable struct. If nothing was read null will be returned.</returns>
public T? ReadStruct<T>(int db, int startByteAdr = 0) where T : struct
{
return ReadStruct(typeof(T), db, startByteAdr) as T?;
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <param name="sourceClass">Instance of the class that will store the values</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>The number of read bytes</returns>
public int ReadClass(object sourceClass, int db, int startByteAdr = 0)
{
int numBytes = (int)Class.GetClassSize(sourceClass);
if (numBytes <= 0)
{
throw new Exception("The size of the class is less than 1 byte and therefore cannot be read");
}
// now read the package
var resultBytes = ReadBytes(DataType.DataBlock, db, startByteAdr, numBytes);
// and decode it
Class.FromBytes(sourceClass, resultBytes);
return resultBytes.Length;
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified. To instantiate the class defined by the generic
/// type, the class needs a default constructor.
/// </summary>
/// <typeparam name="T">The class that will be instantiated. Requires a default constructor</typeparam>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(int db, int startByteAdr = 0) where T : class
{
return ReadClass(() => Activator.CreateInstance<T>(), db, startByteAdr);
}
/// <summary>
/// Reads all the bytes needed to fill a class in C#, starting from a certain address, and set all the properties values to the value that are read from the PLC.
/// This reads only properties, it doesn't read private variable or public variable without {get;set;} specified.
/// </summary>
/// <typeparam name="T">The class that will be instantiated</typeparam>
/// <param name="classFactory">Function to instantiate the class</param>
/// <param name="db">Index of the DB; es.: 1 is for DB1</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <returns>An instance of the class with the values read from the PLC. If no data has been read, null will be returned</returns>
public T ReadClass<T>(Func<T> classFactory, int db, int startByteAdr = 0) where T : class
{
var instance = classFactory();
int readBytes = ReadClass(instance, db, startByteAdr);
if (readBytes <= 0)
{
return null;
}
return instance;
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
public void WriteBytes(DataType dataType, int db, int startByteAdr, byte[] value)
{
//WriteBytes(dataType, db, startByteAdr, value.AsSpan());
int localIndex = 0;
int count = value.Length;
while (count > 0)
{
//TODO: Figure out how to use MaxPDUSize here
//Snap7 seems to choke on PDU sizes above 256 even if snap7
//replies with bigger PDU size in connection setup.
var maxToWrite = Math.Min(count, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value, localIndex, maxToWrite);
count -= maxToWrite;
localIndex += maxToWrite;
}
}
private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
{
try
{
var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value, dataOffset, count);
var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
/// <summary>
/// Write a number of bytes from a DB starting from a specified index. This handles more than 200 bytes with multiple requests.
/// If the write was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
//public void WriteBytes(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
//{
// int localIndex = 0;
// while (value.Length > 0)
// {
// //TODO: Figure out how to use MaxPDUSize here
// //Snap7 seems to choke on PDU sizes above 256 even if snap7
// //replies with bigger PDU size in connection setup.
// var maxToWrite = Math.Min(value.Length, MaxPDUSize - 28);//TODO tested only when the MaxPDUSize is 480
// WriteBytesWithASingleRequest(dataType, db, startByteAdr + localIndex, value.Slice(0, maxToWrite));
// value = value.Slice(maxToWrite);
// localIndex += maxToWrite;
// }
//}
/// <summary>
/// Write a single bit from a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Bytes to write. If more than 200, multiple requests will be made.</param>
public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, bool value)
{
if (bitAdr < 0 || bitAdr > 7)
throw new InvalidAddressException(string.Format("Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid", bitAdr));
WriteBitWithASingleRequest(dataType, db, startByteAdr, bitAdr, value);
}
/// <summary>
/// Write a single bit to a DB with the specified index.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to write DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to write DB1.DBW200, this is 200.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
/// <param name="value">Value to write (0 or 1).</param>
public void WriteBit(DataType dataType, int db, int startByteAdr, int bitAdr, int value)
{
if (value < 0 || value > 1)
throw new ArgumentException("Value must be 0 or 1", nameof(value));
WriteBit(dataType, db, startByteAdr, bitAdr, value == 1);
}
/// <summary>
/// Takes in input an object and tries to parse it to an array of values. This can be used to write many data, all of the same type.
/// You must specify the memory area type, memory are address, byte start address and bytes count.
/// If the read was not successful, check LastErrorCode or LastErrorString.
/// </summary>
/// <param name="dataType">Data type of the memory area, can be DB, Timer, Counter, Merker(Memory), Input, Output.</param>
/// <param name="db">Address of the memory area (if you want to read DB1, this is set to 1). This must be set also for other memory area types: counters, timers,etc.</param>
/// <param name="startByteAdr">Start byte address. If you want to read DB1.DBW200, this is 200.</param>
/// <param name="value">Bytes to write. The lenght of this parameter can't be higher than 200. If you need more, use recursion.</param>
/// <param name="bitAdr">The address of the bit. (0-7)</param>
public void Write(DataType dataType, int db, int startByteAdr, object value, int bitAdr = -1)
{
if (bitAdr != -1)
{
//Must be writing a bit value as bitAdr is specified
if (value is bool boolean)
{
WriteBit(dataType, db, startByteAdr, bitAdr, boolean);
}
else if (value is int intValue)
{
if (intValue < 0 || intValue > 7)
throw new ArgumentOutOfRangeException(
string.Format(
"Addressing Error: You can only reference bitwise locations 0-7. Address {0} is invalid",
bitAdr), nameof(bitAdr));
WriteBit(dataType, db, startByteAdr, bitAdr, intValue == 1);
}
else
throw new ArgumentException("Value must be a bool or an int to write a bit", nameof(value));
}
else WriteBytes(dataType, db, startByteAdr, Serialization.SerializeValue(value));
}
/// <summary>
/// Writes a single variable from the PLC, takes in input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.
/// If the write was not successful, check <see cref="LastErrorCode"/> or <see cref="LastErrorString"/>.
/// </summary>
/// <param name="variable">Input strings like "DB1.DBX0.0", "DB20.DBD200", "MB20", "T45", etc.</param>
/// <param name="value">Value to be written to the PLC</param>
public void Write(string variable, object value)
{
var adr = new PLCAddress(variable);
Write(adr.DataType, adr.DbNumber, adr.StartByte, value, adr.BitNumber);
}
/// <summary>
/// Writes a C# struct to a DB in the PLC
/// </summary>
/// <param name="structValue">The struct to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
public void WriteStruct(object structValue, int db, int startByteAdr = 0)
{
WriteStructAsync(structValue, db, startByteAdr).GetAwaiter().GetResult();
}
/// <summary>
/// Writes a C# class to a DB in the PLC
/// </summary>
/// <param name="classValue">The class to be written</param>
/// <param name="db">Db address</param>
/// <param name="startByteAdr">Start bytes on the PLC</param>
public void WriteClass(object classValue, int db, int startByteAdr = 0)
{
WriteClassAsync(classValue, db, startByteAdr).GetAwaiter().GetResult();
}
//private void ReadBytesWithSingleRequest(DataType dataType, int db, int startByteAdr, Span<byte> buffer)
//{
// try
// {
// // first create the header
// const int packageSize = 19 + 12; // 19 header + 12 for 1 request
// var dataToSend = new byte[packageSize];
// var package = new MemoryStream(dataToSend);
// BuildHeaderPackage(package);
// // package.Add(0x02); // datenart
// BuildReadDataRequestPackage(package, dataType, db, startByteAdr, buffer.Length);
// var s7data = RequestTsdu(dataToSend);
// AssertReadResponse(s7data, buffer.Length);
// s7data.AsSpan(18, buffer.Length).CopyTo(buffer);
// }
// catch (Exception exc)
// {
// throw new PlcException(ErrorCode.ReadData, exc);
// }
//}
/// <summary>
/// Write DataItem(s) to the PLC. Throws an exception if the response is invalid
/// or when the PLC reports errors for item(s) written.
/// </summary>
/// <param name="dataItems">The DataItem(s) to write to the PLC.</param>
public void Write(params DataItem[] dataItems)
{
AssertPduSizeForWrite(dataItems);
var message = new ByteArray();
var length = S7WriteMultiple.CreateRequest(message, dataItems);
var response = RequestTsdu(message.Array, 0, length);
S7WriteMultiple.ParseResponse(response, response.Length, dataItems);
}
//private void WriteBytesWithASingleRequest(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
//{
// try
// {
// var dataToSend = BuildWriteBytesPackage(dataType, db, startByteAdr, value);
// var s7data = RequestTsdu(dataToSend);
// ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
// }
// catch (Exception exc)
// {
// throw new PlcException(ErrorCode.WriteData, exc);
// }
//}
private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, byte[] value, int dataOffset, int count)
{
int varCount = count;
// first create the header
int packageSize = 35 + varCount;
var package = new MemoryStream(new byte[packageSize]);
package.WriteByte(3);
package.WriteByte(0);
//complete package size
package.Write(Int.ToByteArray((short)packageSize));
package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.Write(Word.ToByteArray((ushort)(varCount - 1)));
package.Write(new byte[] { 0, 0x0e });
package.Write(Word.ToByteArray((ushort)(varCount + 4)));
package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
package.Write(Word.ToByteArray((ushort)varCount));
package.Write(Word.ToByteArray((ushort)(db)));
package.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.WriteByte((byte)overflow);
package.Write(Word.ToByteArray((ushort)(startByteAdr * 8)));
package.Write(new byte[] { 0, 4 });
package.Write(Word.ToByteArray((ushort)(varCount * 8)));
// now join the header and the data
package.Write(value, dataOffset, count);
return package.ToArray();
}
//private byte[] BuildWriteBytesPackage(DataType dataType, int db, int startByteAdr, ReadOnlySpan<byte> value)
//{
// int varCount = value.Length;
// // first create the header
// int packageSize = 35 + varCount;
// var packageData = new byte[packageSize];
// var package = new MemoryStream(packageData);
// package.WriteByte(3);
// package.WriteByte(0);
// //complete package size
// package.Write(Int.ToByteArray((short)packageSize));
// // This overload doesn't allocate the byte array, it refers to assembly's static data segment
// package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
// package.Write(Word.ToByteArray((ushort)(varCount - 1)));
// package.Write(new byte[] { 0, 0x0e });
// package.Write(Word.ToByteArray((ushort)(varCount + 4)));
// package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x02 });
// package.Write(Word.ToByteArray((ushort)varCount));
// package.Write(Word.ToByteArray((ushort)(db)));
// package.WriteByte((byte)dataType);
// var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
// package.WriteByte((byte)overflow);
// package.Write(Word.ToByteArray((ushort)(startByteAdr * 8)));
// package.Write(new byte[] { 0, 4 });
// package.Write(Word.ToByteArray((ushort)(varCount * 8)));
// // now join the header and the data
// package.Write(value);
// return packageData;
//}
private byte[] BuildWriteBitPackage(DataType dataType, int db, int startByteAdr, bool bitValue, int bitAdr)
{
var value = new[] { bitValue ? (byte)1 : (byte)0 };
int varCount = 1;
// first create the header
int packageSize = 35 + varCount;
var packageData = new byte[packageSize];
var package = new MemoryStream(packageData);
package.WriteByte(3);
package.WriteByte(0);
//complete package size
package.Write(Int.ToByteArray((short)packageSize));
package.Write(new byte[] { 2, 0xf0, 0x80, 0x32, 1, 0, 0 });
package.Write(Word.ToByteArray((ushort)(varCount - 1)));
package.Write(new byte[] { 0, 0x0e });
package.Write(Word.ToByteArray((ushort)(varCount + 4)));
package.Write(new byte[] { 0x05, 0x01, 0x12, 0x0a, 0x10, 0x01 }); //ending 0x01 is used for writing a sinlge bit
package.Write(Word.ToByteArray((ushort)varCount));
package.Write(Word.ToByteArray((ushort)(db)));
package.WriteByte((byte)dataType);
var overflow = (int)(startByteAdr * 8 / 0xffffU); // handles words with address bigger than 8191
package.WriteByte((byte)overflow);
package.Write(Word.ToByteArray((ushort)(startByteAdr * 8 + bitAdr)));
package.Write(new byte[] { 0, 0x03 }); //ending 0x03 is used for writing a sinlge bit
package.Write(Word.ToByteArray((ushort)(varCount)));
// now join the header and the data
package.Write(value);
return packageData;
}
private void WriteBitWithASingleRequest(DataType dataType, int db, int startByteAdr, int bitAdr, bool bitValue)
{
try
{
var dataToSend = BuildWriteBitPackage(dataType, db, startByteAdr, bitValue, bitAdr);
var s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.WriteData, exc);
}
}
/// <summary>
/// Reads multiple vars in a single request.
/// You have to create and pass a list of DataItems and you obtain in response the same list with the values.
/// Values are stored in the property "Value" of the dataItem and are already converted.
/// If you don't want the conversion, just create a dataItem of bytes.
/// The number of DataItems as well as the total size of the requested data can not exceed a certain limit (protocol restriction).
/// </summary>
/// <param name="dataItems">List of dataitems that contains the list of variables that must be read.</param>
public void ReadMultipleVars(List<DataItem> dataItems)
{
AssertPduSizeForRead(dataItems);
try
{
// first create the header
int packageSize = 19 + (dataItems.Count * 12);
var dataToSend = new byte[packageSize];
var package = new MemoryStream(dataToSend);
BuildHeaderPackage(package, dataItems.Count);
// package.Add(0x02); // datenart
foreach (var dataItem in dataItems)
{
BuildReadDataRequestPackage(package, dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
byte[] s7data = RequestTsdu(dataToSend);
ValidateResponseCode((ReadWriteErrorCode)s7data[14]);
ParseDataIntoDataItems(s7data, dataItems);
}
catch (Exception exc)
{
throw new PlcException(ErrorCode.ReadData, exc);
}
}
private byte[] RequestTsdu(byte[] requestData) => RequestTsdu(requestData, 0, requestData.Length);
private byte[] RequestTsdu(byte[] requestData, int offset, int length)
{
return RequestTsduAsync(requestData, offset, length).GetAwaiter().GetResult();
}
}
}

View File

@@ -0,0 +1,28 @@
namespace S7.Net.Protocol
{
internal static class ConnectionRequest
{
public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
{
byte[] bSend1 = {
3, 0, 0, 22, //TPKT
17, //COTP Header Length
224, //Connect Request
0, 0, //Destination Reference
0, 46, //Source Reference
0, //Flags
193, //Parameter Code (src-tasp)
2, //Parameter Length
tsapPair.Local.FirstByte, tsapPair.Local.SecondByte, //Source TASP
194, //Parameter Code (dst-tasp)
2, //Parameter Length
tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte, //Destination TASP
192, //Parameter Code (tpdu-size)
1, //Parameter Length
10 //TPDU Size (2^10 = 1024)
};
return bSend1;
}
}
}

View File

@@ -0,0 +1,15 @@

namespace S7.Net.Protocol
{
internal enum ReadWriteErrorCode : byte
{
Reserved = 0x00,
HardwareFault = 0x01,
AccessingObjectNotAllowed = 0x03,
AddressOutOfRange = 0x05,
DataTypeNotSupported = 0x06,
DataTypeInconsistent = 0x07,
ObjectDoesNotExist = 0x0a,
Success = 0xff
}
}

View File

@@ -0,0 +1,37 @@
namespace S7.Net.Protocol.S7
{
/// <summary>
/// Represents an area of memory in the PLC
/// </summary>
internal class DataItemAddress
{
public DataItemAddress(DataType dataType, int db, int startByteAddress, int byteLength)
{
DataType = dataType;
DB = db;
StartByteAddress = startByteAddress;
ByteLength = byteLength;
}
/// <summary>
/// Memory area to read
/// </summary>
public DataType DataType { get; }
/// <summary>
/// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45)
/// </summary>
public int DB { get; }
/// <summary>
/// Address of the first byte to read
/// </summary>
public int StartByteAddress { get; }
/// <summary>
/// Length of data to read
/// </summary>
public int ByteLength { get; }
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using S7.Net.Types;
namespace S7.Net.Protocol
{
internal static class S7WriteMultiple
{
public static int CreateRequest(ByteArray message, DataItem[] dataItems)
{
message.Add(Header.Template);
message[Header.Offsets.ParameterCount] = (byte) dataItems.Length;
var paramSize = dataItems.Length * Parameter.Template.Length;
Serialization.SetWordAt(message, Header.Offsets.ParameterSize,
(ushort) (2 + paramSize));
var paramOffset = Header.Template.Length;
var data = new ByteArray();
var itemCount = 0;
foreach (var item in dataItems)
{
itemCount++;
message.Add(Parameter.Template);
var value = Serialization.SerializeDataItem(item);
var wordLen = item.Value is bool ? 1 : 2;
message[paramOffset + Parameter.Offsets.WordLength] = (byte) wordLen;
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.Amount, (ushort) value.Length);
Serialization.SetWordAt(message, paramOffset + Parameter.Offsets.DbNumber, (ushort) item.DB);
message[paramOffset + Parameter.Offsets.Area] = (byte) item.DataType;
data.Add(0x00);
if (item.Value is bool b)
{
if (item.BitAdr > 7)
throw new ArgumentException(
$"Cannot read bit with invalid {nameof(item.BitAdr)} '{item.BitAdr}'.", nameof(dataItems));
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr,
item.BitAdr);
data.Add(0x03);
data.AddWord(1);
data.Add(b ? (byte)1 : (byte)0);
if (itemCount != dataItems.Length) {
data.Add(0);
}
}
else
{
Serialization.SetAddressAt(message, paramOffset + Parameter.Offsets.Address, item.StartByteAdr, 0);
var len = value.Length;
data.Add(0x04);
data.AddWord((ushort) (len << 3));
data.Add(value);
if ((len & 0b1) == 1 && itemCount != dataItems.Length)
{
data.Add(0);
}
}
paramOffset += Parameter.Template.Length;
}
message.Add(data.Array);
Serialization.SetWordAt(message, Header.Offsets.MessageLength, (ushort) message.Length);
Serialization.SetWordAt(message, Header.Offsets.DataLength, (ushort) (message.Length - paramOffset));
return message.Length;
}
public static void ParseResponse(byte[] message, int length, DataItem[] dataItems)
{
if (length < 12) throw new Exception("Not enough data received to parse write response.");
var messageError = Serialization.GetWordAt(message, 10);
if (messageError != 0)
throw new Exception($"Write failed with error {messageError}.");
if (length < 14 + dataItems.Length)
throw new Exception("Not enough data received to parse individual item responses.");
IList<byte> itemResults = new ArraySegment<byte>(message, 14, dataItems.Length);
List<Exception> errors = null;
for (int i = 0; i < dataItems.Length; i++)
{
try
{
Plc.ValidateResponseCode((ReadWriteErrorCode)itemResults[i]);
}
catch(Exception e)
{
if (errors == null) errors = new List<Exception>();
errors.Add(new Exception($"Write of dataItem {dataItems[i]} failed: {e.Message}."));
}
}
if (errors != null)
throw new AggregateException(
$"Write failed for {errors.Count} items. See the innerExceptions for details.", errors);
}
private static class Header
{
public static byte[] Template { get; } =
{
0x03, 0x00, 0x00, 0x00, // TPKT
0x02, 0xf0, 0x80, // ISO DT
0x32, // S7 protocol ID
0x01, // JobRequest
0x00, 0x00, // Reserved
0x05, 0x00, // PDU reference
0x00, 0x0e, // Parameters length
0x00, 0x00, // Data length
0x05, // Function: Write var
0x00, // Number of items to write
};
public static class Offsets
{
public const int MessageLength = 2;
public const int ParameterSize = 13;
public const int DataLength = 15;
public const int ParameterCount = 18;
}
}
private static class Parameter
{
public static byte[] Template { get; } =
{
0x12, // Spec
0x0a, // Length of remaining bytes
0x10, // Addressing mode
0x02, // Transport size
0x00, 0x00, // Number of elements
0x00, 0x00, // DB number
0x84, // Area type
0x00, 0x00, 0x00 // Area offset
};
public static class Offsets
{
public const int WordLength = 3;
public const int Amount = 4;
public const int DbNumber = 6;
public const int Area = 8;
public const int Address = 9;
}
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using S7.Net.Types;
namespace S7.Net.Protocol
{
internal static class Serialization
{
public static ushort GetWordAt(IList<byte> buf, int index)
{
return (ushort)((buf[index] << 8) + buf[index]);
}
public static byte[] SerializeDataItem(DataItem dataItem)
{
if (dataItem.Value == null)
{
throw new Exception($"DataItem.Value is null, cannot serialize. StartAddr={dataItem.StartByteAdr} VarType={dataItem.VarType}");
}
if (dataItem.Value is string s)
{
switch (dataItem.VarType)
{
case VarType.S7String:
return S7String.ToByteArray(s, dataItem.Count);
case VarType.S7WString:
return S7WString.ToByteArray(s, dataItem.Count);
default:
return Types.String.ToByteArray(s, dataItem.Count);
}
}
return SerializeValue(dataItem.Value);
}
public static byte[] SerializeValue(object value)
{
switch (value.GetType().Name)
{
case "Boolean":
return new[] { (byte)((bool)value ? 1 : 0) };
case "Byte":
return Types.Byte.ToByteArray((byte)value);
case "Int16":
return Types.Int.ToByteArray((Int16)value);
case "UInt16":
return Types.Word.ToByteArray((UInt16)value);
case "Int32":
return Types.DInt.ToByteArray((Int32)value);
case "UInt32":
return Types.DWord.ToByteArray((UInt32)value);
case "Single":
return Types.Real.ToByteArray((float)value);
case "Double":
return Types.LReal.ToByteArray((double)value);
case "DateTime":
return Types.DateTime.ToByteArray((System.DateTime)value);
case "Byte[]":
return (byte[])value;
case "Int16[]":
return Types.Int.ToByteArray((Int16[])value);
case "UInt16[]":
return Types.Word.ToByteArray((UInt16[])value);
case "Int32[]":
return Types.DInt.ToByteArray((Int32[])value);
case "UInt32[]":
return Types.DWord.ToByteArray((UInt32[])value);
case "Single[]":
return Types.Real.ToByteArray((float[])value);
case "Double[]":
return Types.LReal.ToByteArray((double[])value);
case "String":
// Hack: This is backwards compatible with the old code, but functionally it's broken
// if the consumer does not pay attention to string length.
var stringVal = (string)value;
return Types.String.ToByteArray(stringVal, stringVal.Length);
case "DateTime[]":
return Types.DateTime.ToByteArray((System.DateTime[])value);
case "DateTimeLong[]":
return Types.DateTimeLong.ToByteArray((System.DateTime[])value);
default:
throw new InvalidVariableTypeException();
}
}
public static void SetAddressAt(ByteArray buffer, int index, int startByte, byte bitNumber)
{
var start = startByte * 8 + bitNumber;
buffer[index + 2] = (byte)start;
start >>= 8;
buffer[index + 1] = (byte)start;
start >>= 8;
buffer[index] = (byte)start;
}
public static void SetWordAt(ByteArray buffer, int index, ushort value)
{
buffer[index] = (byte)(value >> 8);
buffer[index + 1] = (byte)value;
}
}
}

View File

@@ -0,0 +1,31 @@
namespace S7.Net.Protocol
{
/// <summary>
/// Provides a representation of the Transport Service Access Point, or TSAP in short. TSAP's are used
/// to specify a client and server address. For most PLC types a default TSAP is available that allows
/// connection from any IP and can be calculated using the rack and slot numbers.
/// </summary>
public struct Tsap
{
/// <summary>
/// First byte of the TSAP.
/// </summary>
public byte FirstByte { get; set; }
/// <summary>
/// Second byte of the TSAP.
/// </summary>
public byte SecondByte { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="Tsap" /> class using the specified values.
/// </summary>
/// <param name="firstByte">The first byte of the TSAP.</param>
/// <param name="secondByte">The second byte of the TSAP.</param>
public Tsap(byte firstByte, byte secondByte)
{
FirstByte = firstByte;
SecondByte = secondByte;
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
namespace S7.Net.Protocol
{
/// <summary>
/// Implements a pair of TSAP addresses used to connect to a PLC.
/// </summary>
public class TsapPair
{
/// <summary>
/// The local <see cref="Tsap" />.
/// </summary>
public Tsap Local { get; set; }
/// <summary>
/// The remote <see cref="Tsap" />
/// </summary>
public Tsap Remote { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="TsapPair" /> class using the specified local and
/// remote TSAP.
/// </summary>
/// <param name="local">The local TSAP.</param>
/// <param name="remote">The remote TSAP.</param>
public TsapPair(Tsap local, Tsap remote)
{
Local = local;
Remote = remote;
}
/// <summary>
/// Builds a <see cref="TsapPair" /> that can be used to connect to a PLC using the default connection
/// addresses.
/// </summary>
/// <remarks>
/// The remote TSAP is constructed using <code>new Tsap(0x03, (byte) ((rack &lt;&lt; 5) | slot))</code>.
/// </remarks>
/// <param name="cpuType">The CPU type of the PLC.</param>
/// <param name="rack">The rack of the PLC's network card.</param>
/// <param name="slot">The slot of the PLC's network card.</param>
/// <returns>A TSAP pair that matches the given parameters.</returns>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="cpuType"/> is invalid.
///
/// -or-
///
/// The <paramref name="rack"/> parameter is less than 0.
///
/// -or-
///
/// The <paramref name="rack"/> parameter is greater than 15.
///
/// -or-
///
/// The <paramref name="slot"/> parameter is less than 0.
///
/// -or-
///
/// The <paramref name="slot"/> parameter is greater than 15.</exception>
public static TsapPair GetDefaultTsapPair(CpuType cpuType, int rack, int slot)
{
if (rack < 0) throw InvalidRackOrSlot(rack, nameof(rack), "minimum", 0);
if (rack > 0x0F) throw InvalidRackOrSlot(rack, nameof(rack), "maximum", 0x0F);
if (slot < 0) throw InvalidRackOrSlot(slot, nameof(slot), "minimum", 0);
if (slot > 0x0F) throw InvalidRackOrSlot(slot, nameof(slot), "maximum", 0x0F);
switch (cpuType)
{
case CpuType.S7200:
return new TsapPair(new Tsap(0x10, 0x00), new Tsap(0x10, 0x01));
case CpuType.Logo0BA8:
// The actual values are probably on a per-project basis
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x01, 0x02));
case CpuType.S7200Smart:
case CpuType.S71200:
case CpuType.S71500:
case CpuType.S7300:
case CpuType.S7400:
// Testing with S7 1500 shows only the remote TSAP needs to match. This might differ for other
// PLC types.
return new TsapPair(new Tsap(0x01, 0x00), new Tsap(0x03, (byte) ((rack << 5) | slot)));
default:
throw new ArgumentOutOfRangeException(nameof(cpuType), "Invalid CPU Type specified");
}
}
private static ArgumentOutOfRangeException InvalidRackOrSlot(int value, string name, string extrema,
int extremaValue)
{
return new ArgumentOutOfRangeException(name,
$"Invalid {name} value specified (decimal: {value}, hexadecimal: {value:X}), {extrema} value " +
$"is {extremaValue} (decimal) or {extremaValue:X} (hexadecimal).");
}
}
}

View File

@@ -0,0 +1,301 @@
using System;
using System.Text;
using S7.Net;
using S7.Net.Types;
namespace MES.Utility.Network.S7netplus
{
public class S7Helper : IDisposable
{
private object _lock = new object();
private Plc plc;
public S7Helper(CpuType cpuType, string ipAddress) : this(cpuType, ipAddress, 0, 1)
{
}
public S7Helper(CpuType cpuType, string ipAddress, short rack, short slot)
{
plc = new Plc(cpuType, ipAddress, 102, rack, slot);
plc.Open(1000);
}
#region BOOL类型读写
/// <summary>
/// 写入BOOL
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">true/false</param>
/// <returns></returns>
public bool WriteBool(string address, bool value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// 读取bool类型
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">输出bool值</param>
/// <returns>返回false 读取失败PLC通信异常</returns>
public bool ReadBool(string address, out bool value)
{
lock (_lock)
try
{
value = (bool)plc.Read(address);
return true;
}
catch
{
value = false;
return false;
}
}
#endregion
#region INT16读写
public bool ReadInt16(string address, out ushort value)
{
lock (_lock)
try
{
value = (ushort)plc.Read(address);
return true;
}
catch
{
value = 0;
return false;
}
}
public bool WriteInt16(string address, ushort value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
#endregion
#region REAL读写
/// <summary>
///
/// </summary>
/// <param name="address">例如 DB1.DBD4.0</param>
/// <param name="value"></param>
/// <returns></returns>
public bool ReadSingle(string address, out float value)
{
lock (_lock)
try
{
value = ((uint)plc.Read(address)).ConvertToFloat();
return true;
}
catch
{
value = 0;
return false;
}
}
public bool ReadBytes(int db, int address, int count, out byte[] value)
{
try
{
value = plc.ReadBytes(DataType.DataBlock, db, address, count);
return true;
}
catch
{
value = null;
return false;
}
}
/// <summary>
///
/// </summary>
/// <param name="address">例如 DB1.DBD4.0</param>
/// <param name="value"></param>
/// <returns></returns>
public bool WriteSingle(string address, float value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
#endregion
#region String读写
/// <summary>
///
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="strLength">字符串长度</param>
/// <param name="value">返回字符串数据</param>
/// <returns></returns>
public bool ReadString(string address, int strLength, out string value)
{
lock (_lock)
try
{
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
value = (string)plc.Read(DataType.DataBlock, db, startAddress, VarType.S7String, strLength);//获取对应长度的字符串
return true;
}
catch
{
value = "";
return false;
}
}
/// <summary>
/// 写入字符串
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">写入数据</param>
/// <param name="value">写入长度</param>
/// <returns></returns>
public bool WriteString(string address, string value, int strLength)
{
lock (_lock)
try
{
//转换address,取到DB 和 开始地址
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
var bytes = S7.Net.Types.S7String.ToByteArray(value, strLength);
plc.WriteBytes(DataType.DataBlock, db, startAddress, bytes);
return true;
}
catch
{
return false;
}
}
#endregion
public bool ReadBytesString(string address, int strLength, out string value)
{
lock (_lock)
try
{
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
byte[] values = plc.ReadBytes(DataType.DataBlock, db, startAddress, strLength);
value = Encoding.ASCII.GetString(values);
return true;
}
catch
{
value = "";
return false;
}
}
/// <summary>
/// 写入字符串
/// </summary>
/// <param name="address">例如DB45.DBX52.0</param>
/// <param name="value">写入数据</param>
/// <param name="value">写入长度</param>
/// <returns></returns>
public bool WriteBytesString(string address, string value, int strLength)
{
lock (_lock)
try
{
//转换address,取到DB 和 开始地址
string[] array = address.Split('.');
int db = Convert.ToInt32(array[0].Replace("DB", ""));
int startAddress = Convert.ToInt32(array[1].Replace("DBX", ""));
var bytes = Encoding.ASCII.GetBytes(value);
if (bytes.Length > strLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({strLength}).");
var buffer = new byte[strLength];
Array.Copy(bytes, 0, buffer, 0, bytes.Length);
plc.WriteBytes(DataType.DataBlock, db, startAddress, buffer);
return true;
}
catch
{
return false;
}
}
#region INT32读写
/// <summary>
///
/// </summary>
/// <param name="address">例如DB1.DBD264.0</param>
/// <param name="value"></param>
/// <returns></returns>
public bool ReadInt32(string address, out int value)
{
lock (_lock)
try
{
value = (int)plc.Read(address);
return true;
}
catch
{
value = 0;
return false;
}
}
public bool WriteInt32(string address, int value)
{
lock (_lock)
try
{
plc.Write(address, value);
return true;
}
catch
{
return false;
}
}
#endregion
public void Dispose()
{
plc?.Close();
plc = null;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// Extensions for Streams
/// </summary>
public static class StreamExtensions
{
/// <summary>
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
/// </summary>
/// <param name="stream">the Stream to read from</param>
/// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <returns>returns the amount of read bytes</returns>
public static int ReadExact(this Stream stream, byte[] buffer, int offset, int count)
{
int read = 0;
int received;
do
{
received = stream.Read(buffer, offset + read, count - read);
read += received;
}
while (read < count && received > 0);
return read;
}
/// <summary>
/// Reads bytes from the stream into the buffer until exactly the requested number of bytes (or EOF) have been read
/// </summary>
/// <param name="stream">the Stream to read from</param>
/// <param name="buffer">the buffer to read into</param>
/// <param name="offset">the offset in the buffer to read into</param>
/// <param name="count">the amount of bytes to read into the buffer</param>
/// <returns>returns the amount of read bytes</returns>
public static async Task<int> ReadExactAsync(this Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
int read = 0;
int received;
do
{
received = await stream.ReadAsync(buffer, offset + read, count - read, cancellationToken).ConfigureAwait(false);
read += received;
}
while (read < count && received > 0);
return read;
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace S7.Net
{
/// <summary>
/// Describes a TPKT Packet
/// </summary>
internal class TPKT
{
public byte Version;
public byte Reserved1;
public int Length;
public byte[] Data;
private TPKT(byte version, byte reserved1, int length, byte[] data)
{
Version = version;
Reserved1 = reserved1;
Length = length;
Data = data;
}
/// <summary>
/// Reads a TPKT from the socket Async
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <returns>Task TPKT Instace</returns>
public static async Task<TPKT> ReadAsync(Stream stream, CancellationToken cancellationToken)
{
var buf = new byte[4];
int len = await stream.ReadExactAsync(buf, 0, 4, cancellationToken).ConfigureAwait(false);
if (len < 4) throw new TPKTInvalidException("TPKT is incomplete / invalid");
var version = buf[0];
var reserved1 = buf[1];
var length = buf[2] * 256 + buf[3]; //BigEndian
var data = new byte[length - 4];
len = await stream.ReadExactAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false);
if (len < data.Length)
throw new TPKTInvalidException("TPKT payload incomplete / invalid");
return new TPKT
(
version: version,
reserved1: reserved1,
length: length,
data: data
);
}
public override string ToString()
{
return string.Format("Version: {0} Length: {1} Data: {2}",
Version,
Length,
BitConverter.ToString(Data)
);
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Bit from S7 plc to C#.
/// </summary>
public static class Bit
{
/// <summary>
/// Converts a Bit to bool
/// </summary>
public static bool FromByte(byte v, byte bitAdr)
{
return (((int)v & (1 << bitAdr)) != 0);
}
/// <summary>
/// Converts an array of bytes to a BitArray.
/// </summary>
/// <param name="bytes">The bytes to convert.</param>
/// <returns>A BitArray with the same number of bits and equal values as <paramref name="bytes"/>.</returns>
public static BitArray ToBitArray(byte[] bytes) => ToBitArray(bytes, bytes.Length * 8);
/// <summary>
/// Converts an array of bytes to a BitArray.
/// </summary>
/// <param name="bytes">The bytes to convert.</param>
/// <param name="length">The number of bits to return.</param>
/// <returns>A BitArray with <paramref name="length"/> bits.</returns>
public static BitArray ToBitArray(byte[] bytes, int length)
{
if (length > bytes.Length * 8) throw new ArgumentException($"Not enough data in bytes to return {length} bits.", nameof(bytes));
var bitArr = new BitArray(bytes);
var bools = new bool[length];
for (var i = 0; i < length; i++) bools[i] = bitArr[i];
return new BitArray(bools);
}
}
}

View File

@@ -0,0 +1,64 @@
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to read, set and reset bits inside bytes
/// </summary>
public static class Boolean
{
/// <summary>
/// Returns the value of a bit in a bit, given the address of the bit
/// </summary>
public static bool GetValue(byte value, int bit)
{
return (((int)value & (1 << bit)) != 0);
}
/// <summary>
/// Sets the value of a bit to 1 (true), given the address of the bit. Returns
/// a copy of the value with the bit set.
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to set.</param>
/// <returns>The modified value with the bit at index set.</returns>
public static byte SetBit(byte value, int bit)
{
SetBit(ref value, bit);
return value;
}
/// <summary>
/// Sets the value of a bit to 1 (true), given the address of the bit.
/// </summary>
/// <param name="value">The value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to set.</param>
public static void SetBit(ref byte value, int bit)
{
value = (byte) ((value | (1 << bit)) & 0xFF);
}
/// <summary>
/// Resets the value of a bit to 0 (false), given the address of the bit. Returns
/// a copy of the value with the bit cleared.
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to clear.</param>
/// <returns>The modified value with the bit at index cleared.</returns>
public static byte ClearBit(byte value, int bit)
{
ClearBit(ref value, bit);
return value;
}
/// <summary>
/// Resets the value of a bit to 0 (false), given the address of the bit
/// </summary>
/// <param name="value">The input value to modify.</param>
/// <param name="bit">The index (zero based) of the bit to clear.</param>
public static void ClearBit(ref byte value, int bit)
{
value = (byte) (value & ~(1 << bit) & 0xFF);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from bytes to byte arrays
/// </summary>
public static class Byte
{
/// <summary>
/// Converts a byte to byte array
/// </summary>
public static byte[] ToByteArray(byte value)
{
return new byte[] { value }; ;
}
/// <summary>
/// Converts a byte array to byte
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static byte FromByteArray(byte[] bytes)
{
if (bytes.Length != 1)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 1 bytes.");
}
return bytes[0];
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
namespace S7.Net.Types
{
class ByteArray
{
List<byte> list = new List<byte>();
public byte this[int index]
{
get => list[index];
set => list[index] = value;
}
public byte[] Array
{
get { return list.ToArray(); }
}
public int Length => list.Count;
public ByteArray()
{
list = new List<byte>();
}
public ByteArray(int size)
{
list = new List<byte>(size);
}
public void Clear()
{
list = new List<byte>();
}
public void Add(byte item)
{
list.Add(item);
}
public void AddWord(ushort value)
{
list.Add((byte) (value >> 8));
list.Add((byte) value);
}
public void Add(byte[] items)
{
list.AddRange(items);
}
public void Add(IEnumerable<byte> items)
{
list.AddRange(items);
}
public void Add(ByteArray byteArray)
{
list.AddRange(byteArray.Array);
}
}
}

View File

@@ -0,0 +1,354 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert a C# class to S7 data types
/// </summary>
public static class Class
{
private static IEnumerable<PropertyInfo> GetAccessableProperties(Type classType)
{
return classType
.GetProperties(
BindingFlags.SetProperty |
BindingFlags.Public |
BindingFlags.Instance)
.Where(p => p.GetSetMethod() != null);
}
private static double GetIncreasedNumberOfBytes(double numBytes, Type type, PropertyInfo propertyInfo)
{
switch (type.Name)
{
case "Boolean":
numBytes += 0.125;
break;
case "Byte":
numBytes = Math.Ceiling(numBytes);
numBytes++;
break;
case "Int16":
case "UInt16":
IncrementToEven(ref numBytes);
numBytes += 2;
break;
case "Int32":
case "UInt32":
IncrementToEven(ref numBytes);
numBytes += 4;
break;
case "Single":
IncrementToEven(ref numBytes);
numBytes += 4;
break;
case "Double":
IncrementToEven(ref numBytes);
numBytes += 8;
break;
case "String":
S7StringAttribute attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
IncrementToEven(ref numBytes);
numBytes += attribute.ReservedLengthInBytes;
break;
default:
var propertyClass = Activator.CreateInstance(type);
numBytes = GetClassSize(propertyClass, numBytes, true);
break;
}
return numBytes;
}
/// <summary>
/// Gets the size of the class in bytes.
/// </summary>
/// <param name="instance">An instance of the class</param>
/// <returns>the number of bytes</returns>
public static double GetClassSize(object instance, double numBytes = 0.0, bool isInnerProperty = false)
{
var properties = GetAccessableProperties(instance.GetType());
foreach (var property in properties)
{
if (property.PropertyType.IsArray)
{
Type elementType = property.PropertyType.GetElementType();
Array array = (Array)property.GetValue(instance, null);
if (array.Length <= 0)
{
throw new Exception("Cannot determine size of class, because an array is defined which has no fixed size greater than zero.");
}
IncrementToEven(ref numBytes);
for (int i = 0; i < array.Length; i++)
{
numBytes = GetIncreasedNumberOfBytes(numBytes, elementType, property);
}
}
else
{
numBytes = GetIncreasedNumberOfBytes(numBytes, property.PropertyType, property);
}
}
if (false == isInnerProperty)
{
// enlarge numBytes to next even number because S7-Structs in a DB always will be resized to an even byte count
numBytes = Math.Ceiling(numBytes);
if ((numBytes / 2 - Math.Floor(numBytes / 2.0)) > 0)
numBytes++;
}
return numBytes;
}
private static object GetPropertyValue(Type propertyType, PropertyInfo propertyInfo, byte[] bytes, ref double numBytes)
{
object value = null;
switch (propertyType.Name)
{
case "Boolean":
// get the value
int bytePos = (int)Math.Floor(numBytes);
int bitPos = (int)((numBytes - (double)bytePos) / 0.125);
if ((bytes[bytePos] & (int)Math.Pow(2, bitPos)) != 0)
value = true;
else
value = false;
numBytes += 0.125;
break;
case "Byte":
numBytes = Math.Ceiling(numBytes);
value = (byte)(bytes[(int)numBytes]);
numBytes++;
break;
case "Int16":
IncrementToEven(ref numBytes);
// hier auswerten
ushort source = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
value = source.ConvertToShort();
numBytes += 2;
break;
case "UInt16":
IncrementToEven(ref numBytes);
// hier auswerten
value = Word.FromBytes(bytes[(int)numBytes + 1], bytes[(int)numBytes]);
numBytes += 2;
break;
case "Int32":
IncrementToEven(ref numBytes);
var wordBuffer = new byte[4];
Array.Copy(bytes, (int)numBytes, wordBuffer, 0, wordBuffer.Length);
uint sourceUInt = DWord.FromByteArray(wordBuffer);
value = sourceUInt.ConvertToInt();
numBytes += 4;
break;
case "UInt32":
IncrementToEven(ref numBytes);
var wordBuffer2 = new byte[4];
Array.Copy(bytes, (int)numBytes, wordBuffer2, 0, wordBuffer2.Length);
value = DWord.FromByteArray(wordBuffer2);
numBytes += 4;
break;
case "Single":
IncrementToEven(ref numBytes);
// hier auswerten
value = Real.FromByteArray(
new byte[] {
bytes[(int)numBytes],
bytes[(int)numBytes + 1],
bytes[(int)numBytes + 2],
bytes[(int)numBytes + 3] });
numBytes += 4;
break;
case "Double":
IncrementToEven(ref numBytes);
var buffer = new byte[8];
Array.Copy(bytes, (int)numBytes, buffer, 0, 8);
// hier auswerten
value = LReal.FromByteArray(buffer);
numBytes += 8;
break;
case "String":
S7StringAttribute attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
IncrementToEven(ref numBytes);
// get the value
var sData = new byte[attribute.ReservedLengthInBytes];
Array.Copy(bytes, (int)numBytes, sData, 0, sData.Length);
switch (attribute.Type)
{
case S7StringType.S7String:
value = S7String.FromByteArray(sData); break;
case S7StringType.S7WString:
value = S7WString.FromByteArray(sData); break;
default:
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
}
numBytes += sData.Length;
break;
default:
var propClass = Activator.CreateInstance(propertyType);
numBytes = FromBytes(propClass, bytes, numBytes);
value = propClass;
break;
}
return value;
}
/// <summary>
/// Sets the object's values with the given array of bytes
/// </summary>
/// <param name="sourceClass">The object to fill in the given array of bytes</param>
/// <param name="bytes">The array of bytes</param>
public static double FromBytes(object sourceClass, byte[] bytes, double numBytes = 0, bool isInnerClass = false)
{
if (bytes == null)
return numBytes;
var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties)
{
if (property.PropertyType.IsArray)
{
Array array = (Array)property.GetValue(sourceClass, null);
IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
array.SetValue(
GetPropertyValue(elementType, property, bytes, ref numBytes),
i);
}
}
else
{
property.SetValue(
sourceClass,
GetPropertyValue(property.PropertyType, property, bytes, ref numBytes),
null);
}
}
return numBytes;
}
private static double SetBytesFromProperty(object propertyValue, PropertyInfo propertyInfo, byte[] bytes, double numBytes)
{
int bytePos = 0;
int bitPos = 0;
byte[] bytes2 = null;
switch (propertyValue.GetType().Name)
{
case "Boolean":
// get the value
bytePos = (int)Math.Floor(numBytes);
bitPos = (int)((numBytes - (double)bytePos) / 0.125);
if ((bool)propertyValue)
bytes[bytePos] |= (byte)Math.Pow(2, bitPos); // is true
else
bytes[bytePos] &= (byte)(~(byte)Math.Pow(2, bitPos)); // is false
numBytes += 0.125;
break;
case "Byte":
numBytes = (int)Math.Ceiling(numBytes);
bytePos = (int)numBytes;
bytes[bytePos] = (byte)propertyValue;
numBytes++;
break;
case "Int16":
bytes2 = Int.ToByteArray((Int16)propertyValue);
break;
case "UInt16":
bytes2 = Word.ToByteArray((UInt16)propertyValue);
break;
case "Int32":
bytes2 = DInt.ToByteArray((Int32)propertyValue);
break;
case "UInt32":
bytes2 = DWord.ToByteArray((UInt32)propertyValue);
break;
case "Single":
bytes2 = Real.ToByteArray((float)propertyValue);
break;
case "Double":
bytes2 = LReal.ToByteArray((double)propertyValue);
break;
case "String":
S7StringAttribute attribute = propertyInfo?.GetCustomAttributes<S7StringAttribute>().SingleOrDefault();
if (attribute == default(S7StringAttribute))
throw new ArgumentException("Please add S7StringAttribute to the string field");
switch (attribute.Type)
{
case S7StringType.S7String:
bytes2 = S7String.ToByteArray((string)propertyValue, attribute.ReservedLength); break;
case S7StringType.S7WString:
bytes2 = S7WString.ToByteArray((string)propertyValue, attribute.ReservedLength); break;
default:
throw new ArgumentException("Please use a valid string type for the S7StringAttribute");
}
break;
default:
numBytes = ToBytes(propertyValue, bytes, numBytes);
break;
}
if (bytes2 != null)
{
IncrementToEven(ref numBytes);
bytePos = (int)numBytes;
for (int bCnt = 0; bCnt < bytes2.Length; bCnt++)
bytes[bytePos + bCnt] = bytes2[bCnt];
numBytes += bytes2.Length;
}
return numBytes;
}
/// <summary>
/// Creates a byte array depending on the struct type.
/// </summary>
/// <param name="sourceClass">The struct object</param>
/// <returns>A byte array or null if fails.</returns>
public static double ToBytes(object sourceClass, byte[] bytes, double numBytes = 0.0)
{
var properties = GetAccessableProperties(sourceClass.GetType());
foreach (var property in properties)
{
if (property.PropertyType.IsArray)
{
Array array = (Array)property.GetValue(sourceClass, null);
IncrementToEven(ref numBytes);
Type elementType = property.PropertyType.GetElementType();
for (int i = 0; i < array.Length && numBytes < bytes.Length; i++)
{
numBytes = SetBytesFromProperty(array.GetValue(i), property, bytes, numBytes);
}
}
else
{
numBytes = SetBytesFromProperty(property.GetValue(sourceClass, null), property, bytes, numBytes);
}
}
return numBytes;
}
private static void IncrementToEven(ref double numBytes)
{
numBytes = Math.Ceiling(numBytes);
if (numBytes % 2 > 0) numBytes++;
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Counter from S7 plc to C# ushort (UInt16).
/// </summary>
public static class Counter
{
/// <summary>
/// Converts a Counter (2 bytes) to ushort (UInt16)
/// </summary>
public static UInt16 FromByteArray(byte[] bytes)
{
if (bytes.Length != 2)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes.");
}
// bytes[0] -> HighByte
// bytes[1] -> LowByte
return (UInt16)((bytes[0] << 8) | bytes[1]);
}
/// <summary>
/// Converts a ushort (UInt16) to word (2 bytes)
/// </summary>
public static byte[] ToByteArray(UInt16 value)
{
byte[] bytes = new byte[2];
bytes[0] = (byte)((value << 8) & 0xFF);
bytes[1] = (byte)((value) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of ushort (UInt16) to an array of bytes
/// </summary>
public static byte[] ToByteArray(UInt16[] value)
{
ByteArray arr = new ByteArray();
foreach (UInt16 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of bytes to an array of ushort
/// </summary>
public static UInt16[] ToArray(byte[] bytes)
{
UInt16[] values = new UInt16[bytes.Length / 2];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 2; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,65 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert DInt from S7 plc to C# int (Int32).
/// </summary>
public static class DInt
{
/// <summary>
/// Converts a S7 DInt (4 bytes) to int (Int32)
/// </summary>
public static Int32 FromByteArray(byte[] bytes)
{
if (bytes.Length != 4)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
return bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3];
}
/// <summary>
/// Converts a int (Int32) to S7 DInt (4 bytes)
/// </summary>
public static byte[] ToByteArray(Int32 value)
{
byte[] bytes = new byte[4];
bytes[0] = (byte)((value >> 24) & 0xFF);
bytes[1] = (byte)((value >> 16) & 0xFF);
bytes[2] = (byte)((value >> 8) & 0xFF);
bytes[3] = (byte)((value) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of int (Int32) to an array of bytes
/// </summary>
public static byte[] ToByteArray(Int32[] value)
{
ByteArray arr = new ByteArray();
foreach (Int32 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 DInt to an array of int (Int32)
/// </summary>
public static Int32[] ToArray(byte[] bytes)
{
Int32[] values = new Int32[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert DWord from S7 plc to C#.
/// </summary>
public static class DWord
{
/// <summary>
/// Converts a S7 DWord (4 bytes) to uint (UInt32)
/// </summary>
public static UInt32 FromByteArray(byte[] bytes)
{
return (UInt32)(bytes[0] << 24 | bytes[1] << 16 | bytes[2] << 8 | bytes[3]);
}
/// <summary>
/// Converts 4 bytes to DWord (UInt32)
/// </summary>
public static UInt32 FromBytes(byte b1, byte b2, byte b3, byte b4)
{
return (UInt32)((b4 << 24) | (b3 << 16) | (b2 << 8) | b1);
}
/// <summary>
/// Converts a uint (UInt32) to S7 DWord (4 bytes)
/// </summary>
public static byte[] ToByteArray(UInt32 value)
{
byte[] bytes = new byte[4];
bytes[0] = (byte)((value >> 24) & 0xFF);
bytes[1] = (byte)((value >> 16) & 0xFF);
bytes[2] = (byte)((value >> 8) & 0xFF);
bytes[3] = (byte)((value) & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of uint (UInt32) to an array of S7 DWord (4 bytes)
/// </summary>
public static byte[] ToByteArray(UInt32[] value)
{
ByteArray arr = new ByteArray();
foreach (UInt32 val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 DWord to an array of uint (UInt32)
/// </summary>
public static UInt32[] ToArray(byte[] bytes)
{
UInt32[] values = new UInt32[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,104 @@
using S7.Net.Protocol.S7;
using System;
namespace S7.Net.Types
{
/// <summary>
/// Create an instance of a memory block that can be read by using ReadMultipleVars
/// </summary>
public class DataItem
{
/// <summary>
/// Memory area to read
/// </summary>
public DataType DataType { get; set; }
/// <summary>
/// Type of data to be read (default is bytes)
/// </summary>
public VarType VarType { get; set; }
/// <summary>
/// Address of memory area to read (example: for DB1 this value is 1, for T45 this value is 45)
/// </summary>
public int DB { get; set; }
/// <summary>
/// Address of the first byte to read
/// </summary>
public int StartByteAdr { get; set; }
/// <summary>
/// Addess of bit to read from StartByteAdr
/// </summary>
public byte BitAdr { get; set; }
/// <summary>
/// Number of variables to read
/// </summary>
public int Count { get; set; }
/// <summary>
/// Contains the value of the memory area after the read has been executed
/// </summary>
public object Value { get; set; }
/// <summary>
/// Create an instance of DataItem
/// </summary>
public DataItem()
{
VarType = VarType.Byte;
Count = 1;
}
/// <summary>
/// Create an instance of <see cref="DataItem"/> from the supplied address.
/// </summary>
/// <param name="address">The address to create the DataItem for.</param>
/// <returns>A new <see cref="DataItem"/> instance with properties parsed from <paramref name="address"/>.</returns>
/// <remarks>The <see cref="Count" /> property is not parsed from the address.</remarks>
public static DataItem FromAddress(string address)
{
PLCAddress.Parse(address, out var dataType, out var dbNumber, out var varType, out var startByte,
out var bitNumber);
return new DataItem
{
DataType = dataType,
DB = dbNumber,
VarType = varType,
StartByteAdr = startByte,
BitAdr = (byte)(bitNumber == -1 ? 0 : bitNumber)
};
}
/// <summary>
/// Create an instance of <see cref="DataItem"/> from the supplied address and value.
/// </summary>
/// <param name="address">The address to create the DataItem for.</param>
/// <param name="value">The value to be applied to the DataItem.</param>
/// <returns>A new <see cref="DataItem"/> instance with properties parsed from <paramref name="address"/> and the supplied value set.</returns>
public static DataItem FromAddressAndValue<T>(string address, T value)
{
var dataItem = FromAddress(address);
dataItem.Value = value;
if (typeof(T).IsArray)
{
var array = ((Array)dataItem.Value);
if (array != null)
{
dataItem.Count = array.Length;
}
}
return dataItem;
}
internal static DataItemAddress GetDataItemAddress(DataItem dataItem)
{
return new DataItemAddress(dataItem.DataType, dataItem.DB, dataItem.StartByteAdr, Plc.VarTypeToByteLength(dataItem.VarType, dataItem.Count));
}
}
}

View File

@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert between <see cref="T:System.DateTime"/> and S7 representation of datetime values.
/// </summary>
public static class DateTime
{
/// <summary>
/// The minimum <see cref="T:System.DateTime"/> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1990, 1, 1);
/// <summary>
/// The maximum <see cref="T:System.DateTime"/> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2089, 12, 31, 23, 59, 59, 999);
/// <summary>
/// Parses a <see cref="T:System.DateTime"/> value from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>A <see cref="T:System.DateTime"/> object representing the value read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
/// <paramref name="bytes"/> is not 8 or any value in <paramref name="bytes"/>
/// is outside the valid range of values.</exception>
public static System.DateTime FromByteArray(byte[] bytes)
{
return FromByteArrayImpl(bytes);
}
/// <summary>
/// Parses an array of <see cref="T:System.DateTime"/> values from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>An array of <see cref="T:System.DateTime"/> objects representing the values read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of
/// <paramref name="bytes"/> is not a multiple of 8 or any value in
/// <paramref name="bytes"/> is outside the valid range of values.</exception>
public static System.DateTime[] ToArray(byte[] bytes)
{
if (bytes.Length % 8 != 0)
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing an array of DateTime requires a multiple of 8 bytes of input data, input data is '{bytes.Length}' long.");
var cnt = bytes.Length / 8;
var result = new System.DateTime[bytes.Length / 8];
for (var i = 0; i < cnt; i++)
result[i] = FromByteArrayImpl(new ArraySegment<byte>(bytes, i * 8, 8));
return result;
}
private static System.DateTime FromByteArrayImpl(IList<byte> bytes)
{
if (bytes.Count != 8)
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Count,
$"Parsing a DateTime requires exactly 8 bytes of input data, input data is {bytes.Count} bytes long.");
int DecodeBcd(byte input) => 10 * (input >> 4) + (input & 0b00001111);
int ByteToYear(byte bcdYear)
{
var input = DecodeBcd(bcdYear);
if (input < 90) return input + 2000;
if (input < 100) return input + 1900;
throw new ArgumentOutOfRangeException(nameof(bcdYear), bcdYear,
$"Value '{input}' is higher than the maximum '99' of S7 date and time representation.");
}
int AssertRangeInclusive(int input, byte min, byte max, string field)
{
if (input < min)
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is lower than the minimum '{min}' allowed for {field}.");
if (input > max)
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is higher than the maximum '{max}' allowed for {field}.");
return input;
}
var year = ByteToYear(bytes[0]);
var month = AssertRangeInclusive(DecodeBcd(bytes[1]), 1, 12, "month");
var day = AssertRangeInclusive(DecodeBcd(bytes[2]), 1, 31, "day of month");
var hour = AssertRangeInclusive(DecodeBcd(bytes[3]), 0, 23, "hour");
var minute = AssertRangeInclusive(DecodeBcd(bytes[4]), 0, 59, "minute");
var second = AssertRangeInclusive(DecodeBcd(bytes[5]), 0, 59, "second");
var hsec = AssertRangeInclusive(DecodeBcd(bytes[6]), 0, 99, "first two millisecond digits");
var msec = AssertRangeInclusive(bytes[7] >> 4, 0, 9, "third millisecond digit");
var dayOfWeek = AssertRangeInclusive(bytes[7] & 0b00001111, 1, 7, "day of week");
return new System.DateTime(year, month, day, hour, minute, second, hsec * 10 + msec);
}
/// <summary>
/// Converts a <see cref="T:System.DateTime"/> value to a byte array.
/// </summary>
/// <param name="dateTime">The DateTime value to convert.</param>
/// <returns>A byte array containing the S7 date time representation of <paramref name="dateTime"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the value of
/// <paramref name="dateTime"/> is before <see cref="P:SpecMinimumDateTime"/>
/// or after <see cref="P:SpecMaximumDateTime"/>.</exception>
public static byte[] ToByteArray(System.DateTime dateTime)
{
byte EncodeBcd(int value)
{
return (byte) ((value / 10 << 4) | value % 10);
}
if (dateTime < SpecMinimumDateTime)
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 date time representation.");
if (dateTime > SpecMaximumDateTime)
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 date time representation.");
byte MapYear(int year) => (byte) (year < 2000 ? year - 1900 : year - 2000);
int DayOfWeekToInt(DayOfWeek dayOfWeek) => (int) dayOfWeek + 1;
return new[]
{
EncodeBcd(MapYear(dateTime.Year)),
EncodeBcd(dateTime.Month),
EncodeBcd(dateTime.Day),
EncodeBcd(dateTime.Hour),
EncodeBcd(dateTime.Minute),
EncodeBcd(dateTime.Second),
EncodeBcd(dateTime.Millisecond / 10),
(byte) (dateTime.Millisecond % 10 << 4 | DayOfWeekToInt(dateTime.DayOfWeek))
};
}
/// <summary>
/// Converts an array of <see cref="T:System.DateTime"/> values to a byte array.
/// </summary>
/// <param name="dateTimes">The DateTime values to convert.</param>
/// <returns>A byte array containing the S7 date time representations of <paramref name="dateTime"/>.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any value of
/// <paramref name="dateTimes"/> is before <see cref="P:SpecMinimumDateTime"/>
/// or after <see cref="P:SpecMaximumDateTime"/>.</exception>
public static byte[] ToByteArray(System.DateTime[] dateTimes)
{
var bytes = new List<byte>(dateTimes.Length * 8);
foreach (var dateTime in dateTimes) bytes.AddRange(ToByteArray(dateTime));
return bytes.ToArray();
}
}
}

View File

@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert between <see cref="T:System.DateTime" /> and S7 representation of DateTimeLong (DTL) values.
/// </summary>
public static class DateTimeLong
{
public const int TypeLengthInBytes = 12;
/// <summary>
/// The minimum <see cref="T:System.DateTime" /> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMinimumDateTime = new System.DateTime(1970, 1, 1);
/// <summary>
/// The maximum <see cref="T:System.DateTime" /> value supported by the specification.
/// </summary>
public static readonly System.DateTime SpecMaximumDateTime = new System.DateTime(2262, 4, 11, 23, 47, 16, 854);
/// <summary>
/// Parses a <see cref="T:System.DateTime" /> value from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>A <see cref="T:System.DateTime" /> object representing the value read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the length of
/// <paramref name="bytes" /> is not 12 or any value in <paramref name="bytes" />
/// is outside the valid range of values.
/// </exception>
public static System.DateTime FromByteArray(byte[] bytes)
{
return FromByteArrayImpl(bytes);
}
/// <summary>
/// Parses an array of <see cref="T:System.DateTime" /> values from bytes.
/// </summary>
/// <param name="bytes">Input bytes read from PLC.</param>
/// <returns>An array of <see cref="T:System.DateTime" /> objects representing the values read from PLC.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the length of
/// <paramref name="bytes" /> is not a multiple of 12 or any value in
/// <paramref name="bytes" /> is outside the valid range of values.
/// </exception>
public static System.DateTime[] ToArray(byte[] bytes)
{
if (bytes.Length % TypeLengthInBytes != 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing an array of DateTimeLong requires a multiple of 12 bytes of input data, input data is '{bytes.Length}' long.");
}
var cnt = bytes.Length / TypeLengthInBytes;
var result = new System.DateTime[cnt];
for (var i = 0; i < cnt; i++)
{
var slice = new byte[TypeLengthInBytes];
Array.Copy(bytes, i * TypeLengthInBytes, slice, 0, TypeLengthInBytes);
result[i] = FromByteArrayImpl(slice);
}
return result;
}
private static System.DateTime FromByteArrayImpl(byte[] bytes)
{
if (bytes.Length != TypeLengthInBytes)
{
throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
$"Parsing a DateTimeLong requires exactly 12 bytes of input data, input data is {bytes.Length} bytes long.");
}
var year = AssertRangeInclusive(Word.FromBytes(bytes[1], bytes[0]), 1970, 2262, "year");
var month = AssertRangeInclusive(bytes[2], 1, 12, "month");
var day = AssertRangeInclusive(bytes[3], 1, 31, "day of month");
var dayOfWeek = AssertRangeInclusive(bytes[4], 1, 7, "day of week");
var hour = AssertRangeInclusive(bytes[5], 0, 23, "hour");
var minute = AssertRangeInclusive(bytes[6], 0, 59, "minute");
var second = AssertRangeInclusive(bytes[7], 0, 59, "second");
;
var nanoseconds = AssertRangeInclusive<uint>(DWord.FromBytes(bytes[11], bytes[10], bytes[9], bytes[8]), 0,
999999999, "nanoseconds");
var time = new System.DateTime(year, month, day, hour, minute, second);
return time.AddTicks(nanoseconds / 100);
}
/// <summary>
/// Converts a <see cref="T:System.DateTime" /> value to a byte array.
/// </summary>
/// <param name="dateTime">The DateTime value to convert.</param>
/// <returns>A byte array containing the S7 DateTimeLong representation of <paramref name="dateTime" />.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when the value of
/// <paramref name="dateTime" /> is before <see cref="P:SpecMinimumDateTime" />
/// or after <see cref="P:SpecMaximumDateTime" />.
/// </exception>
public static byte[] ToByteArray(System.DateTime dateTime)
{
if (dateTime < SpecMinimumDateTime)
{
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 DateTimeLong representation.");
}
if (dateTime > SpecMaximumDateTime)
{
throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
$"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 DateTimeLong representation.");
}
var stream = new MemoryStream(TypeLengthInBytes);
// Convert Year
stream.Write(Word.ToByteArray(Convert.ToUInt16(dateTime.Year)), 0, 2);
// Convert Month
stream.WriteByte(Convert.ToByte(dateTime.Month));
// Convert Day
stream.WriteByte(Convert.ToByte(dateTime.Day));
// Convert WeekDay. NET DateTime starts with Sunday = 0, while S7DT has Sunday = 1.
stream.WriteByte(Convert.ToByte(dateTime.DayOfWeek + 1));
// Convert Hour
stream.WriteByte(Convert.ToByte(dateTime.Hour));
// Convert Minutes
stream.WriteByte(Convert.ToByte(dateTime.Minute));
// Convert Seconds
stream.WriteByte(Convert.ToByte(dateTime.Second));
// Convert Nanoseconds. Net DateTime has a representation of 1 Tick = 100ns.
// Thus First take the ticks Mod 1 Second (1s = 10'000'000 ticks), and then Convert to nanoseconds.
stream.Write(DWord.ToByteArray(Convert.ToUInt32(dateTime.Ticks % 10000000 * 100)), 0, 4);
return stream.ToArray();
}
/// <summary>
/// Converts an array of <see cref="T:System.DateTime" /> values to a byte array.
/// </summary>
/// <param name="dateTimes">The DateTime values to convert.</param>
/// <returns>A byte array containing the S7 DateTimeLong representations of <paramref name="dateTimes" />.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when any value of
/// <paramref name="dateTimes" /> is before <see cref="P:SpecMinimumDateTime" />
/// or after <see cref="P:SpecMaximumDateTime" />.
/// </exception>
public static byte[] ToByteArray(System.DateTime[] dateTimes)
{
var bytes = new List<byte>(dateTimes.Length * TypeLengthInBytes);
foreach (var dateTime in dateTimes)
{
bytes.AddRange(ToByteArray(dateTime));
}
return bytes.ToArray();
}
private static T AssertRangeInclusive<T>(T input, T min, T max, string field) where T : IComparable<T>
{
if (input.CompareTo(min) < 0)
{
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is lower than the minimum '{min}' allowed for {field}.");
}
if (input.CompareTo(max) > 0)
{
throw new ArgumentOutOfRangeException(nameof(input), input,
$"Value '{input}' is higher than the maximum '{max}' allowed for {field}.");
}
return input;
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
[Obsolete("Class Double is obsolete. Use Real instead for 32bit floating point, or LReal for 64bit floating point.")]
public static class Double
{
/// <summary>
/// Converts a S7 Real (4 bytes) to double
/// </summary>
public static double FromByteArray(byte[] bytes) => Real.FromByteArray(bytes);
/// <summary>
/// Converts a S7 DInt to double
/// </summary>
public static double FromDWord(Int32 value)
{
byte[] b = DInt.ToByteArray(value);
double d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a S7 DWord to double
/// </summary>
public static double FromDWord(UInt32 value)
{
byte[] b = DWord.ToByteArray(value);
double d = FromByteArray(b);
return d;
}
/// <summary>
/// Converts a double to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(double value) => Real.ToByteArray((float)value);
/// <summary>
/// Converts an array of double to an array of bytes
/// </summary>
public static byte[] ToByteArray(double[] value)
{
ByteArray arr = new ByteArray();
foreach (double val in value)
arr.Add(ToByteArray(val));
return arr.Array;
}
/// <summary>
/// Converts an array of S7 Real to an array of double
/// </summary>
public static double[] ToArray(byte[] bytes)
{
double[] values = new double[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Int from S7 plc to C#.
/// </summary>
public static class Int
{
/// <summary>
/// Converts a S7 Int (2 bytes) to short (Int16)
/// </summary>
public static short FromByteArray(byte[] bytes)
{
if (bytes.Length != 2)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 2 bytes.");
}
// bytes[0] -> HighByte
// bytes[1] -> LowByte
return (short)((int)(bytes[1]) | ((int)(bytes[0]) << 8));
}
/// <summary>
/// Converts a short (Int16) to a S7 Int byte array (2 bytes)
/// </summary>
public static byte[] ToByteArray(Int16 value)
{
byte[] bytes = new byte[2];
bytes[0] = (byte) (value >> 8 & 0xFF);
bytes[1] = (byte)(value & 0xFF);
return bytes;
}
/// <summary>
/// Converts an array of short (Int16) to a S7 Int byte array (2 bytes)
/// </summary>
public static byte[] ToByteArray(Int16[] value)
{
byte[] bytes = new byte[value.Length * 2];
int bytesPos = 0;
for(int i=0; i< value.Length; i++)
{
bytes[bytesPos++] = (byte)((value[i] >> 8) & 0xFF);
bytes[bytesPos++] = (byte) (value[i] & 0xFF);
}
return bytes;
}
/// <summary>
/// Converts an array of S7 Int to an array of short (Int16)
/// </summary>
public static Int16[] ToArray(byte[] bytes)
{
int shortsCount = bytes.Length / 2;
Int16[] values = new Int16[shortsCount];
int counter = 0;
for (int cnt = 0; cnt < shortsCount; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++] });
return values;
}
/// <summary>
/// Converts a C# int value to a C# short value, to be used as word.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static Int16 CWord(int value)
{
if (value > 32767)
{
value -= 32768;
value = 32768 - value;
value *= -1;
}
return (short)value;
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
public static class LReal
{
/// <summary>
/// Converts a S7 LReal (8 bytes) to double
/// </summary>
public static double FromByteArray(byte[] bytes)
{
if (bytes.Length != 8)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 8 bytes.");
}
var buffer = bytes;
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
Array.Reverse(buffer);
}
return BitConverter.ToDouble(buffer, 0);
}
/// <summary>
/// Converts a double to S7 LReal (8 bytes)
/// </summary>
public static byte[] ToByteArray(double value)
{
var bytes = BitConverter.GetBytes(value);
// sps uses bigending so we have to check if platform is same
if (BitConverter.IsLittleEndian)
{
Array.Reverse(bytes);
}
return bytes;
}
/// <summary>
/// Converts an array of double to an array of bytes
/// </summary>
public static byte[] ToByteArray(double[] value) => TypeHelper.ToByteArray(value, ToByteArray);
/// <summary>
/// Converts an array of S7 LReal to an array of double
/// </summary>
public static double[] ToArray(byte[] bytes) => TypeHelper.ToArray(bytes, FromByteArray);
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.IO;
namespace S7.Net.Types
{
/// <summary>
/// Contains the conversion methods to convert Real from S7 plc to C# double.
/// </summary>
public static class Real
{
/// <summary>
/// Converts a S7 Real (4 bytes) to float
/// </summary>
public static float FromByteArray(byte[] bytes)
{
if (bytes.Length != 4)
{
throw new ArgumentException("Wrong number of bytes. Bytes array must contain 4 bytes.");
}
// sps uses bigending so we have to reverse if platform needs
if (BitConverter.IsLittleEndian)
{
// create deep copy of the array and reverse
bytes = new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
return BitConverter.ToSingle(bytes, 0);
}
/// <summary>
/// Converts a float to S7 Real (4 bytes)
/// </summary>
public static byte[] ToByteArray(float value)
{
byte[] bytes = BitConverter.GetBytes(value);
// sps uses bigending so we have to check if platform is same
if (!BitConverter.IsLittleEndian) return bytes;
// create deep copy of the array and reverse
return new byte[] { bytes[3], bytes[2], bytes[1], bytes[0] };
}
/// <summary>
/// Converts an array of float to an array of bytes
/// </summary>
public static byte[] ToByteArray(float[] value)
{
var buffer = new byte[4 * value.Length];
var stream = new MemoryStream(buffer);
foreach (var val in value)
{
stream.Write(ToByteArray(val), 0, 4);
}
return buffer;
}
/// <summary>
/// Converts an array of S7 Real to an array of float
/// </summary>
public static float[] ToArray(byte[] bytes)
{
var values = new float[bytes.Length / 4];
int counter = 0;
for (int cnt = 0; cnt < bytes.Length / 4; cnt++)
values[cnt] = FromByteArray(new byte[] { bytes[counter++], bytes[counter++], bytes[counter++], bytes[counter++] });
return values;
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Text;
namespace S7.Net.Types
{
/// <summary>
/// Contains the methods to convert from S7 strings to C# strings
/// An S7 String has a preceeding 2 byte header containing its capacity and length
/// </summary>
public static class S7String
{
private static Encoding stringEncoding = Encoding.ASCII;
/// <summary>
/// The Encoding used when serializing and deserializing S7String (Encoding.ASCII by default)
/// </summary>
/// <exception cref="ArgumentNullException">StringEncoding must not be null</exception>
public static Encoding StringEncoding
{
get => stringEncoding;
set => stringEncoding = value ?? throw new ArgumentNullException(nameof(StringEncoding));
}
/// <summary>
/// Converts S7 bytes to a string
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FromByteArray(byte[] bytes)
{
if (bytes.Length < 2)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / too short");
}
int size = bytes[0];
int length = bytes[1];
if (length > size)
{
throw new PlcException(ErrorCode.ReadData, "Malformed S7 String / length larger than capacity");
}
try
{
return StringEncoding.GetString(bytes, 2, length);
}
catch (Exception e)
{
throw new PlcException(ErrorCode.ReadData,
$"Failed to parse {VarType.S7String} from data. Following fields were read: size: '{size}', actual length: '{length}', total number of bytes (including header): '{bytes.Length}'.",
e);
}
}
/// <summary>
/// Converts a <see cref="T:string"/> to S7 string with 2-byte header.
/// </summary>
/// <param name="value">The string to convert to byte array.</param>
/// <param name="reservedLength">The length (in characters) allocated in PLC for the string.</param>
/// <returns>A <see cref="T:byte[]" /> containing the string header and string value with a maximum length of <paramref name="reservedLength"/> + 2.</returns>
public static byte[] ToByteArray(string value, int reservedLength)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (reservedLength > 254) throw new ArgumentException($"The maximum string length supported is 254.");
var bytes = StringEncoding.GetBytes(value);
if (bytes.Length > reservedLength) throw new ArgumentException($"The provided string length ({bytes.Length} is larger than the specified reserved length ({reservedLength}).");
var buffer = new byte[2 + reservedLength];
Array.Copy(bytes, 0, buffer, 2, bytes.Length);
buffer[0] = (byte)reservedLength;
buffer[1] = (byte)bytes.Length;
return buffer;
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
namespace S7.Net.Types
{
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public sealed class S7StringAttribute : Attribute
{
private readonly S7StringType type;
private readonly int reservedLength;
/// <summary>
/// Initializes a new instance of the <see cref="S7StringAttribute"/> class.
/// </summary>
/// <param name="type">The string type.</param>
/// <param name="reservedLength">Reserved length of the string in characters.</param>
/// <exception cref="ArgumentException">Please use a valid value for the string type</exception>
public S7StringAttribute(S7StringType type, int reservedLength)
{
if (!Enum.IsDefined(typeof(S7StringType), type))
throw new ArgumentException("Please use a valid value for the string type");
this.type = type;
this.reservedLength = reservedLength;
}
/// <summary>
/// Gets the type of the string.
/// </summary>
/// <value>
/// The string type.
/// </value>
public S7StringType Type => type;
/// <summary>
/// Gets the reserved length of the string in characters.
/// </summary>
/// <value>
/// The reserved length of the string in characters.
/// </value>
public int ReservedLength => reservedLength;
/// <summary>
/// Gets the reserved length in bytes.
/// </summary>
/// <value>
/// The reserved length in bytes.
/// </value>
public int ReservedLengthInBytes => type == S7StringType.S7String ? reservedLength + 2 : (reservedLength * 2) + 4;
}
/// <summary>
/// String type.
/// </summary>
public enum S7StringType
{
/// <summary>
/// ASCII string.
/// </summary>
S7String = VarType.S7String,
/// <summary>
/// Unicode string.
/// </summary>
S7WString = VarType.S7WString
}
}

Some files were not shown because too many files have changed in this diff Show More