驅動開發實戰之TcpClient
場景模擬
假設你有一批非標裝置需要對接,對方提供瞭如下協議文件:
協議概述
裝置作為TCPServer,埠6666
位元組序:Little-Endian,即低地址存放低位
請求回覆
需要你主動發起讀取請求:0x01 02 03 04
裝置回覆:0x08 01 41 D6 3D 71 1A 20
引數說明
-
總位元組數
(byte[0])即0x08:用於簡單的校驗
-
執行狀態
(byte[1])即0x01:1為執行;其他為停止
-
裝置溫度
(byte[2]-byte[5])即0x41 D6 3D 71:單精度浮點數值26.78
-
電機轉速
(byte[6]-byte[7])即0x1A 20:對應16進位制無符號整型,倍率0.01值66.88
驅動開發
我們根據上面的協議,開發驅動。請先瀏覽上一篇驅動簡介
建立驅動專案
-
在解決方案->Drivers資料夾,右鍵新增->新建專案->C#類庫
-
專案名DriverSimTcpClient,放在
iotgateway\Plugins\Drivers
路徑下 -
修改Class1為SimTcpClient
-
雙擊專案,修改配置
<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 } }
註冊驅動
- 生成
DriverSimTcpClient
專案iotgateway\IoTGateway\bin\Debug\net6.0\drivers\net6.0
路徑下可以看到生成了DriverSimTcpClient.dll - 執行IoTGateway,訪問本地518埠
- 新增驅動
閘道器配置->驅動管理->新增
:::warning 注意
新增驅動後需要重啟一下專案,後面會優化
:::
建立裝置
- 採集配置->裝置維護->新增裝置
新增變數
- 採集配置->裝置維護->新增裝置
手動新增或者通過excel批量匯入下面變數
變數名 | 方法 | 地址 | 型別 | 表示式 | 裝置名 |
---|---|---|---|---|---|
執行狀態 | Read | 1 | uint8 | 模擬裝置 | |
裝置溫度 | Read | 2 | float | 模擬裝置 | |
電機轉速 | Read | 6 | int16 | raw*0.01 | 模擬裝置 |
開始採集
採集配置->裝置維護->編輯裝置
啟動TcpServer
執行你熟悉的TCPServer測試工具,啟動埠6666,閘道器客戶端連線後傳送響應報文