基於WebSocket的modbus通訊(二)- 客戶端

ggtc發表於2024-06-01

上一篇已經實現了ModbusTcp伺服器和8個主要的功能碼,只是還沒有實現錯誤處理功能。
但是在測試客戶端時卻發現了上一篇的一個錯誤,那就是寫資料成功,伺服器不需要響應。
接下來要做的就是實現ModbusTcp客戶端。有了清晰的協議,程式碼循規蹈矩的寫就行了。

效果

  • 原始資料
    其中只讀暫存器和線圈都有可分辨的值
    image

  • 互動
    改變線圈和暫存器的值

    • 向線圈寫入4個1
    • 向暫存器寫入4個11
    • 將每個棧的值查詢出來

image

  • 結果
    可以看到資料已變成我們設定的值
    image

客戶端解析

  • 工作流程
    • 命令列輸入指令
    • 解析指令
    • 根據功能碼跳轉到相應分支
    • 構造、傳送請求
    • 解析響應
  • 根據協議,每次發請求,事務識別符號都會自增。
  • 客戶端需要實現8種功能碼,因此每個功能碼都需要一個方法去實現。
      //WebModbus.cs
    
      // 讀 讀寫線圈
      public async Task<bool[]> Request_01(ushort startIndex, ushort length){}
      // 讀 只讀線圈
      public async Task<bool[]> Request_02(ushort startIndex, ushort length){}
      // 讀 讀寫暫存器
      public async Task<ushort[]> Request_03(ushort startIndex, ushort length){}
      // 讀 只讀暫存器
      public async Task<ushort[]> Request_04(ushort startIndex, ushort length){}
      // 寫 讀寫一個線圈
      public async Task<ADUMessage> Request_05(ushort startIndex, bool coil){}
      // 寫 讀寫一個暫存器
      public async Task<ADUMessage> Request_06(ushort startIndex, ushort register){}
      // 寫 讀寫多個線圈
      public async Task<ADUMessage> Request_0f(ushort startIndex, bool[] coils){}
      // 寫 讀寫多個暫存器
      public async Task<ADUMessage> Request_10(ushort startIndex, ushort[] registers){}
    
  • 為了便於觀察訊息,我在請求發出後和接到響應後都列印了出來。
      PrintBytes(ADUMessage.Serialze(request), "請求");
      if (Client != null)
      {
      	await Client.Client.SendAsync(new Memory<byte>(ADUMessage.Serialze(request)));
      	byte[] bytes = new byte[1024];
      	int msgLength = await Client.Client.ReceiveAsync(new ArraySegment<byte>(bytes));
      	PrintBytes(bytes.Take(msgLength).ToArray(), "響應");
      }
    
  • 線圈儲存時使用bool值,傳輸時使用bit,而且還是按位的,這需要用到位運算子。所以需要一對轉換方法
      public bool[] BytesToBools(byte[] bytes,ushort dataNumber){}
      public byte[] BoolToBytes(bool[] bools){}
    

測試類

我們還需要一個介面區使用這個協議,所以還需要一個測試類。
命令列程式的話,就是使用while迴圈了,在迴圈中接收指令

private static async Task StartClient(string[] args)
{
    //其他程式碼...
	
    while (true)
    {
        Console.WriteLine("請輸入指令");
        string? msg = Console.ReadLine();
        while (msg == null)
        {
            //功能碼 資料
            msg = Console.ReadLine();
        }
        try
        {
            string[] data = msg.Split(' ');
            ushort funCode = ushort.Parse(data[0],NumberStyles.HexNumber);
            ushort startIndex;
            ushort length;
            switch (funCode)
            {
                //讀 讀寫線圈
                case 0x01:
                    startIndex = ushort.Parse(data[1]);
                    length= ushort.Parse(data[2]);
                    var rs_01 = await webModbusClient.Request_01(startIndex, length);
                    PrintBools(rs_01);
                    break;
                //讀 只讀線圈
                case 0x02:
                    startIndex = ushort.Parse(data[1]);
                    length = ushort.Parse(data[2]);
                    var rs_02 = await webModbusClient.Request_02(startIndex, length);
                    PrintBools(rs_02);
                    break;
                //讀 讀寫暫存器
                case 0x03:
                    startIndex = ushort.Parse(data[1]);
                    length = ushort.Parse(data[2]);
                    var rs_03 = await webModbusClient.Request_03(startIndex, length);
                    for (global::System.Int32 i = 0; i < length; i++)
                    {
                        Console.Write(rs_03[i]+" ");
                    }
                    Console.WriteLine();
                    break;
                //讀 只讀暫存器
                case 0x04:
                    startIndex = ushort.Parse(data[1]);
                    length = ushort.Parse(data[2]);
                    var rs_04 = await webModbusClient.Request_04(startIndex, length);
                    for (global::System.Int32 i = 0; i < length; i++)
                    {
                        Console.Write(rs_04[i] + " ");
                    }
                    Console.WriteLine();
                    break;
                //寫 讀寫一個線圈
                case 0x05:
                    startIndex = ushort.Parse(data[1]);
                    var coil = bool.Parse(data[2]);
                    var rs_05 = await webModbusClient.Request_05(startIndex, coil);
                    break;
                //寫 讀寫一個暫存器
                case 0x06:
                    startIndex = ushort.Parse(data[1]);
                    var register = ushort.Parse(data[2]);
                    var rs_06 = await webModbusClient.Request_06(startIndex, register);
                    break;
                //寫 讀寫多個線圈
                case 0x0f:
                    startIndex = ushort.Parse(data[1]);
                    bool[] coils = new bool[data.Length - 2];
                    for (global::System.Int32 i = 2; i < data.Length; i++)
                    {
                        coils[i - 2] = bool.Parse(data[i]);
                    }
                    var rs_0f = await webModbusClient.Request_0f(startIndex, coils);
                    break;
                //寫 讀寫多個暫存器
                case 0x10:
                    startIndex = ushort.Parse(data[1]);
                    ushort[] registers = new ushort[data.Length - 2];
                    for (global::System.Int32 i = 2; i < data.Length; i++)
                    {
                        registers[i - 2] = ushort.Parse(data[i]);
                    }
                    var rs_10 = await webModbusClient.Request_10(startIndex, registers);
                    break;
                default:
                    //return Response_01(request);
                    break;
            }
        }
        catch (Exception e)
        {

        }
    }
}

