1、介绍
前面已经完成了7项工作:
C#上位机与欧姆龙PLC的通信01----项目背景-CSDN博客
C#上位机与欧姆龙PLC的通信02----搭建仿真环境-CSDN博客
C#上位机与欧姆龙PLC的通信03----创建项目工程-CSDN博客
C#上位机与欧姆龙PLC的通信04---- 欧姆龙plc的存储区
C#上位机与欧姆龙PLC的通信05---- HostLink协议(C-Mode版)
C#上位机与欧姆龙PLC的通信06---- HostLink协议(FINS版)
C#上位机与欧姆龙PLC的通信07----使用第3方通讯库读写数据
这当中,06是重点的重点,需要非常熟悉才能自己写通讯库,封装自己的库需要掌握socket通讯,串口通讯,同步异步,集合数组,字节序列等技能点,这是走向武林高手的必经之路,这样才能强大自己,丰满的肌肉需要一步步啃。
2、开搞
1、创建VS解决方案项目
2、添加类库项目
类库项目创建目录Omron及Base及5个基础类
添加对类库的引用
3、编写基础类库文件
1)AreaType.cs
存储区对应厂家手册规定的值,不能自己改。
(D位:02,D字:82,W位:31,C位:30,W字:B1,C字:B0,H字:B2)
/// <summary>
/// 存储区枚举
/// </summary>
public enum AreaType
{
CIOBIT = 0x30,
WBIT = 0x31,
DMBIT = 0x02,
ABIT = 0x33,
HBIT = 0x32,
CIOWORD = 0xB0,
WWORD = 0xB1,
DMWORD = 0x82,
AWORD = 0xB3,
HWORD = 0xB2
}
2)DataAddress.cs
/// <summary>
/// 地址模型类
/// </summary>
public class DataAddress
{
/// <summary>
/// 区域类型
/// </summary>
public AreaType AreaType { get; set; }
/// <summary>
/// Word起始地址
/// </summary>
public ushort WordAddress { get; set; }
/// <summary>
/// Bit起始地址
/// </summary>
public byte BitAddress { get; set; }
}
3)EndianType.cs
这就是大小端的列举,涉及字节序列中的高位低位交换等处理
namespace Zhaoxi.Communication.Omron.Base
{
/// <summary>
/// 字节序列
/// </summary>
public enum EndianType
{
AB, BA,
ABCD, CDAB, BADC, DCBA,
ABCDEFGH, GHEFCDAB, BADCFEHG, HGFEDCBA
}
}
4)Result.cs
通信结果类
namespace Omron.Communimcation.Fins.Omron
{
/// <summary>
/// 通讯结果类
/// </summary>
/// <typeparam name="T"></typeparam>
public class Result<T>
{
/// <summary>
/// 状态
/// </summary>
public bool IsSuccessed { get; set; }
/// <summary>
/// 对应的消息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 数据列表
/// </summary>
public List<T> Datas { get; set; }
public Result() : this(true, "OK") { }
public Result(bool state, string msg) : this(state, msg, new List<T>()) { }
public Result(bool state, string msg, List<T> datas)
{
this.IsSuccessed = state; Message = msg; Datas = datas;
}
}
public class Result : Result<bool> { }
}
4、编写核心的通信类Fins
这个类就是实现读取和写入数据的方法,封装了报文的每个组成部分,象请求的报文头,命令码,数据处理,发送和接收处理等,字节处理,寄存器的数据类型处理,辅助方法等很多。该类很强大,能很好的处理short,ushort,bool,float,OOP的思想在这个类中得到了很好的发挥,注释很详细,代码很规范,需要很深的功底才可以写得出来的,需要对fins的报文结构指令内容非常精通才能写好,各位高僧能人自行阅读,欢迎留言。
using Omron.Communimcation.Fins.Omron.Base;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Omron.Communimcation.Fins.Omron
{
/// <summary>
/// 欧姆龙FINS协议通讯库
/// </summary>
public class FinsTcp
{
/// <summary>
/// tcp服务器地址
/// </summary>
string _ip;
/// <summary>
/// tcp端口号
/// </summary>
int _port;
/// <summary>
/// 目标节点
/// </summary>
byte _da;
/// <summary>
/// 源节点
/// </summary>
byte _sa;
/// <summary>
/// socket连接对象(全局变量)
/// </summary>
Socket socket = null;
/// <summary>
/// 超时对象
/// </summary>
ManualResetEvent TimeoutObject = new ManualResetEvent(false);
/// <summary>
/// 连接状态
/// </summary>
bool connectState = false;
/// <summary>
/// 头部字节
/// </summary>
byte[] finsTcpHeader = new byte[] { 0x46, 0x49, 0x4E, 0x53 };
/// <summary>
/// 构造方法
/// </summary>
/// <param name="ip">TCP服务器IP</param>
/// <param name="port">TCP服务器端口</param>
/// <param name="da">PLC节点</param>
/// <param name="sa">PC端节点</param>
public FinsTcp(string ip, int port, byte da = 0x00, byte sa = 0x00)
{
_ip = ip;
_port = port;
_da = da;
_sa = sa;
// 初始化一个通信对象
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
#region 操作PLC
/// <summary>
/// 连接PLC
/// </summary>
/// <param name="timeout">超时时间</param>
/// <returns></returns>
public Result Connect(int timeout = 50)
{
TimeoutObject.Reset();
Result result = new Result();
try
{
if (socket == null)
{
throw new Exception("通信对象未初始化");
}
int count = 0;
while (count < timeout)
{
// 断线重连:
// 1、被服务端主动踢掉(服务连接列表里已经没有当前客户端信息了)
// 2、断网(拔网线) 客户端(不知道-》新的端口进行连接)、服务端(可以知道、检查客户端的心跳)
if (!(!socket.Connected || (socket.Poll(200, SelectMode.SelectRead) && (socket.Available == 0))))
{
return result;
}
try
{
socket?.Close();
socket.Dispose();
socket = null;
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//异步连接PLC
socket.BeginConnect(_ip, _port, callback =>
{
connectState = false;
var cbSocket = callback.AsyncState as Socket;
if (cbSocket != null)
{
connectState = cbSocket.Connected;
if (cbSocket.Connected)
{
cbSocket.EndConnect(callback);
}
}
TimeoutObject.Set();
}, socket);
TimeoutObject.WaitOne(2000, false);
if (!connectState)
{
throw new Exception("网络连接异常");
}
else
{
break;
}
}
catch (SocketException ex)
{
if (ex.ErrorCode == 10060)
{
throw new Exception(ex.Message);
}
}
catch (Exception)
{
throw new Exception("网络连接异常");
}
finally
{
count++;
}
}
if (socket == null || !socket.Connected || ((socket.Poll(200, SelectMode.SelectRead) && (socket.Available == 0))))
{
throw new Exception("网络连接失败");
}
else
{
// 2、建立连接
result = this.SetupConnect();
if (!result.IsSuccessed)
{
return result;
}
}
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
/// <summary>
/// 启动PLC
/// </summary>
/// <returns></returns>
public Result Run()
{
return PlcSate(0x01);
}
/// <summary>
/// 停止PLC
/// </summary>
/// <returns></returns>
public Result Stop()
{
return PlcSate(0x02);
}
/// <summary>
/// PLC状态
/// </summary>
/// <param name="state">0401启动,0402停止</param>
/// <returns></returns>
public Result PlcSate(byte state)
{
Result result = new Result();
try
{
var connectState = this.Connect();
if (!connectState.IsSuccessed)
{
throw new Exception(connectState.Message);
}
List<byte> baseBytes = this.GetBaseCommand(0x04, state); //构建命令
this.Send(baseBytes);//发送命令
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
#endregion
#region 读取数据
/ <summary>
/ 读取命令,原有方法
/ </summary>
/ <typeparam name="T">返回的数据类型,如ushort,short,int32,float等</typeparam>
/ <param name="areaCode">存储区代码</param>
/ <param name="wStartAddr">开始地址(16进制格式,如ox64)</param>
/ <param name="bStartAddr">Bit地址(16进制格式,如ox00)</param>
/ <param name="count">读取个数(16进制格式,如读取12个,就是ox0b)</param>
/ <returns></returns>
//public Result<T> Read<T>(AreaType areaCode, ushort wStartAddr, byte bStartAddr, ushort count)
//{
// //发送: 46 49 4E 53 00 00 00 1A 00 00 00 02 00 00 00 00 80 00 02 00 02 00 00 0A 00 00 01 01 82 00 C8 00 00 01
// //接收: 46 49 4E 53 00 00 00 18 00 00 00 02 00 00 00 00 C0 00 02 00 02 00 00 0A 00 00 01 01 00 00 02 2E
// Result<T> result = new Result<T>();
// int typeLen = 1;//数据类型所占的字节宽度
// if (!typeof(T).Equals(typeof(bool)))
// {
// typeLen = Marshal.SizeOf<T>() / 2; // 每一个数据需要多少个寄存器
// }
// try
// {
// byte[] bytes = new byte[] {
// 0x00,0x00,0x00,0x02, // 读写的时候固定传这个值
// 0x00,0x00,0x00,0x00, // 错误代码;
// 0x80,// ICF
// 0x00, // Rev
// 0x02, // GCT
// 0x00,_da,0x00, // DNA DA1 DA2 ,即目标网络号,目标节点号,目标单元号
// 0x00,_sa,0x00,// SNA SA1 SA2,即源网络号,源节点号,源单元号
// 0x00, // SID,固定值
// 0x01,0x01,// 命令码,读操作固定值为0101
// (byte)areaCode, // 存储区
// (byte)(wStartAddr/256%256),//Word起始地址,占2个字节
// (byte)( wStartAddr%256),
// bStartAddr,//起始位地址,占1个字节
// (byte)(count*typeLen/256%256),//读取个数,占2个字节
// (byte)(count*typeLen%256)
// };
// //命令字节
// List<byte> commandBytes = new List<byte>();
// // 计算字节长度,占4个字节
// List<byte> lengthbyte = new List<byte>();
// lengthbyte.Add((byte)(bytes.Length / 256 / 256 / 256 % 256));
// lengthbyte.Add((byte)(bytes.Length / 256 / 256 % 256));
// lengthbyte.Add((byte)(bytes.Length / 256 % 256));
// lengthbyte.Add((byte)(bytes.Length % 256));
// commandBytes.AddRange(finsTcpHeader);//加入头部
// commandBytes.AddRange(lengthbyte.ToArray());//加入长度
// commandBytes.AddRange(bytes);//加入协议内容
// socket.Send(commandBytes.ToArray());//发送报文
// //1.1接收数据中的前8个字节,即fins头部
// byte[] respHeader = new byte[8];
// socket.Receive(respHeader, 0, 8, SocketFlags.None);
// //1.2 判断前面4个字节是否是FINS
// for (int i = 0; i < 4; i++)
// {
// if (respHeader[i] != finsTcpHeader[i])
// {
// throw new Exception("响应报文无效");
// }
// }
// //1.3、获取剩余报文长度: 00 00 00 18
// byte[] lenBytes = new byte[4];
// lenBytes[0] = respHeader[7];
// lenBytes[1] = respHeader[6];
// lenBytes[2] = respHeader[5];
// lenBytes[3] = respHeader[4];
// int len = BitConverter.ToInt32(lenBytes);
// Console.WriteLine("剩余报文长度:" + len);
// //1.4 接收剩余字节
// byte[] dataBytes = new byte[len];
// socket.Receive(dataBytes, 0, len, SocketFlags.None);
// // 1.5 错误信息判断 ,确定没有ErrorCode
// var code = dataBytes[4] | dataBytes[5] | dataBytes[6] | dataBytes[7];
// if (code > 0)
// {
// Console.WriteLine($"有错误[{code}]");
// }
// code = (dataBytes[20] << 8) | dataBytes[21]; // 01 01 0000 0001 0000 0001
// if (code > 0)
// {
// Console.WriteLine($"有错误[{code}]"); // 字典处理:{257:"当前网络环境中无法匹配此节点"}
// }
// // 1.6 开始解析数据部分
// List<byte> dataList = new List<byte>(dataBytes);
// dataList.RemoveRange(0, 22);//移除指定部分,保留数据内容
// //循环处理数据
// for (int i = 0; i < dataList.Count;)
// {
// if (typeof(T) == typeof(bool))//布尔类型
// {
// Type tConvert = typeof(Convert);
// //查找 convert这个类中的toboolean方法
// MethodInfo method = tConvert.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(mi => mi.Name == "ToBoolean") as MethodInfo;
// result.Datas.Add((T)method.Invoke(tConvert, new object[] { int.Parse(dataList[i++].ToString()) }));
// }
// else //short,ushort,float,double类型
// {
// List<byte> datas = new List<byte>();
// // Word -> Short,DWrod-> Float,DD-> Double
// for (int j = 0; j < typeLen * 2; j++)
// {
// datas.Add(dataList[i++]); // 只能处理2个字节的情况,其他的处理不了,需要进行字节序的调整
// }
// if (typeLen == 1)//2个字节,适合ushort,short,int16
// {
// datas = new List<byte>(this.SwitchEndian(datas.ToArray(), EndianType.AB));
// }
// else if (typeLen == 2)//4个字节,适合int32,float
// {
// datas = new List<byte>(this.SwitchEndian(datas.ToArray(), EndianType.CDAB));
// }
// Type tBitConverter = typeof(BitConverter);
// MethodInfo method = tBitConverter.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(mi => mi.ReturnType == typeof(T)) as MethodInfo;
// if (method == null)
// {
// Console.WriteLine("未找到匹配的数据类型转换方法");
// }
// result.Datas.Add((T)method?.Invoke(tBitConverter, new object[] { datas.ToArray(), 0 }));
// }
// }
// }
// catch (Exception ex)
// {
// return new Result<T>(false, ex.Message);
// }
// return result;
//}
/// <summary>
/// 读取操作
/// </summary>
/// <typeparam name="T">返回的数据类型</typeparam>
/// <param name="address">存储区地址</param>
/// <param name="count">读取数量</param>
/// <returns></returns>
public Result<T> Read<T>(string address, ushort count)
{
Result<T> result = new Result<T>();
try
{
DataAddress dataAddress = this.AnalysisAddress(address);
result = this.Read<T>(dataAddress, count);
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
/// <summary>
/// 读取操作
/// </summary>
/// <typeparam name="T">返回的数据类型</typeparam>
/// <param name="dataAddress">地址模型</param>
/// <param name="count">读取数量</param>
/// <returns></returns>
public Result<T> Read<T>(DataAddress dataAddress, ushort count)
{
Result<T> result = new Result<T>();
int typeLen = 1;
if (!typeof(T).Equals(typeof(bool)))
{
typeLen = Marshal.SizeOf<T>() / 2;// 每一个数据需要多少个寄存器
}
try
{
//1、判断连接状态
var connectState = this.Connect();
//没有连接成功时则返回
if (!connectState.IsSuccessed)
{
throw new Exception(connectState.Message);
}
//2、构建基本命令:0101表示读取,0102表示写入
//命令头部
List<byte> baseBytes = this.GetBaseCommand(0x01, 0x01);
//存储区类型
baseBytes.Add((byte)dataAddress.AreaType);
//开始地址,占3个字节
baseBytes.Add((byte)(dataAddress.WordAddress / 256 % 256));
baseBytes.Add((byte)(dataAddress.WordAddress % 256));
baseBytes.Add(dataAddress.BitAddress);
//读取数量,占2个字节
baseBytes.Add((byte)(count * typeLen / 256 % 256));
baseBytes.Add((byte)(count * typeLen % 256));
//3、发送命令
this.Send(baseBytes);
//4、获取响应的数据报文
List<byte> dataList = this.CheckResponseBytes();
//5、解析数据报文
for (int i = 0; i < dataList.Count;)
{
if (typeof(T) == typeof(bool))//bool类型的处理
{
dynamic boolValue = dataList[i++] == 0x01;
result.Datas.Add(boolValue);
}
else //short,ushort,float,double类型的处理
{
List<byte> datas = new List<byte>();
for (int j = 0; j < typeLen * 2; j++)
{
datas.Add(dataList[i++]); // 只能处理2个字节的情况,其他的处理不了,需要进行字节序的调整
}
datas = this.SwitchBytes(datas); //交换字节顺序
Type tBitConverter = typeof(BitConverter);
MethodInfo method = tBitConverter.GetMethods(BindingFlags.Public | BindingFlags.Static).FirstOrDefault(mi => mi.ReturnType == typeof(T)) as MethodInfo;
if (method == null)
{
throw new Exception("未找到匹配的数据类型转换方法");
}
result.Datas.Add((T)method?.Invoke(tBitConverter, new object[] { datas.ToArray(), 0 }));
}
}
}
catch (Exception ex)
{
return new Result<T>(false, ex.Message);
}
return result;
}
#endregion
#region 写入数据
/// <summary>
/// 写入操作
/// </summary>
/// <typeparam name="T">写入的数据类型</typeparam>
/// <param name="values">写入的具体数据</param>
/// <param name="address">写入的具体地址</param>
/// <returns></returns>
public Result Write<T>(List<T> values, string address)
{
Result result = new Result();
try
{
DataAddress dataAddress = this.AnalysisAddress(address);
result = this.Write<T>(values, dataAddress);
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
/// <summary>
/// 写入操作
/// </summary>
/// <typeparam name="T">写入的数据类型</typeparam>
/// <param name="values">写入的具体数据</param>
/// <param name="dataAddress">地址模型</param>
/// <returns></returns>
public Result Write<T>(List<T> values, DataAddress dataAddress)
{
Result result = new Result();
int typeLen = 1;
if (!typeof(T).Equals(typeof(bool)))
{
typeLen = Marshal.SizeOf<T>() / 2; // 每一个数据需要多少个寄存器
}
try
{
//1、建立连接,如果连接失败则抛出异常
var connectState = this.Connect();
if (!connectState.IsSuccessed)
{
throw new Exception(connectState.Message);
}
//2、加入基本命令,0102表示写,0101表示读
List<byte> baseBytes = this.GetBaseCommand(0x01, 0x02);
//2.1加入存储区
baseBytes.Add((byte)dataAddress.AreaType);
//2.2加入地址,占3个字节
baseBytes.Add((byte)(dataAddress.WordAddress / 256 % 256));
baseBytes.Add((byte)(dataAddress.WordAddress % 256));
baseBytes.Add(dataAddress.BitAddress);
//2.3 加入长度, short 是1字占1个长度 , float是2字占2个长度
baseBytes.Add((byte)(values.Count * typeLen / 256 % 256));
baseBytes.Add((byte)(values.Count * typeLen % 256));
//处理数值
foreach (dynamic item in values)
{
if (typeof(T) == typeof(bool))//bool类型处理
{
baseBytes.Add((byte)(bool.Parse(item.ToString()) ? 0x01 : 0x00));
}
else // short,ushort,int32,float类型处理
{
List<byte> vBytes = new List<byte>(BitConverter.GetBytes(item));
vBytes = this.SwitchBytes(vBytes);
baseBytes.AddRange(vBytes);
}
}
//3、发送命令
this.Send(baseBytes);
//4、检查报文,写入时不需要处理返回数据
this.CheckResponseBytes();
}
catch (Exception ex)
{
result.IsSuccessed = false;
result.Message = ex.Message;
}
return result;
}
#endregion
#region 基本方法
/// <summary>
/// 初始化内容
/// </summary>
/// <returns></returns>
private Result SetupConnect()
{
byte[] connectBytes = new byte[] {
// Header FINS对应的Ascii编码(16进制)
0x46,0x49,0x4E,0x53,
// Length
0x00,0x00,0x00,0x0C,
// Command
0x00,0x00,0x00,0x00,
// Error code
0x00,0x00,0x00,0x00,
// Client node addr
0x00,0x00,0x00,0x04
};
try
{
socket.Send(connectBytes);//发送握手报文
byte[] respBytes = new byte[24];
int count = socket.Receive(respBytes, 0, 24, SocketFlags.None);
// 判断是否FINS开头的报文
for (int i = 0; i < 4; i++)
{
if (respBytes[i] != finsTcpHeader[i])
throw new Exception("连接请求响应报文无效");
}
// 确定没有ErrorCode
var state = respBytes[12] | respBytes[13] | respBytes[14] | respBytes[15];
if (state > 0)
throw new Exception($"有错误[{state}]");
}
catch (Exception ex)
{
return new Result() { IsSuccessed = false, Message = ex.Message };
}
return new Result();
}
/// <summary>
/// 调整字节序
/// </summary>
/// <param name="value"></param>
/// <param name="endianType"></param>
/// <returns></returns>
byte[] SwitchEndian(byte[] value, EndianType endianType)
{
List<byte> result = new List<byte>(value);
switch (endianType)
{
case EndianType.AB:
case EndianType.ABCD:
case EndianType.ABCDEFGH:
result.Reverse();
return result.ToArray();
case EndianType.CDAB: // 4字节处理
if (value.Length == 4)
{
result[3] = value[2];
result[2] = value[3];
result[1] = value[0];
result[0] = value[1];
}
return result.ToArray();
case EndianType.BADC: // 4字节处理
if (value.Length == 4)
{
result[3] = value[1];
result[2] = value[0];
result[1] = value[3];
result[0] = value[2];
}
return result.ToArray();
case EndianType.GHEFCDAB: // 8字节处理
if (value.Length == 8)
{
result[7] = value[6];
result[6] = value[7];
result[5] = value[4];
result[4] = value[5];
result[3] = value[2];
result[2] = value[3];
result[1] = value[0];
result[0] = value[1];
}
return result.ToArray();
case EndianType.BADCFEHG: // 8字节处理
if (value.Length == 8)
{
result[7] = value[1];
result[6] = value[0];
result[5] = value[3];
result[4] = value[2];
result[3] = value[5];
result[2] = value[4];
result[1] = value[7];
result[0] = value[6];
}
return result.ToArray();
case EndianType.BA:
case EndianType.DCBA:
case EndianType.HGFEDCBA:
return value;
default:
break;
}
return null;
}
/// <summary>
/// 字节交换
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
private List<byte> SwitchBytes(List<byte> bytes)
{
byte temp;
for (int i = 0; i < bytes.Count; i += 2)
{
temp = bytes[i];
bytes[i] = bytes[i + 1];
bytes[i + 1] = temp;
}
return bytes;
}
/// <summary>
/// 地址解析
/// </summary>
/// <param name="addr">地址</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private DataAddress AnalysisAddress(string addr)
{
DataAddress dataAddress = new DataAddress();
addr = addr.ToUpper();
AreaType wordCode = 0x00, bitCode = 0x00;
if (addr.Substring(0, 3) == "CIO")
{
wordCode = AreaType.CIOWORD;
bitCode = AreaType.CIOBIT;
addr = addr.Substring(3);
}
else if (addr.Substring(0, 2) == "DM")
{
wordCode = AreaType.DMWORD;
bitCode = AreaType.DMBIT;
addr = addr.Substring(2);
}
else
{
switch (addr[0])
{
case 'W':
bitCode = AreaType.WBIT;
wordCode = AreaType.WWORD;
break;
case 'A':
bitCode = AreaType.ABIT;
wordCode = AreaType.AWORD;
break;
case 'H':
bitCode = AreaType.HBIT;
wordCode = AreaType.HWORD;
break;
}
addr = addr.Substring(1);
}
if (bitCode == 0x00)
{
throw new Exception("地址类型暂不支持!");
}
string[] tempAddr = addr.Split('.');
dataAddress.AreaType = wordCode;
if (string.IsNullOrEmpty(tempAddr[0]))
{
throw new Exception("地址标记错误");
}
ushort ws = 0;
if (ushort.TryParse(tempAddr[0], out ws))
{
dataAddress.WordAddress = ws;
}
// 如果有小数点
if (tempAddr.Length > 1)
{
dataAddress.AreaType = bitCode;
if (string.IsNullOrEmpty(tempAddr[1]))
{
throw new Exception("地址标记错误");
}
byte bs = 0;
if (byte.TryParse(tempAddr[1], out bs))
{
dataAddress.BitAddress = bs;
}
}
return dataAddress;
}
/// <summary>
/// 创建基本命令
/// </summary>
/// <param name="mCommand">主命令</param>
/// <param name="sCommand">次命令</param>
/// <returns></returns>
private List<byte> GetBaseCommand(byte mCommand, byte sCommand)
{
return new List<byte> {
// Command 读写的时候固定传这个值
0x00,0x00,0x00,0x02,
// Error code 错误代码;
0x00,0x00,0x00,0x00,
// ICF
0x80,
// Rev
0x00,
// GCT
0x02,
// DNA DA1 DA2,即目标网络号,目标节点号,目标单元号,也就是指PLC网络
0x00,_da,0x00,
// SNA SA1 SA2,即源网络号,源节点号,源单元号,也就是指PC网络
0x00,_sa,0x00,
// SID ,固定值
0x00,
//读写命令
mCommand,
sCommand
};
}
/// <summary>
/// 检查响应报文,返回数据内容
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
private List<byte> CheckResponseBytes()
{
byte[] respHeader = new byte[8];//头部数据,占8个字节
socket.Receive(respHeader, 0, 8, SocketFlags.None); //接收fins头部
//1、判断前面4个字节是否是FINS
for (int i = 0; i < 4; i++)
{
if (respHeader[i] != finsTcpHeader[i])
{
throw new Exception("响应报文无效");
}
}
//2、获取剩余报文长度 00 00 00 18
byte[] lenBytes = new byte[4];
lenBytes[0] = respHeader[7];
lenBytes[1] = respHeader[6];
lenBytes[2] = respHeader[5];
lenBytes[3] = respHeader[4];
int len = BitConverter.ToInt32(lenBytes, 0);//字节数组转换成int32
//Console.WriteLine("剩余报文长度:" + len);
//3、接收剩余字节
byte[] dataBytes = new byte[len];
socket.Receive(dataBytes, 0, len, SocketFlags.None);
//4、错误信息解读 确定没有ErrorCode ResponseCode
var code = dataBytes[4] | dataBytes[5] | dataBytes[6] | dataBytes[7];
if (code > 0)
{
throw new Exception($"有错误[{code}]");
}
code = (dataBytes[20] << 8) | dataBytes[21]; /// 判断End Code 01 01 0000 0001 0000 0001 0x10 0x03 "1003"
if (code > 0)
{
throw new Exception($"有错误[{code}]"); // 字典处理:{257:"当前网络环境中无法匹配此节点"}
}
//5、获取数据部分
List<byte> dataList = new List<byte>(dataBytes);
dataList.RemoveRange(0, 22);//移除前面22个字节,剩下的就是数据部分
//6、返回数据部分
return dataList;
}
/// <summary>
/// 发送TCP数据
/// </summary>
/// <param name="baseBytes">命令字节集合</param>
private void Send(List<byte> baseBytes)
{
//命令字节
List<byte> commandBytes = new List<byte>();
// 计算字节长度,占4个字节
List<byte> lengthbyte = new List<byte>();
lengthbyte.Add((byte)(baseBytes.Count / 256 / 256 / 256 % 256));
lengthbyte.Add((byte)(baseBytes.Count / 256 / 256 % 256));
lengthbyte.Add((byte)(baseBytes.Count / 256 % 256));
lengthbyte.Add((byte)(baseBytes.Count % 256));
commandBytes.AddRange(finsTcpHeader);//加入头部
commandBytes.AddRange(lengthbyte.ToArray());//加入长度
commandBytes.AddRange(baseBytes);//加入协议内容
socket.Send(commandBytes.ToArray());//发送命令
}
#endregion
}
}
5、测试通讯库-读取数据
这里还是以前面的存储区数据进行测试:
读取CIO区0.0开始的6个bool数据
读取D区100开始的4个数据,ushort类型
读取H区100开始的4个short类型的数据
读取W区100开始的5个float浮点字
读取W区104开始的2个float数据
1)连接PLC
namespace Omron.Communimcation.Test
{
internal class Program
{
static void Main(string[] args)
{
#region 通讯库
FinsTcpLibTest();
Console.WriteLine("执行完成!");
#endregion
Console.ReadKey();
}
private static void FinsTcpLibTest()
{
FinsTcp finsTcp = new FinsTcp("192.168.1.4", 7788, (byte)10, (byte)04);// 创建连接
var result = finsTcp.Connect();// 开始连接PLC
if (!result.IsSuccessed)
{
Console.WriteLine(result.Message);
return;
}
}
}
}
通讯的TCP报文
2)读取CIO区0.0开始的6个bool数据
设置PLC内存区数据
private static void FinsTcpLibTest()
{
FinsTcp finsTcp = new FinsTcp("192.168.1.4", 7788, (byte)10, (byte)04);// 创建连接
var result = finsTcp.Connect();// 开始连接PLC
if (!result.IsSuccessed)
{
Console.WriteLine(result.Message);
return;
}
//1,读取CIO区0.0开始的6个bool数据,
var datas5 = finsTcp.Read<bool>("CIO0.0", 6);
if (!datas5.IsSuccessed)
{
Console.WriteLine(datas5.Message);
return;
}
Console.WriteLine("读取CIO区0.0开始的6个bool数据");
datas5.Datas.ForEach(dd => Console.WriteLine(dd));
}
通信报文结果
3)读取D区100开始的4个数据,ushort类型
private static void FinsTcpLibTest()
{
FinsTcp finsTcp = new FinsTcp("192.168.1.4", 7788, (byte)10, (byte)04);// 创建连接
var result = finsTcp.Connect();// 开始连接PLC
if (!result.IsSuccessed)
{
Console.WriteLine(result.Message);
return;
}
1,读取CIO区0.0开始的6个bool数据,
// var datas5 = finsTcp.Read<bool>("CIO0.0", 6);
//if (!datas5.IsSuccessed)
//{
// Console.WriteLine(datas5.Message);
// return;
//}
//Console.WriteLine("读取CIO区0.0开始的6个bool数据");
//datas5.Datas.ForEach(dd => Console.WriteLine(dd));
//2、读取D区100开始的4个数据,ushort类型,
var datas1 = finsTcp.Read<ushort>("DM100", 4);
if (!datas1.IsSuccessed)
{
Console.WriteLine(datas1.Message);
return;
}
Console.WriteLine("读取D区100开始的4个ushort类型数据");
datas1.Datas.ForEach(dd => Console.WriteLine(dd));
}
通讯报文结构
4)读取H区100开始的4个short类型的数据
private static void FinsTcpLibTest()
{
FinsTcp finsTcp = new FinsTcp("192.168.1.4", 7788, (byte)10, (byte)04);// 创建连接
var result = finsTcp.Connect();// 开始连接PLC
if (!result.IsSuccessed)
{
Console.WriteLine(result.Message);
return;
}
1,读取CIO区0.0开始的6个bool数据,
// var datas5 = finsTcp.Read<bool>("CIO0.0", 6);
//if (!datas5.IsSuccessed)
//{
// Console.WriteLine(datas5.Message);
// return;
//}
//Console.WriteLine("读取CIO区0.0开始的6个bool数据");
//datas5.Datas.ForEach(dd => Console.WriteLine(dd));
2、读取D区100开始的4个数据,ushort类型,
// var datas1 = finsTcp.Read<ushort>("DM100", 4);
//if (!datas1.IsSuccessed)
//{
// Console.WriteLine(datas1.Message);
// return;
//}
//Console.WriteLine("读取D区100开始的4个ushort类型数据");
//datas1.Datas.ForEach(dd => Console.WriteLine(dd));
//3、 读取H区100开始的4个short类型的数据
var datas2 = finsTcp.Read<short>("H100", 4);
if (!datas2.IsSuccessed)
{
Console.WriteLine(datas2.Message);
return;
}
Console.WriteLine("读取H区100开始的4个short类型数据");
datas2.Datas.ForEach(dd => Console.WriteLine(dd));
}
可以看到,通讯库对负数的处理,报文返回的数据需要对short类型的负数进行处理,因为short是有符号的10进制数,包括正负整数,如908,-85
5)读取W区100开始的5个float浮点字
private static void FinsTcpLibTest()
{
FinsTcp finsTcp = new FinsTcp("192.168.1.4", 7788, (byte)10, (byte)04);// 创建连接
var result = finsTcp.Connect();// 开始连接PLC
if (!result.IsSuccessed)
{
Console.WriteLine(result.Message);
return;
}
1,读取CIO区0.0开始的6个bool数据,
// var datas5 = finsTcp.Read<bool>("CIO0.0", 6);
//if (!datas5.IsSuccessed)
//{
// Console.WriteLine(datas5.Message);
// return;
//}
//Console.WriteLine("读取CIO区0.0开始的6个bool数据");
//datas5.Datas.ForEach(dd => Console.WriteLine(dd));
2、读取D区100开始的4个数据,ushort类型,
// var datas1 = finsTcp.Read<ushort>("DM100", 4);
//if (!datas1.IsSuccessed)
//{
// Console.WriteLine(datas1.Message);
// return;
//}
//Console.WriteLine("读取D区100开始的4个ushort类型数据");
//datas1.Datas.ForEach(dd => Console.WriteLine(dd));
3、 读取H区100开始的4个short类型的数据
// var datas2 = finsTcp.Read<short>("H100", 4);
//if (!datas2.IsSuccessed)
//{
// Console.WriteLine(datas2.Message);
// return;
//}
//Console.WriteLine("读取H区100开始的4个short类型数据");
//datas2.Datas.ForEach(dd => Console.WriteLine(dd));
//4、读取W区100开始的5个float浮点字,包括正负整数,如223,-987和正负小数,如2.34,-87.65
var datas3 = finsTcp.Read<float>("W100", 5);
if (!datas3.IsSuccessed)
{
Console.WriteLine(datas3.Message);
return;
}
Console.WriteLine("读取W区100开始的5个float类型数据");
datas3.Datas.ForEach(dd => Console.WriteLine(dd));
}
fins报文
6)读取W区104开始的2个float数据
private static void FinsTcpLibTest()
{
FinsTcp finsTcp = new FinsTcp("192.168.1.4", 7788, (byte)10, (byte)04);// 创建连接
var result = finsTcp.Connect();// 开始连接PLC
if (!result.IsSuccessed)
{
Console.WriteLine(result.Message);
return;
}
1,读取CIO区0.0开始的6个bool数据,
// var datas5 = finsTcp.Read<bool>("CIO0.0", 6);
//if (!datas5.IsSuccessed)
//{
// Console.WriteLine(datas5.Message);
// return;
//}
//Console.WriteLine("读取CIO区0.0开始的6个bool数据");
//datas5.Datas.ForEach(dd => Console.WriteLine(dd));
2、读取D区100开始的4个数据,ushort类型,
// var datas1 = finsTcp.Read<ushort>("DM100", 4);
//if (!datas1.IsSuccessed)
//{
// Console.WriteLine(datas1.Message);
// return;
//}
//Console.WriteLine("读取D区100开始的4个ushort类型数据");
//datas1.Datas.ForEach(dd => Console.WriteLine(dd));
3、 读取H区100开始的4个short类型的数据
// var datas2 = finsTcp.Read<short>("H100", 4);
//if (!datas2.IsSuccessed)
//{
// Console.WriteLine(datas2.Message);
// return;
//}
//Console.WriteLine("读取H区100开始的4个short类型数据");
//datas2.Datas.ForEach(dd => Console.WriteLine(dd));
4、读取W区100开始的5个float浮点字,包括正负整数,如223,-987和正负小数,如2.34,-87.65
// var datas3 = finsTcp.Read<float>("W100", 5);
//if (!datas3.IsSuccessed)
//{
// Console.WriteLine(datas3.Message);
// return;
//}
//Console.WriteLine("读取W区100开始的5个float类型数据");
//datas3.Datas.ForEach(dd => Console.WriteLine(dd));
//4,读取W区104开始的2个float数据
var datas4 = finsTcp.Read<float>("W104", 2);
if (!datas4.IsSuccessed)
{
Console.WriteLine(datas4.Message);
return;
}
Console.WriteLine("读取W区104开始的2个float类型数据");
datas4.Datas.ForEach(dd => Console.WriteLine(dd));
}
通讯报文
可以对照前面讲的C#上位机与欧姆龙PLC的通信05---- HostLink协议(C-Mode版)
C#上位机与欧姆龙PLC的通信06---- HostLink协议(FINS版)
串口报文和TCP报文,熟悉每个报文的组成部分,Hostlink通讯协议有两种模式:C-mode和FINS
C-mode报文在串口上通信的,FINS报文在网络上通信的。
6、测试通讯库-写入数据
这里测试4种数据的写入。
写入CIO区1.0开始的6个bool数据
写入D区30开始的4个数据,ushort类型
写入H区30开始的4个short类型的数据
写入W区30开始的5个float浮点字
1)写入CIO区1.0开始的6个bool数据true, true, false, , false,true , true
写入成功,看PLC内存数据
通讯报文
2)写入D区30开始的4个数据,ushort类型
写入成功
3)写入H区30开始的4个short类型的数据
通讯报文
4)写入W区30开始的5个float浮点字
可以对照前面讲的C#上位机与欧姆龙PLC的通信05---- HostLink协议(C-Mode版)
C#上位机与欧姆龙PLC的通信06---- HostLink协议(FINS版)
串口报文和TCP报文,熟悉每个报文的组成部分,Hostlink通讯协议有两种模式:C-mode和FINS
C-mode报文在串口上通信的,FINS报文在网络上通信的。
7、小结
自己写的通讯库很强大,能读取写入单个或多个数据值,可以是多种数据类型,可以是多个存储区的操作,通讯库最后就是一个dll文件,项目中直接引用就可以啦。