Untiy Modbus 西门子 S7-1200 基础通信

在本篇博客中,我们将探讨如何Modbus 协议来使用Unity和西门子S7-1200通信。

Modbus

Modbus是什么

在这里插入图片描述

Modbus是一种串行通信协议,是Modicon公司(现在的施耐德电气)于1979年为使用可编程逻辑控制器
(PLC)通信而发表。
Modbus已经成为工业领域通信协议事实上的业界标准,并且现在是工业电子设备之间常用的连接方式。

Modbus比其他通信协议使用的更广泛的主要原因有:
	1. 公开发表并且无著作权要求
	2. 易于部署和维护
	3. 对供应商来说,修改移动原生的比特或字节没有很多限制
	
Modbus允许多个 (大约240) 设备连接在同一个网络上进行通信。
举个例子,一个由测量温度和湿度的设备并且将结果发送给计算机。
在数据采集与监视控制系统(SCADA)中,Modbus通常用来连接监控计算机和远程终端控制系统(RTU)。

Modbus 协议版本

在这里插入图片描述

Modbus协议目前存在用于串口、以太网以及其他支持互联网协议的网络的版本。

大多数Modbus设备通信通过串EIA-485物理层进行。

对于串行连接,存在两个变种,它们在数值数据表示不同和协议细节上略有不同。
Modbus RTU是一种紧凑的,采用二进制表示数据的方式,Modbus ASCII是一种人类可读的,冗长的表示方式
这两个变种都使用串行通信(serial communication)方式。
RTU格式后续的命令/数据带有循环冗余校验的校验和,而ASCII格式采用纵向冗余校验的校验和。
被配置为RTU变种的节点不会和设置为ASCII变种的节点通信,反之亦然。

对于通过TCP/IP(例如以太网)的连接,存在多个Modbus/TCP变种,这种方式不需要校验和计算。

对于所有的这三种通信协议在数据模型和功能调用上都是相同的,只有封装方式是不同的。

Modbus有一个扩展版本Modbus Plus(Modbus+或者MB+),不过此协议是Modicon专有的,和Modbus不同。
它需要一个专门的协处理器来处理类似HDLC的高速令牌旋转。
它使用1Mbit/s的双绞线,并且每个节点都有转换隔离设备,
是一种采用转换/边缘触发而不是电压/水平触发的设备。
连接Modbus Plus到计算机需要特别的接口,通常是支持ISASA85),PCI或者PCMCIA总线的板卡。

Modbus 通信和设备

在这里插入图片描述

Modbus协议是一个master/slave架构的协议。
有一个节点是master节点,其他使用Modbus协议参与通信的节点是slave节点。
每一个slave设备都有一个唯一的地址。
在串行和MB+网络中,只有被指定为主节点的节点可以启动一个命令
(在以太网上,任何一个设备都能发送一个Modbus命令,但是通常也只有一个主节点设备启动指令)。

一个ModBus命令包含了打算执行的设备的Modbus地址。
所有设备都会收到命令,但只有指定位置的设备会执行及回应指令
(地址0例外,指定地址0的指令是广播指令,所有收到指令的设备都会执行,不过不回应指令)。
所有的Modbus命令包含了检查码,以确定到达的命令没有被破坏。
基本的ModBus命令能指挥一个RTU改变它的寄存器的某个值,控制或者读取一个I/O端口,
以及指挥设备回送一个或者多个其寄存器中的数据。

有许多modems和网关支持Modbus协议,因为Modbus协议很简单而且容易复制。
它们当中一些为这个协议特别设计的。有使用有线、无线通信甚至短消息和GPRS的不同实现。
不过设计者需要克服一些包括高延迟和时序的问题。

Modbus 如何实现

在这里插入图片描述

几乎所有的实现都是官方标准的某种变体。不同的供应商设备之间可能无法正确的通信。
一些主要的变化有:

数据类型
	1. IEEE标准的浮点数
	2. 32位整型数
	3. 8位数据
	4. 混合数据类型
	5. 整数中的位域
	6. multipliers to change data to/from integer. 10, 100, 1000, 256 ...

协议扩展
	1. 16位的从站地址
	2. 32位的数据大小(1个地址 = 返回32位数据)
	3. 字交换数据

Modbus 使用限制

在这里插入图片描述

	1. Modbus是在1970年末为可编程逻辑控制器通信开发的,
       这些有限的数据类型在那个时代是可以被PLC理解的,大型二进制对象数据是不支持的。
       
	2. 对节点而言,没有一个标准的方法找到数据对象的描述信息,
       举个例子,确定一个寄存器数据是否表示一个介于30-175度之间的温度。
       
	3. 由于Modbus是一个主/从协议,没有办法要求设备“报告异常”(构建在以太网的TCP/IP协议之上,
       被称为open-mbus除外)- 主节点必须循环的询问每个节点设备,并查找数据中的变化。
       在带宽可能比较宝贵的应用中,这种方式在应用中消耗带宽和网络时间,例如在低速率的无线链路上。
       
	4. Modbus在一个数据链路上只能处理247个地址,这种情况限制了可以连接到主控站点的设备数量
      (再一次指出以太网TCP/IP除外)
      
	5. Modbus传输在远端通讯设备之间缓冲数据的方式进行,有对通信一定是连续的限制,
       避免了传输中的缓冲区漏洞的问题
       
	7. Modbus协议针对未经授权的命令或截取数据没有安全性。

Modbus 通信协议学理上的弱点分析

在这里插入图片描述

Modbus 当初设计的时候,主要着重两点,
分别是简单-易于各项系统或是设备上的实现与各项系统所需求的资源较低,以利降低成本,
另一则是通用-便于集成各式各样设备或是平台,同样地,这样也带来一些缺点,
从信息安全的角度上去解析可发现具有三个主要的弱点:

	1. 没有保护机制-指令明码传输
	   透过第三方数据包侧录软件就可以截取数据包内容,无须解密。
	2. 没有认证机制-符合规范就执行 
	   只要符合Modbus规范之数据包传输便可透过第三方控制软件监控接受端设备
	3. 有可能有实现上的问题
	   针对未定义参考位置输入指令值,可能造成接收端传输异常进而瘫痪接收端设备。

Unity

NModbus4.dll 和 EasyModbus.dll 资源包: 包含脚本

Unity ModbusTCP

Unity ModbusTCP 单个线圈读取方法

        /// <summary>
        /// 构建读取线圈寄存器的请求  Int
        /// </summary>
        /// <param 设备 ID="_UnitId"></param>
        /// 范围为 0 到 255
        /// <param 起始地址="_StartAddress"></param>
        /// 范围为 0 到 65535
        /// <param 寄存器数量="_CoilCount"></param>
        /// 范围为 1 到 2000
        /// <returns></returns>
        public static byte[] BuildReadCoilsRequest(byte _UnitId, ushort _StartAddress, ushort _CoilCount)
        {
            byte[] _Request = new byte[12];

            // Modbus TCP报文头部
            _Request[0] = 0; // 事务标识符高字节
            _Request[1] = 0; // 事务标识符低字节
            _Request[2] = 0; // 协议标识符高字节
            _Request[3] = 0; // 协议标识符低字节
            _Request[4] = 0; // 报文长度高字节
            _Request[5] = 6; // 报文长度低字节
            _Request[6] = _UnitId; // 设备ID
            _Request[7] = 1; // 功能码(读取线圈寄存器)

            // 起始地址
            byte[] _StartAddressBytes = BitConverter.GetBytes(_StartAddress);
            _Request[8] = _StartAddressBytes[1]; // 起始地址高字节
            _Request[9] = _StartAddressBytes[0]; // 起始地址低字节

            // 寄存器数量
            byte[] _CoilCountBytes = BitConverter.GetBytes(_CoilCount);
            _Request[10] = _CoilCountBytes[1]; // 寄存器数量高字节
            _Request[11] = _CoilCountBytes[0]; // 寄存器数量低字节

            return _Request;
        }

