开发者中心 开发者中心
  • 简体中文
  • English
视频教程
敢为云网站
  • 6.0版本
  • 6.1 版本
视频教程
敢为云网站
  • 平台概述
  • 平台功能
  • 平台安装
  • 开发者指南
    • 协议插件开发
      • 原理解析
      • 开发第一个插件
        • 开发工具&环境
        • 开发流程
          • 1. 新建项目
          • 2. 创建入口类
          • 3. 示例代码
          • 4. 配置设备
          • 5. 附加调试
        • 如何高效采集设备数据
        • HTTP/HTTPS 接口调用
          • 1. 基础学习
          • 2. 开发天气协议插件
          • 3.集成调试
        • 常见问题
      • 进阶
      • Java协议插件开发实践
      • Python协议插件开发实践
      • GWDataCenter常用接口API
    • 扩展插件开发
    • 报警插件开发
    • 应用插件开发
    • Web可视化开发
    • 3D可视化开发
    • 桌面可视化开发
    • 小程序开发
    • 应用模块接口
  • 项目实战
  • 附录

协议插件开发

# 开发工具&环境

  • Visual Studio 2022
  • .NET Framework 6.0

# 开发流程

下面介绍开发一个协议插件的流程

注意

  1. 前置条件:本地IoTCenter平台已经部署完成。能够正常登录。
  2. 开源Demo地址:
 https://gitee.com/ganweicloud/GWDriverDemo.STD

# 1. 新建项目

操作步骤
  1. 首先在本地文件夹中,创建命名为 GwDllSample 的文件夹,并在该文件夹下创建src子文件夹。

  2. 打开 Visual Studio 选择 新建项目,创建类库项目,其类型为面向.NET或.NET Standard的项目,其文件命名为GwDllSample.STD。将该文件保存在上述src文件夹下。

# 2. 创建入口类

创建CEquip类
  1. 在该项目文件中,创建入口类,其类名为CEquip,由于VisualStudio在创建类库项目时已经默认创建了Class1的类文件,该文件改名为CEquip。

  2. 在项目文件右击,选择NuGet包管理器,添加ganweisoft的程序包源服务器,并安装GWDataCenter包,包版本为所部署的IoTCenter平台bin目录下的GWDataCenter.dll的版本。

  3. 安装GWDataCenter包。

  4. 继承CEquipBase类

注意

CEquip 类为固定命名必须按规范,如有任何不同,都可能导致程序无法正常加载。

# 3. 示例代码

示例代码为根据设备配置的最大值与最小值获取随机数

示例代码
  1. 将示例代码覆盖到CEquip类文件中。
using GWDataCenter;
using GWDataCenter.Database;

namespace GwDllSample
{
   public class CEquip : CEquipBase
   {

       /// <summary>
       /// 设备通信间隔时间
       /// </summary>
       private int _sleepInterval = 0;
       /// <summary>
       /// 初始化设备相关参数
       /// 在界面添加完成后,会进入到该方法进行初始化
       /// 之后再界面修改连接参数后,会再一次进入该方法。
       /// </summary>
       /// <param name="item">equip表对象属性</param>
       /// <returns></returns>
       public override bool init(EquipItem item)
       {
           /*
      item.Equip_addr 设备地址
      解释:通常存放设备的唯一标识或者设备的连接地址。这里需要根据具体的协议来区分,如果一对一的直连设备

      item.communication_param 设备连接参数
      解释:通常存放设备的连接信息,具体由当前协议插件来约定,在配置文档中写明即可。

      item.Local_addr 通讯端口(也叫通讯线程),任意字符,不宜过长。
      解释:在Equip表,你可能会发现不少设备的Local_addr字段可能都是空的,也可能都是一个具体的字符串。
      我们按照该字段的值进行Group By归类后,就得到了同一个值的设备数量有多少个,这个就代表一个线程管控了多少个设备。

      item.communication_time_param
      解释:在设备线程组里面,一个设备多久通信一次,即多久采集一次数据,单位毫秒。 
      如果communication_time_param职能比较多,也可以将多个参数的拼接,此时需要自行处理拆分后再转换。
      配置举例:假设1个线程管控10个设备,要求每个设备每秒采集一次数据,那么这个字段的值应不大于100毫秒。其他场景同理计算即可。

      item.Reserve2 设备自定义参数
      解释:一般一些连接参数较多,需要规范化存储时,可以将属性放到自定义参数中,直观一些。当然也可以使用其他字段去拼接起来,但不建议这样做。
      在6.1版本中,该字段在数据库中存储的值为一个JSON格式的数据。
      在低版本中可以按照JSON格式来存储这个数据。
       */

           //获取设备连接通讯的间隔时间。
           _ = int.TryParse(item.communication_time_param, out _sleepInterval);

           /*
           在构造连接参数数,根据实际情况,以下展示一个连接参数模型的赋值。
           如果连接参数简单,也可以使用自定义连接参数,直接使用communication_param更好,减少配置项,这里需要开发人员自己确定好。

            /*if (!string.IsNullOrWhiteSpace(item.Reserve2))
           {
               var dictParams = JsonConvert.DeserializeObject<Dictionary<string, string>>(item.Reserve2);
               _connectionConfig = new ConnectionConfig
               {
                   ServerUrl = item.Equip_addr,
                   UserName = dictParams.TryGetValue("UserName", out var userName) ? userName : string.Empty,
                   Password = dictParams.TryGetValue("Password", out var password) ? password : string.Empty,
                   CertificatePath = dictParams.TryGetValue("CertificatePath", out var certPath) ? certPath : string.Empty,
                   CertificatePwd = dictParams.TryGetValue("CertificatePwd", out var certPwd) ? certPwd : string.Empty,
               };

               //我们可以定义多个事件名称的级别,命名方式如DefaultEventMessageLevel,如果未取到,默认值给0,但最好要区分好,因为使用0的事件级别很多场景都使用。
               _ = int.TryParse(dictParams.TryGetValue("DefaultEventMessageLevel", out var defaultEventMessageLevelStr) ? defaultEventMessageLevelStr : "0", out _defaultEventMessageLevel);
           }*/

           return base.init(item);
       }

