入门
# 协议插件开发
介绍协议插件开发的入门知识
# 1. 创建一个协议插件
创建一个协议插件
协议插件是IoTCenter用以实现数据采集的业务单元,必须遵循一定的开发规范,该规范主要包括以下内容:
协议插件依赖于敢为网关数据采集服务(GWDataCenter),并作为其插件运行,GWDataCenter作为宿主程序,负责管理插件的加载、生命周期,并负责对实时数据进行存储,本身提供了一组读写实时数据的逻辑。
协议插件的.NET框架应保持与宿主程序一致,过低或过高的版本均可能导致插件无法运行,宿主将会主动停止运行。
协议插件的入口类名为CEquip,并必须重载init、GetData、GetYC、GetYX、SetParm五个模板方法。
命名:为了更好的标识协议插件的产权信息,建议使用公司代号作为统一命名,敢为软件对协议插件的命名规则为:
适用于公司范围的插件,基本命名规则为GW{行业协议}.{.NET 版本}.dll。
如果该协议插件为基于 .NET Framework的版本,其命名格式为GW{行业协议}.NET.dll。
如果该协议插件为基于 .NET(包括且不限定于 .NET Core/.NET Strandard/.NET 6及后续版本),其命名格式为GW{行业协议}.STD.dll。
例如:GWJieLink.STD.dll,表示基于捷顺RestFulApi标准协议文档开发的协议插件。
程序包定义:创建好的协议插件,应在其项目包属性中,定义公司版权信息和产品包信息,且每个版本发布时,应升级相应的版本号。
文件夹命名:
--根目录
--/src/ 存放源代码
--/docs/ 存放文件
----/database/ ---数据库脚本
----/协议/ ---第三方协议
----/软件设计/ ---设计文档
--README.MD ---协议插件的说明文件
--.gitignore ---需要忽略的文件,可参考github上提供的gitingore示例 (opens new window)。
备注:该教程主要基于VisualStudio2022进行开发,读者可以通过微软官方网站 (opens new window)获得此软件的开发包。
# 2. 开发步骤
# 1. 新建项目
操作步骤
首先在本地文件夹中,创建命名为
GwDllSample
的文件夹,并在该文件夹下创建src子文件夹。打开
Visual Studio
选择新建项目
,创建类库项目,其类型为面向.NET或.NET Standard的项目
,其文件命名为GwDllSample.STD
。将该文件保存在上述src文件夹下。
# 2. 创建入口类
创建方法
在该项目文件中,创建入口类,其类名为CEquip
,由于VisualStudio
在创建类库项目时已经默认创建了Class1
的类文件,也可以将该文件改名为CEquip
。
注意
此文件命名必须按规范,如有任何不同,都可能导致程序无法正常加载。
# 3. 重载模板方法
重载模板方法
在项目文件右击,选择NuGet包管理器,添加ganweisoft的程序包源服务器,并安装
GWDataCenter
包,包版本为最新版本。在
CEquip
类需要继承CEquipBase
类。注意,要想能够继承CEquipBase,还需添加GWDataCenter
的命名空间引用。重载CEquipBase提供的模板方法。
public class CEquip:CEquipBase
{
/// <summary>
/// 初始化方法
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public override bool init(EquipItem item)
{
return base.init(item);
}
/// <summary>
/// 加载完成处理函数。在第一次加载之后或者系统重置之后调用
/// </summary>
/// <param name="sender"></param>
public virtual bool OnLoaded()
{
return base.OnLoaded();
}
/// <summary>
/// 获取数据
/// </summary>
/// <param name="pEquip"></param>
/// <returns></returns>
public override CommunicationState GetData(CEquipBase pEquip)
{
return base.GetData(pEquip);
}
/// <summary>
/// 获取遥测
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYC(YcpTableRow r)
{
return base.GetYC(r);
}
/// <summary>
/// 获取遥信
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYX(YxpTableRow r)
{
return base.GetYX(r);
}
/// <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);
}
}
注意
此处应安装最新版本GWDataCenter包。
# 4. 设置生成目录
设置生成目录
设置项目的生成路径为D:\IoTCenter\dll
目录,并在项目属性中添加如下节点,避免生成的目录包含.net框架命名。
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
如果未设置上述节点,生成的文件夹将为D:\IoTCenter\dll\net6.0\GwDllSample.STD.dll
如果路径为此路径,还需手动将文件拷贝到D:\IoTCenter\dll
目录下。
# 5. 配置设备
配置设备
在完成协议插件发布后,还需在IoTCenter中配置物模型,以便完成设备的加载过程。
开启网关服务(IoTCenter Service)和Web服务。
在IoTCenter中配置产品示例插件,并配置测点。
基于产品创建设备。
在设备列表中查看设备运行的情况。
预期,如下图所示,按该步骤添加的测点,其实时值显示为默认值"***"。
# 6. 修改设备,以便使设备正确运行
修改设备,以便使设备正确运行
由于该设备目前上述代码未给设备的遥测配置任何实际值,所以我们看到的设备状态为灰色,且实时值也为初始状态"***
"。此时我们需要对上述代码做小幅修改,即可看到更好的展示效果。
/// <summary>
/// 获取遥测
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYC(YcpTableRow r)
{
base.SetYCData(r, 1);
return true;
}
/// <summary>
/// 获取遥信
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYX(YxpTableRow r)
{
base.SetYXData(r, 0);
return true; //若此处未返回状态,遥信会显示为"`***`"
}
按下列步骤修改后,还需关闭GWHost1.exe程序,并将代码重新生成,确认在D:\IoTCenter\dll
目录下已经有新版本的指定插件后,再启动GWHost1.exe程序,并进入Web页面所有设备中进行查看。
# 7. 附加调试
附加调试
如果我们想对代码进行调试,可以通过点击VisualStudio调试菜单上的附加到进程
(快捷键为ctrl+a+p)功能进行调试。协议插件运行的宿主进程,在windows下为GWHost1.exe
,在linux下为GWHost1.dll
。
# 协议插件原理解析
随着我们编写了第一个示例插件,接下来我们来深入了解以下协议插件的运行机制。
# 1. 原理解析
原理解析
说明
协议插件为随物联网平台主宿主启动的独立会话,采用独立的线程与物联网设备(物联网平台)进行通信。协议插件的使用过程中,主要分成以下几个步骤:
插件运行前准备:该工作在数据采集平台调用插件前提前准备线程资源。
协议初始化(
init)
:用于初始化线程资源,建立与设备侧的连接状态。获取数据(
GetData
):按照配置的通讯时间参数,定时从设备侧获取数据。解析遥测数据(
GetYC
):根据设备关联的遥测点,对采集的数据进行解析,并调用SetYcpTableRowData
方法,将数据写入到物联网平台的共享内存状态中。解析遥信(
GetYX
):对设备遥信状态进行解析,并调用SetYxpTableRowData
方法,并将实时设备遥信状态写入到宿主实时内存中。设置命令(
SetParm
):外部应用可通过调用设置,将状态回调到协议插件中,实现某些特定的功能。
# 2. 运行示意图
运行示意图
上图为协议插件的运行示意图,以下将通过一个示例代码来讲解此示意图。
需求概述:已知某温湿度设备,对外提供温度和湿度展示,并可进行在线状态设置。
实现概述:为该设备定义设备属性通信时间为1秒一次,包含温度和湿度两个遥测测点,及在线的遥信测点,并提供【在线离线设置】的设置项。
# init() 设备初始化方法
init() 设备初始化方法
init() 方法用于初始化设备,在数据库中提前设备信息、开始对通讯设备建立连接等,是动态库最先被执行的方法。
定义:
public override bool init(EquipItem item)
使用示例:
int SleepInterval = (int)TimeSpan.FromSeconds(1).TotalMilliseconds; //控制刷新频次,根据通信时间参数来休眠线程
bool IsOnline = true;
/// <summary>
/// 初始化方法
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public override bool init(EquipItem item)
{
if (!base.init(item))//设备初始,失败就记录日志
{
DataCenter.WriteLogFile("Demo初始化失败");//写日志在Data目录XLog中
return false; //设备通讯异常
}
//读取数据库Equip表communication_time_param字段配置,表休眠时间
if (!string.IsNullOrWhiteSpace(item.communication_time_param))
SleepInterval = Convert.ToInt32(item.communication_time_param);
return true; //设备通讯正常
}
/// <summary>
/// 加载完成处理函数,在第一次加载之后或者系统重置之后调用
/// </summary>
/// <param name="sender"></param>
public virtual bool OnLoaded()
{
DataCenter.WriteLogFile("Demo初始化成功");//写日志在Data目录XLog中
return true;
}
工作原理:
CEquip
类里的方法会被不断循环执行,但init
方法只在设备第一次运行以及设备或测点(包括设置点)配置进行了更改后执行。同时在基类中提供了Onloaded
虚方法在第一次加载之后或者系统重置之后自动执行。比如可以将写日志的操作写在Onloaded
方法中。base.init(item)
是基类方法用于初始化。基类中默认支持Modbus
等协议。
解析:
Init()
方法在框架中会被循环调用,返回值类型为bool
类型,当其返回值为Ture
时代表设备初始化成功,并开始执行其他成员方法,为false
时则代表设备初始化失败,初始化失败时则阻塞此类中其他方法的运行。Equipitem
参数为GWDataCenter.DataCenter
命名空间下的Equipitem
类,并继承IComparable
接口,其包含了设备的所有属性,并聚合了数据库的操作类Database
和用于与设备通讯的SerialPort
类。DataCenter.WriteLogFile()
是GWDataCenter.DataCenter
命名空间下的DataCenter
类中的一个记录错误日志的方法,其日志记录在\IoTCenter\log\XLog.txt
文件中。item.communication_time_param
是Equipitem
类中的一个字段,它映射自数据库Equip
表的communication_time_param
字段,代表刷新间隔。
# GetData() 方法
GetData() 方法
初始化设备后,开始执行此方法,根据 CEquip 的工作任务,可以分为 ycp/yxp 表读取数据、setparm 表设置数据两大部分。一般来说,setparm 控制某些动作,这样会影响数据的准确性,因此 GetData() 方法在此控制程序的工作。
定义:
public override CommunicationState GetData(CEquipBase pEquip)
使用示例:
/// <summary>
/// 获取数据
/// </summary>
/// <param name="pEquip"></param>
/// <returns></returns>
public override CommunicationState GetData(CEquipBase pEquip)
{
//GetData为阻塞式编程,一旦定义了设置,则先需要提前结束GetData方法,再执行设置操作。
if (RunSetParmFlag)
{
return CommunicationState.setreturn;
}
Sleep(SleepInterval);//线程等待,控制刷新间隔
return base.GetData(pEquip);
}
解析:
CommunicationState
是一个状态枚举,代表当前数据解析成功,失败等状态信息,如果验证成功后则返回基类的GetData()
方法继续执行,失败则阻塞GetYC()
、GetYX()
,SetParm()
方法的运行。GetData
方法在运行时包含CEquipBase
类对象,它包含了所有测点信息,可以在其中做数据校验,因为在具体设备通讯中,需要对返回的数据进行验证。一般进行数据验证会从以下几个方面开始,如:设备地址(如果协议有定义的话)、返回数据长度、完整性校验(根据协议可能有CRC、和校验等)。如 Modbus 协议中,最后两个字节进行 CRC 校验,同时还需要对设备地址进行全面的校验,否则收到错误的数据包将导致实际设备异常响应。Sleep()
方法是CEquipBase
基类中提供用于休眠GetData()
方法。
# GetYC() 遥测方法
GetYC() 遥测方法
定义:
public override bool GetYC(YcpTableRow r)
使用示例:
/// <summary>
/// 获取遥测,为当前设备设计两个遥测属性点,分别为temperature 温度, humidity 湿度
/// </summary>
/// <param name="YcpTableRow"></param>
/// <returns></returns>
public override bool GetYC(YcpTableRow r)
{
string mainInstruction = r.main_instruction.ToLower();
switch (mainInstruction)
{
case "temperature":
SetYCData(r, 100);//为main_instruction字段配置为A的遥测点赋值为100
break;
case "humidity":
SetYCData(r, 50);//否则,为遥测点赋值50
break;
}
return true;//响应测点状态为成功
}
解析:
GetYC()
方法根据当前设备所拥有的遥测点数 N 条执行 N 次,参数YcpTableRow
表示映射自数据库ycp
遥测表的某一个实体即数据行。bool
表示为当前测点设置值是否成功,不成功则设备故障。r.main_instruction
表示获取数据库中的main_instruction
列的值,对应界面中的操作命令
可以根据不同测点配置的参数,判断该测点需要何种数据。SetYCData()
上传遥测值给上层应用,该方法有多个重载版本支持string
、int
、float
、double
、元组等多种数据类型的数据,其中元组类型支持指定时间插入double
值以及double
类型的元组(元组的中double类型元素的数量支持2-7个)。其方法原型为:public void SetYCData(YcpTableRow r, object o); public void SetYcpTableRowData(YcpTableRow r, float o); public void SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double, double) o); public void SetYcpTableRowData(YcpTableRow r, string o); public void SetYcpTableRowData(YcpTableRow r, int o); public void SetYcpTableRowData(YcpTableRow r, double o); public void SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double, double, double) o); public void SetYcpTableRowData(YcpTableRow r, (double, double) o); public void SetYcpTableRowData(YcpTableRow r, (DateTime, double) o); public void SetYcpTableRowData(YcpTableRow r, (double, double, double, double) o); public void SetYcpTableRowData(YcpTableRow r, (double, double, double, double, double) o); public void SetYcpTableRowData(YcpTableRow r, (double, double, double) o);
注意:
- 返回值
动态库开发中对于获取失败的测点应当返回false,不可将值设置为类似“无数据”。
public override bool GetYC(YcpTableRow r)
{
//获取数据失败
return false;
}
错误的写法
public override bool GetYC(YcpTableRow r)
{
//获取数据失败
SetYCData(r, "无数据");
return true;
}
- 数据类型
动态库开发中,获取到设备的值需要转换为对应数据类型,一般情况下,遥测值为double类型,遥信值为Bool类型;
正确的写法
public override bool GetYC(YcpTableRow r)
{
//获取到字符串值
var value = "67.8";
SetYCData(r, double.Parse(value));
return true;
}
错误的写法
public override bool GetYC(YcpTableRow r)
{
//获取到字符串值
var value = "67.8";
SetYCData(r, value);
return true;
}
# GetYX() 遥信方法
GetYX() 遥信方法
定义:
public override bool GetYX(YxpTableRow r)
使用示例:
/// <summary>
/// 获取遥信,设计了一个遥信值,其遥信属性为online 在线。
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYX(YxpTableRow r)
{
string mainInstruction = r.main_instruction.ToLower();
switch (mainInstruction)
{
case "online":
SetYXData(r, IsOnline);
break;
}
return true;//设置成功
}
解析:
YxpTableRow
映射了 yxp 表的数据行,bool
表示为当前测点设置状态是否成功,不成功则设备状态显示为故障(灰色灯)。GetYX()
方法根据当前设备所拥有的遥信点数 N 条执行 N 次。SetYXData()
表示为当前的测点设置状态,这个状态是一个bool
类型,返回True
则正常(绿灯),False
则警报(红灯)。该方法有两个重载版本分别支持bool
和string
类型的参数。其原型为:public void SetYXData(YxpTableRow r, object o); public void SetYxpTableRowData(YxpTableRow r, bool o); public void SetYxpTableRowData(YxpTableRow r, string o);
# SetParm() 设置方法
SetParm() 设置方法
定义:
public override bool SetParm(string MainInstruct, string MinorInstruct, string Value)
使用示例:
/// <summary>
/// 设置,定义设置为SetOnline,其输入值为1,0,为1则设置为在线,为0则设置为离线
/// </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)
{
string mainInstruction = MainInstruct.ToLower();
switch (mainInstruction)//获取SetParm表MainInstruct字段数据
{
case "setonline":
{
IsOnline = string.Equals(Value, "0") ? false : true;
return true;//控制成功
}
default:
return false;
}
}
解析:
SetParm()
方法用以从外部向协议插件内部进行参数设置,也用于向设备发送命令。提供映射
Setparm
表的三个字段设置命令
、设置参数
、设置值
用于提供命令参数和区分发送的命令。返回值
True
表示当前命令设置成功,False
表示设置失败。
备注:
如果插件运行失败,可在宿主程序GWHost1
的控制台输出中看到错误信息,此时根据该输出堆栈信息中针对性采取解决措施。
# 调用WebAPI
介绍如何调用WebAPI进行通信
# 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. 示例
# 需求分析设计
在进行协议对接之前,务必要获取供应商提供的协议文本,对该文本进行分析,提取与本项目相关的要素,设计成物模型,并进行相应插件的开发。
- 协议解析
协议解析
(1)在浏览器中调用完整的 API,如城市柳河的天气(http://www.weather.com.cn/data/sk/101060503.html (opens new window))
(2)其网络 API 格式为(http://www.weather.com.cn/data/sk/城市代码.html (opens new window)), 城市代码则请访问https://www.cnblogs.com/oucbl/p/6138963.html (opens new window) 查询。
(3)在浏览器中获取json格式(如图所示)。
- 设计物模型
设计物模型
(1)在协议解析中,我们已经获得了原始数据的结构,可以看到该数据为Json结构,可依托该json结构设计物模型。
(2)由于该结构为城市天气预报,我们可每个城市设计为独立的设备,并对其属性值设计测点,文本和数值型的一般可设计为遥测,布尔型或状态量一般设置为遥信,当然,本示例中目前没有可直接表达为布尔型的状态值,而通过观察,“Rain"(可能为降水概率或降水量,也有可能为是否降水)这个字段的值为”0“,为了演示方便,可根据需求将其设计为【是否降水】,作为遥测值。
(3)设置报警条件,若温度超过35度时,低于15度,就触发高温预警。
(4)综上,我们设计出如下的物模型。
设备属性
参数名称 | 参数值 |
---|---|
设备名称 | 输入城市名称 |
设备地址 | 输入链接地址,如 http://www.weather.com.cn/data/sk/101060503.html |
通讯时间 | 由于weather.cn为每日天气预报,可按天,此处也可设置为按小时或按分钟 |
驱动文件 | GwWeatherCnSample.STD.dll 开发完成后配置 |
遥测属性
参数名称 | 参数值 | 报警条件 |
---|---|---|
Temperature(气温) | 无 | 下限值15,上限值35,恢复值下限值18,恢复上限值32 |
WD(风向) | 无 | 无 |
WS(风力) | 无 | 无 |
SD(湿度) | 无 | 下限值20,上限值80,恢复下限值22,恢复上限值78 |
Time(数据时间) | 无 | 无 |
遥信属性
参数名称:IsRain(是否降雨)
(5)在IoTCenter平台中,配置相关设备。
- 对接开发
条件:GWDataCenter(v6.0.1以上版本,具体版本可参照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
{
public Weatherinfo weatherinfo { get; set; }
}
/// <summary>
/// 天气预报实体对象
/// </summary>
public class Weatherinfo
{
public string city { get; set; }
public string cityid { get; set; }
public string temp { get; set; }
public string WD { get; set; }
public string WS { get; set; }
public string SD { get; set; }
public string AP { get; set; }
public string njd { get; set; }
public string WSE { get; set; }
public string time { get; set; }
public string sm { get; set; }
public string isRadar { get; set; }
public string Radar { get; set; }
/// <summary>
/// 由于部分城市数据的json结构未返回降雨信息,需要额外新增字段
/// </summary>
public string Rain { get; set; }
}
(5)还需准备一个Json序列化的扩展类。由于此类是对已有.NET 基础对象行为的扩展,推荐使用静态的Extension
为后缀的类名。
Json序列化的扩展类
/// <summary>
/// Json序列化
/// </summary>
public static class JsonExtensions
{
/// <summary>
/// 提供简单的对象Json字符串反序列化对象的方法
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>___
/// <returns></returns>
public static T FromJson<T>(this string obj) where T : class
{
var jSetting = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
TypeNameHandling = TypeNameHandling.All,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
if (obj != null)
{
return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(obj);
}
return null;
}
/// <summary>
/// 提供简单的对象序列化Json字符串方法
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static string ToJson<T>(this T obj) where T : class
{
var jSetting = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
return Newtonsoft.Json.JsonConvert.SerializeObject(obj, jSetting);
}
}
(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)定义协议插件的初始化逻辑。根据本示例的需求,主要对WebAPI进行初始化,并在设备的初始化函数中实现:
协议插件的初始化逻辑
/// <summary>
/// 硬编码远程地址,作为Http请求的预热地址,提高请求效率
/// </summary>
string baseUrl = "http://www.weather.com.cn/";
int _sleepTime = 300;//通讯延迟
Weatherinfo _weatherinfo;//城市天气实体
IHttpClientService _httpClientService = null;
/// <summary>
/// 初始化连接
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public override bool init(EquipItem item)
{
if (!base.init(item))
{
return false;//通讯失败
}
try
{
_httpClientService = new HttpClientServiceImpl(url: baseUrl);//设置预热的Http链接地址
//获取Equip表communication_time_param中的延迟值
_sleepTime = Convert.ToInt32(item.communication_time_param);
}
catch
{
//获取失败或信息不正确则默认300
_sleepTime = 300;
}
return base.init(item);
}
(8)获取设备数据。此步骤,实现GetData
方法获取城市天气数据。该过程既是数据采集的过程,也实现了模型转换,实现从远程物联网设备相应的非结构化模型数据映射为可用键值匹配的结构化数据:
获取设备数据
/// <summary>
/// 获取城市天气预报对象
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public async System.Threading.Tasks.Task<Weatherinfo> GetCityWeather(string address)
{
var result = await _httpClientService.GetString(address);
var weatherDto = result.FromJson<WeatherDto>();
return weatherDto.weatherinfo;
}
/// <summary>
/// 获取数据
/// </summary>
/// <param name="pEquip"></param>
/// <returns></returns>
public override CommunicationState GetData(CEquipBase pEquip)
{
Sleep(_sleepTime);//休眠
_weatherinfo = GetCityWeather(pEquip.equipitem.Equip_addr).Result;
return base.GetData(pEquip);
}
(9)遥测数据采集。通过预先配置的物模型遥测参数,从结构化模型中,解析获得符合指定遥测指标的属性。
遥测数据采集
注意
遥测模型映射方法(GetYC)中,不推荐编写获取数据的逻辑,尽量在GetData
方法中获取数据。因为同一个物模型实例,触发一次GetData
方法时将触发多次GetYC
方法,如果GetYC
方法职责过大,可能会影响数据采集应用的性能。
我们可编写如下代码,实现简单的模型数据提取:
/// <summary>
/// 遥测属性映射
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYC(YcpTableRow r)
{
switch (r.main_instruction.ToLower())
{
case "temperature":
SetYCData(r, double.Parse(_weatherinfo.temp));
break;
case "wd":
SetYCData(r, _weatherinfo.WD);
break;
case "ws":
SetYCData(r, _weatherinfo.WS);
break;
case "sd":
SetYCData(r, _weatherinfo.SD);
break;
case "ap":
SetYCData(r, _weatherinfo.AP);
break;
case "njd":
SetYCData(r, _weatherinfo.njd);
break;
case "wse":
SetYCData(r, _weatherinfo.WSE);
break;
case "time":
SetYCData(r, _weatherinfo.time);
break;
case "sm":
SetYCData(r, _weatherinfo.sm);
break;
}
return true;
}
(10)遥信数据采集。与遥测属性映射逻辑一致,入口方法为GetYX
,写入数据的方法为SetYXData
。
遥信数据采集
/// <summary>
/// 遥信属性映射
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYX(YxpTableRow r)
{
switch (r.main_instruction.ToLower())
{
case "IsRain":
SetYXData(r, string.Equals(_weatherinfo.Rain, "1", StringComparison.InvariantCultureIgnoreCase));
break;
}
return true;
}
# 集成调试
集成调试
完成上述步骤后,可通过IoTCenter查看插件运行的实际效果。
我们也可参考此示例创建更多城市的天气预报。
可运行示例下载:下载示例代码
# 随机数示例
将介绍使用随机数模拟测点值变化
# 1. 简介
简介
需求:已知C#提供了随机数类 Random
,开发一个协议插件,模拟物联网设备的实时数据波动情况,掌握上限下限报警功能,并可通过IoTCenter查看设备的历史曲线。
要求:该协议插件可通过随机生成指定范围内的整数,数值型,布尔值,同时,可记录历史曲线,并产生报警记录。
需求分析设计
分析数据协议,本插件无需外部协议文档。
物模型设计
设备属性
参数名称 参数值 设备名称 随机数生成器 通讯时间 1000毫秒 遥测属性
参数名称 参数值 部分属性 报警条件 记录历史曲线 任意值 Integer 有效范围:实际最小值0,实际最大值100,单位摄氏度(℃),历史曲线记录阈值为10 ,超出10的变化才会波动 下限值15,上限值35,恢复下限18,恢复上限30 是 任意值 Decimal 有效范围:实际最小值0,实际最大值100,单位摄氏度(℃),历史曲线记录阈值为10 ,超出10的变化才会波动 下限值15,上限值35,恢复下限18,恢复上限30 是 遥信属性
参数名称 参数值 部分属性 任意值 Boolean 0-1事件:正常,1-0事件:异常 设计一个协议插件,其主要作用是生成指定范围内的随机数。
配置设备。
# 2. 编写协议插件
编写协议插件
(1)参考创建一个设备协议插件步骤,搭建好基本的协议插件使用环境。
(2)本实例不涉及远程物联数据采集,只需编写获取遥测值和遥信实时值的方法。
(3)示例代码-获取遥测:
/// <summary>
/// 获取遥测
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYC(YcpTableRow r)
{
var random = new Random();
switch (r.minor_instruction.ToLower())
{
//随机生成整形数据
case "integer":
var intResult = random.Next((int)r.yc_min, (int)r.yc_max);
SetYCData(r, intResult);
break;
//随机生成数值
case "decimal":
var dbResult = random.Next((int)r.yc_min, (int)r.yc_max - 1) + random.NextDouble();
SetYCData(r, dbResult);
break;
}
return true;
}
(4)示例代码-获取遥信:
/// <summary>
/// 获取遥信
/// </summary>
/// <param name="r"></param>
/// <returns></returns>
public override bool GetYX(YxpTableRow r)
{
var random = new Random();
switch (r.minor_instruction.ToLower())
{
//随机生成整数,并判断是否为偶数
case "boolean":
var boolResult = random.Next() % 2 == 0;
SetYXData(r, boolResult);
break;
}
return true;
}
# 3. 查看运行效果
查看运行效果
# 4. 优化
优化
如果你有细心留意多条随机数,会发现跳跃范围太大,使用 Random
类,会在范围内随机产生一个数,如果我们想控制随机数在 (0,100) 范围内波动,但是要平稳地过渡,又或者要实现模拟浮点数。可以参考下面的模拟代码。
以下代码可以复制放到程序中直接调用。
int 模拟数据
range 是指每次生成 [0,range] 范围的增/减量,例如 初始值 56 , range = 2
,那么可能 56±0 或 56±1 或 56±2 , 是增还是减,是随机的。但是设置 min 、 max 后,最后生成的值会在此范围内波动。
float、double 模拟数据
对应 float、double,range 的值越大,波动范围越小。默认 range = 8
,大概就是每次 0.1 的波动范围。其中,float 小数保留两位, double 小数保留 4 位,需要更高或减少小数位数,修改一下 ...ToString("#0.0000")
/// <summary>
/// 用来模拟平稳的整数波动
/// </summary>
/// <param name="original">原始数据</param>
/// <param name="range">波动范围</param>
/// <param name="min">最小值</param>
/// <param name="max">最大值</param>
/// <returns></returns>
public static int Property(ref int original, int min, int max, int range)
{
int num = (new Random()).Next(0, range + 1);
bool addorrm;
if (original + num > max || original > max)
addorrm = false;
else if (original < min || original - num < min)
addorrm = true;
else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false;
if (addorrm == true)
original += num;
else
original -= num;
return original;
}
// 模拟float,带两位小数
public static float Property(ref float original, float min, float max, int range = 8)
{
original = float.Parse(original.ToString("#0.00"));
float num = float.Parse(((new Random()).NextDouble() / range).ToString("#0.00"));
bool addorrm;
if (original + num > max || original > max)
addorrm = false;
else if (original < min || original - num < min)
addorrm = true;
else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false;
if (addorrm == true)
original += num;
else
original -= num;
original = float.Parse(original.ToString("#0.00"));
return original;
}
// 模拟double,带4位小数。
public static double Property(ref double original, double min, double max, int range = 8)
{
original = double.Parse(original.ToString("#0.0000"));
double num = double.Parse(((new Random()).NextDouble() / range).ToString("#0.0000"));
bool addorrm;
if (original + num > max || original > max)
addorrm = false;
else if (original < min || original - num < min)
addorrm = true;
else addorrm = ((new Random()).Next(1, 3) > 1) ? true : false;
if (addorrm == true)
original += num;
else original -= num;
original = double.Parse(original.ToString("#0.0000"));
return original;
}
# 5. 测试
测试
那么,如何测试上面上个获取随机波动数据的方法呢?可以使用如下代码测试上面的方法
float a = 26.6F;
for (int i = 0; i < 20; i++)
{
Console.Write(Property(ref a, 10, 30, 8) + "|");
}
20 次测模拟数据:
提示:由于20次测试数据在 毫秒内完成,因此不会出现明显变化。
可以看到每次的数据变化在零点几,因此十分合适作为模拟数据的方法。
1-4 | 5-8 | 9-12 | 13-16 | 17-20 |
---|---|---|---|---|
26.68 | 26.79 | 26.71 | 26.65 | 26.72 |
26.68 | 26.77 | 26.67 | 26.57 | 26.45 |
26.41 | 26.39 | 26.39 | 26.32 | 26.28 |
26.38 | 26.34 | 26.44 | 26.43 | 26.29 |
# 练习
现由客户想针对现场某电脑进行状态监测,已知电脑为windows10,请基于该场景进行相关协议对接。需要采集的状态包括且不限于(CPU占用,内存占用,磁盘,网络带宽实时状态)等属性。
某项目需要进行天气预报数据呈现,计划通过百度天气预报API来实现,请基于该场景进行相关协议对接。