完整程式碼

WebModbus.cs
/// <summary>
/// 資料倉儲,144KB
/// </summary>
public class DataStore
{
    /// <summary>
    /// 讀寫16位暫存器,64KB
    /// </summary>
    public ushort[] HoldingRegisters;
    /// <summary>
    /// 只讀16位暫存器,64KB
    /// </summary>
    public ushort[] InputRegisters;
    /// <summary>
    /// 讀寫1位線圈,8KB
    /// </summary>
    public bool[] CoilDiscretes;
    /// <summary>
    /// 只讀1位線圈,8KB
    /// </summary>
    public bool[] CoilInputs;

    public DataStore()
    {
        HoldingRegisters = new ushort[65536];
        InputRegisters = new ushort[65536];
        CoilDiscretes = new bool[65536];
        CoilInputs = new bool[65536];
    }



    /// <summary>
    /// 讀 讀寫16位暫存器
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public ushort[] ReadHoldingRegisters(ushort startIndex, ushort length)
    {
        return HoldingRegisters.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
    }
    /// <summary>
    /// 讀 只讀16位暫存器
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public ushort[] ReadInputRegisters(ushort startIndex, ushort length)
    {
        return InputRegisters.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
    }
    /// <summary>
    /// 讀 讀寫1位線圈
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public bool[] ReadCoilDiscretes(ushort startIndex, ushort length)
    {
        return CoilDiscretes.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
    }
    /// <summary>
    /// 讀 只讀1位線圈
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public bool[] ReadCoilInputs(ushort startIndex, ushort length)
    {
        return CoilInputs.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
    }
    /// <summary>
    /// 寫 讀寫16位暫存器
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="data"></param>
    public void WriteHoldingRegisters(ushort startIndex, ushort[] data)
    {
        for (int i = 0; i < data.Length; i++)
        {
            if (startIndex+i < 65536)
            {
                HoldingRegisters[startIndex + i] = data[i];
            }
        }
    }
    /// <summary>
    /// 寫 讀寫1位線圈
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="data"></param>
    public void WriteCoilDiscretes(ushort startIndex, bool[] data)
    {
        for (int i = 0; i < data.Length; i++)
        {
            if (startIndex + i < 65536)
            {
                CoilDiscretes[startIndex + i] = data[i];
            }
        }
    }
}

/// <summary>
/// Modbus報文
/// </summary>
public class ADUMessage
{
    /// <summary>
    /// 事務識別符號
    /// </summary>
    public ushort Transaction { get; set; }
    /// <summary>
    /// 協議識別符號
    /// </summary>
    public ushort Protocol { get; set; }
    /// <summary>
    /// 報文長度
    /// </summary>
    public ushort Length { get; set; }
    /// <summary>
    /// 單元識別符號
    /// </summary>
    public byte Unit { get; set; }
    /// <summary>
    /// 功能碼
    /// </summary>
    public byte FunctionCode { get; set; }
    /// <summary>
    /// 資料
    /// </summary>
    public byte[] Data { get; set; }