       /// <summary>
       /// 设备连接初始化
       /// 对于设备的连接地址,连接账号密码发生更改后,可以进行重连。
       /// </summary>
       /// <returns></returns>
       public override bool OnLoaded()
       {
           //TODO 这里可以写于设备连接的具体代码了。根据_connectionConfig连接参数,去创建自己的连接对象。
           //ConnClientManager.Instance.CreateClientSession(_connectionConfig);
           //返回默认值
           return base.OnLoaded();
       }

       /// <summary>
       /// 获取设备状态及实时数据
       /// 注意要控制好该方法不要出异常,否则会出现设备一直处于初始化状态中
       /// </summary>
       /// <param name="pEquip">设备基类对象</param>
       /// <returns></returns>
       public override CommunicationState GetData(CEquipBase pEquip)
       {
           //通过等待间隔时间,来达到多久取一次的。
           if (_sleepInterval > 0)
               base.Sleep(_sleepInterval);

           //当然开发者也可以在此次在增加相关业务逻辑。

           //获取当前连接地址的状态
           //var equipStatus = ConnClientManager.Instance.GetClientSessionStatus(_connectionConfig.ServerUrl);

           //如果连接状态正常,设置为在线
           /*if (equipStatus)
           {
               //只有在线是才采集数据
               _currentValue = ConnClientManager.Instance.GetCurrentValues(_connectionConfig.ServerUrl, pEquip.m_equip_no);
               return CommunicationState.ok;
           }
           else
           {
               //否则设置离线
               return CommunicationState.fail;
           }*/

           return base.GetData(pEquip);
           }

           /// <summary>
           /// 遥测点设置
           /// </summary>
           /// <param name="r">ycp表对象属性(不是全部)</param>
           /// <returns></returns>
      public override bool GetYC(YcpTableRow r)
       {
           /*
     注意:在此处最好不用打印日志,因为这里会产生大量的日志,如果需要调试某个点位时,可以在自定义参数里面加参数,针对固定的遥测进行日志调试。
     r.main_instruction 操作命令,如EquipCurrentInfo
     r.minor_instruction 操作参数,如Temperature,Humidness等
     r.Reserve2 自定义参数,以json结构存储,同设备的自定义参数一样。

     在给遥测赋值时提供了诸多方法,支持单个类型,多元组类型,可以根据实际需要使用。
     SetYCData(YcpTableRow r, object o);
     SetYCDataNoRead(IQueryable<YcpTableRow> Rows);
     SetYcpTableRowData(YcpTableRow r, float o);
     SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double, double) o);
     SetYcpTableRowData(YcpTableRow r, string o);
     SetYcpTableRowData(YcpTableRow r, int o);
     SetYcpTableRowData(YcpTableRow r, double o);
     SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double, double, double) o);
     SetYcpTableRowData(YcpTableRow r, (double, double) o);
     SetYcpTableRowData(YcpTableRow r, (DateTime, double) o);
     SetYcpTableRowData(YcpTableRow r, (double, double, double, double) o);
     SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double) o);
     SetYcpTableRowData(YcpTableRow r, (double, double, double) o);
         */

           /* 实时数据示例代码,可以根据自己的业务进行处理*/

           var random = new Random();


           //  if (_currentValue == null) return true;
           try
           {
               //此处的Key值需要根据实际情况去处理。如果构造实时数据缓存字典是需要由开发去定义。
               //总的来说,按照设备+遥测遥信的方式构造缓存数据是比较合理的。
               switch (r.main_instruction.ToLower())
               {
                   case "num":
                       var intResult = random.Next((int)r.yc_min, (int)r.yc_max);
                       SetYCData(r, intResult);
                       break;
               }
           }
           catch (Exception ex)
           {
               SetYCData(r, "测点赋值出现异常,请查看日志");
               DataCenter.Write2Log($"记录报错日志:{ex}", LogLevel.Error);
           }

           return true;
       }

