1. 程式人生 > 其它 >驅動開發實戰之TcpClient

驅動開發實戰之TcpClient

場景模擬

假設你有一批非標裝置需要對接,對方提供瞭如下協議文件:

協議概述

裝置作為TCPServer,埠6666
位元組序:Little-Endian,即低地址存放低位

請求回覆

需要你主動發起讀取請求:0x01 02 03 04
裝置回覆:0x08 01 41 D6 3D 71 1A 20

引數說明

  1. 總位元組數

    (byte[0])即0x08:用於簡單的校驗

  2. 執行狀態

    (byte[1])即0x01:1為執行;其他為停止

  3. 裝置溫度

    (byte[2]-byte[5])即0x41 D6 3D 71:單精度浮點數值26.78

  4. 電機轉速

    (byte[6]-byte[7])即0x1A 20:對應16進位制無符號整型,倍率0.01值66.88

驅動開發

我們根據上面的協議,開發驅動。請先瀏覽上一篇驅動簡介

建立驅動專案

  1. 在解決方案->Drivers資料夾,右鍵新增->新建專案->C#類庫

  2. 專案名DriverSimTcpClient,放在iotgateway\Plugins\Drivers路徑下

  3. 修改Class1SimTcpClient

  4. 雙擊專案,修改配置

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
  	<TargetFramework>net6.0</TargetFramework>
  	<OutputPath>../../../IoTGateway/bin/Debug/net6.0/drivers</OutputPath>
  	<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
  	<ImplicitUsings>enable</ImplicitUsings>
  	<Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="SimpleTCP.Core" Version="1.0.4" />
  </ItemGroup>
  <ItemGroup>
  	<ProjectReference Include="..\..\PluginInterface\PluginInterface.csproj" />
  </ItemGroup>
</Project>

:::info 說明

OutputPath節點指定了生成專案的資料夾

SimpleTCP.Core是一個TCP客戶端庫(你也可以自己寫)

ProjectReference節點引用了PluginInterface專案

CopyLocalLockFileAssemblies節點可以確保你引用的nuget拷貝到driver資料夾下
:::

編寫專案程式碼

using PluginInterface;
using SimpleTCP;
using System;
using System.Text;

namespace DriverSimTcpClient
{
  [DriverSupported("SimTcpServerDevice")]
  [DriverInfoAttribute("SimTcpClient", "V1.0.0", "Copyright iotgateway© 2022-06-04")]
  public class SimTcpClient : IDriver
  {
      /// <summary>
      /// tcp客戶端
      /// </summary>
      private SimpleTcpClient? client;
      /// <summary>
      /// 快取最新的伺服器返回的原始資料
      /// </summary>
      private byte[] latestRcvData;
      #region 配置引數

      [ConfigParameter("裝置Id")]
      public Guid DeviceId { get; set; }

      [ConfigParameter("IP地址")]
      public string IpAddress { get; set; } = "127.0.0.1";

      [ConfigParameter("埠號")]
      public int Port { get; set; } = 6666;

      /// <summary>
      /// 為了演示列舉型別在web端的錄入,這裡沒用到 但是你可以拿到
      /// </summary>
      [ConfigParameter("連線型別")]
      public ConnectionType ConnectionType { get; set; } = ConnectionType.Long;

      [ConfigParameter("超時時間ms")]
      public int Timeout { get; set; } = 300;

      [ConfigParameter("最小通訊週期ms")]
      public uint MinPeriod { get; set; } = 3000;

      #endregion

      public SimTcpClient(Guid deviceId)
      {
          DeviceId = deviceId;
      }


      /// <summary>
      /// 判斷連線狀態
      /// </summary>
      public bool IsConnected
      {
          get
          {
              //客戶端物件不為空並且客戶端已連線則返回true
              return client != null && client.TcpClient.Connected;
          }
      }

      /// <summary>
      /// 進行連線
      /// </summary>
      /// <returns>連線是否成功</returns>
      public bool Connect()
      {
          try
          {
              //進行連線
              client = new SimpleTcpClient().Connect(IpAddress, Port);
              client.DataReceived += Client_DataReceived;
          }
          catch (Exception)
          {
              return false;
          }
          return IsConnected;
      }
      /// <summary>
      /// 收到服務端資料
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      private void Client_DataReceived(object? sender, Message e)
      {
          //如果收到的資料校驗正確,則放在記憶體中
          if (e.Data.Length == 8 && e.Data[0] == 0x08)
              latestRcvData = e.Data;
      }

