C#上位机与欧姆龙PLC的通信08----开发自己的通讯库读写数据
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);//异步连接PLCsocket.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,// Length0x00,0x00,0x00,0x0C,// Command0x00,0x00,0x00,0x00,// Error code0x00,0x00,0x00,0x00,// Client node addr0x00,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("连接请求响应报文无效");}// 确定没有ErrorCodevar 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, // ICF0x80,// Rev 0x00,// GCT0x02,// 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个字节是否是FINSfor (int i = 0; i < 4; i++){if (respHeader[i] != finsTcpHeader[i]){throw new Exception("响应报文无效");}}//2、获取剩余报文长度 00 00 00 18byte[] 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 ResponseCodevar 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();// 开始连接PLCif (!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();// 开始连接PLCif (!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();// 开始连接PLCif (!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();// 开始连接PLCif (!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();// 开始连接PLCif (!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.65var 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();// 开始连接PLCif (!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文件,项目中直接引用就可以啦。


相关文章:
C#上位机与欧姆龙PLC的通信08----开发自己的通讯库读写数据
1、介绍 前面已经完成了7项工作: C#上位机与欧姆龙PLC的通信01----项目背景-CSDN博客 C#上位机与欧姆龙PLC的通信02----搭建仿真环境-CSDN博客 C#上位机与欧姆龙PLC的通信03----创建项目工程-CSDN博客 C#上位机与欧姆龙PLC的通信04---- 欧姆龙plc的存储区 C#上…...
【Redis技术专区】「原理分析」探讨Redis6.0为何需要启用多线程
探讨Redis 6.0为何需要启用多线程 背景介绍开启多线程多线程的CPU核心配置IO多线程模式单线程处理方式多线程处理方式 为什么要开启多线程?充分利用多核CPU提高网络I/O效率响应现代应用需求 多线程实现启用多线程 最后总结 背景介绍 在Redis 6.0版本中,…...
simulink代码生成(六)——多级中断的配置
假如系统中存在多个中断,需要合理的配置中断的优先级与中断向量表;在代码生成中,要与中断向量表对应;中断相关的知识参照博客: DSP28335学习——中断向量表的初始化_中断向量表什么时候初始化-CSDN博客 F28335中断系…...
【Minikube Prometheus】基于Prometheus Grafana监控由Minikube创建的K8S集群
文章目录 1. 系统信息参数说明2. Docker安装3. minikube安装4. kubectl安装5. Helm安装6. 启动Kubernetes集群v1.28.37. 使用helm安装Prometheus8. 使用helm安装Grafana9. Grafana的Dashboard设定10. 设定Prometheus数据源11. 导入Kubernetes Dashboard12. 实验过程中的常见问题…...
无需翻墙|Stable Diffusion WebUI 安装|AI绘画
前言 最近终于有机会从围墙里往外看,了解到外面的世界已经有了天翻地覆的变化,感叹万千,笔者在本地mac,windows,linux,docker部署了不下20遍后,整理出来的linux极简避坑安装方案,供…...
在FC中手工创建虚拟机模板
1、Linux去除个性化信息 (1)编辑网卡配置文件,只保留以下内容(以RHEL 7为例) (2)清除主机密钥信息(开机会自动生成) (3)清除Machine IDÿ…...
OpenSSL provider
提供者 标准提供者默认提供者传统提供者FIPS 提供者基本提供者空提供者加载提供者 标准提供者 提供者是算法实现的容器。每当通过高级别 API 使用加密算法时,都会选择一个提供者。实际上是由该提供者实现执行所需的工作。OpenSSL 自带了五个提供者。在未来&#…...
pandas处理双周数据
处理文件题头格式 部门名称 年度名称 季节名称 商品名称 商品代码 品牌名称 品类名称 颜色名称 商店名称 0M 1L 1XL 27 28 29 2XL 30 31 32 33 3XL 4XL 5XL 6XL S 均1.导入包 导入源 pip install openpyxl -i https://pypi.doubanio.com/simple pip install pandas -i https…...
2023结婚成家,2024借势起飞
您好,我是码农飞哥(wei158556),感谢您阅读本文,欢迎一键三连哦。 💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精…...
linux SHELL语句
shell编程 shell编程 一、初识shell 程序 语言 编程语言 自然语言 汉语 英语 计算机语言 c语言cjava php python go shell 编译型语言 c c java解释型语言 php python bash (不能闭源,开发难度低) 编译型语言:运行编译型语言是相对于解释型语言存在的ÿ…...
音频修复和增强软件:iZotope RX 10 (Win/Mac)中文汉化版
iZotope RX 是一款专业的音频修复和增强软件,一直是电影和电视节目中使用的行业标准音频修复工具,iZotope能够帮助用户对音频进行制作、后期合成处理、混音以及对损坏的音频进行修复,再解锁更多功能之后还能够对电影、游戏、电视之中的音频进…...
复试 || 就业day03(2023.12.29)算法篇
文章目录 前言同构字符串存在重复元素有效的字母异位词丢失的数字单词规律 前言 💫你好,我是辰chen,本文旨在准备考研复试或就业 💫文章题目大多来自于 leetcode,当然也可能来自洛谷或其他刷题平台 💫欢迎大…...
处理urllib.request.urlopen报错UnicodeEncodeError:‘ascii‘
参考:[Python3填坑之旅]一urllib模块网页爬虫访问中文网址出错 目录 一、报错内容 二、报错截图 三、解决方法 四、实例代码 五、运行截图 六、其他UnicodeEncodeError: ascii codec 问题 一、报错内容 UnicodeEncodeError: ascii codec cant encode charac…...
数据结构模拟实现LinkedList双向不循环链表
目录 一、双向不循环链表的概念 二、链表的接口 三、链表的方法实现 (1)display方法 (2)size方法 (3)contains方法 (4)addFirst方法 (5)addLast方法 …...
性能优化-如何提高cache命中率
本文主要介绍性能优化领域常见的cache的命中率问题,旨在全面的介绍提高cache命中率的方法,以供大家编写出性能友好的代码,并且可以应对性能优化领域的面试问题。 🎬个人简介:一个全栈工程师的升级之路! &am…...
分布式【4. 什么是 CAP?】
什么是 CAP? C 代表 Consistency,一致性,是指所有节点在同一时刻的数据是相同的,即更新操作执行结束并响应用户完成后,所有节点存储的数据会保持相同。 A 代表 Availability,可用性,是指系统提…...
<软考高项备考>《论文专题 - 39采购管理(3) 》
3 过程2-实施采购 3.1 问题 4W1H过程做什么获取卖方应答、选择卖方并授予合同的过程作用:选定合格卖方并签署关于货物或服务交付的法律协议。本过程的最后成果是签订的协议,包括正式合同。为什么做实际进行采购谁来做组织中的职能部门或项目经理什么时…...
Java在SpringCloud中自定义Gateway负载均衡策略
Java在SpringCloud中自定义Gateway负载均衡策略 一、前言 spring-cloud-starter-netflix-ribbon已经不再更新了,最新版本是2.2.10.RELEASE,最后更新时间是2021年11月18日,详细信息可以看maven官方仓库:org.springframework.clou…...
前端 js 基础(1)
js 结果输出 (点击按钮修改文字 ) <!DOCTYPE html> <html> <head></head><body><h2>Head 中的 JavaScript</h2><p id"demo">一个段落。</p><button type"button" onclic…...
Android : 使用GestureOverlayView进行手势识别—简单应用
示例图: GestureOverlayView介绍: GestureOverlayView 是 Android 开发中用于识别和显示手势的视图组件。它允许用户在屏幕上绘制手势,并且应用程序可以检测和响应这些手势。以下是关于 GestureOverlayView 的主要特点: 手势识别…...
IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
Python|GIF 解析与构建(5):手搓截屏和帧率控制
目录 Python|GIF 解析与构建(5):手搓截屏和帧率控制 一、引言 二、技术实现:手搓截屏模块 2.1 核心原理 2.2 代码解析:ScreenshotData类 2.2.1 截图函数:capture_screen 三、技术实现&…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)
前言: 双亲委派机制对于面试这块来说非常重要,在实际开发中也是经常遇见需要打破双亲委派的需求,今天我们一起来探索一下什么是双亲委派机制,在此之前我们先介绍一下类的加载器。 目录 编辑 前言: 类加载器 1. …...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