       /// <summary>
       /// 遥信点设置
       /// </summary>
       /// <param name="r">yxp表对象属性(不是全部)</param>
       /// <returns></returns>
       public override bool GetYX(YxpTableRow r)
       {
           /*
               注意:在此处最好不用打印日志,因为这里会产生大量的日志,如果需要调试某个点位时,可以在自定义参数里面加参数,针对固定的遥测进行日志调试。
               r.main_instruction 操作命令,如EquipCurrentInfo
               r.minor_instruction 操作参数,如Temperature,Humidness等
               r.Reserve2 自定义参数,以json结构存储,同设备的自定义参数一样。

               在给遥测赋值时提供了诸多方法,支持bool、string类型,正常使用bool就够了,特殊情况可自行处理。
               SetYXData(YxpTableRow r, object o);
               SetYxpTableRowData(YxpTableRow r, string o);
               SetYxpTableRowData(YxpTableRow r, bool o);
            */

           /* 实时数据示例代码,可以根据自己的业务进行处理*/
           try
           {
               var random = new Random();
               switch (r.main_instruction.ToLower())
               {
                   //随机生成整数,并判断是否为偶数
                   case "boolean":
                       var boolResult = random.Next() % 2 == 0;
                       SetYXData(r, boolResult);
                       break;
               }
           }
           catch (Exception ex)
           {
               SetYXData(r, "遥信赋值出现异常,请查看日志");
               DataCenter.Write2Log($"记录报错日志:{ex}", LogLevel.Error);
           }
           return true;
       }

       /// <summary>
       /// 事件发布
       /// 如门禁设备的一些通行记录数据。
       /// 如果对事件记录实时性有非常高的要求,可以接收到事件后直接转。
       /// </summary>
       /// <returns></returns>
       public override bool GetEvent()
       {
           //从当前设备连接中获取事件列表
           //_currentEvents = ConnClientManager.Instance.GetCurrentEvents(_connectionConfig.ServerUrl, this.m_equip_no);
           //if (_currentEvents == null) return true;

           //假设_currentEvents对象每次都是新的数据,不存在旧数据,需开发者自行处理好.
           //foreach (var eventItem in _currentEvents)
           //{
           //EquipEvent中的事件级别根据当前事件名称定义好的级别。便于北向上报数据时的甄别。
           //var evt = new EquipEvent(JsonConvert.SerializeObject(eventItem), "可以自定义的消息格式", (MessageLevel)_defaultEventMessageLevel, DateTime.Now);
           //EquipEventList.Add(evt);
           //}
           //_currentEvents = null; //循环完成后,将事件记录置空,避免下次重复产生相同的事件.
           return base.GetEvent();
       }


       /// <summary>
       /// 设备命令下发
       /// </summary>
       /// <param name="mainInstruct">操作命令</param>
       /// <param name="minorInstruct">操作参数</param>
       /// <param name="value">传入的值</param>
       /// <returns></returns>
       public override bool SetParm(string mainInstruct, string minorInstruct, string value)
       {
           /*
           注意:建议在此处打印日志,便于记录由平台执行命令的情况,用于追溯命令下发情况。
           mainInstruct 操作命令,如:Control
           minorInstruct 操作参数,如:SetTemperature,SetHumidness
           value 命令下发的参数值,如:22
           */

           //获取设备实际执行的结果
           //dynamic controlResponse = ConnClientManager.Instance.WriteValueAsync(_connectionConfig.ServerUrl, mainInstruct, value);

           //将执行结果对象转换成json字符串
           //var csResponse = JsonConvert.SerializeObject(controlResponse);

           //给当前设置点赋值响应内容,用于北向转发时告知设备实际执行结果
           //this.equipitem.curSetItem.csResponse = csResponse;

           //记录执行传参及响应结果到日志中,便于追溯。
           //string logMsg = string.Format("命令下发参数,设备号:{0},mainInstruct:{1},minorInstruct:{2},value:{3},下发执行结果:{4}",
           //this.equipitem.iEquipno, mainInstruct, minorInstruct, value, csResponse);
           //DataCenter.Write2Log(logMsg, LogLevel.Warn);

           //根据设备执行状态,返回状态,对于发布订阅模式可直接返回true,在相关地方做好日志记录即可。
           //if (controlResponse.Code == 200) return true;
           //else return false;
           return base.SetParm(mainInstruct, minorInstruct, value);
       }
   }
}

  1. 右击解决方案->生成解决方案。

  2. 将生成的文件拷贝到D:\ganwei\IoTCenter\dll目录下。