Unity ModbusTCP 单个线圈写入方法 Int

        /// <summary>
        /// 构建写单个寄存器的请求  Int
        /// </summary>
        /// <param 设备ID ="_UnitId"></param>
        /// 范围为 0 到 255
        /// <param 寄存器地址 ="_RegisterAddress"></param>
        /// 范围为 0 到 65535
        /// <param 寄存器值 ="_Value"></param>
        /// 范围为 0 到 65535
        /// <returns></returns>
        public static byte[] BuildWriteSingleRegisterRequest(byte _UnitId, ushort _RegisterAddress, ushort _Value)
        {
            byte[] _Request = new byte[12];

            // Modbus TCP报文头部
            _Request[0] = 0; // 事务标识符高字节
            _Request[1] = 0; // 事务标识符低字节
            _Request[2] = 0; // 协议标识符高字节
            _Request[3] = 0; // 协议标识符低字节
            _Request[4] = 0; // 报文长度高字节
            _Request[5] = 6; // 报文长度低字节
            _Request[6] = _UnitId; // 设备ID
            _Request[7] = 6; // 功能码(写单个寄存器)

            // 寄存器地址
            byte[] _RegisterAddressBytes = BitConverter.GetBytes(_RegisterAddress);
            _Request[8] = _RegisterAddressBytes[1]; // 寄存器地址高字节
            _Request[9] = _RegisterAddressBytes[0]; // 寄存器地址低字节

            // 寄存器值
            byte[] _ValueBytes = BitConverter.GetBytes(_Value);
            _Request[10] = _ValueBytes[1]; // 寄存器值高字节
            _Request[11] = _ValueBytes[0]; // 寄存器值低字节

            return _Request;
        }

Unity ModbusTCP 单个线圈写入方法 Bool

        /// <summary>
        /// 构建写单个线圈的请求 Bool
        /// </summary>
        /// <param 设备ID="_UnitId"></param>
        /// 范围为 0 到 255
        /// <param 线圈地址="_CoilAddress"></param>
        /// 范围为 0 到 65535
        /// <param 线圈状态="_State"></param>
        /// true表示置位(ON),false表示复位(OFF)
        /// <returns></returns>
        public static byte[] BuildWriteSingleCoilRequest(byte _UnitId, ushort _CoilAddress, bool _State)
        {
            byte[] _Request = new byte[12];

            // Modbus TCP报文头部
            _Request[0] = 0; // 事务标识符高字节
            _Request[1] = 0; // 事务标识符低字节
            _Request[2] = 0; // 协议标识符高字节
            _Request[3] = 0; // 协议标识符低字节
            _Request[4] = 0; // 报文长度高字节
            _Request[5] = 6; // 报文长度低字节
            _Request[6] = _UnitId; // 设备ID
            _Request[7] = 5; // 功能码(写单个线圈)

            // 线圈地址
            byte[] _CoilAddressBytes = BitConverter.GetBytes(_CoilAddress);
            _Request[8] = _CoilAddressBytes[1]; // 线圈地址高字节
            _Request[9] = _CoilAddressBytes[0]; // 线圈地址低字节

            // 线圈状态
            if (_State)
            {
                _Request[10] = 0xFF;
                _Request[11] = 0x00;
            }
            else
            {
                _Request[10] = 0x00;
                _Request[11] = 0x00;
            }

            return _Request;
        }

Unity NModbus4

Unity NModbus4 读取 Int 方法

异步 方法
    /// <summary>
    /// 异步读取保持寄存器的值  Int
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <returns></returns>
    private async Task<int> ReadIntAsync(ushort _RegisterAddress)
    {
        try
        {
            // 异步读取寄存器值
            ushort[] _Registers = await _ModbusMaster.ReadHoldingRegistersAsync(1, _RegisterAddress, 1);

            // 将ushort值转换为int
            int _Value = _Registers[0];
            return _Value;
        }
        catch (Exception ex)
        {
            Debug.LogError($"读取寄存器失败: {ex.Message}");
            // 返回默认值或抛出异常取决于您的错误处理策略
            return default(int);
        }
    }
主线程 方法
    /// <summary>
    /// 读取 Int 方法
    /// </summary>
    /// <param 寄存器地址="_RegisterAddress"></param>
    /// <returns></returns>
    public int ReadInt(ushort _RegisterAddress)
    {
        // 读取两个寄存器的值
        ushort[] _Registers = _ModbusMaster.ReadHoldingRegisters(1, _RegisterAddress, 1);

        // 将两个ushort值组合成一个int值
        //int _Value = (_Registers[0] << 16) | _Registers[1];
        int _Value = _Registers[0];

        return _Value;
    }

Unity NModbus4 读取 Float 方法

异步 方法
    /// <summary>
    /// 异步读取保持寄存器的值  float
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <returns></returns>
    private async Task<float> ReadFloatAsync(ushort _RegisterAddress)
    {
        try
        {
            // 异步读取寄存器值
            ushort[] _Registers = await _ModbusMaster.ReadHoldingRegistersAsync(1, _RegisterAddress, 2);

            // 将寄存器值转换为浮点数
            byte[] _Bytes = new byte[4];
            _Bytes[0] = (byte)(_Registers[1] & 0xFF); // LSB of second register
            _Bytes[1] = (byte)(_Registers[1] >> 8);   // MSB of second register
            _Bytes[2] = (byte)(_Registers[0] & 0xFF); // LSB of first register
            _Bytes[3] = (byte)(_Registers[0] >> 8);   // MSB of first register

            return BitConverter.ToSingle(_Bytes, 0);
        }
        catch (Exception ex)
        {
            Debug.LogError($"读取寄存器失败: {ex.Message}");
            // 返回默认值或抛出异常取决于您的错误处理策略
            return default(float);
        }
    }
主线程 方法
    /// <summary>
    /// 读取浮点数 方法
    /// </summary>
    /// <param 寄存器地址 ="_StartAddress"></param>
    /// <returns></returns>
    public float ReadFloat(ushort _StartAddress)
    {
        // 读取两个保持寄存器
        ushort[] _Registers = _ModbusMaster.ReadHoldingRegisters(1, _StartAddress, 2);

        // 将寄存器值转换为浮点数
        byte[] _Bytes = new byte[4];
        _Bytes[0] = (byte)(_Registers[1] & 0xFF); // LSB of second register
        _Bytes[1] = (byte)(_Registers[1] >> 8);   // MSB of second register
        _Bytes[2] = (byte)(_Registers[0] & 0xFF); // LSB of first register
        _Bytes[3] = (byte)(_Registers[0] >> 8);   // MSB of first register

        return BitConverter.ToSingle(_Bytes, 0);
    }

Unity NModbus4 写入 Int 方法

异步 方法
    /// <summary>
    /// 异步写入保持寄存器的值  Int
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <param 写入值="_Value"></param>
    /// <returns></returns>
    private async Task WriteIntAsync(ushort _RegisterAddress, int _Value)
    {
        try
        {
            // 异步写入寄存器
            await _ModbusMaster.WriteSingleRegisterAsync(1, _RegisterAddress, (ushort)_Value);
        }
        catch (Exception ex)
        {
            Debug.LogError($"写入寄存器失败: {ex.Message}");
        }
    }
