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

news2024/12/24 9:20:07

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

  • Modbus
    • Modbus是什么
    • Modbus 协议版本
    • Modbus 通信和设备
    • Modbus 如何实现
    • Modbus 使用限制
    • Modbus 通信协议学理上的弱点分析
  • Unity
    • Unity ModbusTCP
      • Unity ModbusTCP 单个线圈读取方法
      • Unity ModbusTCP 单个线圈写入方法 Int
      • Unity ModbusTCP 单个线圈写入方法 Bool
    • Unity NModbus4
      • Unity NModbus4 读取 Int 方法
      • Unity NModbus4 读取 Float 方法
      • Unity NModbus4 写入 Int 方法
      • Unity NModbus4 写入 Float 方法
    • Unity EasyModbus
      • Unity EasyModbus 读取 Int 方法
      • Unity EasyModbus 读取 Float 方法
      • Unity EasyModbus 写入 Int 方法
      • Unity EasyModbus 写入 Float 方法
    • Unity ModbusTCP 完整代码
    • Unity NModbus4 完整代码
    • Unity easyModbus 完整代码
    • Unity 场景
      • Unity 场景 Hierarchy窗口
      • Unity 场景 脚本搭载
      • Unity 场景 效果
  • 遇到的问题

在本篇博客中,我们将探讨如何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 基础通信的需求。
如果您有任何特定的问题或需要更深入的讨论,请随时提出。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1992332.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android 让程序随系统自动启动并允许后台运行(白名单)

最近制作一个管理程序&#xff0c;需要在开机时候启动&#xff0c;并持续运行。这里简单记录下如何制作。 自启动原理 系统在启动的时候会广播一个ACTION_BOOT_COMPLETED&#xff0c;带有接收的程序可以收到&#xff0c;所以我们在接收到以后把程序运行起来。 清单文件设置 …

PyMysql快速上手操作详解

PyMySQL是从Python连接到MySQL数据库服务器的接口。 它实现了Python数据库API v2.0&#xff0c;并包含一个纯Python的MySQL客户端库 一、PyMysql安装 pip install pymysql 或者 pip3 install pymysql二、连接数据库 pymysql连接数据库使用的是 pymsql.connect() 函数&#xff…

数据库设计笔记3-事务管理,冲突串行化,锁定协议,死锁测试

#1.指令冲突 注&#xff1a;读读操作不冲突&#xff0c;剩下的冲突。 #2.冲突串行化 <1理解 如果一个调度可以在不改变冲突操作顺序的情况转换为任意串行调度&#xff0c;那么两个调度的结果是相同的&#xff0c;也就是说这个调度是可冲突串行化的 <2正反举例 <3测…

【教师秘籍】AI预测学生未来?职场规划大揭秘!

​声明&#xff1a;此篇为 ai123.cn 原创文章&#xff0c;转载请标明出处链接&#xff1a;https://ai123.cn/2150.html 嘿老师们&#xff0c;你们有没有和我一样的烦恼&#xff1a;学生各有千秋&#xff0c;家长各有各的操心&#xff0c;信息一箩筐却总是不够用&#xff1f;&am…

基于JAVA的校园跑腿网站的设计与实现

摘要 随着计算机技术&#xff0c;网络技术的迅猛发展&#xff0c;Internet 的不断普及&#xff0c;网络在各个领域里发挥了越来越重要的作用。特别是随着近年人民生活水平不断提高&#xff0c;校园跑腿网站给学校的业务带来了更大的发展机遇。 在经济快速发展的带动下&#xff…

[NPUCTF2020]ReadlezPHP1

打开题目&#xff0c;看到信息 ctrlu查看源代码 看到php代码&#xff0c;打开 代码审计看一下&#xff0c;进行代码审计&#xff0c;发现存在反序列化语句&#xff1a;$ppp unserialize($_GET["data"]);和执行漏洞&#xff1a;echo $b($a);&#xff0c;此处未想到fl…

荟萃科技:海外问卷调查项目可以长期做不?

当然可以&#xff01; 我已经做了两年多的海外问卷调查了&#xff0c;我觉得还是蛮有发言权的。 现在网络飞速发展&#xff0c;实体的生意也越来越来越不好做了&#xff0c;有人就把目标转移到了网络上&#xff0c;想知道一些其他的网络项目能不能做&#xff0c;如果能找到一…

【可控图像生成系列论文(六)】ECCV24-Glyph-ByT5 微软亚研院、清华、北大合作工作(上)

系列文章目录 【可控图像生成系列论文&#xff08;一&#xff09;】 简要介绍了 MimicBrush 的整体流程和方法&#xff1b;【可控图像生成系列论文&#xff08;二&#xff09;】 就 MimicBrush 的具体模型结构、训练数据和纹理迁移进行了更详细的介绍。【可控图像生成系列论文…

提示词工程学的前世今生:Generative Pre-trained Transformer 到AIGC,再到Prompt Engineering