注意

  1. 在NuGet服务中安装GWDataCenter包之前需要先配置好本地的NuGet服务
  2. 此处安装的GWDataCenter包的版本不能大于所部署的IoTCenter平台bin目录下的GWDataCenter.dll的版本,否则在该IoTCenter平台使用该协议插件会报错。
  3. 将生成的文件拷贝到D:\ganwei\IoTCenter\dll目录下且平台为不安全模式。IoTCenter 平台才会加载该插件。(安全模式下只加载从应用商店安装的插件,手动拷贝的插件不会进行加载)

# 4. 配置设备

配置设备

在完成协议插件发布后,还需在IoTCenter中配置产品,以便完成设备的加载过程。

  1. 开启IoTCenter平台服务,并登录。

  2. 在IoTCenter平台中配置产品示例插件,并配置测点。

    添加遥测

    添加遥信

  3. 基于产品创建设备。

  4. 在设备列表中查看设备运行的情况。

# 5. 附加调试

附加调试

如果我们想对代码进行调试,可以通过点击调试->附加到进程(快捷键为ctrl+a+p)功能进行调试。协议插件运行的宿主进程,在windows下为GWHost1.exe,在linux下为GWHost1.dll。

# 如何高效采集设备数据

如何提高通讯效率,什么样的协议驱动需要做设备拆分。

某个协议通过一个服务地址,就可以将所有数据进行传输,如OPCUA,Modbus,MQTT,TCP等。以下将以OPC举例,如何高效的采集数据。下图中展示了一个OPCUA服务下的节点信息。

通常,不同的节点都是来自各种各样的终端设备,如:ns=3;i=1001,ns=3;i=1002,ns=3;i=1003,ns=3;i=1004,这4个节点可能来自一个或者多个终端设备,在这里并不能看出具体的终端名称,但可能有相应的终端点位映射说明。

那么我们是否就就基于OPCUA协议插件,在代码逻辑中将设备及属性自动拆分好呢?

其实想这样一步到位也无可厚非,但这样会带来几个问题:

  1. 配置问题,每个设备需要配置OPCUA的连接信息,连接信息修改后相关设备都需要修改。

  2. 性能问题,设备数量多,占用通讯线程数量,采集数据实时性下降。

对于这种场景,我们约定采用如下方案:

  1. 一个OPCUA连接就只建一个设备,将当前连接下的所有节点数据采集到遥测中。这样一个设备连接独享一个线程进行通讯,采集效率将大幅提升,同时也可以降低资源的消耗。

如:有一个OPC服务,采集每层楼的机房温湿度传感器数据,如红框中,温度和湿度是属于不同楼层的一个终端设备。从截图中,我们可以分成5个设备,即每个楼层一个温湿度传感器设备。

  1. 使用虚拟设备协议插件(GWVirtualEquip.STD)拆分成终端设备及属性。虚拟设备协议插件因不需要与实际设备进行通讯连接,没有连接的开销,直接从缓存字典获取OPCUA服务#1设备中拿出相应属性,采集数据非常快。

如下图所示,已将OPCUA服务#1设备中的遥测量全部拆分到每个实际的传感器设备实例中。关于使用虚拟设备协议插件使用,可以参考这个连接。

# HTTP/HTTPS 接口调用

介绍如何调用HTTP 接口进行通信

# 1. 基础学习

以下将讲解如何通过 C# 中的网络库获取WebApi数据或者提交数据。

WebAPI 是 Web 服务器或者 Web 浏览器的应用程序编程接口,通常被各种客户端调用。WebAPI支持基于 Http/Https 协议的请求-响应操作,它只关注数据。请求的回复格式支持 JSON,XML,并且可以扩展添加其他格式。

在此之前,需要了解 HTTP 协议 (opens new window),例如 POST、GET;I/O 流,如文件流、网络流。

WebAPI 是面向用户的一种数据接口,其用户可以根据相应的数据接口(WebAPI)获取相应的数据,获取到的权威数据,用户自己定义喜欢的形式去展现数据。

简而言之,通过 API 地址,我们可以获取数据、提交数据等。