主线程 方法
    /// <summary>
    /// 写入 Int 方法
    /// </summary>
    /// <param 寄存器地址="_RegisterAddress"></param>
    /// <param 写入值="_Value"></param>
    public void WriteInt(ushort _RegisterAddress, int _Value)
    {
         将整数值拆分成两个ushort值(高16位和低16位)
        //ushort[] _Registers = new ushort[2];
        //_Registers[0] = (ushort)(_Value >> 16);
        //_Registers[1] = (ushort)(_Value & 0xFFFF);

         写入寄存器
        //_ModbusMaster.WriteMultipleRegisters(1, _RegisterAddress, _Registers);

        try
        {
            // 写入单个寄存器
            _ModbusMaster.WriteSingleRegister(1, _RegisterAddress, (ushort)_Value);
        }
        catch (Exception ex)
        {

            Debug.LogError($"写入值失败 {_Value} 寄存器地址 {_RegisterAddress}: {ex.Message}");
            GameObject.Find("读取信息").GetComponent<Text>().text += $"\n写入值失败 {_Value} 寄存器地址 {_RegisterAddress}: {ex.Message}";
        }



    }

Unity NModbus4 写入 Float 方法

异步 方法
    /// <summary>
    /// 异步写入保持寄存器的值  float
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <param 写入值="_\Value"></param>
    /// <returns></returns>
    private async Task WriteFloatAsync(ushort _RegisterAddress, float _Value)
    {
        try
        {
            // 将浮点数值转换为字节数组
            byte[] _Bytes = BitConverter.GetBytes(_Value);

            // 将字节数组拆分为两个寄存器值
            ushort[] _Registers = new ushort[2];
            _Registers[0] = BitConverter.ToUInt16(_Bytes, 2);
            _Registers[1] = BitConverter.ToUInt16(_Bytes, 0);

            // 异步写入两个保持寄存器
            await _ModbusMaster.WriteMultipleRegistersAsync(1, _RegisterAddress, _Registers);
        }
        catch (Exception ex)
        {
            Debug.LogError($"写入寄存器失败: {ex.Message}");
        }
    }
主线程 方法
    /// <summary>
    /// 写入浮点数 方法
    /// </summary>
    /// <param 寄存器地址="_StartAddress"></param>
    /// <param 写入值="_Value"></param>
    public void WriteFloat(ushort _StartAddress, float _Value)
    {
        // 将浮点数值转换为字节数组
        byte[] _Bytes = BitConverter.GetBytes(_Value);

        // 将字节数组拆分为两个寄存器值
        ushort[] _Registers = new ushort[2];
        _Registers[0] = BitConverter.ToUInt16(_Bytes, 2);
        _Registers[1] = BitConverter.ToUInt16(_Bytes, 0);


        try
        {

            // 写入两个保持寄存器
            _ModbusMaster.WriteMultipleRegisters(01, _StartAddress, _Registers);
        }
        catch (Exception ex)
        {

            Debug.LogError($"写入值失败 {_Registers} 寄存器地址 {_StartAddress}: {ex.Message}");
            GameObject.Find("读取信息").GetComponent<Text>().text += $"\n写入值失败 {_Value} 寄存器地址 {_StartAddress}: {ex.Message}";
        }
    }

Unity EasyModbus

Unity EasyModbus 读取 Int 方法

   /// <summary>
    /// 读取Int
    /// </summary>
    /// <param name="startAddress"></param>
    private void ReadHoldingRegistersInt(int startAddress)
    {
        //while (true)
        {
            try
            {
                // 读取从地址40001开始的寄存器,读取2个寄存器(每个寄存器2个字节,4字节表示一个整数)
                int[] registers = _ModbusClient.ReadHoldingRegisters(startAddress, 2);

                // 将读取到的寄存器值转换为整数
                int intValue = ModbusClient.ConvertRegistersToInt(registers);


                // 处理读取到的数据
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n读寄存器 Int: " + intValue;
            }
            catch (Exception e)
            {
                Debug.LogError("读取寄存器失败: " + e.Message);

                GameObject.Find("读取信息").GetComponent<Text>().text += "读取寄存器失败: " + e.Message;
            }

            // 每秒读取一次
            //yield return new WaitForSeconds(1);
        }
           
    }

Unity EasyModbus 读取 Float 方法

    /// <summary>
    /// 读取 Float
    /// </summary>
    /// <param name="startAddress"></param>
    private void ReadHoldingRegistersFloat(int startAddress)
    {
        //while (true)
        {
            try
            {
                // 读取从地址40001开始的寄存器,读取2个寄存器(每个寄存器2个字节,4字节表示一个整数)
                int[] registers = _ModbusClient.ReadHoldingRegisters(startAddress, 2);

                // 将读取到的寄存器值转换为浮点数
                float floatValue = ModbusClient.ConvertRegistersToFloat(registers);

                // 处理读取到的数据
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n读寄存器 Float: " + floatValue;
            }
            catch (Exception e)
            {

                GameObject.Find("读取信息").GetComponent<Text>().text += "读取寄存器失败: " + e.Message;
            }

            // 每秒读取一次
            //yield return new WaitForSeconds(1);
        }
     
    }

Unity EasyModbus 写入 Int 方法

    /// <summary>
    /// 写入整数
    /// </summary>
    /// <param name="address"></param>
    /// <param name="value"></param>
    private void WriteSingleRegister(int address, int value)
    {
        try
        {
            // 将浮点数值转换为寄存器值
            int[] registers = ModbusClient.ConvertIntToRegisters(value);

            _ModbusClient.WriteMultipleRegisters(address, registers);

       

            GameObject.Find("读取信息").GetComponent<Text>().text += "\n写地址寄存器 Int:" + address + " 写入值:" + value;
        }
        catch (Exception e)
        {

            GameObject.Find("读取信息").GetComponent<Text>().text += "写寄存器失败: " + e.Message;
        }
    }

Unity EasyModbus 写入 Float 方法

    /// <summary>
    /// 写入浮点数
    /// </summary>
    /// <param name="address"></param>
    /// <param name="value"></param>
    private void WriteSingleRegister(int address, float value)
    {
        try
        {
            // 将浮点数值转换为寄存器值
            int[] registers = ModbusClient.ConvertFloatToRegisters(value);

            _ModbusClient.WriteMultipleRegisters(address, registers);


            GameObject.Find("读取信息").GetComponent<Text>().text += "\n写地址寄存器 Float:" + address + " 写入值:" + value;
        }
        catch (Exception e)
        {
            GameObject.Find("读取信息").GetComponent<Text>().text += "写寄存器失败: " + e.Message;
        }
    }

Unity ModbusTCP 完整代码

using System;
using System.Collections;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;
using EasyModbus;
using System.Text;

/// <summary>
/// Modbus TCP 处理
/// </summary>
public class ModbusTCP_ZH : MonoBehaviour
{
    public static ModbusTCP_ZH _Instance;
    // TCP 网络
    private TcpClient _TcpClient;
    private NetworkStream _NetworkStream;
    private Thread _ReceiveThread;
    private byte[] _ReceiveBuffer = new byte[256];

    [Header("PLC IP地址")]
    public string _IpAddress = "192.168.0.100";
    [Header("Modbus TCP端口号")]
    public int _Port = 502;
    [Header("PLC设备的Unit ID")]
    public int _UnitId = 1;

    [Header("线圈寄存器 起始地址")]
    public ushort _CoilStartAddress = 0;
    [Header("线圈寄存器 数量")]
    public ushort _CoilCount = 10;



    [Header("写入 地址")]
    public ushort _IDAA;

    [Header("写入 值")]
    public ushort _IDSSValue;





    //连接成功 布尔
    [HideInInspector]
    public bool _ConnectSuccess = false;

    private void Awake()
    {
        _Instance = this;
    }