    public static ADUMessage Deserialize(byte[] buffer) 
    {
        //BinaryReader讀取方式是小端(右邊是高位元組),而modbus是大端傳輸(左邊是高位元組)
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(buffer));
        ADUMessage adu = new ADUMessage()
        {
            Transaction = reader.ReadUInt16(),
            Protocol = reader.ReadUInt16(),
            Length = reader.ReadUInt16(),
            Unit = reader.ReadByte(),
            FunctionCode = reader.ReadByte(),
            Data = reader.ReadBytes(buffer.Length - 8)
        };
        return adu;
    }

    public static byte[] Serialze(ADUMessage message)
    {
        using (MemoryStream ms=new MemoryStream())
        {
            BinaryWriter writer = new BigEndianBinaryWriter(ms);
            writer.Write(message.Transaction);
            writer.Write(message.Protocol);
            writer.Write(message.Length);
            writer.Write(message.Unit);
            writer.Write(message.FunctionCode);
            writer.Write(message.Data);
            return ms.ToArray();
        }
    }
}

/// <summary>
/// Modbus伺服器
/// </summary>
public class WebModbusServer
{
    public DataStore store = new DataStore();

    public ADUMessage HandleRequest(byte[] buffer)
    {
        ADUMessage request = ADUMessage.Deserialize(buffer);
        switch (request.FunctionCode)
        {
            //讀 讀寫線圈
            case 0x01:
                return Response_01(request);
            //讀 只讀線圈
            case 0x02:
                return Response_02(request);
            //讀 讀寫暫存器
            case 0x03:
                return Response_03(request);
            //讀 只讀暫存器
            case 0x04:
                return Response_04(request);
            //寫 讀寫一個線圈
            case 0x05:
                return Response_05(request);
            //寫 讀寫一個暫存器
            case 0x06:
                return Response_06(request);
            //寫 讀寫多個線圈
            case 0x0f:
                return Response_0f(request);
            //寫 讀寫多個暫存器
            case 0x10:
                return Response_10(request);
            default:
                return Response_01(request);
        }
    }

    public byte[] CoilToBytes(bool[] bools)
    {
        int byteCount = (bools.Length + 7) / 8; // 計算所需的位元組數
        byte[] bytes = new byte[byteCount];

        for (int i = 0; i < bools.Length; i++)
        {
            int byteIndex = i / 8; // 計算當前布林值應該儲存在哪個位元組中
            int bitIndex = i % 8; // 計算當前布林值應該儲存在位元組的哪個位上

            if (bools[i])
            {
                // 設定對應位為 1
                bytes[byteIndex] |= (byte)(1 << bitIndex);
            }
            else
            {
                // 對應位保持為 0,無需額外操作
            }
        }

        return bytes;
    }

    /// <summary>
    /// 讀 讀寫線圈
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_01(ADUMessage request)
    {
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        BinaryWriter writer;
        ushort StartAddress, DataNumber;
        StartAddress = reader.ReadUInt16();
        DataNumber = reader.ReadUInt16();
        bool[] data = store.ReadCoilDiscretes(StartAddress, DataNumber);
        byte[] coilBytes = CoilToBytes(data);
        byte[] dataBytes = new byte[coilBytes.Length + 1];
        writer = new BinaryWriter(new MemoryStream(dataBytes));
        writer.Write((byte)coilBytes.Length);
        writer.Write(coilBytes);
        ADUMessage response = new ADUMessage()
        {
            Transaction = request.Transaction,
            Protocol = request.Protocol,
            Length = (ushort)(dataBytes.Length + 2),
            Unit = request.Unit,
            FunctionCode = request.FunctionCode,
            Data = dataBytes,
        };
        return response;
    }

    /// <summary>
    /// 讀 只讀線圈
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_02(ADUMessage request)
    {
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        BinaryWriter writer;
        ushort StartAddress, DataNumber;
        StartAddress = reader.ReadUInt16();
        DataNumber = reader.ReadUInt16();
        bool[] data = store.ReadCoilInputs(StartAddress, DataNumber);
        byte[] coilBytes = CoilToBytes(data);
        byte[] dataBytes = new byte[coilBytes.Length + 1];
        writer = new BinaryWriter(new MemoryStream(dataBytes));
        writer.Write((byte)coilBytes.Length);
        writer.Write(coilBytes);
        ADUMessage response = new ADUMessage()
        {
            Transaction = request.Transaction,
            Protocol = request.Protocol,
            Length = (ushort)(dataBytes.Length + 2),
            Unit = request.Unit,
            FunctionCode = request.FunctionCode,
            Data = dataBytes,
        };
        return response;
    }