在 .NET 平台中一般都建议使用 HttpClient (opens new window) 来进行网络传输,另外,也可以使用 HttpClientFactory 。HttpClientFactory 优化对底层网络传输控制做了优化,能够减少资源消耗,提高传输速度。在.NET 6.0以上版本中,使用静态的HttpClient与HttpClientFactory区别不大,开发者若有兴趣,可对此问题进行深入探究。

在本文档的早期版本中,介绍了使用WebRequest来实现WebApi请求调用,不过在.NET Core的迭代过程中,该WebRequest一度被废弃,并在.NETCore3.1+以上版本中,基于HttpClient重写了相关逻辑,因此,在本次更新中,将使用HttpClient来介绍基于WebApi的插件开发流程。

对于C#.NET初学者来说,本节的内容可能会过于复杂,对应不理解的内容,不必纠结,在后面的工作中慢慢了解。

网络请求主要有 GET 和 POST 形式:

# GET 方式

GET 方式

可通过HttpClient组件调用GET方法获取远程数据:

// HttpClient推荐在单个应用中通过静态变量来复用,而不是每个请求实例化一次.
static readonly HttpClient client = new HttpClient();

static async Task Main()
{
    // 通过异步调用,并捕获异常
    try	
    {
        HttpResponseMessage response = await client.GetAsync("http://www.ganweisoft.com/");
        response.EnsureSuccessStatusCode();
        string responseBody = await response.Content.ReadAsStringAsync(); 
        Console.WriteLine(responseBody);
    }
    catch(HttpRequestException e)
    {
        Console.WriteLine("\nException Caught!");	
        Console.WriteLine("Message :{0} ",e.Message);
    }
}

# POST 方式

POST 方式

POST方法可通过两种方式进行参数提交,

1)通过json格式提交相关参数,这种方式提交的参数为模型格式,并序列化为json格式,参数类型类似于{"Id":"123e4","Value":"12345"}这种形式来提交参数。

httpClient = new HttpClient();
var postData = new { Id = "12345", Value = "123456" };
var strContent = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(postData));
strContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
HttpResponseMessage response = await httpClient.PostAsync("http://www.ganweisoft.com/", strContent);
response.EnsureSuccessStatusCode();

2)通过表单urlencoded方式来提交相关参数,这种方式通过在页面内容中填写参数的方法来完成数据的提交,参数的格式和 GET 方式一样,是类似于 hl=zh-CN&newwindow=1 这样的结构。

httpClient = new HttpClient(); 
var strContent = new StringContent($"Id=12345&Value=123456");
strContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded");
HttpResponseMessage response = await httpClient.PostAsync("http://www.ganweisoft.com/", strContent);
response.EnsureSuccessStatusCode();

值得注意的是,如果要通过UrlEncode传输中文内容时,有时先使用 UrlEncode 方法将中文字符转换为编码后的 ASCII 码,然后提交到webapi,提交的时候可以说明编码的方式,用来使对方接口能够正确的解析。

# 2. 开发天气协议插件

# 需求分析设计

在进行协议对接之前,务必要获取供应商提供的协议文本,对该文本进行分析,提取与本项目相关的要素,设计成物模型,并进行相应插件的开发。 本示例参考高德天气API文档 (opens new window)

  1. 协议解析
协议解析
  1. 天气查询API服务地址
URL 请求方式
https://restapi.amap.com/v3/weather/weatherInfo?parameters GET
  1. 请求参数
参数名 含义 规则说明 是否必须 缺省值
key 请求服务权限标识用户在高德地图官网 申请 web 服务 API 类型 KEY 必填 无
city 城市编码 输入城市的 adcode,adcode 信息可参考 城市编码表 必填 无
extensions 气象类型 可选值:base/allbase:返回实况天气all:返回预报天气 可选 无
output 返回格式 可选值:JSON,XML 可选 JSON
  1. 返回参数说明
名称 含义 规则说明
status 返回状态 值为0或1 1:成功;0:失败
count 返回结果总数目
info 返回的状态信息
infocode 返回状态说明,10000代表正确
lives 实况天气数据信息

lives

名称 含义 规则说明
province 省份名
city 城市名
adcode 区域编码
weather 天气现象(汉字描述)
temperature 实时气温,单位:摄氏度
winddirection 风向描述
windpower 风力级别,单位:级
humidity 空气湿度
reporttime 数据发布的时间
  1. 设计物模型
设计物模型

(1)在协议解析中,我们已经获得了原始数据的结构,可以看到该数据为Json结构,可依托该json结构设计物模型。

(2)由于该结构为城市天气信息,我们可每个城市设计为独立的设备,并对其属性值设计测点,文本和数值型的一般可设计为遥测。

(3)设置报警条件,若温度超过35度时,低于15度,就触发高温预警。

