设备协议开发指南
目录
每个设备接口(代码中称为Interface)包含一个协议指针,指向ProtocolSvr对象,该指针在加载配置时根据配置的协议创建。
目前内置协议如果多个设备接口使用同一个协议,并不会共享协议对象。当然这仅仅是因为这些协议没什么需要共享的数据,如果有必要,多个设备接口共享一个协议对象甚至不同协议共享协议对象都是没有问题的。
每个设备接口只有一个协议指针,因此这个设备接口下的所有设备都只能是同一个协议。一个设备接口下多个设备的情形通常是串口之类的总线结构,总线上的所有设备具有相同的协议和通讯参数。特殊情况下编写一个复杂协议使用不同参数交替读取不同设备也是可能的。
自定义协议以动态库的方式实现,程序扫描所有特定前缀的动态库加载所需的协议。
动态库所需提供的入口点是“CreateIProtocol”,声明位于“interface/IProtocol.h”,声明如下:
extern "C"
{
//协议驱动提供此接口以便主程序获得协议对象指针
DLL_PUBLIC IProtocol* CreateIProtocol(char const* ProtocolCode);
}
很显然,所有的秘密都藏在IProtocol里。
gwprocotol目录下面是示例协议,其功能与隧道协议相同。
参照或修改此代码实现自定义协议。
一个设备接口的配置,包含多个设备。结构如下:
struct InterfaceDeviceConfig
{
string protocolCode;//MODBUS,OPCUA,DLT645,S7
string interfaceCode;//接口名
vector<Device* > devices;
ProtocolSvr* _ProtocolSvr = nullptr;//运行时协议服务
InterfaceState _InterfaceState = InterfaceState::DEFAULT;
string ToString(bool withDetail = true)const;
string AllDeviceName()const
{
stringstream ss;
for (auto& v : devices)
{
ss << v->deviceCode << ";";
}
return ss.str();
}
};
结构相当简单,主要包含了设备数组和协议服务指针。
协议服务不是接口,是一个执行框架,以一个协议接口为参数,调用协议接口执行协议处理。
协议服务定义如下:
class ProtocolSvr : private IWorkThread
{
private://IWorkThread
virtual void worker_job()override { pIProtocol->protocol_process_job(); }
virtual void timer_job()override { pIProtocol->protocol_process_timer_job(); }
CWorkerThread m_WorkThread;
private:
IProtocol* pIProtocol;
public:
ProtocolSvr(IProtocol * p):pIProtocol(p){}
IProtocol* getIProtocol()
{
return pIProtocol;
}
string getProtocolCode()const { return pIProtocol->m_ProtocolCode; }
bool isSerialPort()const { return pIProtocol->m_isSerialPort; }
//启动
bool StartProtocolSvr();
//激活
void ProtoSvrActive()
{
m_WorkThread.ActiveWorkerThread();
}
};
这是一个定时循环加手动触发的线程框架。
设备对象,这是主要的对象,有很多属性,包含一组通道。定义如下:
//设备
struct Device
{
Device(IDeviceParam* p) :deviceParam(p) {}
string deviceCode;//设备的代码,唯一标识一个设备,跨越接口的唯一
int rate = 0;//每多少毫秒采集一次,此值不能高于所有通道设置的值
IDeviceParam* deviceParam;//设备参数,与协议相关
vector<Channel*> channels;//设备的通道
LockedList<WriteInfo > writeList;//待写入的列表
DeviceState _DeviceState = DeviceState::DEFAULT;
int _rate = 0;//实际使用的值,由数采服务设置
CMyTime _allTime;//上次全部采集的时间
string _events;//未处理的事件,目前仅有变化全部上传
Channel* FindByChannelNo(int channelNo)
{
for (auto& c : channels)
{
if (c->channelNo == channelNo)return c;
}
return NULL;
}
Channel* FindByParamCode(char const* paramCode)
{
for (auto& c : channels)
{
if (c->paramCode == paramCode)return c;
}
return NULL;
}
void ToTable(string const& protocolCode, CHtmlDoc::CHtmlTable2& table, bool bEdit)const
{
table.SetTitle(this->deviceCode.c_str());
if (0 == channels.size())
{
//由于不知道实际结构,只显示标准表头
Channel tmp(NULL);
tmp.AddHtmlTalbeCols(protocolCode, table, bEdit);
}
else
{
channels[0]->AddHtmlTalbeCols(protocolCode, table, bEdit);
for (auto v : channels)
{
table.AddLine();
v->AddToHtmlTableLine(protocolCode, table, bEdit);
}
}
}
string ToString(string const& protocolCode, bool withDetail = true)const
{
stringstream ss;
ss << "\t设备[" << deviceCode << "] deviceParam:" << deviceParam->ToString() << " rate: " << rate << " _rate:" << _rate << " _DeviceState:" << (int)_DeviceState << endl;
if (!withDetail)
{
ss << "\t共有通道 " << channels.size() << " 个" << endl;
}
else
{
{
ss << "\t共有通道 " << channels.size() << " 个:" << endl;
CHtmlDoc::CHtmlTable2 table;
ToTable(protocolCode, table, false);
ss << table.MakeTextTable() << endl;
}
{
LockedList<WriteInfo >* pWriteList = (LockedList<WriteInfo >*) & writeList;
ss << "\t待写入 " << pWriteList->locked_size() << " 个:" << endl;
if (pWriteList->locked_size() > 0)
{
CHtmlDoc::CHtmlTable2 table;
table.AddCol("uid");
table.AddCol("c");
table.AddCol("v");
pWriteList->lock();
for (auto v : pWriteList->getForForeach())
{
table.AddLine();
table.AddData(v.uid);
table.AddData(v.channelNo);
table.AddData(v.value);
}
pWriteList->unlock();
ss << table.MakeTextTable() << endl;
}
}
}
return ss.str();
}
};
待写入的列表由平台下发指令产生,协议需要解读数据并正确发送给设备。
上次全部采集的时间和未处理的事件由协议酌情处理,Modbus协议是完整处理的例子。
每个设备包含一组通道。
通道对应一个可采集的数据,具有数据类型、长度等多种属性,以及多个采集控制参数。
通道定义如下:
class Channel
{
public:
int channelNo = 0;//通道号,唯一标识一个通道,与外部交互时可能用channelNo或paramCode来标识通道
string channelComment;//通道注释
string paramCode;//点位编码,唯一标识一个通道
string paramName;//名称
string dataType;//有ushort short ubyte byte uint int ulong long float double boolean
int dataLength = 0;//字符串和数组使用(数组尚未支持)
string charsetCode;//字符串编码 gbk utf-8,ChannelValue存储的是原始数据,也就是这个编码的数据,显示时要转换为程序所需的编码,与平台交互则需要转换为协商的编码,一般是utf-8
int processType = 0;//操作类型1-可读 2-可写 3-可读写
int collectorRate = 0;//采集频率,毫秒,或变化百分比,或依赖的channelNo
ReportStrategy reportStrategy;//上报策略 1-按采集频率上报 2-变化上报 3-监听上报(尚未支持) 4-条件上报(尚未支持) 5-依赖上报(随dependParam上报)6-变化触发全量
string dependParamCode = "";//依赖的点位编码,会转换为channelNo放在collectorRate
string transferRuleStr;//字节顺序ABCD
IChannelParam* pChannelParam;//通道的自定义参数
bool _changed = false;//检测到数据改变(仅对需要检测变化的类型)
bool _new_report = false;//新上报值(即数据需要放入上报数据包)
private:
ChannelValue _value;
CMyTime _valueTime;//读取的时间
ChannelState _ChannelState = ChannelState::DEFAULT;
ChannelValue _writeValue;//此值有效时将执行写入,写入完成后状态写入_ChannelWriteState
CMyTime _writeTime;//写入的时间
ChannelWriteState _ChannelWriteState = ChannelWriteState::DEFAULT;
public:
Channel(IChannelParam* p) :pChannelParam(p) {}
void SetChannelState(ChannelState state) { _ChannelState = state; }
int GetChannelState()const { return (int)_ChannelState; }
bool HasValue()const { return _value.bHasValue; }
ChannelValue& GetValue() { return _value; }
CMyTime const& GetValueTime()const { return _valueTime; }
void SetValue(string const& value)
{
_value._strValue = value;
_value.bHasValue = true;
_valueTime.SetCurrentTime();
}
void SetValueFromBuffer(uint16_t* regBuffer)
{
_value.ChannelValue_LoadFromBuffer(dataType, transferRuleStr, dataLength, regBuffer);
_valueTime.SetCurrentTime();
}
int GetChannelWriteState()const { return (int)_ChannelWriteState; }
bool HasWriteValue()const { return _writeValue.bHasValue; }
ChannelValue& GetWriteValue() { return _writeValue; }
void SetWriteValue(string const& dataType, string const& charset, string const& value)
{
_writeValue.ChannelValue_SetWriteValue(dataType, charsetCode, value);
_ChannelWriteState = ChannelWriteState::DEFAULT;
}
void FinishWriteValue(bool succeed)
{
if (succeed)
{
_writeValue.bHasValue = false;
_writeTime.SetCurrentTime();
_ChannelWriteState = ChannelWriteState::OK;
}
else
{
_ChannelWriteState = ChannelWriteState::WRITE_ERROR;
}
}
string ProcessTypeStr()const
{
if (1 == processType)return "只读";
else if (2 == processType)return "只写";
else if (3 == processType)return "读写";
else
{
stringstream ss;
ss << processType;
return ss.str();
}
}
bool CanRead()const
{
return 1 == processType || 3 == processType;
}
bool CanWrite()const
{
return 2 == processType || 3 == processType;
}
string ToString()const
{
stringstream ss;
ss << "\t\t" << channelNo << " : " << channelComment << " : " << paramCode << " : " << dataType << " " << dataLength << " " << pChannelParam->_ToString() << " processType: " << processType
<< " s:" << static_cast<int>(_ChannelState) << " v:" << _value.ChannelValue_ShowValue(dataType, charsetCode)
<< " w:" << _writeValue.ChannelValue_ShowValue(dataType, charsetCode) << " ws:" << static_cast<int>(_ChannelWriteState);
return ss.str();
}
void AddHtmlTalbeCols(string const& protocolCode, CHtmlDoc::CHtmlTable2& table, bool bEdit)
{
table.SetColFormInput(table.AddCol("channelNo", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 4, true);
table.SetColFormInput(table.AddCol("paramCode"), 16);
table.SetColFormInput(table.AddCol("paramName"), 16);
table.AddCol("channelComment");
table.SetColFormInput2(table.AddCol("数据类型"), 8, "float uint boolean string ushort");
table.SetColFormInput(table.AddCol("长度", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 2);
if (pChannelParam)pChannelParam->AddHtmlTalbeCols(protocolCode, table, bEdit);
if (!bEdit)
{
table.AddCol("s", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
table.AddCol("值", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
table.AddCol("v", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
}
table.SetColFormInput(table.AddCol("字符集", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 8);
table.SetColFormInput2(table.AddCol("pT", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 4, "只读 读写 只写");
table.SetColFormInput(table.AddCol("采集间隔", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 6);
table.SetColFormInput2(table.AddCol("上报策略", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 4, "1-周期采集 2-变化上报 5-依赖上报 6-变化全报");
table.SetColFormInput2(table.AddCol("字节序"), 4, "ABCD CDAB BADC DCBA");
if (!bEdit)
{
table.AddCol("s", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
table.AddCol("写", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
table.AddCol("w", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
}
}
void AddToHtmlTableLine(string const& protocolCode, CHtmlDoc::CHtmlTable2& table, bool bEdit)
{
table.AddData(channelNo);
table.AddData(paramCode);
table.AddData(paramName);
table.AddData(ReportStrategy_DEPEND == reportStrategy.reportStrategy ? channelComment + " " + dependParamCode : channelComment);
table.AddData(dataType);
table.AddData(dataLength);
pChannelParam->AddToHtmlTableLine(protocolCode, table, bEdit);
if (!bEdit)
{
table.AddData(static_cast<int>(_ChannelState));
table.AddData(_value.ChannelValue_ShowValue(dataType, charsetCode));
table.AddData(_valueTime.hasTime() ? _valueTime.GetTimeSpanS() : -1);
}
table.AddData(charsetCode);
table.AddData(ProcessTypeStr());
table.AddData(collectorRate);
table.AddData(reportStrategy.ToString());
table.AddData(transferRuleStr);
if (!bEdit)
{
table.AddData(static_cast<int>(_ChannelWriteState));
table.AddData(_writeValue.ChannelValue_ShowValue(dataType, charsetCode));
table.AddData(_writeTime.hasTime() ? _writeTime.GetTimeSpanS() : -1);
}
}
};
通道号是一个顺序号,一些设备可以导出配置,如昆仑的触摸屏可以导出CSV文件,网关可以直接读取导出的CSV文件,顺序号可以和CSV文件的行对应。
与平台交互时使用paramCode作为通道唯一标识。
这个类的大部分内容是为Modbus协议设计的,其他协议可以使用“IChannelParam* pChannelParam”来处理自定义参数。
此接口由协议驱动动态库的入口点返回给主程序使用。
//协议接口
class IProtocol
{
public:
std::string m_ProtocolCode;//协议代码
std::string m_DriverFile;//驱动文件名
bool m_isSerialPort;
InterfaceDeviceConfig* m_pInertfaceDevice = nullptr;
public:
IProtocol(char const* code, bool isSerialPort) :m_ProtocolCode(code), m_isSerialPort(isSerialPort) {}
//返回协议参数接口指针供框架调用
virtual IProtocolParam* getIProtocolParam() = 0;
//返回设备参数接口指针供框架调用,每个协议服务对象实例可能包含多个设备
virtual IDeviceParam* getIDeviceParam(char const* deviceCode) = 0;
//返回通道参数接口指针供框架调用,每个设备包含多个通道
virtual IChannelParam* getIChannelParam(char const* deviceCode, int channelNo) = 0;
//初始化
virtual bool ProtoSvrInit(InterfaceDeviceConfig* p) = 0;
//在设备配置加载之后执行
virtual bool OnAfterLoadDeviceConfig(Device* pDevice) = 0;
//卸载
virtual bool ProtoSvrUnInit() = 0;
//析构
virtual ~IProtocol() {}
//工作函数,由定时器激活,如果函数内部自己做循环不会退出,激活动作没有实际影响
virtual void protocol_process_job() = 0;
//定时器函数,在激活工作线程之前执行
virtual void protocol_process_timer_job() = 0;
private:
map<string, map<int, IChannelParam*> > m_device_channelparams;
};
必须实现所有未定义的虚函数。
三级参数:协议、设备、通道,分别由IProtocolParam、IDeviceParam、IChannelParam定义,接口结构相似,主要包含一个加载配置接口和一个输出文本描述接口。IChannelParam还包括输出到表格的接口。
框架对自定义参数的处理仅仅包括加载配置和输出文本(用于控制台或Show指令显示)。
协议参数接口,定义如下:
struct IProtocolParam
{
//自定义参数的输出
virtual std::string _ToString()const { return ""; }
//从配置json中加载
virtual bool LoadConfig(InterfaceDeviceConfig* pInterface, cJSON* cjson_resolvePropertyMap) { return true; }
//通用
int rate = 0;//每多少毫秒采集一次,此值不能高于所有通道设置的值
int _rate = 0;//实际使用的值,由数采服务设置
std::string ToString()const
{
std::stringstream ss;
ss << " rate:" << rate << " _rate:" << _rate << std::endl;
ss << _ToString() << std::endl;
return ss.str();
}
};
配置加载时传入设备接口对象指针,可以读取或修改对象的属性,传入的json对应配置文件的“resolvePropertyMap”对象。
如果协议处理函数自己做循环处理,不退出,可以无视通用参数rate。通用参数已经由框架处理,注意,自定义参数使用的配置项名称尽量不要和通用参数冲突。
设备参数接口如下:
class IDeviceParam
{
public:
//自定义参数的输出
virtual std::string _ToString()const { return ""; }
//从配置json中加载
virtual bool LoadConfig(InterfaceDeviceConfig* pInterface, cJSON* cjson_confTabs_item) { return true; }
std::string ToString()const
{
std::stringstream ss;
ss << _ToString() << std::endl;
return ss.str();
}
};
更简单,连一个通用参数都没有。
配置加载时传入的参数为设备对象指针,json则为对应的confTabs数组的一个成员。
通道参数接口如下:
class IChannelParam
{
public:
virtual ~IChannelParam() {}
//自定义参数的输出
virtual std::string _ToString()const = 0;
//从配置json中加载
virtual bool LoadConfig(string const& protocolCode, cJSON* cjson_resolveParamConfigVOList_item) = 0;
//输出表格头
virtual void AddHtmlTalbeCols(string const& protocolCode, CHtmlDoc::CHtmlTable2& table, bool bEdit) = 0;
//输出表格数据
virtual void AddToHtmlTableLine(string const& protocolCode, CHtmlDoc::CHtmlTable2& table, bool bEdit) = 0;
};
配置加载接口仅传入了协议代码,可以根据这个做适当的区分,传入的json对应通道的参数。
输出表格的接口用于表格输出,框架先调用AddHtmlTalbeCols输出表格头,也就是添加对应的列,然后逐行调用AddToHtmlTableLine输出列数据,注意表格头和输出数据的个数必须严格一致。