    /// <summary>
    /// 讀 讀寫暫存器
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_03(ADUMessage request)
    {
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        BinaryWriter writer;
        ushort StartAddress, DataNumber;
        StartAddress = reader.ReadUInt16();
        DataNumber = reader.ReadUInt16();
        ushort[] data = store.ReadHoldingRegisters(StartAddress, DataNumber);
        byte[] dataBytes = new byte[data.Length * 2 + 1];
        writer = new BigEndianBinaryWriter(new MemoryStream(dataBytes));
        writer.Write((byte)(data.Length * 2));
        foreach (ushort value in data)
        {
            writer.Write(value);
        }
        Array.Resize(ref dataBytes, dataBytes.Length + 1);
        ADUMessage response = new ADUMessage()
        {
            Transaction = request.Transaction,
            Protocol = request.Protocol,
            Length = (ushort)(dataBytes.Length + 2),
            Unit = request.Unit,
            FunctionCode = request.FunctionCode,
            Data = dataBytes,
        };
        return response;
    }

    /// <summary>
    /// 讀 只讀暫存器
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_04(ADUMessage request)
    {
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        BinaryWriter writer;
        ushort StartAddress, DataNumber;
        StartAddress = reader.ReadUInt16();
        DataNumber = reader.ReadUInt16();
        ushort[] data = store.ReadInputRegisters(StartAddress, DataNumber);
        byte[] dataBytes = new byte[data.Length * 2 + 1];
        writer = new BigEndianBinaryWriter(new MemoryStream(dataBytes));
        writer.Write((byte)(data.Length * 2));
        foreach (ushort value in data)
        {
            writer.Write(value);
        }
        Array.Resize(ref dataBytes, dataBytes.Length + 1);
        ADUMessage response = new ADUMessage()
        {
            Transaction = request.Transaction,
            Protocol = request.Protocol,
            Length = (ushort)(dataBytes.Length + 2),
            Unit = request.Unit,
            FunctionCode = request.FunctionCode,
            Data = dataBytes,
        };
        return response;
    }

    /// <summary>
    /// 寫 讀寫一個線圈
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_05(ADUMessage request)
    {
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        ushort StartAddress, coli;
        StartAddress = reader.ReadUInt16();
        coli = reader.ReadUInt16();
        store.WriteCoilDiscretes(StartAddress, new bool[] { coli ==0xff00?true:false});
        return request;
    }

    /// <summary>
    /// 寫 讀寫一個暫存器
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_06(ADUMessage request)
    {
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        ushort StartAddress, register;
        StartAddress = reader.ReadUInt16();
        register = reader.ReadUInt16();
        store.WriteHoldingRegisters(StartAddress, new ushort[] { register });
        return request;
    }

    /// <summary>
    /// 寫 讀寫多個線圈
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_0f(ADUMessage request)
    {
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        ushort StartAddress, DataNumber;
        StartAddress = reader.ReadUInt16();
        DataNumber = reader.ReadUInt16();
        byte byteNumber = reader.ReadByte();
        //線圈是小端傳輸
        byte[] bytes = reader.ReadBytes(byteNumber);
        bool[] data=new bool[DataNumber];
        byte index = 0;
        foreach (var item in bytes)
        {
            //1000 0000
            byte rr = (byte)0x01;
            for (int i = 0; i < 8; i++)
            {
                if (index< DataNumber)
                {
                    var result = rr & item;
                    if (result > 0)
                    {
                        data[index] = true;
                    }
                    else
                    {
                        data[index] = false;
                    }
                    //0100 0000
                    rr <<= 1;
                    index++;
                }
                else
                {
                    break;
                }
            }
        }
        store.WriteCoilDiscretes(StartAddress, data);
        return request;
    }

    /// <summary>
    /// 寫 讀寫多個暫存器
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private ADUMessage Response_10(ADUMessage request)
    {
        //暫存器是大端傳輸
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
        ushort StartAddress, DataNumber;
        StartAddress = reader.ReadUInt16();
        DataNumber = reader.ReadUInt16();
        byte byteNumber = reader.ReadByte();
        ushort[] data = new ushort[byteNumber / 2];
        for (int i = 0; i < data.Length; i++)
        {
            data[i] = reader.ReadUInt16();
        }
        store.WriteHoldingRegisters(StartAddress, data);
        return request;
    }
}

/// <summary>
/// Modbus客戶端
/// </summary>
public class WebModbusClient
{
    public ushort Transaction { get; set; }
    public TcpClient Client { get; }
    public WebSocket WebSocket { get; set; }
    public ADUMessage request { get; set; }
    public ADUMessage response { get; set; }

    public WebModbusClient(TcpClient client)
    {
        Transaction = 0x00;
        Client = client;
    }

    public WebModbusClient(WebSocket webSocket)
    {
        Transaction = 0x00;
        WebSocket = webSocket;
    }