    private void Start()
    {
        //ConnectToPLC();

    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.T))
        {
            //ReadCoilData(1, 40001, 1);
            //ReadCoilData(1,0, 2);

            ReadRegisters(1, 1);

        }

        if (Input.GetKeyDown(KeyCode.Y))
        {
            //ControlDevice(1, 1);
            ControlDevice(_IDAA, _IDSSValue);
        }
    }


    /// <summary>
    /// PLC 连接
    /// </summary>
    private void ConnectToPLC()
    {
        try
        {
            _TcpClient = new TcpClient();
            _TcpClient.Connect(_IpAddress, _Port);
            _NetworkStream = _TcpClient.GetStream();
            _ReceiveBuffer = new byte[1024];

            // 启动接收数据的线程
            _ReceiveThread = new Thread(ReceiveData);
            _ReceiveThread.IsBackground = true;
            _ReceiveThread.Start();

            Debug.Log("PLC 连接成功。");
        }
        catch (Exception e)
        {
            Debug.LogError("PLC 连接失败: " + e.Message);
        }
    }
    /// <summary>
    /// PLC 连接
    /// </summary>
    /// <param 地址 ="_IpAddress"></param>
    /// <param 端口 ="_Port"></param>
    public bool ConnectToPLC(string _IpAddress, int _Port)
    {
        try
        {
            _TcpClient = new TcpClient();
            _TcpClient.Connect(_IpAddress, _Port);
            _NetworkStream = _TcpClient.GetStream();
            _ReceiveBuffer = new byte[1024];

            // 启动接收数据的线程
            _ReceiveThread = new Thread(ReceiveData);
            _ReceiveThread.IsBackground = true;
            _ReceiveThread.Start();

            Debug.Log("PLC 连接成功。");
            return _ConnectSuccess = true;
        }
        catch (Exception e)
        {
            Debug.LogError("PLC 连接失败: " + e.Message);
            return _ConnectSuccess = false;
        }
    }

    /// <summary>
    /// 数据接收
    /// </summary>
    private void ReceiveData()
    {
        while (_TcpClient != null && _TcpClient.Connected)
        {
            try
            {
                byte[] _ReceiveBuffer = new byte[1024];
                // 数据接收
                int _BytesRead = _NetworkStream.Read(_ReceiveBuffer, 0, _ReceiveBuffer.Length);
                if (_BytesRead > 0)
                {
                    // 处理接收到的数据
                    ProcessReceivedData(_ReceiveBuffer, _BytesRead);
                }
            }
            catch (Exception e)
            {
                Debug.LogError("接收数据 错误: " + e.Message);
                break;
            }
        }
    }

    /// <summary>
    /// 数据接收响应
    /// </summary>
    /// <param 接收数据 ="_Data"></param>
    /// <param 接收数据长度 ="_Length"></param>
    private void ProcessReceivedData(byte[] _Data, int _Length)
    {
        // 解析接收到的Modbus响应
        // 根据协议解析数据并更新 coilData 数组
        //Debug.Log("读取值: " + _Data);
        Debug.Log("读取长度: " + _Length);
        Debug.Log("读取值: " + BitConverter.ToUInt16(new byte[] { _Data[10], _Data[9] }, 0));

        string _ReceivedData = "";
        for (int i = 0; i < _Data.Length; i++)
        {
            _ReceivedData += _Data[i].ToString() + "\n";
        }
        Debug.Log(_ReceivedData);

    }

    /// <summary>
    /// Modbus 读取
    /// </summary>
    private void ReadRegisters(int _CoilStartAddress, ushort _CoilCount)
    {
        try
        {
            ModbusClient _ModbusClient = new ModbusClient("192.168.2.1", 502);

            // 读取保持寄存器的值
            int[] _Values = _ModbusClient.ReadHoldingRegisters(_CoilStartAddress, _CoilCount);

            // 处理读取的结果
            StringBuilder _Builder = new StringBuilder();
            for (int i = 0; i < _Values.Length; i++)
            {
                _Builder.AppendLine("寄存器 " + (_CoilStartAddress + i) + " 的值: " + _Values[i]);
            }

            // 输出读取的结果
            Debug.Log("读取结果:");
            Debug.Log(_Builder.ToString());
        }
        catch (Exception ex)
        {
            Debug.LogError("发生错误: " + ex.Message);
        }
    }



    /// <summary>
    ///  发送Modbus 读取 请求
    /// </summary>
    /// <param 设备ID="_UnitId"></param>
    /// <param 起始地址="_CoilStartAddress"></param>
    /// <param 读取的线圈寄存器的数量 ="_CoilCount"></param>
    private void ReadCoilData(int _UnitId, ushort _CoilStartAddress, ushort _CoilCount)
    {
        //ReadCoilData(1, 40001, 1);
        // 构造Modbus请求
        byte[] _Request = ModbusUtils.BuildReadCoilsRequest((byte)_UnitId, _CoilStartAddress, _CoilCount);

        try
        {
            // 发送Modbus请求
            _NetworkStream.Write(_Request, 0, _Request.Length);
            _NetworkStream.Flush();
        }
        catch (Exception e)
        {
            Debug.LogError("发送数据 错误: " + e.Message);
        }
    }

    /// <summary>
    ///  设备状态写入  Bool
    /// </summary>
    /// <param 线圈地址="_CoilAddress"></param>
    /// <param 状态="_State"></param>
    public void ControlDevice(ushort _CoilAddress, bool _State)
    {

        // 写入数据示例
        //ControlDevice(16, true); // 写入值 true 到地址 40016

        byte[] _Request = ModbusUtils.BuildWriteSingleCoilRequest((byte)_UnitId, _CoilAddress, _State);

        try
        {
            _NetworkStream.Write(_Request, 0, _Request.Length);
            _NetworkStream.Flush();
        }
        catch (Exception e)
        {
            Debug.LogError("发送数据 错误: " + e.Message);
        }
    }


    /// <summary>
    /// 设备状态写入 方法  Int
    /// </summary>
    /// <param 线圈地址="_RegisterAddress"></param>
    /// <param 写入值="_Value"></param>
    public void ControlDevice(ushort _RegisterAddress, ushort _Value)
    {
        // 写入数据示例
        //ControlDevice(16, 1234); // 写入值 1234 到地址 40016

        byte[] _Request = ModbusUtils.BuildWriteSingleRegisterRequest(1, _RegisterAddress, _Value);

        try
        {
            _NetworkStream.Write(_Request, 0, _Request.Length);
            _NetworkStream.Flush();

            print("写入成功:" + _RegisterAddress.ToString() + "_______" + _Value.ToString());
        }
        catch (Exception e)
        {
            Debug.LogError("发送数据错误: " + e.Message);
        }
    }


    /// <summary>
    /// Modbus协议 处理
    /// </summary>
    public static class ModbusUtils
    {
        /// <summary>
        /// 构建读取线圈寄存器的请求  Int
        /// </summary>
        /// <param 设备 ID="_UnitId"></param>
        /// 范围为 0 到 255
        /// <param 起始地址="_StartAddress"></param>
        /// 范围为 0 到 65535
        /// <param 寄存器数量="_CoilCount"></param>
        /// 范围为 1 到 2000
        /// <returns></returns>
        public static byte[] BuildReadCoilsRequest(byte _UnitId, ushort _StartAddress, ushort _CoilCount)
        {
            byte[] _Request = new byte[12];

            // Modbus TCP报文头部
            _Request[0] = 0; // 事务标识符高字节
            _Request[1] = 0; // 事务标识符低字节
            _Request[2] = 0; // 协议标识符高字节
            _Request[3] = 0; // 协议标识符低字节
            _Request[4] = 0; // 报文长度高字节
            _Request[5] = 6; // 报文长度低字节
            _Request[6] = _UnitId; // 设备ID
            _Request[7] = 1; // 功能码(读取线圈寄存器)

            // 起始地址
            byte[] _StartAddressBytes = BitConverter.GetBytes(_StartAddress);
            _Request[8] = _StartAddressBytes[1]; // 起始地址高字节
            _Request[9] = _StartAddressBytes[0]; // 起始地址低字节

            // 寄存器数量
            byte[] _CoilCountBytes = BitConverter.GetBytes(_CoilCount);
            _Request[10] = _CoilCountBytes[1]; // 寄存器数量高字节
            _Request[11] = _CoilCountBytes[0]; // 寄存器数量低字节

            return _Request;
        }


        /// <summary>
        /// 构建写单个线圈的请求 Bool
        /// </summary>
        /// <param 设备ID="_UnitId"></param>
        /// 范围为 0 到 255
        /// <param 线圈地址="_CoilAddress"></param>
        /// 范围为 0 到 65535
        /// <param 线圈状态="_State"></param>
        /// true表示置位(ON),false表示复位(OFF)
        /// <returns></returns>
        public static byte[] BuildWriteSingleCoilRequest(byte _UnitId, ushort _CoilAddress, bool _State)
        {
            byte[] _Request = new byte[12];

            // Modbus TCP报文头部
            _Request[0] = 0; // 事务标识符高字节
            _Request[1] = 0; // 事务标识符低字节
            _Request[2] = 0; // 协议标识符高字节
            _Request[3] = 0; // 协议标识符低字节
            _Request[4] = 0; // 报文长度高字节
            _Request[5] = 6; // 报文长度低字节
            _Request[6] = _UnitId; // 设备ID
            _Request[7] = 5; // 功能码(写单个线圈)

            // 线圈地址
            byte[] _CoilAddressBytes = BitConverter.GetBytes(_CoilAddress);
            _Request[8] = _CoilAddressBytes[1]; // 线圈地址高字节
            _Request[9] = _CoilAddressBytes[0]; // 线圈地址低字节

            // 线圈状态
            if (_State)
            {
                _Request[10] = 0xFF;
                _Request[11] = 0x00;
            }
            else
            {
                _Request[10] = 0x00;
                _Request[11] = 0x00;
            }

            return _Request;
        }

        /// <summary>
        /// 构建写单个寄存器的请求  Int
        /// </summary>
        /// <param 设备ID ="_UnitId"></param>
        /// 范围为 0 到 255
        /// <param 寄存器地址 ="_RegisterAddress"></param>
        /// 范围为 0 到 65535
        /// <param 寄存器值 ="_Value"></param>
        /// 范围为 0 到 65535
        /// <returns></returns>
        public static byte[] BuildWriteSingleRegisterRequest(byte _UnitId, ushort _RegisterAddress, ushort _Value)
        {
            byte[] _Request = new byte[12];

            // Modbus TCP报文头部
            _Request[0] = 0; // 事务标识符高字节
            _Request[1] = 0; // 事务标识符低字节
            _Request[2] = 0; // 协议标识符高字节
            _Request[3] = 0; // 协议标识符低字节
            _Request[4] = 0; // 报文长度高字节
            _Request[5] = 6; // 报文长度低字节
            _Request[6] = _UnitId; // 设备ID
            _Request[7] = 6; // 功能码(写单个寄存器)

            // 寄存器地址
            byte[] _RegisterAddressBytes = BitConverter.GetBytes(_RegisterAddress);
            _Request[8] = _RegisterAddressBytes[1]; // 寄存器地址高字节
            _Request[9] = _RegisterAddressBytes[0]; // 寄存器地址低字节

            // 寄存器值
            byte[] _ValueBytes = BitConverter.GetBytes(_Value);
            _Request[10] = _ValueBytes[1]; // 寄存器值高字节
            _Request[11] = _ValueBytes[0]; // 寄存器值低字节

            return _Request;
        }
    }
}

