进阶
# 协议插件开发
在初步了解了协议插件开发的过程后,我们将通过常见的协议来进一步深入协议插件开发的实战内容。
# 串口通讯
# 概述
串口通信是串行接口(serial port)的简称,也称为串行通信接口或 COM 接口。串口通讯是指其它设备和计算机之间,通过一条数据信号线(地线、控制线等),按位进行传输数据的一种通讯方式,串口通讯的标准协议包括RS-232-C、RS-422、RS485 等。
串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且能够实现远距离通信。比如IEEE488定义并行通行状态时,规定设备线总长不得超过20米,并且任意两个设备间的长度不得超过2米;而对于串口而言,长度可达1200米。典型地,串口用于ASCII码字符的传输。
通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但不是必须的。
# 重要参数
串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通信的端口,这些参数必须匹配。
- 波特率
这是一个衡量符号传输速率的参数。指的是信号被调制以后在单位时间内的变化,即单位时间内载波参数变化的次数,如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,比特率为10位*240个/秒=2400bps。一般调制速率大于波特率,比如曼彻斯特编码)。通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信。
- 数据位
这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据往往不会是8位的,标准的值是6、7和8位。如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7位)。扩展的ASCII码是0~255(8位)。如果数据使用简单的文本(标准 ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语“包”指任何通信的情况。
- 停止位
用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
- 奇偶校验位
在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位为1,这样就有3个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。
# 数据格式
串口通讯协议的数据格式一般包含起始位(start bit)、数据位(data bit)、奇偶校验位(parity bit)和停止位(stop bit)。
提示
图中无奇偶效验位,奇偶检验位不是必须有的,如果有奇偶检验位,则奇偶检验位应该在数据位之后,停止位之前。
起始位:标志传输一个字符的开始。
数据位:以由通信双方共同约定,一般可以是 5 位、7 位或 8 位,标准的 ASCII 码是 0~127(7位),扩展的ASCII 码是0~255(8位)。传输数据时先传送字符的低位,后传送字符的高位。
奇偶校验位:奇偶校验位仅占一位,用于进行奇校验或偶校验,奇偶检验位不是必须有的。如果是奇校验,需要保证传输的数据总共有奇数个逻辑高位;如果是偶校验,需要保证传输的数据总共有偶数个逻辑高位。
停止位:停止位可以是是 1 位、1.5 位或 2 位,标志着传输一个字符的结束。
空闲位:空闲位是指从一个字符的停止位结束到下一个字符的起始位开始。
# 物模型设计及参数配置
- 物模型设计
串口插件的物模型主要包括设备的基础信息,如串口所使用的端口,通讯参数(波特率/数据位/停止位/校验/),及使用的协议插件。以及基于该串口机制向设备侧下发的指令信息。
- 参数配置
参考协议插件开发入门的步骤五和步骤六,配置相关参数。
添加新产品,通讯地址 填写串口(如COM1),通讯参数列按格式:波特率/数据位/停止位/校验位(如19200/8/1/no),协议文件 列则是所要开发的协议插件名称,并以 .dll 作为后缀.
在设置添加几条命令(如图所示),所配设置的参数和参数值,各填入控制命令与命令的内容。例如,我们可开发的插件定义了一个命令 【VPAGE】,并使用该命令触发大屏联动。
# 协议插件开发
条件:GWDataCenter 组件包
目的:了解基类 CEquipBase 中的 .Initialize()
进行串口通讯操作
- 按照协议要求配置好 Equip 表后,就可直接使用封装在 GWDataCenter.dll 类库中 CEquipBase 类的
serialport.Initialize()
方法进行串口通讯,如以下代码:
public override bool init(EquipItem item)
{
base.init(item);
try
{
//与设备进行串口通讯,返回True则不成功
if ((!serialport.Initialize(item)))
{
GWDataCenter.DataCenter.WriteLogFile("设备配置异常");
return false;
}
}
catch (Exception ex)
{
GWDataCenter.DataCenter.WriteLogFile("初始化错误" + ex);
return false;
}
return true;
}
在于设备通讯成功后,则使用serialport.Write()方法发送数据给下层设备,假设协议格式:
- 控制命令共 7 个字节
- 按 ASCII 编码表编码
- 第一个为起始命令 F5
- 第二个为识别码 B0
- 第三个字节和第四个字节分别表示控制1参数和控制2参数
- 第五个字节为控制命令
- 第六个字节为调节命令 A1
- 第七个为结束命令 AE
那么,可以在动态库中使用于SetParm()方法中发送设置命令:
public override bool SetParm(string MainInstruct, string MinorInstruct, string Value)
{
var listbyte = new List<byte>{0xf5,0xB0};//第一个和第二个字节
//第三字节与第四个字节为控制命令的内容即配置在数据库的MinorInstruct字段
var Minor = Encoding.ASCII.GetBytes(MinorInstruct);
listbyte.AddRange(Minor);//将解析后的Byte数组添加到listbyte结尾处
//第五字节是起始命令在数据库的MainInstruct列获取
var MainIn = Encoding.ASCII.GetBytes(MainInstruct);
listbyte.AddRange(MainIn);//将解析后的Byte数组添加到listbyte结尾处
listbyte.Add(0xA1);//添加一个字节(调节命令)
listbyte.Add(0xAE);//添加一个字节(结束命令)
var buffer = listbyte.ToArray();
serialport.Write(buffer, 0, buffer.Length); //0是发送的数据的起始位,buffer.Length为发送数据的字节个数
return true;//发送成功
}
- 生成并创建设备
配置设备
参考创建一个设备动态库的步骤七至步骤十,按照相应要求编译并配置在IoTCenter中配置相关设备。
配置设置
打开设备页面,在设置项下就出现如需设置指令。左键点击它们,将会触发协议插件的 SetParm()
方法向底层设备发送指令(如图所示)。
# Modbus协议开发
# 背景概述
从前面的示例我们可以看到,协议插件的开发似乎很简单,仿佛我们无需费太多时间就能入门了,那么成为一位物联网开发者难道也这么简单么?(lll¬ω¬)
事实上当我们开始踏上这段旅途开始,可能会面临着一些毒打,例如,客户突然给我们提供了一份这样的一份需求,我们该怎么破?
# 需求概述
在某智慧园区项目中,客户采购了一款自动火灾报警系统,用以实现火灾报警提醒功能,请对该设备进行协议对接和应用场景开发。
当我们初次接触这样的需求时,难免会感觉有点难以理解,所以我们有必要继续回顾前面我们所接触到的协议插件开发的基本步骤,分析,开发,设计这个流程。于是,我们从获得了这样一份协议文档。
这份专业的协议文档可能会使我们头秃,那我们该如何对该协议进行协议插件的开发呢?在此将基于此应用场景进行插件开发的介绍。
# 知识点
- 网络通讯编程 SOCKET,TCP
- Modbus TCP 协议
- 数据类型转换
# Modbus TCP
Modbus协议是一个master/slave架构的协议。有一个节点是master节点,其他使用Modbus协议参与通信的节点是slave节点。
简单点理解,Modbus协议规定了一个主机,从机通过Modbus规定的格式发送指令给主机,主机收到之后会返回协议约定到的数据给从机。
# Modbus 协议格式
Modbus 协议格式
具体的Modbus协议格式可以去了解Modbus协议规约,下面我简单举例说明
客户端->服务器(请求指令)
索引 | 示例 | 说明 |
---|---|---|
1 | 0x00 | 事务标识高位 |
2 | 0x01 | 事务标识低位 |
3 | 0x00 | 协议标识高位 |
4 | 0x00 | 协议标识低位 |
5 | 0x00 | 数据包长度高位 |
6 | 0x06 | 数据包长度低位,从该字节下个开始计算 |
7 | 0x01 | 设备id |
8 | 0x03 | 功能码 |
9 | 0x00 | 寄存器地址高位 |
10 | 0x00 | 寄存器地址低位 |
11 | 0x00 | 读取长度高位 |
12 | 0x02 | 读取长度低位 |
HEX:00 01 00 00 00 06 01 03 00 00 00 02
服务器->客户端(服务响应数据)
索引 | 示例 | 说明 |
---|---|---|
1 | 0x00 | 事务标识高位 |
2 | 0x01 | 事务标识低位 |
3 | 0x00 | 协议标识高位 |
4 | 0x00 | 协议标识低位 |
5 | 0x00 | 数据包长度高位 |
6 | 0x06 | 数据包长度低位,从该字节下个开始计算 |
7 | 0x01 | 设备id |
8 | 0x03 | 功能码 |
9 | 0x04 | 返回长度 |
10 | 0x00 | 数据寄存器1高位 |
11 | 0x00 | 数据寄存器1低位 |
12 | 0x00 | 数据寄存器2高位 |
13 | 0x02 | 数据寄存器2低位 |
HEX:00 01 00 00 00 06 01 03 04 00 00 00 02
以上就是一个完整问答指令格式说明,接下来我们可以使用ModScan工具验证测试一下,ModScan使用方法参考常用调试工具_MODBUS篇。
ModScan包含两个工具,1、模拟Modbus主机服务(modsim32),2、客户端工具(modscan32)
首先,使用modsim32开启一个Modbus服务
然后,使用modscan32读取数据
接着,使用MiniSniffer抓包观察验证是否与协议一致
抓取到的数据
发送:7C 01 00 00 00 06 01 03 00 00 00 02
接收:7C 01 00 00 00 07 01 03 04 00 00 00 00
与我们上面描述的一致
# 编码实现
- 初始化Socket通讯组件
//初始化
public override bool init(EquipItem item)
{
if (!base.init(item))
return false;
//初始化通讯组件
if (!serialport.Initialize(item))
return false;
return true;
}
//发送&接收
public override CommunicationState GetData(CEquipBase pEquip)
{
//请求指令
var request = new byte[] { 0x7C, 0x01, 0x00, 0x00, 0x00, 0x06, 0x01, 0x03, 0x00, 0x00, 0x00, 0x02 };
//将指令发送到服务
serialport.Write(request, 0, request.Length);
var buffer = new byte[256];
//接收服务返回的数据
var len = serialport.Read(buffer, 0, 256);
if(len <= 0)
{
return CommunicationState.fail;
}
//将接收到的数据存到成员变量中
_data = new byte[len];
Array.Copy(buffer, 0, buffer, 0, len);
return base.GetData(pEquip);
}
//解析
public override bool GetYC(YcpTableRow r)
{
//使用操作命令表示需要读取的寄存器地址
var addr = Convert.ToInt32(r.main_instruction);
//读取数据域数据
var readLen = _data.Length - 9;
var data = new byte[readLen];
Array.Copy(_data, 9, data, 0, readLen);
//modbus一个寄存器表示2个字节
var value = data[addr * 2] * 256 + data[addr * 2 + 1];
SetYCData(r, value);
return true;
}
# 协议插件中访问IoT数据库
案例描述
查询IoT数据库中AlarmProc表中的第一条数据,将查询到的数据通过测点进行呈现
注意
本案例同样适用于将设备的数据保存到IoT数据库的场景。此时只需要在IoT数据库中创建新表,再按照本案例提供的方式访问数据库即可。
IoTCenter.Extensions.EfCore
本案例中需要使用IoTCenter.Extensions.EfCore包访问IoT数据库,因此首先介绍IoTCenter.Extensions.EfCore包的使用说明
# 模型配置
模型必须通过IoTEntity特性标识,例如:
[IoTEntity]
public class Test
{
[Key]
public int Id { get; set; }
public string UserId { get; set; }
public string Name { get; set; }
}
EF Core提供注解和Fluent APi配置映射,比如上述用注解[Key]
来标识主键。大部分情况下都可以使用注解来完成映射,但在有些情况下,EF Core不支持通过注解映射,比如联合主键,此时通过Fluent APi即实现IEntityTypeConfiguration(EF Core内置APi)接口来配置映射。例如如下映射配置Id
和UserId
作为联合主键:
public class TestEntityTypeConfiguration : IEntityTypeConfiguration<Test>
{
public void Configure(EntityTypeBuilder<Test> builder)
{
builder.HasKey(k => new { k.Id, k.UserId });
}
}
注意
若同一列既通过注解也通过Fluent APi配置了映射,则Fluent APi会覆盖注解映射配置。
# 实例化上下文
使用如下代码创建上下文实例
using var context = IoTEfCore.CreateDbContext();
注意
必须加上using,保证上下文在操作完成后得以释放。
# 上下文操作
通过
context.Set<T>()
获取模型DbSet
属性,T
为用IoTEntity
特性所标识的模型调用扩展库暴露方法
context.IoTSet<T>()
获取模型DbSet
属性(适用于模型没有在调用创建上下文所在程序集中)
SQL语句操作
Execute/ExecuteAsync
:增、删、改
例如:context.Execute(“insert into test(id) values(1)”);
ExecuteScalar/ExecuteScalarAsync
:查询单个值
例如:context.ExecuteScalar(“select max(id) from test”);
Query/QueryAsync:
查询集合
例如:context.Query(“select * from test”);
QueryFirstOrDefault/QueryFirstOrDefaultAsync
:查询单行记录
例如:context.QueryFirstOrDefault(“select * from test where id = 1”);
# 编码实现
- 模型类
[IoTEntity]
public class AlarmProc
{
[Key]
[Column("Proc_Code")]
public int ProcCode { get; set; }
[Column("Proc_Module")]
public string ProcModule { get; set; }
[Column("Proc_name")]
public string ProcName { get; set; }
[Column("Proc_parm")]
public string ProcParm { get; set; }
}
- 数据库操作服务类
public static class AppService
{
public static AlarmProc GetAlarmProc()
{
using (var context = IoTEfCore.CreateDbContext())
{
return context.Set<AlarmProc>().FirstOrDefault();
}
}
}
- CEquip类
public class CEquip : CEquipBase
{
AlarmProc alarmProc = null;
int _sleep;
public override bool init(EquipItem item)
{
if (!base.init(item))
{
return false;
}
try
{
_sleep = int.Parse(item.communication_time_param);
}
catch
{
_sleep = 1000;
}
return true;
}
public override CommunicationState GetData(CEquipBase pEquip)
{
if(RunSetParmFlag)
{
return CommunicationState.setreturn;
}
Sleep(_sleep);
alarmProc = AppService.GetAlarmProc();
return base.GetData(pEquip);
}
public override bool GetYC(YcpTableRow r)
{
switch (r.main_instruction.ToLower().Trim())
{
case "code":
SetYCData(r, alarmProc?.ProcCode ?? 0);
break;
case "name":
SetYCData(r, alarmProc?.ProcName ?? "无");
break;
case "module":
SetYCData(r, alarmProc?.ProcModule ?? "无");
break;
}
return true;
}
public override bool GetYX(YxpTableRow r)
{
return base.GetYX(r);
}
public override bool SetParm(string MainInstruct, string MinorInstruct, string Value)
{
return base.SetParm(MainInstruct, MinorInstruct, Value);
}
}
提示
对IoT数据库的其它操作与上述代码实现类似,可以自行实现。
至此即可实现在协议插件中连接IoT数据中点击点击源代码进行下载。
# 练习
- 客户需要对多台电脑进行状态监控,并计划使用MQTT机制将各节点数据收集起来,通过IoTCenter进行显示,请基于该场景进行协议对接。