    private ADUMessage CreateMsg()
    {
        ADUMessage message = new ADUMessage();
        message.Transaction = Transaction;
        Transaction++;
        message.Protocol = 0x00;
        message.Unit = 0x00;
        this.request = message;
        return message;
    }
    public void PrintBytes(byte[] bytes, string prefix = "")
    {
        Console.Write(prefix);
        for (int i = 0; i < bytes.Length; i++)
        {
            if (i < 2)
            {
                Console.ForegroundColor = ConsoleColor.Red;
            }
            else if (i < 4)
            {
                Console.ForegroundColor = ConsoleColor.Green;
            }
            else if (i < 6)
            {
                Console.ForegroundColor = ConsoleColor.Blue;
            }
            else if (i < 7)
            {
                Console.ForegroundColor = ConsoleColor.Yellow;
            }
            else if (i < 8)
            {
                Console.ForegroundColor = ConsoleColor.DarkCyan;
            }
            else
            {
                Console.ForegroundColor = ConsoleColor.White;
            }
            Console.Write(bytes[i].ToString("X2") + " ");
        }
        Console.WriteLine();
    }
    public bool[] BytesToBools(byte[] bytes,ushort dataNumber)
    {
        int index = 0;
        bool[] bools = new bool[dataNumber];
        foreach (var item in bytes)
        {
            //1000 0000
            byte rr = (byte)0x01;
            for (int i = 0; i < 8; i++)
            {
                if (index < dataNumber)
                {
                    var result = rr & item;
                    if (result > 0)
                    {
                        bools[index] = true;
                    }
                    else
                    {
                        bools[index] = false;
                    }
                    //0100 0000
                    rr <<= 1;
                    index++;
                }
                else
                {
                    break;
                }
            }
        }
        return bools;
    }

    private async Task<ADUMessage> SendWithResponse(ADUMessage request)
    {
        PrintBytes(ADUMessage.Serialze(request), "請求");
        if (Client != null)
        {
            await Client.Client.SendAsync(new Memory<byte>(ADUMessage.Serialze(request)));
            byte[] bytes = new byte[1024];
            int msgLength = await Client.Client.ReceiveAsync(new ArraySegment<byte>(bytes));
            this.response = ADUMessage.Deserialize(bytes.Take(msgLength).ToArray());
            PrintBytes(bytes.Take(msgLength).ToArray(), "響應");
            return response;
        }
        else if(WebSocket != null)
        {
            await WebSocket.SendAsync(new ArraySegment<byte>(ADUMessage.Serialze(request)),WebSocketMessageType.Binary,true,CancellationToken.None);
            byte[] bytes = new byte[1024];
            var result = await WebSocket.ReceiveAsync(new ArraySegment<byte>(bytes),CancellationToken.None);
            this.response = ADUMessage.Deserialize(bytes.Take(result.Count).ToArray());
            PrintBytes(bytes.Take(result.Count).ToArray(), "響應");
            return response;
        }
        else
        {
            throw new Exception("沒有傳入連線");
        }
    }
    private async Task SendNoResponse(ADUMessage request)
    {
        PrintBytes(ADUMessage.Serialze(request), "請求");
        if (Client != null)
        {
            await Client.Client.SendAsync(new Memory<byte>(ADUMessage.Serialze(request)));
        }
        else if (WebSocket != null)
        {
            await WebSocket.SendAsync(new ArraySegment<byte>(ADUMessage.Serialze(request)), WebSocketMessageType.Binary, true, CancellationToken.None);
        }
        else
        {
            throw new Exception("沒有傳入連線");
        }
    }

    public byte[] BoolToBytes(bool[] bools)
    {
        int byteCount = (bools.Length + 7) / 8; // 計算所需的位元組數
        byte[] bytes = new byte[byteCount];

        for (int i = 0; i < bools.Length; i++)
        {
            int byteIndex = i / 8; // 計算當前布林值應該儲存在哪個位元組中
            int bitIndex = i % 8; // 計算當前布林值應該儲存在位元組的哪個位上

            if (bools[i])
            {
                // 設定對應位為 1
                bytes[byteIndex] |= (byte)(1 << bitIndex);
            }
            else
            {
                // 對應位保持為 0,無需額外操作
            }
        }

        return bytes;
    }

    /// <summary>
    /// 讀 讀寫線圈
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public async Task<bool[]> Request_01(ushort startIndex, ushort length)
    {
        var request = CreateMsg();
        request.Length = 0x06;
        request.FunctionCode= 0x01;
        request.Data = new byte[4];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write(startIndex);
        writer.Write(length);
        var response = await SendWithResponse(request);
        BinaryReader reader = new BinaryReader(new MemoryStream(response.Data));
        byte byteLength=reader.ReadByte();
        byte[] bytes = reader.ReadBytes(byteLength);
        bool[] bools= BytesToBools(bytes,length);
        return bools;
    }