(4)综上,我们设计出如下的物模型。

设备属性

参数名称 参数值
设备名称 输入城市名称
通讯时间参数 由于为每日天气预报,可按天,此处也可设置为按小时或按分钟
驱动文件 GwWeatherCnSample.STD.dll 开发完成后配置
自定义属性 见下图

自定义属性

参数 参数说明
baseUrl API地址:https://restapi.amap.com/v3/weather/weatherInfo API地址
key key
city 城市编码可以点击此处进行下载 (opens new window)

遥测属性

参数名称 描述 报警条件
province 省份名
city 城市名
adcode 区域编码
weather 天气现象(汉字描述)
temperature 实时气温,单位:摄氏度 小于15或者大于35
winddirection 风向描述
windpower 风力级别,单位:级
humidity 空气湿度
reporttime 数据发布的时间
  1. 对接开发

条件:GWDataCenter(6.1.1.9以上版本,具体版本可参照IoTCenter\bin目录下的GWDataCenter.dll的版本)

目的:了解调用网络API的过程。

(1)根据分析,再次明确需求为按物模型采集指定数据。

(2)配置物模型。参考2.1.2设计的物模型,在IoTCenter中进行物模型配置。

(3)分析数据实体模型(Dto)。

首先根据浏览器查询到的天气数据,需要将温度 temp ,城市 city ,城市编号 cityid 定义成一个实体类(Dto),并将浏览器中的相应数据取出,作为一个实体对象保存在实体类中,而 .net 类库中的NewtownJson 组件提供了序列化方法,可以实现 json 格式到实体对象的这一序列化过程。值得注意的是,在使用NewtownJson这样的基础组件时,尽量使用和IoTCenter\bin目录下相同版本号的组件。

image-20210918204158131

(4)定义这个实体类(Dto),由于结构为json,可使用Visual Studio自带的功能”选择性剪切“创建与原始数据匹配的数据对象。

