协议插件开发
# 开发流程
下面介绍开发一个协议插件的流程
注意
- 前置条件:本地IoTCenter平台已经部署完成。能够正常登录。
- 开源Demo地址:
https://gitee.com/ganweicloud/GWDriverDemo.STD
# 1. 新建项目
操作步骤
首先在本地文件夹中,创建命名为
GwDllSample
的文件夹,并在该文件夹下创建src子文件夹。打开
Visual Studio
选择新建项目
,创建类库项目,其类型为面向.NET或.NET Standard的项目
,其文件命名为GwDllSample.STD
。将该文件保存在上述src文件夹下。
# 2. 创建入口类
创建CEquip类
在该项目文件中,创建入口类,其类名为
CEquip
,由于VisualStudio
在创建类库项目时已经默认创建了Class1
的类文件,该文件改名为CEquip
。在项目文件右击,选择NuGet包管理器,添加ganweisoft的程序包源服务器,并安装
GWDataCenter
包,包版本为所部署的IoTCenter平台bin目录下的GWDataCenter.dll的版本。安装
GWDataCenter
包。继承
CEquipBase
类
注意
CEquip 类为固定命名必须按规范,如有任何不同,都可能导致程序无法正常加载。
# 3. 示例代码
示例代码为根据设备配置的最大值与最小值获取随机数
示例代码
- 将示例代码覆盖到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);
}
}
}
右击解决方案->生成解决方案。
将生成的文件拷贝到D:\IoTCenter\dll目录下。
注意
- 在NuGet服务中安装GWDataCenter包之前需要先配置好本地的NuGet服务
- 此处安装的GWDataCenter包的版本不能大于所部署的IoTCenter平台bin目录下的GWDataCenter.dll的版本,否则在该IoTCenter平台使用该协议插件会报错。
- 将生成的文件拷贝到D:\IoTCenter\dll目录下且平台为不安全模式。IoTCenter 平台才会加载该插件。(安全模式下只加载从应用商店安装的插件,手动拷贝的插件不会进行加载)
# 4. 配置设备
配置设备
在完成协议插件发布后,还需在IoTCenter中配置产品,以便完成设备的加载过程。
开启IoTCenter平台服务,并登录。
在IoTCenter平台中配置产品示例插件,并配置测点。
添加遥测
添加遥信
基于产品创建设备。
在设备列表中查看设备运行的情况。
# 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协议插件,在代码逻辑中将设备及属性自动拆分好呢?
其实想这样一步到位也无可厚非,但这样会带来几个问题:
配置问题,每个设备需要配置OPCUA的连接信息,连接信息修改后相关设备都需要修改。
性能问题,设备数量多,占用通讯线程数量,采集数据实时性下降。
对于这种场景,我们约定采用如下方案:
- 一个OPCUA连接就只建一个设备,将当前连接下的所有节点数据采集到遥测中。这样一个设备连接独享一个线程进行通讯,采集效率将大幅提升,同时也可以降低资源的消耗。
如:有一个OPC服务,采集每层楼的机房温湿度传感器数据,如红框中,温度和湿度是属于不同楼层的一个终端设备。从截图中,我们可以分成5个设备,即每个楼层一个温湿度传感器设备。
- 使用虚拟设备协议插件(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)
- 协议解析
协议解析
- 天气查询API服务地址
URL | 请求方式 |
---|---|
https://restapi.amap.com/v3/weather/weatherInfo?parameters | GET |
- 请求参数
参数名 | 含义 | 规则说明 | 是否必须 | 缺省值 |
---|---|---|---|---|
key | 请求服务权限标识用户在高德地图官网 申请 web 服务 API 类型 KEY | 必填 | 无 | |
city | 城市编码 | 输入城市的 adcode,adcode 信息可参考 城市编码表 | 必填 | 无 |
extensions | 气象类型 | 可选值:base/allbase:返回实况天气all:返回预报天气 | 可选 | 无 |
output | 返回格式 | 可选值:JSON,XML | 可选 | JSON |
- 返回参数说明
名称 | 含义 | 规则说明 |
---|---|---|
status | 返回状态 | 值为0或1 1:成功;0:失败 |
count | 返回结果总数目 | |
info | 返回的状态信息 | |
infocode | 返回状态说明,10000代表正确 | |
lives | 实况天气数据信息 |
lives
名称 | 含义 | 规则说明 |
---|---|---|
province | 省份名 | |
city | 城市名 | |
adcode | 区域编码 | |
weather | 天气现象(汉字描述) | |
temperature | 实时气温,单位:摄氏度 | |
winddirection | 风向描述 | |
windpower | 风力级别,单位:级 | |
humidity | 空气湿度 | |
reporttime | 数据发布的时间 |
- 设计物模型
设计物模型
(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 | 数据发布的时间 |
- 对接开发
条件: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目录下相同版本号的组件。
(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查看插件运行的实际效果。
我们也可参考此示例创建更多城市的天气预报。
可运行示例下载:下载示例代码
# 常见问题
常见问题
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值获取与使用。