    /// <summary>
    /// 讀 只讀線圈
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public async Task<bool[]> Request_02(ushort startIndex, ushort length)
    {
        var request = CreateMsg();
        request.Length = 0x06;
        request.FunctionCode = 0x02;
        request.Data = new byte[4];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write(startIndex);
        writer.Write(length);
        var response = await SendWithResponse(request);
        BinaryReader reader = new BinaryReader(new MemoryStream(response.Data));
        byte byteLength = reader.ReadByte();
        byte[] bytes = reader.ReadBytes(byteLength);
        bool[] bools = BytesToBools(bytes, length);
        return bools;
    }

    /// <summary>
    /// 讀 讀寫暫存器
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public async Task<ushort[]> Request_03(ushort startIndex, ushort length)
    {
        var request = CreateMsg();
        request.Length = 0x06;
        request.FunctionCode = 0x03;
        request.Data = new byte[4];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write(startIndex);
        writer.Write(length);
        var response = await SendWithResponse(request);
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(response.Data));
        byte byteLength = reader.ReadByte();
        ushort[] registers = new ushort[length];
        for (int i = 0; i < length; i++)
        {
            registers[i] = reader.ReadUInt16();
        }
        return registers;
    }

    /// <summary>
    /// 讀 只讀暫存器
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="length"></param>
    /// <returns></returns>
    public async Task<ushort[]> Request_04(ushort startIndex, ushort length)
    {
        var request = CreateMsg();
        request.Length = 0x06;
        request.FunctionCode = 0x04;
        request.Data = new byte[4];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write(startIndex);
        writer.Write(length);
        var response = await SendWithResponse(request);
        BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(response.Data));
        byte byteLength = reader.ReadByte();
        ushort[] registers = new ushort[length];
        for (int i = 0; i < registers.Length; i++)
        {
            registers[i] = reader.ReadUInt16();
        }
        return registers;
    }

    /// <summary>
    /// 寫 讀寫一個線圈
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="coil"></param>
    /// <returns></returns>
    public async Task<ADUMessage> Request_05(ushort startIndex, bool coil)
    {
        var request = CreateMsg();
        request.Length = 0x06;
        request.FunctionCode = 0x05;
        request.Data = new byte[4];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write(startIndex);
        if (coil)
        {
            writer.Write((ushort)0xff00);
        }
        else
        {
            writer.Write((ushort)0x0000);
        }
        await SendNoResponse(request);
        return request;
    }

    /// <summary>
    /// 寫 讀寫一個暫存器
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="register"></param>
    /// <returns></returns>
    public async Task<ADUMessage> Request_06(ushort startIndex, ushort register)
    {
        var request = CreateMsg();
        request.Length = 0x06;
        request.FunctionCode = 0x06;
        request.Data = new byte[4];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write(startIndex);
        writer.Write(register);
        await SendNoResponse(request);
        return request;
    }

    /// <summary>
    /// 寫 讀寫多個線圈
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="coils"></param>
    /// <returns></returns>
    public async Task<ADUMessage> Request_0f(ushort startIndex, bool[] coils)
    {
        var request = CreateMsg();
        request.FunctionCode = 0x0f;
        request.Data = new byte[4+1+(coils.Length+7)/8];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write((ushort)startIndex);
        var coilBytes = BoolToBytes(coils);
        request.Length = (ushort)(7 + coilBytes.Length);
        writer.Write((ushort)coils.Length);
        writer.Write((byte)coilBytes.Length);
        writer.Write(coilBytes);
        await SendNoResponse(request);
        return request;
    }

    /// <summary>
    /// 寫 讀寫多個暫存器
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="registers"></param>
    /// <returns></returns>
    public async Task<ADUMessage> Request_10(ushort startIndex, ushort[] registers)
    {
        var request = CreateMsg();
        request.Length = (ushort)(7+ registers.Length * 2);
        request.FunctionCode = 0x10;
        request.Data = new byte[4+1+registers.Length*2];
        BinaryWriter writer = new BigEndianBinaryWriter(new MemoryStream(request.Data));
        writer.Write((ushort)startIndex);
        writer.Write((ushort)registers.Length);
        writer.Write((byte)(registers.Length * 2));
        for (int i = 0; i < registers.Length; i++)
        {
            writer.Write(registers[i]);
        }
        await SendNoResponse(request);
        return request;
    }
}
Program.cs
    internal class Program
    {
        static WebModbusServer webModbusServer;
        static void Main(string[] args)
        {
            webModbusServer = new WebModbusServer();
            //伺服器
            if (args.Length == 1)
            {
                //webModbusServer.store.WriteCoilDiscretes(0, new bool[] { true, true });
                //webModbusServer.store.CoilInputs[0] = true;
                //webModbusServer.store.CoilInputs[1] = true;
                StartServer(args[0]);
            }
            //客戶端
            else
            {
                Task.Run(async () =>
                {
                    await StartClient(args);
                }).Wait();
            }
        }

        private static void StartServer(string args)
        {

            int serverPort = Convert.ToInt32(args);
            var server = new TcpListener(IPAddress.Parse("127.0.0.1"), serverPort);
            Console.WriteLine($"TCP伺服器  127.0.0.1:{serverPort}");
            server.Start();
            int cnt = 0;
            Task.Run(async () =>
            {
                List<TcpClient> clients = new List<TcpClient>();
                while (true)
                {
                    TcpClient client = await server.AcceptTcpClientAsync();
                    clients.Add(client);
                    cnt++;
                    var ep = client.Client.RemoteEndPoint as IPEndPoint;
                    Console.WriteLine($"TCP客戶端_{cnt}  {ep.Address}:{ep.Port}");
                    //給這個客戶端開一個聊天執行緒
                    //作業系統將會根據遊客埠對應表將控制權交給對應遊客執行緒
                    //StartChat(client);
                    StartModbus(client);
                }
            }).Wait();
        }

        private static async Task StartClient(string[] args)
        {
            int clientPort = Convert.ToInt32(args[0]);
            int serverPort = Convert.ToInt32(args[1]);
            var client = new TcpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), clientPort));
            Console.WriteLine($"TCP客戶端  127.0.0.1:{clientPort}");
            await client.ConnectAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), serverPort));
            Console.WriteLine($"連線到 127.0.0.1:{serverPort}");
            WebModbusClient webModbusClient = new WebModbusClient(client);
            Console.WriteLine("【功能碼】 【地址】 【數量|資料】");
            while (true)
            {
                Console.WriteLine("請輸入指令");
                string? msg = Console.ReadLine();
                while (msg == null)
                {
                    //功能碼 資料
                    msg = Console.ReadLine();
                }
                try
                {
                    string[] data = msg.Split(' ');
                    ushort funCode = ushort.Parse(data[0],NumberStyles.HexNumber);
                    ushort startIndex;
                    ushort length;
                    switch (funCode)
                    {
                        //讀 讀寫線圈
                        case 0x01:
                            startIndex = ushort.Parse(data[1]);
                            length= ushort.Parse(data[2]);
                            var rs_01 = await webModbusClient.Request_01(startIndex, length);
                            PrintBools(rs_01);
                            break;
                        //讀 只讀線圈
                        case 0x02:
                            startIndex = ushort.Parse(data[1]);
                            length = ushort.Parse(data[2]);
                            var rs_02 = await webModbusClient.Request_02(startIndex, length);
                            PrintBools(rs_02);
                            break;
                        //讀 讀寫暫存器
                        case 0x03:
                            startIndex = ushort.Parse(data[1]);
                            length = ushort.Parse(data[2]);
                            var rs_03 = await webModbusClient.Request_03(startIndex, length);
                            for (global::System.Int32 i = 0; i < length; i++)
                            {
                                Console.Write(rs_03[i]+" ");
                            }
                            Console.WriteLine();
                            break;
                        //讀 只讀暫存器
                        case 0x04:
                            startIndex = ushort.Parse(data[1]);
                            length = ushort.Parse(data[2]);
                            var rs_04 = await webModbusClient.Request_04(startIndex, length);
                            for (global::System.Int32 i = 0; i < length; i++)
                            {
                                Console.Write(rs_04[i] + " ");
                            }
                            Console.WriteLine();
                            break;
                        //寫 讀寫一個線圈
                        case 0x05:
                            startIndex = ushort.Parse(data[1]);
                            var coil = bool.Parse(data[2]);
                            var rs_05 = await webModbusClient.Request_05(startIndex, coil);
                            break;
                        //寫 讀寫一個暫存器
                        case 0x06:
                            startIndex = ushort.Parse(data[1]);
                            var register = ushort.Parse(data[2]);
                            var rs_06 = await webModbusClient.Request_06(startIndex, register);
                            break;
                        //寫 讀寫多個線圈
                        case 0x0f:
                            startIndex = ushort.Parse(data[1]);
                            bool[] coils = new bool[data.Length - 2];
                            for (global::System.Int32 i = 2; i < data.Length; i++)
                            {
                                coils[i - 2] = bool.Parse(data[i]);
                            }
                            var rs_0f = await webModbusClient.Request_0f(startIndex, coils);
                            break;
                        //寫 讀寫多個暫存器
                        case 0x10:
                            startIndex = ushort.Parse(data[1]);
                            ushort[] registers = new ushort[data.Length - 2];
                            for (global::System.Int32 i = 2; i < data.Length; i++)
                            {
                                registers[i - 2] = ushort.Parse(data[i]);
                            }
                            var rs_10 = await webModbusClient.Request_10(startIndex, registers);
                            break;
                        default:
                            //return Response_01(request);
                            break;
                    }
                }
                catch (Exception e)
                {

                }
            }
        }

        public static async Task StartModbus(TcpClient client)
        {
            var buffer = new byte[1024 * 4];
            while (client.Connected)
            {
                int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
                //關閉連線時會接收到一次空訊息,不知道為什麼
                if (msgLength>0)
                {
                    PrintBytes(buffer.Take(msgLength).ToArray(), "請求 ");
                    ADUMessage response = webModbusServer.HandleRequest(buffer.Take(msgLength).ToArray());
                    await client.Client.SendAsync(ADUMessage.Serialze(response));
                    PrintBytes(ADUMessage.Serialze(response), "響應 ");
                }
            }
        }

        public static void PrintBytes(byte[] bytes,string prefix="")
        {
            Console.Write(prefix);
            for (int i = 0; i < bytes.Length; i++)
            {
                if (i < 2)
                {
                    Console.ForegroundColor = ConsoleColor.Red;
                }
                else if(i<4)
                {
                    Console.ForegroundColor = ConsoleColor.Green;
                }
                else if(i<6)
                {
                    Console.ForegroundColor= ConsoleColor.Blue;
                }
                else if (i < 7)
                {
                    Console.ForegroundColor = ConsoleColor.Yellow;
                }
                else if (i<8)
                {
                    Console.ForegroundColor = ConsoleColor.DarkCyan;
                }
                else
                {
                    Console.ForegroundColor = ConsoleColor.White;
                }
                Console.Write(bytes[i].ToString("X2") + " ");
            }
            Console.WriteLine();
        }
        public static void PrintBools(bool[] bools)
        {
            for (int i = 0; i < bools.Length; i++)
            {
                Console.Write(bools[i] + " ");
            }
            Console.WriteLine();
        }
    }