Unity NModbus4 完整代码

using Modbus.Device;
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// NModbus4 读写测试
/// </summary>
public class NModbus4_ZH : MonoBehaviour
{
    public static NModbus4_ZH _Instance;

    //数据通信
    private TcpClient _TcpClient;
    private IModbusMaster _ModbusMaster;
    private Thread _ReceiveThread;

    //数据接收字典
    private Dictionary<int, int> _IntValueDic = new Dictionary<int, int>();
    private Dictionary<int, float> _FloatValueDic = new Dictionary<int, float>();

    [Header("写入 Button")]
    public Button _WriteButton;
    [Header("读取 Button")]
    public Button _ReadButton;
    [Header("连接 Button")]
    public Button _ConnectButton;


    [Header("IP 输入")]
    public InputField _IPInput;
    [Header("端口号 输入")]
    public InputField _PortInput;

    [Header("寄存器 输入")]
    public InputField _RegisterInput;
    [Header("写入值 输入")]
    public InputField _ValueInput;


    //连接成功 布尔
    [HideInInspector]
    public bool _ConnectSuccess = false;

    //读取缓存
    int _ReadIntCache = 109527;

    void Start()
    {
        _Instance = this;

        //写入按钮点击事件
        _WriteButton.onClick.AddListener(delegate
        {

            //判断 _ValueInput.text 是 int 还是 float
            string _ResultStr = CheckValueType(_ValueInput.text);
            if (_ResultStr == "整数")
            {
                {
                    try
                    {
                        //写入单个寄存器
                        WriteInt(Convert.ToUInt16(_RegisterInput.text), Convert.ToUInt16(_ValueInput.text));
                        //WriteSingleRegister(01, 1);
                        GameObject.Find("读取信息").GetComponent<Text>().text += "\n整数写入" + Convert.ToUInt16(_RegisterInput.text) + "--" + Convert.ToUInt16(_ValueInput.text);
 
                    }
                    catch (Exception ex)
                    {
                        Debug.LogError("发送数据时发生错误: " + ex.Message);
                    }
                }
            }
            else if (_ResultStr == "浮点数")
            {
                {
                    try
                    {
                        //写入寄存器 float
                        //WriteSingleRegister(Convert.ToUInt16(_RegisterInput.text), float.Parse(_ValueInput.text));
                        WriteFloat(Convert.ToUInt16(_RegisterInput.text), float.Parse(_ValueInput.text));
                        //SendFloatData(Convert.ToUInt16(_RegisterInput.text), float.Parse(_ValueInput.text));
                        //WriteSingleRegister(0x0010,1.5f);
                        GameObject.Find("读取信息").GetComponent<Text>().text += "\n浮点数数写入" + Convert.ToUInt16(_RegisterInput.text) + "--" + float.Parse(_ValueInput.text);
                    }
                    catch (Exception ex)
                    {
                        Debug.LogError("发送数据时发生错误: " + ex.Message);
                    }
                }
            }
            else
            {
                print("输入值不是有效的整数或浮点数");
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n输入值不是有效的整数或浮点数";
            }
        });

        //读取按钮点击事件
        _ReadButton.onClick.AddListener(delegate
        {
            //寄存器地址
            int _ResultInt = Convert.ToUInt16(_RegisterInput.text);
            if (_ResultInt <= 8)
            {
                //读取单个保持寄存器
                GameObject.Find("读取信息").GetComponent<Text>().text += _RegisterInput.text + "  Int:" + ReadInt(Convert.ToUInt16(_RegisterInput.text));
            }
            else if (_ResultInt >= 9 && _ResultInt <= 90)
            {

                //读取多个保持寄存器
                GameObject.Find("读取信息").GetComponent<Text>().text += _RegisterInput.text + "  Float:" + ReadFloat(Convert.ToUInt16(_RegisterInput.text));
            }
            else
            {
                print("输入值不是有效的整数或浮点数");
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n 请检查 寄存器地址和输入值是否够正确";
            }
        });

        //连接按钮点击事件
        _ConnectButton.onClick.AddListener(delegate
        {
            try
            {
                // 创建TCP客户端和Modbus主机
                _TcpClient = new TcpClient(_IPInput.text, Convert.ToInt32(_PortInput.text));
                _ModbusMaster = ModbusIpMaster.CreateIp(_TcpClient);

                // 尝试读取从设备的标识符
                GameObject.Find("读取信息").GetComponent<Text>().text += "\nPLC 已连接。";

                _ConnectSuccess = true;

            }
            catch (Exception e)
            {
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n" + e.Message;

                _ConnectSuccess = false;
            }
        });
    }