      /// <summary>
      /// 斷開連線
      /// </summary>
      /// <returns>斷開是否成功</returns>
      public bool Close()
      {
          try
          {
              client.DataReceived -= Client_DataReceived;
              //斷開連線
              client?.Disconnect();
              return !IsConnected;
          }
          catch (Exception)
          {

              return false;
          }
      }

      /// <summary>
      /// 釋放
      /// </summary>
      public void Dispose()
      {
          try
          {
              //釋放資源
              client?.Dispose();
          }
          catch (Exception)
          {

          }
      }

      /// <summary>
      /// 傳送資料
      /// </summary>
      private byte[] sendCmd = new byte[4] { 0x01, 0x02, 0x03, 0x04 };

      /// <summary>
      /// 解析並返回
      /// </summary>
      /// <param name="ioarg">ioarg.Address為起始變數位元組編號;ioarg.ValueType為型別</param>
      /// <returns></returns>
      [Method("讀模擬裝置資料", description: "讀模擬裝置資料,開始位元組和長度")]
      public DriverReturnValueModel Read(DriverAddressIoArgModel ioarg)
      {
          var ret = new DriverReturnValueModel { StatusType = VaribaleStatusTypeEnum.Good };
          ushort startIndex;
          //判斷地址是否為整數
          if (!ushort.TryParse(ioarg.Address, out startIndex))
          {
              ret.StatusType = VaribaleStatusTypeEnum.Bad;
              ret.Message = "起始位元組編號錯誤";
              return ret;
          }
          //連線正常則進行讀取
          if (IsConnected)
          {
              try
              {
                  //傳送請求
                  client?.Write(sendCmd);
                  //等待恢復,這裡可以優化
                  Thread.Sleep(Timeout);
                  if (latestRcvData == null)
                  {
                      ret.StatusType = VaribaleStatusTypeEnum.Bad;
                      ret.Message = "沒有收到資料";
                  }
                  else
                  {
                      //解析資料,並返回
                      switch (ioarg.ValueType)
                      {
                          case DataTypeEnum.UByte:
                          case DataTypeEnum.Byte:
                              ret.Value = latestRcvData[startIndex];
                              break;
                          case DataTypeEnum.Int16:
                              var buffer16 = latestRcvData.Skip(startIndex).Take(2).ToArray();
                              ret.Value = BitConverter.ToInt16(new byte[] { buffer16[0], buffer16[1] }, 0);
                              break;
                          case DataTypeEnum.Float:
                              //拿到有用的資料
                              var buffer32 = latestRcvData.Skip(startIndex).Take(4).ToArray();
                              //大小端轉換一下
                              ret.Value = BitConverter.ToSingle(new byte[] { buffer32[3], buffer32[2], buffer32[1], buffer32[0] }, 0);
                              break;
                          default:
                              break;
                      }
                  }

              }
              catch (Exception ex)
              {

                  ret.StatusType = VaribaleStatusTypeEnum.Bad;
                  ret.Message = $"讀取失敗,{ex.Message}";
              }
          }
          else
          {
              ret.StatusType = VaribaleStatusTypeEnum.Bad;
              ret.Message = "連線失敗";
          }
          return ret;
      }


      public async Task<RpcResponse> WriteAsync(string RequestId, string Method, DriverAddressIoArgModel Ioarg)
      {
          RpcResponse rpcResponse = new() { IsSuccess = false, Description = "裝置驅動內未實現寫入功能" };
          return rpcResponse;
      }
  }

  public enum ConnectionType
  {
      Long,
      Short
  }
}

註冊驅動

  1. 生成DriverSimTcpClient 專案

    iotgateway\IoTGateway\bin\Debug\net6.0\drivers\net6.0路徑下可以看到生成了DriverSimTcpClient.dll

  2. 執行IoTGateway,訪問本地518埠
  3. 新增驅動

    閘道器配置->驅動管理->新增

:::warning 注意

新增驅動後需要重啟一下專案,後面會優化
:::

建立裝置

  1. 採集配置->裝置維護->新增裝置

新增變數

  1. 採集配置->裝置維護->新增裝置

    手動新增或者通過excel批量匯入下面變數

變數名 方法 地址 型別 表示式 裝置名
執行狀態 Read 1 uint8 模擬裝置
裝置溫度 Read 2 float 模擬裝置
電機轉速 Read 6 int16 raw*0.01 模擬裝置

開始採集

採集配置->裝置維護->編輯裝置

啟動TcpServer

執行你熟悉的TCPServer測試工具,啟動埠6666,閘道器客戶端連線後傳送響應報文

檢視資料