BigEndianBinaryReader.cs
public class BigEndianBinaryReader : BinaryReader
{
    public BigEndianBinaryReader(Stream input) : base(input)
    {
    }

    public override short ReadInt16()
    {
        var data = base.ReadBytes(2);
        Array.Reverse(data);
        return BitConverter.ToInt16(data, 0);
    }

    public override ushort ReadUInt16()
    {
        var data = base.ReadBytes(2);
        Array.Reverse(data);
        return BitConverter.ToUInt16(data, 0);
    }

    public override int ReadInt32()
    {
        var data = base.ReadBytes(4);
        Array.Reverse(data);
        return BitConverter.ToInt32(data, 0);
    }

    public override uint ReadUInt32()
    {
        var data = base.ReadBytes(4);
        Array.Reverse(data);
        return BitConverter.ToUInt32(data, 0);
    }

    public override long ReadInt64()
    {
        var data = base.ReadBytes(8);
        Array.Reverse(data);
        return BitConverter.ToInt64(data, 0);
    }

    public override float ReadSingle()
    {
        var data = base.ReadBytes(4);
        Array.Reverse(data);
        return BitConverter.ToSingle(data, 0);
    }

    public override double ReadDouble()
    {
        var data = base.ReadBytes(8);
        Array.Reverse(data);
        return BitConverter.ToDouble(data, 0);
    }