    private async void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            GameObject.Find("读取信息").GetComponent<Text>().text = "读取信息:";
        }

        if (Input.GetKeyDown(KeyCode.R))
        {
            //向指点地址读取值
            var _Variable=ReadInt(80);

            //然后对照表格信息
            if (_Variable == 01)
            {
                //判断值
                if (ReadInt(81) == 01)
                {
                    //执行响应
                    WriteInt(01, 0);
                }
            }
            else if (_Variable == 11)
            {
                //判断值
                if (ReadFloat(82) == 1.3f)
                {
                    //执行响应
                    WriteFloat(11, 2.6f);
                }
            }
        }


        // 调用异步检查数据的方法
        // 每30帧调用一次,可根据需要调整频率
        if (Time.frameCount % 30 == 0) 
        {
           await AsyncCheckData();
        }
    }



    /// <summary>
    /// 数据清除方法  程序退出执行
    /// </summary>
    private void OnApplicationQuit()
    {
        // 停止接收数据的线程并释放资源
        
        if (_ReceiveThread != null && _ReceiveThread.IsAlive)
        {
            _ReceiveThread.Abort();
        }

        if (_TcpClient != null && _TcpClient.Connected)
        {
            _TcpClient.Close();
        }

        if (_ModbusMaster != null)
        {
            _ModbusMaster.Dispose();
        }

        StopAllCoroutines();
    }


    /// <summary>
    /// 数据检测方法
    /// </summary>
    private void CheckData()
    {
        //判断当前值是否有变化
        //规定 80 寄存器地址为判断地址
        int _ReadInt = ReadInt(80);

        if (_ReadIntCache!= _ReadInt)
        {
            _ReadIntCache = _ReadInt;

            //检测数据
            if (_ReadInt == 01)
            {
                //判断值 81 寄存器地址 为Int 输入地址
                if (ReadInt(81) == 01)
                {
                    //执行响应
                    WriteInt(01, 0);
                }
            }
            else if (_ReadInt == 11)
            {
                //判断值  82 寄存器地址 为Float 输入地址
                if (ReadFloat(82) == 1.3f)
                {
                    //执行响应
                    WriteFloat(11, 2.6f);
                }
            }
        }        
    }


    /// <summary>
    /// 异步检查数据的方法
    /// </summary>
    /// <returns></returns>
    private async Task AsyncCheckData()
    {
        // 判断当前值是否有变化
        int _ReadInt = await ReadIntAsync(80);

        if (_ReadIntCache != _ReadInt)
        {
            _ReadIntCache = _ReadInt;

            // 检测数据
            if (_ReadInt == 01)
            {
                // 判断值
                if (await ReadIntAsync(81) == 01)
                {
                    // 执行响应
                    await WriteIntAsync(01, 0);
                }
            }
            else if (_ReadInt == 11)
            {
                // 判断值
                if (await ReadFloatAsync(82) == 1.3f)
                {
                    // 执行响应
                    await WriteFloatAsync(11, 2.6f);
                }
            }
        }
    }




    /// <summary>
    /// 异步读取保持寄存器的值  Int
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <returns></returns>
    private async Task<int> ReadIntAsync(ushort _RegisterAddress)
    {
        try
        {
            // 异步读取寄存器值
            ushort[] _Registers = await _ModbusMaster.ReadHoldingRegistersAsync(1, _RegisterAddress, 1);

            // 将ushort值转换为int
            int _Value = _Registers[0];
            return _Value;
        }
        catch (Exception ex)
        {
            Debug.LogError($"读取寄存器失败: {ex.Message}");
            // 返回默认值或抛出异常取决于您的错误处理策略
            return default(int);
        }
    }

    /// <summary>
    /// 异步写入保持寄存器的值  Int
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <param 写入值="_Value"></param>
    /// <returns></returns>
    private async Task WriteIntAsync(ushort _RegisterAddress, int _Value)
    {
        try
        {
            // 异步写入寄存器
            await _ModbusMaster.WriteSingleRegisterAsync(1, _RegisterAddress, (ushort)_Value);
        }
        catch (Exception ex)
        {
            Debug.LogError($"写入寄存器失败: {ex.Message}");
        }
    }


    /// <summary>
    /// 异步读取保持寄存器的值  float
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <returns></returns>
    private async Task<float> ReadFloatAsync(ushort _RegisterAddress)
    {
        try
        {
            // 异步读取寄存器值
            ushort[] _Registers = await _ModbusMaster.ReadHoldingRegistersAsync(1, _RegisterAddress, 2);

            // 将寄存器值转换为浮点数
            byte[] _Bytes = new byte[4];
            _Bytes[0] = (byte)(_Registers[1] & 0xFF); // LSB of second register
            _Bytes[1] = (byte)(_Registers[1] >> 8);   // MSB of second register
            _Bytes[2] = (byte)(_Registers[0] & 0xFF); // LSB of first register
            _Bytes[3] = (byte)(_Registers[0] >> 8);   // MSB of first register

            return BitConverter.ToSingle(_Bytes, 0);
        }
        catch (Exception ex)
        {
            Debug.LogError($"读取寄存器失败: {ex.Message}");
            // 返回默认值或抛出异常取决于您的错误处理策略
            return default(float);
        }
    }

    /// <summary>
    /// 异步写入保持寄存器的值  float
    /// </summary>
    /// <param 地址="_RegisterAddress"></param>
    /// <param 写入值="_\Value"></param>
    /// <returns></returns>
    private async Task WriteFloatAsync(ushort _RegisterAddress, float _Value)
    {
        try
        {
            // 将浮点数值转换为字节数组
            byte[] _Bytes = BitConverter.GetBytes(_Value);

            // 将字节数组拆分为两个寄存器值
            ushort[] _Registers = new ushort[2];
            _Registers[0] = BitConverter.ToUInt16(_Bytes, 2);
            _Registers[1] = BitConverter.ToUInt16(_Bytes, 0);

            // 异步写入两个保持寄存器
            await _ModbusMaster.WriteMultipleRegistersAsync(1, _RegisterAddress, _Registers);
        }
        catch (Exception ex)
        {
            Debug.LogError($"写入寄存器失败: {ex.Message}");
        }
    }




    /// <summary>
    /// 写入 Int 方法
    /// </summary>
    /// <param 寄存器地址="_RegisterAddress"></param>
    /// <param 写入值="_Value"></param>
    public void WriteInt(ushort _RegisterAddress, int _Value)
    {
         将整数值拆分成两个ushort值(高16位和低16位)
        //ushort[] _Registers = new ushort[2];
        //_Registers[0] = (ushort)(_Value >> 16);
        //_Registers[1] = (ushort)(_Value & 0xFFFF);

         写入寄存器
        //_ModbusMaster.WriteMultipleRegisters(1, _RegisterAddress, _Registers);

        try
        {
            // 写入单个寄存器
            _ModbusMaster.WriteSingleRegister(1, _RegisterAddress, (ushort)_Value);
        }
        catch (Exception ex)
        {

            Debug.LogError($"写入值失败 {_Value} 寄存器地址 {_RegisterAddress}: {ex.Message}");
            GameObject.Find("读取信息").GetComponent<Text>().text += $"\n写入值失败 {_Value} 寄存器地址 {_RegisterAddress}: {ex.Message}";
        }



    }

    /// <summary>
    /// 读取 Int 方法
    /// </summary>
    /// <param 寄存器地址="_RegisterAddress"></param>
    /// <returns></returns>
    public int ReadInt(ushort _RegisterAddress)
    {
        // 读取两个寄存器的值
        ushort[] _Registers = _ModbusMaster.ReadHoldingRegisters(1, _RegisterAddress, 1);

        // 将两个ushort值组合成一个int值
        //int _Value = (_Registers[0] << 16) | _Registers[1];
        int _Value = _Registers[0];

        return _Value;
    }



    /// <summary>
    /// 读取浮点数 方法
    /// </summary>
    /// <param 寄存器地址 ="_StartAddress"></param>
    /// <returns></returns>
    public float ReadFloat(ushort _StartAddress)
    {
        // 读取两个保持寄存器
        ushort[] _Registers = _ModbusMaster.ReadHoldingRegisters(1, _StartAddress, 2);

        // 将寄存器值转换为浮点数
        byte[] _Bytes = new byte[4];
        _Bytes[0] = (byte)(_Registers[1] & 0xFF); // LSB of second register
        _Bytes[1] = (byte)(_Registers[1] >> 8);   // MSB of second register
        _Bytes[2] = (byte)(_Registers[0] & 0xFF); // LSB of first register
        _Bytes[3] = (byte)(_Registers[0] >> 8);   // MSB of first register

        return BitConverter.ToSingle(_Bytes, 0);
    }

    /// <summary>
    /// 写入浮点数 方法
    /// </summary>
    /// <param 寄存器地址="_StartAddress"></param>
    /// <param 写入值="_Value"></param>
    public void WriteFloat(ushort _StartAddress, float _Value)
    {
        // 将浮点数值转换为字节数组
        byte[] _Bytes = BitConverter.GetBytes(_Value);

        // 将字节数组拆分为两个寄存器值
        ushort[] _Registers = new ushort[2];
        _Registers[0] = BitConverter.ToUInt16(_Bytes, 2);
        _Registers[1] = BitConverter.ToUInt16(_Bytes, 0);


        try
        {

            // 写入两个保持寄存器
            _ModbusMaster.WriteMultipleRegisters(01, _StartAddress, _Registers);
        }
        catch (Exception ex)
        {

            Debug.LogError($"写入值失败 {_Registers} 寄存器地址 {_StartAddress}: {ex.Message}");
            GameObject.Find("读取信息").GetComponent<Text>().text += $"\n写入值失败 {_Value} 寄存器地址 {_StartAddress}: {ex.Message}";
        }
    }


    /// <summary>
    /// 判断输入值类型
    /// </summary>
    /// <param 输入值="_InputText"></param>
    /// <returns></returns>
    public string CheckValueType(string _InputText)
    {
        string _Result = string.Empty;
        if (IsInteger(_InputText))
        {
            _Result = "整数";
        }
        else if (IsFloat(_InputText))
        {
            _Result = "浮点数";
        }
        else
        {
            _Result = "不是有效的整数或浮点数";
        }
        return _Result;
    }

    /// <summary>
    /// 输入值 int 判断
    /// </summary>
    /// <param 判断值="_Input"></param>
    /// <returns></returns>
    private bool IsInteger(string _Input)
    {
        int _Result;
        return int.TryParse(_Input, out _Result);
    }

    /// <summary>
    /// 输入值  float 判断
    /// </summary>
    /// <param 判断值="_Input"></param>
    /// <returns></returns>
    private bool IsFloat(string _Input)
    {
        float _Result;
        return float.TryParse(_Input, out _Result);
    }
}