定义这个实体类(Dto)
    /// <summary>
    /// 天气预报结构
    /// </summary>
    public class WeatherDto
    {
        /// <summary>
        /// 
        /// </summary>
        public string status { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public string count { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public string info { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public string infocode { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public List<lives> lives { get; set; }
    }

    /// <summary>
    /// 天气预报实体对象
    /// </summary>
    public class lives
    {
        /// <summary>
        /// 省份
        /// </summary>
        public string province { get; set; }
        /// <summary>
        /// 城市名
        /// </summary>
        public string city { get; set; }
        /// <summary>
        /// 区域编码
        /// </summary>
        public string adcode { get; set; }
        /// <summary>
        /// 天气现象(汉字描述)
        /// </summary>
        public string weather { get; set; }
        /// <summary>
        /// 实时气温,单位:摄氏度
        /// </summary>
        public string temperature { get; set; }
        /// <summary>
        /// 风向描述
        /// </summary>
        public string winddirection { get; set; }
        /// <summary>
        /// 风力级别,单位:级
        /// </summary>
        public string windpower { get; set; }
        /// <summary>
        /// 空气湿度
        /// </summary>
        public string humidity { get; set; }
        /// <summary>
        /// 数据发布的时间
        /// </summary>
        public string reporttime { get; set; }
    }

(6)准备一个用于Http请求的公共类。值得注意的是,此处的Http公共类使用的为Service后缀类名,表示其是以服务的形式提供给其他代码调用的基础设施,与扩展类不同。由于在.NETCore中推荐使用依赖注入框架来管理基础服务,因此,我们需要定义为接口=>实现的模式。基础类为IHttpClientService,实现为HttpClientServiceImpl。

用于Http请求的公共类
/// <summary>
/// Http基础方法实现类
/// </summary>
public class HttpClientServiceImpl : IHttpClientService
{
    private readonly string _baseUrl = "";
    private volatile HttpClient _httpClient;
    private object _objectHelper = new object();
    public HttpClientServiceImpl(string url)
    {
        _baseUrl = url;
    }
    /// <summary>
    /// 初始化创建单例的HttpClient
    /// </summary>
    /// <returns></returns>
    private HttpClient CreateHttpClient()
    {
        if (_httpClient == null)
        {
            lock (_objectHelper)
            {
                HttpClientHandler httpClientHandler = new HttpClientHandler();
                httpClientHandler.UseProxy = false;
                httpClientHandler.Proxy = null;
                _httpClient = new HttpClient(httpClientHandler);
                _httpClient.BaseAddress = new Uri(_baseUrl);
                _httpClient.Timeout = TimeSpan.FromSeconds(60);
            }
        }
        return _httpClient;
    }
    /// <summary>
    ///执行Post方法
    /// </summary>
    /// <param name="url"></param>
    /// <param name="postData"></param>
    /// <returns></returns>
    public Task<HttpResponseMessage> Post(string url, string postData)
    {
        StringContent stringContent = new StringContent(postData);
        stringContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-www-form-urlencoded");
        return CreateHttpClient().PostAsync(url, stringContent);
    }
    /// <summary>
    /// 执行Get方法
    /// </summary>
    /// <param name="url"></param>
    /// <returns></returns>
    public Task<HttpResponseMessage> Get(string url)
    {
        return CreateHttpClient().GetAsync(url);
    }
}
/// <summary>
/// 定义Http服务基础方法
/// </summary>
public interface IHttpClientService
{
    Task<HttpResponseMessage> Post(string url, string postData);
    Task<HttpResponseMessage> Get(string url);
}

(7)完成CEquip.cs

完整代码
using GWDataCenter;
using GWDataCenter.Database;
using GwWeatherCnSample.STD.Models;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System;
using System.Collections.Generic;
using System.Security.Policy;

namespace GwWeatherCnSample.STD
{
    public class CEquip : CEquipBase
    {
        /// <summary>
        /// api地址
        /// </summary>
        string _baseUrl = "";
        /// <summary>
        /// 高德应用key
        /// </summary>
        string _key = "";
        /// <summary>
        /// 城市编码
        /// </summary>
        string _city = "";
        bool _bInit = false;//是否初始化
        int _sleepTime = 300;//通讯延迟
        /// <summary>
        /// 城市天气实体
        /// </summary>
        lives _weatherinfo;
        /// <summary>
        /// 根据api、key、城市编码拼接好的请求地址
        /// </summary>
        string url = "";
        IHttpClientService _httpClientService = null;
        /// <summary>
        /// 初始化连接
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public override bool init(EquipItem item)
        {
            if (!_bInit || ResetFlag)
            {
                if (!base.init(item))
                {
                    return false;//通讯失败
                }
                try
                {
                    DataCenter.WriteLogFile($"GwWeatherCnSample.STD 调试日志!", LogType.Debug);

                    _httpClientService = new HttpClientServiceImpl(url: _baseUrl);//设置预热的Http链接地址
                                                                                 //获取Equip表communication_time_param中的延迟值
                    _sleepTime = Convert.ToInt32(item.communication_time_param);
                    //获取自定义参数
                    var reserveDict = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(item.Reserve2);
                    if (reserveDict.Count < 3)
                        DataCenter.WriteLogFile($"GwWeatherCnSample.STD 中设备号为{item.iEquipno}的设备缺少自定义参数,请检查!",LogType.Error);

                    _baseUrl = reserveDict["baseUrl"];
                    _key = reserveDict["key"];
                    _city = reserveDict["city"];
                    if (string.IsNullOrEmpty(_baseUrl))
                        DataCenter.WriteLogFile($"GwWeatherCnSample.STD 中设备号为{item.iEquipno}的设备缺少自定义参数,请检查!", LogType.Error);
                    if (string.IsNullOrEmpty(_city))
                        DataCenter.WriteLogFile($"GwWeatherCnSample.STD 中设备号为{item.iEquipno}的设备缺少城市编码,请检查!", LogType.Error);
                    if (string.IsNullOrEmpty(_key))
                        DataCenter.WriteLogFile($"GwWeatherCnSample.STD 中设备号为{item.iEquipno}的设备缺少key值,请检查!", LogType.Error);
                    url += _baseUrl + "?city=" + _city + "&key=" + _key;
                    _bInit = true;
                }
                catch(Exception e)
                {
                    //获取失败或信息不正确则默认300
                    _sleepTime = 300;
                    DataCenter.WriteLogFile($"GwWeatherCnSample.STD 设备初始化失败。详细信息:{e.Message}", LogType.Error);
                }
            }
            return base.init(item);
        }
        public override bool OnLoaded()
        {

            return base.OnLoaded();
        }
        /// <summary>
        /// 获取数据
        /// </summary>
        /// <param name="pEquip"></param>
        /// <returns></returns>
        public override CommunicationState GetData(CEquipBase pEquip)
        {
            Sleep(_sleepTime);//休眠
            _weatherinfo = GetCityWeather(url).Result;
            return base.GetData(pEquip);
        }

        /// <summary>
        /// 获取城市天气预报对象
        /// </summary>
        /// <param name="code"></param>
        /// <returns></returns>
        public async System.Threading.Tasks.Task<lives> GetCityWeather(string url)
        {
            //发送请求
            var result = await _httpClientService.GetString(url);
            //序列化获取到的数据
            var weatherDto = result.FromJson<WeatherDto>();
            if (weatherDto.lives.Count==0)
                DataCenter.WriteLogFile($"GwWeatherCnSample.STD 请求地址:{url}无相关天气信息", LogType.Error);
            return weatherDto.lives[0];
        }

        /// <summary>
        /// 遥测属性映射
        /// </summary>
        /// <param name="r"></param>
        /// <returns></returns>
        public override bool GetYC(YcpTableRow r)
        {
            switch (r.main_instruction.ToLower())
            {
                //温度
                case "temperature":
                    SetYCData(r, _weatherinfo.temperature);
                    break;
                case "city"://城市名
                    SetYCData(r, _weatherinfo.city);
                    break;
                case "province"://省份
                    SetYCData(r, _weatherinfo.province);
                    break;
                case "weather":
                    SetYCData(r, _weatherinfo.weather);
                    break;
                case "winddirection":
                    SetYCData(r, _weatherinfo.winddirection);
                    break;
                case "windpower":
                    SetYCData(r, _weatherinfo.windpower);
                    break;
                case "reporttime"://最后更新时间
                    SetYCData(r, _weatherinfo.reporttime);
                    break;
            }
            return true;
        }

        /// <summary>
        /// 遥信属性映射
        /// </summary>
        /// <param name="r"></param>
        /// <returns></returns>
        public override bool GetYX(YxpTableRow r)
        {
           
            return true;
        }

        /// <summary>
        /// 设置量操作
        /// </summary>
        /// <param name="MainInstruct"></param>
        /// <param name="MinorInstruct"></param>
        /// <param name="Value"></param>
        /// <returns></returns>
        public override bool SetParm(string MainInstruct, string MinorInstruct, string Value)
        {
            return base.SetParm(MainInstruct, MinorInstruct, Value);
        }
    }
}

# 3.集成调试

集成调试

完成上述步骤后,可通过IoTCenter查看插件运行的实际效果。

image-20210922162120882

我们也可参考此示例创建更多城市的天气预报。

可运行示例下载:下载示例代码

# 常见问题

常见问题

Q: base.init(item)、base.OnLoaded()、base.GetData(pEquip)、base.GetYC(r)、base.GetYX(r)等基类的方法,实现了什么功能,什么时候需要调用?
A: 重写方法后根据需求去写返回值,正常来说开发的协议插件都不用base.xx。都根据自定义的需求去写。基类只会给默认值具体实现参考原理解析中的CEquipBase类

Q: 平台上设备编辑后,是否必须重启平台才会生效?
A: 在页面上修改设备参数点击保存即可,无需重启平台。 手动修改数据库中的设备表内容需要重启平台。

Q: GetData()方法中返回值CommunicationState几种状态的含义?
A: CommunicationState.fail设置当前设备离线
       CommunicationState.ok设置当前设备在线
       其余的只是对设备进行状态的标记,不常用。我们只需要用这两个就行了

Q: DataCenter.WriteLogFile和DataCenter.Write2Log等几种记录日志的方法,什么区别?建议使用哪种?
A: DataCenter.WriteLogFile输出内容在控制台,不用填写日志类别。例如:DataCenter.WriteLogFile("xx插件加载成功")
       Write2Log需要填写日志类别,不填写级别默认为Error级别。推荐使用Write2Log来输出日志。合理的打印不同级别的日志方便排查问题。 例如:DataCenter.Write2Log($"记录报错日志:{ex}", LogLevel.Error);

Q: 关于断连异常这些,推荐的处理逻辑?比如,获取数据没成功,建议在哪里重试?模板角度建议如何重试?
A: 设备断联推荐在OnLoaded()方法中进行处理。
       设备实时数据没获取成功,需要排查具体问题。正常情况下,当前测点未获取到数据,还是会根据当前设备的通讯时间进行获取,如果一直没获取到就是***。

Q: GetData()方法中使用return base.GetData(pEquip),换成了return CommunicationState.ok后没报错,但好像就没再调用GetYC了。所以是不是GetData最后就需要用return base.GetData(pEquip)?
A: 当GetYX()和GetYX均返回True时。 GetData()方法返回CommunicationState.ok才会往下执行。

Q: 为什么遥测值为什么显示是***?遥测如果没获取到数据会显示灰色吗?
A: 说明遥测数据异常检查是否返回的是null。遥测的状态目前是不支持灰色的。一般只有设备通讯失败设备离线才是灰色。 遥测只存在正常数据和异常数据。

Q: 为什么设备里面的通讯时间参数属性值无效,未按照实际配置值进行获取
A: IoTCenter 平台只是给出当前配置参数,具体的实现需要在协议插件中写代码实现。可以参考示例代码中的_sleepInterval值获取与使用。

上次更新: 2025/4/14 10:15:09

← 原理解析 进阶→

目录
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式