    // 可以繼續新增其他方法來支援更多資料型別的大端讀取
}
public class BigEndianBinaryWriter : BinaryWriter
{
    public BigEndianBinaryWriter(Stream input) : base(input)
    {
    }

    public override void Write(ushort value)
    {
        var bytes = BitConverter.GetBytes(value);
        Array.Reverse(bytes);
        base.Write(bytes);
    }

    public override void Write(short value)
    {
        var bytes = BitConverter.GetBytes(value);
        Array.Reverse(bytes);
        base.Write(bytes);
    }

    public override void Write(uint value)
    {
        var bytes = BitConverter.GetBytes(value);
        Array.Reverse(bytes);
        base.Write(bytes);
    }

    public override void Write(int value)
    {
        var bytes = BitConverter.GetBytes(value);
        Array.Reverse(bytes);
        base.Write(bytes);
    }

    public override void Write(ulong value)
    {
        var bytes = BitConverter.GetBytes(value);
        Array.Reverse(bytes);
        base.Write(bytes);
    }

    public override void Write(long value)
    {
        var bytes = BitConverter.GetBytes(value);
        Array.Reverse(bytes);
        base.Write(bytes);
    }



    // 可以繼續新增其他方法來支援更多資料型別的大端寫入
}

程式所需命令列引數的一種方式是在專案檔案種指定,這在除錯時比較方便

<PropertyGroup>
	<StartArguments>5234</StartArguments>
</PropertyGroup>

可以注意到ModbusTcp訊息的解析和Tcp沒有什麼關係。因此,驗證了伺服器和客戶端的正確性之後,就可以把Tcp連線改為WebSocket連線了。

相關文章