Unity easyModbus 完整代码

using EasyModbus;
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// EasyModbus 读取输入
/// </summary>
public class EasyModbus_ZH : MonoBehaviour
{
    private ModbusClient _ModbusClient;

    [Header("IP 地址")]
    public string _IpAddress = "192.168.2.1";
    [Header("端口")]
    public int _Port = 502;

    [Header("寄存器起始地址")]
    public ushort _StartAddress = 1;

    [Header("寄存器数量")]
    public ushort _NumRegisters = 1;

    [Header("写入值 ushort")]
    public ushort _Value = 1;

    [Header("写入值 float")]
    public float _ValueFloat = 3.5f;


    [Header("写入 Button")]
    public Button _WriteButton;
    [Header("读取 Button")]
    public Button _ReadButton;
    [Header("连接 Button")]
    public Button _ConnectButton;


    [Header("IP 输入")]
    public InputField _IPInput;
    [Header("端口号 输入")]
    public InputField _PortInput;

    [Header("寄存器 输入")]
    public InputField _RegisterInput;
    [Header("写入值 输入")]
    public InputField _ValueInput;

    private void Start()
    {
        //连接按钮点击事件
        _ConnectButton.onClick.AddListener(delegate
        {
            try
            {
                // 创建TCP客户端和Modbus主机
                _ModbusClient = new ModbusClient(_IPInput.text, Convert.ToInt32(_PortInput.text));
                _ModbusClient.Connect();

                // 尝试读取从设备的标识符
                GameObject.Find("读取信息").GetComponent<Text>().text += "\nPLC 已连接。";
            }
            catch (Exception e)
            {
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n" + e.Message;

            }
        });

        //读取按钮点击事件
        _ReadButton.onClick.AddListener(delegate
        {
            //寄存器地址
            int _ResultInt = Convert.ToUInt16(_RegisterInput.text);
            if (_ResultInt <= 8)
            {
                //读取单个保持寄存器
                //GameObject.Find("读取信息").GetComponent<Text>().text += _RegisterInput.text + "  Int:" + ;
                ReadHoldingRegistersInt(Convert.ToUInt16(_RegisterInput.text));
            }
            else if (_ResultInt >= 9 && _ResultInt <= 90)
            {

                //读取多个保持寄存器
                //GameObject.Find("读取信息").GetComponent<Text>().text += _RegisterInput.text + "  Float:" + ReadFloat(Convert.ToUInt16(_RegisterInput.text));
                //StartCoroutine(ReadHoldingRegistersFloat(Convert.ToUInt16(_RegisterInput.text)));
                ReadHoldingRegistersFloat(Convert.ToUInt16(_RegisterInput.text));
            }
            else
            {
                print("输入值不是有效的整数或浮点数");
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n 请检查 寄存器地址和输入值是否够正确";
            }
        });

        //写入按钮点击事件
        _WriteButton.onClick.AddListener(delegate
        {

            //判断 _ValueInput.text 是 int 还是 float
            string _ResultStr = CheckValueType(_ValueInput.text);
            if (_ResultStr == "整数")
            {
                {
                    try
                    {
                        //写入单个寄存器
                        WriteSingleRegister(Convert.ToUInt16(_RegisterInput.text), Convert.ToUInt16(_ValueInput.text));
                        //WriteSingleRegister(01, 1);
                        //GameObject.Find("读取信息").GetComponent<Text>().text += "\n整数写入" + Convert.ToUInt16(_RegisterInput.text) + "--" + Convert.ToUInt16(_ValueInput.text);

                    }
                    catch (Exception ex)
                    {
                        Debug.LogError("发送数据时发生错误: " + ex.Message);
                    }
                }
            }
            else if (_ResultStr == "浮点数")
            {
                {
                    try
                    {
                        //写入寄存器 float
                        //WriteSingleRegister(Convert.ToUInt16(_RegisterInput.text), float.Parse(_ValueInput.text));
                        WriteSingleRegister(Convert.ToUInt16(_RegisterInput.text), (float)Math.Round(float.Parse(_ValueInput.text), 2));
                        //SendFloatData(Convert.ToUInt16(_RegisterInput.text), float.Parse(_ValueInput.text));
                        //WriteSingleRegister(0x0010,1.5f);
                        //GameObject.Find("读取信息").GetComponent<Text>().text += "\n浮点数数写入" + Convert.ToUInt16(_RegisterInput.text) + "--" + float.Parse(_ValueInput.text);
                    }
                    catch (Exception ex)
                    {
                        Debug.LogError("发送数据时发生错误: " + ex.Message);
                    }
                }
            }
            else
            {
                print("输入值不是有效的整数或浮点数");
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n输入值不是有效的整数或浮点数";
            }
        });

    }

    /// <summary>
    /// 程序退出时关闭连接
    /// </summary>
    private void OnDestroy()
    {
        if (_ModbusClient!=null)
        {
            _ModbusClient.Disconnect();
        }
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            GameObject.Find("读取信息").GetComponent<Text>().text = "读取信息:";
        }

