C#分享辅助类:Modbus串口通信(三菱)

using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Wsfly
{
    /// <summary>
    /// 委托-回调
    /// </summary>
    /// <param name="receiveData">十进制列表</param>
    /// <param name="receiveHexData">十六进制字符串</param>
    public delegate void ModbusSL_Callback(bool success, string msg, List<int> receiveData, string receiveHexData, string resultData);

    /// <summary>
    /// 三菱PLC Modbus 指令
    /// </summary>
    public class ModbusSLHandler
    {
        /*
         通讯格式
         命令                                              命令码          目标设备  
         DEVICE READ     CMD                  "0"                X,Y,M,S,T,C,D  
         DEVICE WRITE    CMD                  "1"               X,Y,M,S,T,C,D  
         FORCE  ON         CMD                  "7"               X,Y,M,S,T,C  
         FORCE  OFF        CMD                  "8"               X,Y,M,S,T,C  
        ————————————————————————————————

        16进制代码:  
        ENQ         05H        请求 
        ACK          06H        PLC正确响应 
        NAK         15H        PLC错误响应 
        STX          02H        报文开始 
        ETX          03H        报文结束
        ————————————————————————————————

         发出的命令为11个两位数(如:02 30 31 30 31 34 30 32 03 35 41),并且这些两位数必须为16进制(H)的ASCII码
         命令格式Demo:
         请求          命令             元件首地址                                                BYTE数                          结束              校验总和
         1              2                   3            4            5            6                    7             8                     9                   10           11
         STX          CMD            16(3)      16(2)      16(1)      16(0)              16(1)      16(0)                ETX               16(1)      16(0)
         02            30                31           30          31          34                   30         32                     03                 35          42

        解释说明:
        STX:                 报文开始为 02
        命令:                30  30  为从PLC读取数据,31为写入数据;0的ASCII码16进制表示为30,1的ASCII码为31
        元件首地址:      31  30 31 34  D10查表可得其地址的首地址为1014(行为1010,列4,1010+4=1014  16进制的加法),1位数字对应1位ASCII码的16进制表示,1014即为31 30 31 34(1为31,0为30,4为34)
        BYTE数:           30   32  即 02  因在三菱PLC中数据寄存器D为两个字节的存储,所以读取时必须为2个字节,即02,0对应30,2对应32,即30  32
        ETX:                报文结束为 03
        校验总和:         35   42   按照前述照片协议校验和为30+31+30+31+34+30+32+03=15B(16进制加法),取15B后边两位即5B,5对应ASCII中对应16进制为35,B为42
        */

        /// <summary>
        /// 串口 SerialPort
        /// </summary>
        private SerialPort _serialPort = null;
        /// <summary>
        /// 回调函数
        /// </summary>
        private ModbusSL_Callback _callback = null;
        /// <summary>
        /// 收到的数据 DataReceive
        /// </summary>
        private string _receiveHexData = null;
        /// <summary>
        /// 最后发送的数据
        /// </summary>
        private string _lastSendData = "";

        /*三菱Fx系列PLC地址对应表
            Public Const PLC_D_Base_AddRess = 4096 
            Public Const PLC_D_Special_Base_AddRess = 3584 
            Public Const PLC_Y_Group_Base_AddRess = 160 
            Public Const PLC_PY_Group_Base_AddRess = 672 
            Public Const PLC_T_Group_Base_AddRess = 192 
            Public Const PLC_OT_Group_Base_AddRess = 704 
            Public Const PLC_RT_Group_Base_AddRess = 1216 
            Public Const PLC_M_SINGLE_Base_AddRess = 2048(命令为7或8时) 
            Public Const PLC_M_Group_Base_AddRess = 256 
            Public Const PLC_PM_Group_Base_AddRess = 768 
            Public Const PLC_S_Group_Base_AddRess = 0 
            Public Const PLC_X_Group_Base_AddRess = 128 
            Public Const PLC_C_Group_Base_AddRess = 448 
            Public Const PLC_OC_Group_Base_AddRess = 960 
            Public Const PLC_RC_Group_Base_AddRess = 1472 
            Public Const PLC_TV_Group_Base_AddRess = 2048 
            Public Const PLC_CV16_Group_Base_AddRess = 2560 
            Public Const PLC_CV32_Group_Base_AddRess = 3072 

            当我们用DEVICE READ命令时,D100地址=100*2+4096;M100地址=100+256;
            
            不同的是D类型寄存器存放的是字,M寄存器存放的是位,
            同样是读两个字节,D100返回的就是PLC中D100地址的值,M类型寄存器返回的是M100到M116的值。
            所以当我们用FORCE ON 命令时,M100寄存器地址=100+2048;

            FORCE ON/Off命令中地址排列与DEVICE READ/WRITE不同,是低位在前高位在后。
            如Y20,地址是0510H,代码中4个字节地址表示为:1005。(注意:Y寄存器为八进制,如Y20地址=16+1280=0510H)
         */

        #region 构造函数
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="serialPort"></param>
        public ModbusSLHandler(SerialPort serialPort)
        {
            _serialPort = serialPort;
        }
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="port">端口号</param>
        /// <param name="baudRate">波特率</param>
        /// <param name="dataBits">数据位</param>
        /// <param name="parity">奇偶检验位</param>
        /// <param name="stopBits">停止位</param>
        /// <param name="handshake">握手协议</param>
        public ModbusSLHandler(int port, int baudRate = 9600, int dataBits = 8, Parity parity = Parity.Even, StopBits stopBits = StopBits.One, Handshake handshake = Handshake.None, bool dtrEnable = true, bool rtsEnable = true)
        {
            //实例化串口
            _serialPort = new SerialPort();

            _serialPort.PortName = "COM" + port;            //端口号
            _serialPort.BaudRate = baudRate;                    //波特率
            _serialPort.DataBits = dataBits;                        //数据位
            _serialPort.Parity = parity;                                //奇偶检验位
            _serialPort.StopBits = stopBits;                        //停止位
            _serialPort.Handshake = handshake;              //握手协议
            _serialPort.DtrEnable = dtrEnable;
            _serialPort.RtsEnable = rtsEnable;
        }
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="port">端口号</param>
        /// <param name="baudRate">波特率</param>
        /// <param name="dataBits">数据位</param>
        /// <param name="parity">奇偶检验位</param>
        /// <param name="stopBits">停止位</param>
        /// <param name="handshake">握手协议</param>
        public ModbusSLHandler(int port, int baudRate, int dataBits, int parity, int stopBits, int handshake, bool dtrEnable = true, bool rtsEnable = true)
        {
            //实例化串口
            _serialPort = new SerialPort();

            _serialPort.PortName = "COM" + port;           //端口号
            _serialPort.BaudRate = baudRate;                   //波特率
            _serialPort.DataBits = dataBits;                        //数据位
            _serialPort.Parity = (Parity)parity;                    //奇偶检验位
            _serialPort.StopBits = (StopBits)stopBits;        //停止位
            _serialPort.Handshake = (Handshake)handshake;   //握手协议
            _serialPort.DtrEnable = dtrEnable;
            _serialPort.RtsEnable = rtsEnable;
        }
        #endregion

        #region 打开、关闭串口
        /// <summary>
        /// 打开串口
        /// </summary>
        public void Open()
        {
            //是否有串口
            if (_serialPort == null) return;
            //打开连接
            if (!_serialPort.IsOpen)
            {
                //打开串口
                _serialPort.Open();
                //串口收到数据处理
                _serialPort.DataReceived += _serialPort_DataReceived;
            }
        }
        /// <summary>
        /// 关闭串口
        /// </summary>
        public void Close()
        {
            //是否有串口
            if (_serialPort == null) return;
            //关闭连接
            if (_serialPort.IsOpen)
            {
                //串口收到数据处理
                _serialPort.DataReceived -= _serialPort_DataReceived;
                _serialPort.Close();
                //_serialPort.Dispose();
            }
        }
        #endregion

        #region 接收数据
        /// <summary>
        /// 接收数据处理
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void _serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            //暂停0.1秒
            System.Threading.Thread.Sleep(100);

            //发回的字符数
            int bytesLength = _serialPort.BytesToRead;
            if (bytesLength <= 0) return;

            //发回的数据
            byte[] bytes = new byte[bytesLength];

            //读取数据
            int readDataLength = _serialPort.Read(bytes, 0, bytesLength);
            if (readDataLength <= 2 || bytes == null) return;

            //对数据进行转换
            string resultData = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
            //要返回的十进制列表
            List<int> results = new List<int>();

            //转换为10进制、16进制
            foreach (byte b in bytes)
            {
                results.Add(Convert.ToInt32(b));
                _receiveHexData += b.ToString("X2") + " ";
            }
            _receiveHexData = _receiveHexData.Trim();

            //验证CRC是否正确
            string rhd = _receiveHexData.Replace(" ", "");
            string rhd_Check = bytes[bytes.Length - 2].ToString("X2") + " " + bytes.Last().ToString("X2");
            string rhd_Data = rhd.Replace(rhd_Check, "");

            //验证校验码
            bool isRightCRC = false;
            string rhdCRC = CRC16(rhd_Data);
            if (rhdCRC == rhd_Check)
            {
                //校验码是正确的
                isRightCRC = true;
            }

            //回调通知
            _callback?.Invoke(isRightCRC, "OK", results, _receiveHexData, resultData);
        }
        #endregion

        #region 发送数据
        /// <summary>
        /// 读数据
        /// </summary>
        public void DeviceRead(DeviceType type, int addr, int len, ModbusSL_Callback callback)
        {
            //读寄存器:STX 02H + CMD 30H + 寄存器首地址 + 寄存器位数 + 终止ETX 03H + CRC

            switch (type)
            {
                case DeviceType.S:
                    addr = addr / 8;
                    break;
                case DeviceType.X:
                    addr = addr / 8 + 128; //0x0080
                    break;
                case DeviceType.Y:
                    addr = addr / 8 + 160;//0x00A0
                    break;
                case DeviceType.T:
                    addr = addr / 8 + 192;//0x00C0
                    break;
                case DeviceType.M:
                    addr = addr / 8 + 256;//0x0100
                    break;
                case DeviceType.C:
                    addr = addr / 8 + 448;//0x01C0
                    break;
                case DeviceType.D:
                    addr = addr * 2 + 4096;//0x1000
                    break;
            }

            if (len > 32)
            {
                callback?.Invoke(false, "读取长度不可大于32", null, null, null);
                return;
            }

            //开始STX + CMD
            string zl = "02 30 ";

            //寄存器地址
            string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
            zl += ToASCIIHex(addrStr[0]) + " ";
            zl += ToASCIIHex(addrStr[1]) + " ";
            zl += ToASCIIHex(addrStr[2]) + " ";
            zl += ToASCIIHex(addrStr[3]) + " ";

            //寄存器位数
            string byteLength = Convert.ToString(len * 2, 16).PadLeft(2, '0').ToUpper();
            zl += ToASCIIHex(byteLength[0]) + " ";
            zl += ToASCIIHex(byteLength[1]) + " ";

            //结束ETX
            zl += "03 ";

            //校验CRC
            zl += CRC16(zl);

            //发送数据
            SendData(zl, callback);
        }

        /// <summary>
        /// 写数据
        /// </summary>
        public void DeviceWrite(DeviceType type, int addr, int val, ModbusSL_Callback callback)
        {
            DeviceWrite(type, addr, new int[] { val }, callback);
        }

        /// <summary>
        /// 写数据
        /// </summary>
        public void DeviceWrite(DeviceType type, int addr, int[] vals, ModbusSL_Callback callback)
        {
            //写寄存器:STX 02H+ CMD 31H+ 寄存器首地址 + 寄存器位数 + 写入数据 + 终止ETX 03H + CRC
            int len = vals.Length;

            switch (type)
            {
                case DeviceType.S:
                    addr = addr / 8;
                    break;
                case DeviceType.X:
                    addr = addr / 8 + 128; //0x0080
                    break;
                case DeviceType.Y:
                    addr = addr / 8 + 160;//0x00A0
                    break;
                case DeviceType.T:
                    addr = addr / 8 + 192;//0x00C0
                    break;
                case DeviceType.M:
                    addr = addr / 8 + 256;//0x0100
                    break;
                case DeviceType.C:
                    addr = addr / 8 + 448;//0x01C0
                    break;
                case DeviceType.D:
                    addr = addr * 2 + 4096;//0x1000
                    break;
            }

            if (len > 32)
            {
                callback?.Invoke(false, "写入长度不可大于32", null, null, null);
                return;
            }

            //开始STX + CMD
            string zl = "02 31 ";

            //寄存器地址
            string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
            zl += ToASCIIHex(addrStr[0]) + " ";
            zl += ToASCIIHex(addrStr[1]) + " ";
            zl += ToASCIIHex(addrStr[2]) + " ";
            zl += ToASCIIHex(addrStr[3]) + " ";

            //寄存器位数
            string byteLength = Convert.ToString(len * 2, 16).PadLeft(2, '0').ToUpper();
            zl += ToASCIIHex(byteLength[0]) + " ";
            zl += ToASCIIHex(byteLength[1]) + " ";

            //写入的数据
            for (int i = 0; i < len; i++)
            {
                int val = vals[i];
                string valStr = val.ToString("x").PadLeft(4, '0').ToUpper();

                //高低字节交换
                valStr = valStr.Substring(2, 2) + valStr.Substring(0, 2);

                zl += ToASCIIHex(valStr[0]) + " ";
                zl += ToASCIIHex(valStr[1]) + " ";
                zl += ToASCIIHex(valStr[2]) + " ";
                zl += ToASCIIHex(valStr[3]) + " ";
            }

            //结束ETX
            zl += "03 ";

            //校验CRC
            zl += CRC16(zl);

            //发送数据
            SendData(zl, callback);
        }

        /// <summary>
        /// 置位
        /// </summary>
        /// <param name="type"></param>
        /// <param name="addr"></param>
        /// <param name="callback"></param>
        /// <returns></returns>
        public void ForceOn(DeviceType type, int addr, ModbusSL_Callback callback)
        {
            /*
                FORCE ON 置位
                始命令   地址   终和校验
                STX   CMD   ADDRESS   ETX   SUM
                02H   37H   ADDRESS   03H   SUM
             */

            switch (type)
            {
                case DeviceType.S:
                    break;
                case DeviceType.X:
                    addr = addr + 1024; //0x0400
                    break;
                case DeviceType.Y:
                    addr = addr + 1280; //0x0500
                    break;
                case DeviceType.T:
                    addr = addr + 1536; //0x0600
                    break;
                case DeviceType.M:
                    addr = addr + 2048; //0x0800
                    break;
                case DeviceType.C:
                    addr = addr + 3584; //0x0E00
                    break;
            }

            //开始STX + CMD
            string zl = "02 37 ";

            //寄存器地址 
            string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
            zl += ToASCIIHex(addrStr[2]) + " ";
            zl += ToASCIIHex(addrStr[3]) + " ";
            zl += ToASCIIHex(addrStr[0]) + " ";
            zl += ToASCIIHex(addrStr[1]) + " ";

            //结束ETX
            zl += "03 ";

            //校验CRC
            zl += CRC16(zl);
            
            //发送数据
            SendData(zl, callback);
        }
        /// <summary>
        /// 复位
        /// </summary>
        /// <param name="type"></param>
        /// <param name="addr"></param>
        /// <param name="callback"></param>
        /// <returns></returns>
        public void ForceOff(DeviceType type, int addr, ModbusSL_Callback callback)
        {
            /*
                FORCE OFF 复位
                始  命令  地址   终 和校验
                STX   CMD   ADDRESS   ETX   SUM
                02H   38H   ADDRESS   03H   SUM
             */

            switch (type)
            {
                case DeviceType.S:
                    break;
                case DeviceType.X:
                    addr = addr + 1024; //0x0400
                    break;
                case DeviceType.Y:
                    addr = addr + 1280; //0x0500
                    break;
                case DeviceType.T:
                    addr = addr + 1536; //0x0600
                    break;
                case DeviceType.M:
                    addr = addr + 2048; //0x0800
                    break;
                case DeviceType.C:
                    addr = addr + 3584; //0x0E00
                    break;
            }

            //开始STX + CMD
            string zl = "02 38 ";

            //寄存器地址
            string addrStr = addr.ToString("x").ToUpper().PadLeft(4, '0');
            zl += ToASCIIHex(addrStr[2]) + " ";
            zl += ToASCIIHex(addrStr[3]) + " ";
            zl += ToASCIIHex(addrStr[0]) + " ";
            zl += ToASCIIHex(addrStr[1]) + " ";

            //结束ETX
            zl += "03 ";

            //校验CRC
            zl += CRC16(zl);

            //发送数据
            SendData(zl, callback);
        }

        /// <summary>
        /// 发送数据
        /// </summary>
        /// <param name="data"></param>
        /// <param name="callback"></param>
        public void SendData(string data, ModbusSL_Callback callback)
        {
            //检查参数
            if (_serialPort == null)
            {
                //回调
                callback?.Invoke(false, "串口未定义", null, null, null);
                return;
            }
            if (string.IsNullOrWhiteSpace(data))
            {
                //回调
                callback?.Invoke(false, "发送的数据不可为空", null, null, null);
                return;
            }

            try
            {
                //回调函数
                _callback = callback;
                //最后发送的数据
                _lastSendData = data;
                //清空回传数据
                _receiveHexData = string.Empty;
                //发送十六进制
                SendHexadecimalData(data);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        /// <summary>
        /// 发送16进制数据
        /// </summary>
        /// <param name="sendData">发送的十六进制的数据 如:01 06 01 00 00 01</param>
        private void SendHexadecimalData(string sendData)
        {
            //发送的十六进制的数据
            sendData = sendData.Replace(" ", "");

            //定义发送的字节大小除以2是因为两个字位一个字节,加2是因为RTU发送后面还要加两个字节的校验码
            byte[] sendBytes = new byte[sendData.Length / 2 + 2];
            int k = 0;

            //截取待发送的数据每次截取两位转换成10进制放到一个字节中
            for (int i = 0; i < sendData.Length - 1; i = i + 2)
            {
                //第I个字截取两个
                string str = sendData.Substring(i, 2);
                //将截取到的两个字符转换成10进制方便异或
                sendBytes[k] = Convert.ToByte(str, 16);
                k++;
            }

            //发送数据
            _serialPort.Write(sendBytes, 0, sendBytes.Length);
        }
        #endregion

        #region 转换
        /// <summary>
        /// 转为ASCII的16进制
        /// </summary>
        /// <param name="c"></param>
        /// <returns></returns>
        private string ToASCIIHex(char c)
        {
            return ((int)c).ToString("X2");
        }
        /// <summary>
        /// 生成校验码
        /// </summary>
        /// <param name="zl"></param>
        /// <returns></returns>
        private string CRC16(string zl)
        {
            int value = 0;

            string[] zlArr = zl.Trim().Substring(3).Split(' ');
            foreach (string x in zlArr)
            {
                value += Convert.ToInt32(x, 16);
            }

            var crc = value.ToString("X");
            crc = crc.Length > 2 ? crc.Substring(crc.Length - 2) : crc;

            var crcZL = "";
            crcZL += ToASCIIHex(crc[0]) + " ";
            crcZL += ToASCIIHex(crc[1]);

            return crcZL;
        }
        #endregion

        #region 处理结果
        /// <summary>
        /// 结果转整数
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        public int ResultToInt(string result)
        {
            var vals = ResultToInts(result);
            if (vals != null && vals.Length > 0) return vals[0];
            return -1;
        }
        /// <summary>
        /// 结果转整数数组
        /// </summary>
        /// <param name="result"></param>
        /// <returns></returns>
        public int[] ResultToInts(string result)
        {
            string[] values = result.Trim().Split(' ');
            int datalen = values.Length - 4;

            //结果不正确
            if (datalen % 4 > 0) return null;

            List<int> resultInts = new List<int>();

            for (int i = 1; i < datalen; i += 4)
            {
                //高低位转换
                byte[] buff = new byte[4];
                buff[0] = Convert.ToByte(values[i + 2], 16);
                buff[1] = Convert.ToByte(values[i + 3], 16);
                buff[2] = Convert.ToByte(values[i], 16);
                buff[3] = Convert.ToByte(values[i + 1], 16);

                //转10进制数据
                string hex = Encoding.Default.GetString(buff);
                int res = Convert.ToInt32(hex, 16);

                resultInts.Add(res);
            }

            return resultInts.ToArray();
        }
        #endregion

        #region DEMO
        /// <summary>
        /// 转字符
        /// </summary>
        /// <param name="asciiCode"></param>
        /// <returns></returns>
        private string Chr(int asciiCode)
        {
            if (asciiCode >= 0 && asciiCode <= 255)
            {
                System.Text.ASCIIEncoding asciiEncoding = new System.Text.ASCIIEncoding();
                byte[] byteArray = new byte[] { (byte)asciiCode };
                return asciiEncoding.GetString(byteArray);
            }
            else
            {
                throw new Exception("ASCII Code is not valid.");
            }
        }
        /// <summary>
        /// 转ASCII
        /// </summary>
        /// <param name="character"></param>
        /// <returns></returns>
        private int Asc(string character)
        {
            if (character.Length == 1)
            {
                System.Text.ASCIIEncoding asciiEncoding = new System.Text.ASCIIEncoding();
                int intAsciiCode = (int)asciiEncoding.GetBytes(character)[0];
                return (intAsciiCode);
            }
            else
            {
                throw new Exception("Character is not valid.");
            }
        }
        /// <summary>
        /// 和校验
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public string SumCheck(string data)
        {
            int sum = 0;
            for (int i = 0; i < data.Length; i++)
            {
                sum += Asc(data.Substring(i, 1));
            }

            string res = sum.ToString("X");
            res = res.Substring(res.Length - 2, 2);

            return res;
        }
        public void DeviceWriteDemo()
        {
            string[] write = new string[] { "1234", "4567" }; //将准备写入PLC的值
            //将要写入的值转换成16进制数,补齐两个字节,注意高低字节需要交换
            string sWriteData = "";
            for (int i = 0; i < write.Length; i++)
            {
                int val = Convert.ToInt32(write[i].Length > 0 ? write[i] : "0");
                string s = val.ToString("X");
                while (s.Length < 4)
                {
                    s = "0" + s;
                }
                sWriteData += s.Substring(2, 2) + s.Substring(0, 2);
            }

            //写入命令,1表示写入,1194表示D202这个地址的16进制,04表示D202,D203为4个BYTE,1194=(202*2)+4096的16进制数,至于用它表示D202的起始位置,三菱故意要这么麻烦了.
            sWriteData = "1119404" + sWriteData + Chr(3);
            //chr(2)和chr(3)是构成命令的标志字符,然后加上校验和,命令组织完成
            sWriteData = Chr(2) + sWriteData + SumCheck(sWriteData);
            //写入串口
            //com.Write(sWriteData);
        }
        private void DeviceReadDemo()
        {
            string sReadData = "";
            //在读PLC中的数据之前,需要先发个指令给它,让它将数据发送到串口,下面的字符串中,chr(2),chr(3)为PLC命令的格式标志,0119404中,0表示读,1194表示D202的起始地址,04表示读D202,D203两个字,共4个字节,66为0119404和chr(3)的校验和,向串口写入"读"命令,其实和向plc地址中写入数据是一样的,只是没有数据,用0表示读
            string sReadCmd = Chr(2) + "0119404" + Chr(3) + "66";
            //com.Write(sReadCmd);
            //等待1秒钟
            System.Threading.Thread.Sleep(1000);
            // 从串口读数据
            byte[] data = new byte[1024];
            //com.Read(data, 0, 1024);

            //如果首位为2,则表示数据有效.这里有个问题,在第二次读,第2位才为'2',第三次又是首位为2,需要再测试
            if (data[0] == 2)
            {
                string sReceiveData = System.Text.Encoding.ASCII.GetString(data);
                //解析命令,将读到的字符解析成数字,注意高低位的转换
                for (int i = 1; i < 8; i += 4)
                {
                    string sLow = sReceiveData.Substring(i, 2);
                    string sHigh = sReceiveData.Substring(i + 2, 2);
                    int res = Convert.ToInt32(sHigh, 16) + Convert.ToInt32(sLow, 16);

                    //this.txtRead0.Text += res.ToString() + ",";
                }
            }
        }
        #endregion

        /// <summary>
        /// 目标设备
        /// </summary>
        public enum DeviceType
        {
            X,
            Y,
            M,
            S,
            T,
            C,
            D
        }
    }
}

DEMO:

//实例化
ModbusSLHandler handler = new ModbusSLHandler(3, 19200, 8, 2, 1, 0);
//打开串口
handler.Open();
//读D100
handler.DeviceRead(ModbusSLHandler.DeviceType.D, 100, 1, new ModbusSL_Callback(delegate (bool success, string msg2, List<int> receiveData, string receiveHexData, string resultData)
{
    if (success)
    {
        //成功
        //receiveHexData  十六进制数据
    }
}));