人工智能&#xff08;Artificial intelligence&#xff0c;AI&#xff09;的演进已然变革了我们对于技术的理解以及应用方式。自最初的规则系统直至当下的深度学习&#xff0c;AI 在众多领域均彰显出了极为巨大的潜力。当中&#xff0c;生成式预训练模型&#xff08;Generative…

浮毛烦恼不复存在!不容错过的养宠好物——宠物空气净化器

猫咪一年有两次换毛季&#xff0c;多集中在春夏和秋冬&#xff0c;尤其是在春季&#xff0c;换毛时长可以达到一个月之久。在此期间。猫咪会疯狂掉毛&#xff0c;需要铲屎官们在此期间做好相关措施&#xff0c;让猫咪顺利度过换毛季。其中&#xff0c;最重要的就是猫毛清理&…

PLC远程控制网关再也不用劳累出差

在当今快速发展的工业4.0时代&#xff0c;随着智能制造与物联网技术的深度融合&#xff0c;工厂自动化系统的远程监控与管理已成为提高生产效率、优化资源配置的关键。其中&#xff0c;可编程逻辑控制器&#xff08;PLC&#xff09;作为工业控制的核心部件&#xff0c;其远程控…

Retrofit 自定义注解 实现可选择性的打印接口日志

序言 有时候我们需要打印okhttp的日志&#xff0c;但是现在的日志拦截器&#xff0c;不能做到接口级别的日志输出控制。要么就是全部打印。这样很影响调试效率。所以我在这块做了一些探索。 使用效果 普通输出 只需要在要打印日志的接口上添加 PrintLog 注解就可以打印&…

奥运足球背后的中国小公司

有一家中国的小公司叫做顶碁运动&#xff0c;居然打败了耐克和阿迪达斯这样的巨头&#xff0c;成功地成为了现在巴黎奥运会的足球供应商。 顶碁运动研发的足球&#xff0c;最大的优点就是能够在一秒钟之内精准地识别500次。因为他们在足球的内胆里面装置了芯片和传感器&#xf…

【Linux】:进程控制1(创建、终止、等待)

目录 1.进程创建 2.进程终止&#xff08;退出&#xff09; 2.1 什么是进程终止 2.2 进程退出的场景&#xff08;原因&#xff09; 2.3 进程退出码 2.4 错误码errno 2.5 进程常见的退出方法 正常终止 从main函数返回 调用库函数exit 系统接口_exit 3.进程等待 3.1 …

[qt] 多线程应用02

源码: 点击此处 一 UI 1.1 效果 1.2 代码 首先定义一系列的控件和按钮&#xff0c;用来显示Tcp连接数据信息。 QLabel *m_serverNameLabel;QLineEdit *m_serverLineEdit;QLabel *m_portLabel;QLineEdit *m_portLineEdit;QDateTimeEdit *m_d…

【书生大模型实战营第三期】基础岛 第3关 浦语提示词工程实践

欢迎大家参与第三期书生大模型实战营&#xff01;&#xff01;&#xff01; 1. 基础任务 背景问题&#xff1a;近期相关研究发现&#xff0c;LLM 在对比浮点数字时表现不佳&#xff0c;经验证&#xff0c;internlm2-chat-1.8b&#xff08;internlm2-chat-7b&#xff09;也存在这…

数据结构——排序(2):选择排序+交换排序

目录 一、选择排序 &#xff08;1&#xff09;直接选择排序 ①思路 ②过程图示 ③代码实现 ④代码解释 ⑤优化 1.代码实现 2.过程图示 3.代码解释 4.注意 ⑥直接选择排序的复杂度 &#xff08;2&#xff09;堆排序 ①注意 ②代码实现 二、交换排序 &#xff08…

一键生成!AI绘画、视频制作与写作神助攻

市面上有各种各样的AI助手&#xff0c;它们覆盖了文字处理、图像编辑、视频制作到语音识别等众多领域。这些工具设计得既实用又友好&#xff0c;几乎每个人都能找到适合自己的那一款。 1. 文字处理助手 文本生成&#xff1a;帮你快速创作文章、博客等内容。 内容优化&#xff…

操作ArkTS页面跳转及路由相关心得

本文为JS老狗原创。 当前端不得不关注的点&#xff1a;路由&#xff0c;今天聊一聊鸿蒙相关的一点心得。 总体上套路不意外&#xff0c;基本就是&#xff08;尤其是Web&#xff09;前端那些事&#xff1a;维护路由表、跳转带参数、历史堆栈操作&#xff0c;等等。 历史原因&…

越秀·星汇城|大城好生活

建筑&#xff0c;是美好生活的载体。而户型则是住宅的灵魂&#xff0c;一处好的居所&#xff0c;承载理想生活盛放。 细腻的美好藏在生活各个角落&#xff0c;星汇城以24小时贯穿的细节享受&#xff0c;重新定义幸福该有的舒适。诉说生活的达观&#xff0c;臻藏岁月静好。 8:…