        if (Input.GetKeyDown(KeyCode.P))
        {
            StopAllCoroutines();

            // 读取保持寄存器
            StartCoroutine(ReadHoldingRegistersIntIE(Convert.ToUInt16(_RegisterInput.text)));
            //ReadHoldingRegistersInt(7);
        }
    }


    /// <summary>
    /// 读取Int
    /// </summary>
    /// <param name="startAddress"></param>
    private void ReadHoldingRegistersInt(int startAddress)
    {
        //while (true)
        {
            try
            {
                // 读取从地址40001开始的寄存器,读取2个寄存器(每个寄存器2个字节,4字节表示一个整数)
                int[] registers = _ModbusClient.ReadHoldingRegisters(startAddress, 2);

                // 将读取到的寄存器值转换为整数
                int intValue = ModbusClient.ConvertRegistersToInt(registers);


                // 处理读取到的数据
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n读寄存器 Int: " + intValue;
            }
            catch (Exception e)
            {
                Debug.LogError("读取寄存器失败: " + e.Message);

                GameObject.Find("读取信息").GetComponent<Text>().text += "读取寄存器失败: " + e.Message;
            }

            // 每秒读取一次
            //yield return new WaitForSeconds(1);
        }
           
    }

    /// <summary>
    /// 读取Int
    /// </summary>
    /// <param name="startAddress"></param>
    private IEnumerator ReadHoldingRegistersIntIE(int startAddress)
    {
        while (true)
        {
            try
            {
                // 读取从地址40001开始的寄存器,读取2个寄存器(每个寄存器2个字节,4字节表示一个整数)
                int[] registers = _ModbusClient.ReadHoldingRegisters(startAddress, 2);

                // 将读取到的寄存器值转换为整数
                int intValue = ModbusClient.ConvertRegistersToInt(registers);


                // 处理读取到的数据
                GameObject.Find("读取信息").GetComponent<Text>().text = "\n读寄存器 Int: " + intValue;
            }
            catch (Exception e)
            {
                Debug.LogError("读取寄存器失败: " + e.Message);

                GameObject.Find("读取信息").GetComponent<Text>().text += "读取寄存器失败: " + e.Message;
            }

            // 每秒读取一次
            yield return new WaitForSeconds(0.15f);
        }

    }


    /// <summary>
    /// 读取 Float
    /// </summary>
    /// <param name="startAddress"></param>
    private void ReadHoldingRegistersFloat(int startAddress)
    {
        //while (true)
        {
            try
            {
                // 读取从地址40001开始的寄存器,读取2个寄存器(每个寄存器2个字节,4字节表示一个整数)
                int[] registers = _ModbusClient.ReadHoldingRegisters(startAddress, 2);

                // 将读取到的寄存器值转换为浮点数
                float floatValue = ModbusClient.ConvertRegistersToFloat(registers);

                // 处理读取到的数据
                GameObject.Find("读取信息").GetComponent<Text>().text += "\n读寄存器 Float: " + floatValue;
            }
            catch (Exception e)
            {

                GameObject.Find("读取信息").GetComponent<Text>().text += "读取寄存器失败: " + e.Message;
            }

            // 每秒读取一次
            //yield return new WaitForSeconds(1);
        }
     
    }

    /// <summary>
    /// 写入整数
    /// </summary>
    /// <param name="address"></param>
    /// <param name="value"></param>
    private void WriteSingleRegister(int address, int value)
    {
        try
        {
            // 将浮点数值转换为寄存器值
            int[] registers = ModbusClient.ConvertIntToRegisters(value);

            _ModbusClient.WriteMultipleRegisters(address, registers);

       

            GameObject.Find("读取信息").GetComponent<Text>().text += "\n写地址寄存器 Int:" + address + " 写入值:" + value;
        }
        catch (Exception e)
        {

            GameObject.Find("读取信息").GetComponent<Text>().text += "写寄存器失败: " + e.Message;
        }
    }

    /// <summary>
    /// 写入浮点数
    /// </summary>
    /// <param name="address"></param>
    /// <param name="value"></param>
    private void WriteSingleRegister(int address, float value)
    {
        try
        {
            // 将浮点数值转换为寄存器值
            int[] registers = ModbusClient.ConvertFloatToRegisters(value);

            _ModbusClient.WriteMultipleRegisters(address, registers);


            GameObject.Find("读取信息").GetComponent<Text>().text += "\n写地址寄存器 Float:" + address + " 写入值:" + value;
        }
        catch (Exception e)
        {
            GameObject.Find("读取信息").GetComponent<Text>().text += "写寄存器失败: " + e.Message;
        }
    }




    /// <summary>
    /// 判断输入值类型
    /// </summary>
    /// <param 输入值="_InputText"></param>
    /// <returns></returns>
    public string CheckValueType(string _InputText)
    {
        string _Result = string.Empty;
        if (IsInteger(_InputText))
        {
            _Result = "整数";
        }
        else if (IsFloat(_InputText))
        {
            _Result = "浮点数";
        }
        else
        {
            _Result = "不是有效的整数或浮点数";
        }
        return _Result;
    }

    /// <summary>
    /// 输入值 int 判断
    /// </summary>
    /// <param 判断值="_Input"></param>
    /// <returns></returns>
    private bool IsInteger(string _Input)
    {
        int _Result;
        return int.TryParse(_Input, out _Result);
    }

    /// <summary>
    /// 输入值  float 判断
    /// </summary>
    /// <param 判断值="_Input"></param>
    /// <returns></returns>
    private bool IsFloat(string _Input)
    {
        float _Result;
        return float.TryParse(_Input, out _Result);
    }
}

Unity 场景

Unity 场景 Hierarchy窗口

我是这样搭建的 可以参考一下,比较好调试。当然大家可以自由发挥

在这里插入图片描述

Unity 场景 脚本搭载

这里使用的是 NModbus4_ZH 脚本根据需求进行使用

在这里插入图片描述

Unity 场景 效果

Game 运行窗口

在这里插入图片描述

TIA 和 软件执行情况

在这里插入图片描述

遇到的问题

感觉最好用和最完善的是 NModbus4_ZH 脚本 因为异步和主线程执行方法都有,大家根据需求来使用就行。

1. 为什么无法连接上设备?
	:查看自己的电脑是否和设备处于一个局域网内。

2. 为什么无法正确读取 real 类型?
	:因为在Unity中 real类型类同于float类型,查看是否使用正确的方法。

3. 为什么我无法读取40之后的地址?
	:查看博图软件中授权的读取地址是否大于自己的读取地址。

4. 为什么我连接上设备就会变得特别卡?
	:查看 Update 中是否在持续使用读取方法;也有可能是你再读取的时候正在使用写入方法,
	   注意下数据冲突问题。最好使用异步的方法来进行读写。

5. 为什么我读取出来的值跟博图的值对不上?
	:一般来说是没有这个问题的,除非你接收到数据之后又进行了数据转换。

6. 为什么我只能读取 Int 类型?
	:要注意检查方法啊,一般 int\word 类型就是用 Int读取方法,
	   real类型使用 Float读取方法,写入同理。具体按需求来就行。

7. 为什么有的数据我一直在读取,有的时候能读取到,有的时候读取不到?
	:因为程序运行都有运行时间的,如果不做存储就会丢失。
	   解决方法当然也很简单,就是用队列,来一个存一个就行了,使用完踢出就行了。

8. 为什么重启之后就无法用了?
	:除非特殊情况一般不会出现这个问题,如果经常出现,就要使用 OnApplicationQuit 方法,
	   程序退出之后关闭所有的连接和所有的协程方法。

希望这些信息能够进一步满足您对Untiy Modbus 西门子 S7-1200 基础通信的需求。
如果您有任何特定的问题或需要更深入的讨论,请随时提出。

路漫漫其修远兮,与君共勉。

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Maddie_Mo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值