Device Protocol Development Guide
directory
1.1 Device Interface and Protocol Pointers
1.2 Discovery and Loading of Custom Protocols
2.1 Configuration structure of the framework
2.2 IProtocol protocol interface
Each device interface (referred to as an Interface in code) contains a protocol pointer that points to a ProtocolSvr object, which is created based on the configured protocol when the configuration is loaded.
Currently, if multiple device interfaces use the same protocol, the built-in protocol does not share the protocol object. Of course, this is only because these protocols don't have much data to share, and if necessary, multiple device interfaces can share a protocol object or even different protocols can share protocol objects.
Each device interface has only one protocol pointer, so all devices under this device interface can only be the same protocol. In the case of multiple devices under a device interface, a bus structure such as a serial port is usually used, and all devices on the bus have the same protocol and communication parameters. In special cases, it is also possible to write a complex protocol that alternately reads different devices with different parameters.
Custom protocols are implemented in the form of dynamic libraries, where the program scans for all the protocols required for dynamic library loading for specific prefixes.
The entry point required for the dynamic library is "CreateIProtocol", the declaration is located in "interface/IProtocol.h", and the declaration is as follows:
extern "C"
{
//The protocol driver provides this interface so that the main program obtains the protocol object pointer
DLL_PUBLIC IProtocol* CreateIProtocol(char const* ProtocolCode);
}
Obviously, all the secrets are hidden in the IProtocol.
Below the gwprocotol directory is an example protocol that functions the same as a tunneling protocol.
Refer to or modify this code to implement a custom protocol.
A configuration of a device interface that contains multiple devices. The structure is as follows:
struct InterfaceDeviceConfig
{
string protocolCode; //MODBUS,OPCUA,DLT645,S7
string interfaceCode; The name of the interface
vector<Device* > devices;
ProtocolSvr* _ProtocolSvr = nullptr; Runtime Protocol Service
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();
}
};
The structure is fairly simple, consisting mainly of an array of devices and protocol service pointers.
The protocol service is not an interface, but an execution framework, which uses a protocol interface as a parameter and calls the protocol interface to perform protocol processing.
The Agreement Service is defined as follows:
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; }
//initiate
bool StartProtocolSvr();
//activation
void ProtoSvrActive()
{
m_WorkThread.ActiveWorkerThread();
}
};
This is a threading framework with a timed loop plus manual triggering.
The device object, which is the primary object, has a lot of properties and contains a set of channels. The definition is as follows:
//equipment
struct Device
{
Device(IDeviceParam* p) :deviceParam(p) {}
string deviceCode; The code of a device, which uniquely identifies a device and is unique across interfaces
int rate = 0; This value cannot be higher than the value set for all channels every millisecond of acquisition
IDeviceParam* deviceParam; Device parameters, protocol-related
vector<Channel*> channels; The channel of the device
LockedList<WriteInfo > writeList; List to be written
DeviceState _DeviceState = DeviceState::DEFAULT;
int _rate = 0; The value to be used is set by DAQ
CMyTime _allTime; The time when all were last collected
string _events; For unprocessed events, only the changes are currently uploaded
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())
{
//Since the actual structure is not known, only the standard header is displayed
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 << "\tDevice[" << deviceCode << "] deviceParam:" << deviceParam->ToString() << " rate: " << rate << " _rate:" << _rate << " _DeviceState:" << (int)_DeviceState << endl;
if (!withDetail)
{
ss << "\tcommon channels" << channels.size() << " << endl;
}
else
{
{
ss << "\tcommon channels" << channels.size() << " counts:" << endl;
CHtmlDoc::CHtmlTable2 table;
ToTable(protocolCode, table, false);
ss << table. MakeTextTable() << endl;
}
{
LockedList<WriteInfo >* pWriteList = (LockedList<WriteInfo >*) & writeList;
ss << "\tTo be written " << pWriteList->locked_size() << " counts:" << 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();
}
};
The list to be written is generated by the instructions issued by the platform, and the protocol needs to interpret the data and send it to the device correctly.
The time of the last full acquisition and unprocessed events are handled at the discretion of the protocol, with the Modbus protocol being an example of complete processing.
Each device contains a set of channels.
The channel corresponds to a collectible data with multiple attributes such as data type, length, and multiple acquisition control parameters.
The channel is defined as follows:
class Channel
{
public:
int channelNo = 0; The channel number, which uniquely identifies a channel, may be identified by channelNo or paramCode when interacting with the outside
string channelComment; Channel annotations
string paramCode; Point code, which uniquely identifies a channel
string paramName; Name
string dataType; There is ushort short ubyte byte uint int ulong long float double boolean
int dataLength = 0; String and array usage (arrays are not yet supported).
string charsetCode; String-encoded GBK utf-8, ChannelValue stores the original data, that is, the encoded data, which needs to be converted into the encoding required by the program when displayed, and the encoding that needs to be negotiated when interacting with the platform, which is generally utf-8
int processType = 0; Operation Type 1 - Readable 2 - Writable 3 - Read and Write
int collectorRate = 0; Acquisition frequency, milliseconds, or percent change, or dependent channelNo
ReportStrategy reportStrategy; Escalation Policy 1 - Report by collection frequency 2 - Report change 3 - Listen report (not yet supported) 4 - Conditional report (not yet supported) 5 - Dependency report (report with dependParam) 6- The change triggers the full amount
string dependParamCode = ""; The dependent point code will be converted to channelNo and placed in collectorRate
string transferRuleStr; Byte order ABCD
IChannelParam* pChannelParam; Custom parameters for the channel
bool _changed = false; Data changes are detected (only for the type of change that needs to be detected).
bool _new_report = false; The new reported value (that is, the data needs to be put into the reported data package).
private:
ChannelValue _value;
CMyTime _valueTime; The time of the read
ChannelState _ChannelState = ChannelState::DEFAULT;
ChannelValue _writeValue; Writes are performed when this value is valid, and the state writes to _ChannelWriteState when the writes are complete
CMyTime _writeTime; The time at which it was written
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 "read-only";
else if (2 == processType)return "write-only";
else if (3 == processType)return "read and write";
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("Data Type"), 8, "float uint boolean string ushort");
table. SetColFormInput(table. AddCol("length", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 2);
if (pChannelParam)pChannelParam->AddHtmlTalbeCols(protocolCode, table, bEdit);
if (!bEdit)
{
table. AddCol("s", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
table. AddCol("value", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
table. AddCol("v", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
}
table. SetColFormInput(table. AddCol("character set", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 8);
table. SetColFormInput2(table. AddCol("pT", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 4, "Read-only Read and write only write");
table. SetColFormInput(table. AddCol("Acquisition Interval", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 6);
table. SetColFormInput2(table. AddCol("Escalation Policy", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT), 4, "1-Cycle Acquisition 2-Change Report 5-Dependent Report 6-Change Full Report");
table. SetColFormInput2(table. AddCol("endian"), 4, "ABCD CDAB BADC." DCBA");
if (!bEdit)
{
table. AddCol("s", CHtmlDoc::CHtmlDoc_DATACLASS_RIGHT);
table. AddCol("write", 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);
}
}
};
The channel number is a sequence number, some devices can export configurations, such as Kunlun's touch screen can export CSV files, the gateway can directly read the exported CSV files, and the sequence number can correspond to the rows of the CSV file.
When interacting with the platform, paramCode is used as the unique identifier of the channel.
Most of this class is designed for the Modbus protocol, and other protocols can use "IChannelParam* pChannelParam" to handle custom parameters.
This interface is returned to the main program by the entry point of the protocol-driven dynamic library.
//Protocol interfaces
class IProtocol
{
public:
std::string m_ProtocolCode; Protocol code
std::string m_DriverFile; The name of the driver file
bool m_isSerialPort;
InterfaceDeviceConfig* m_pInertfaceDevice = nullptr;
public:
IProtocol(char const* code, bool isSerialPort) :m_ProtocolCode(code), m_isSerialPort(isSerialPort) {}
//Returns protocol parameter interface pointers for the framework to call
virtual IProtocolParam* getIProtocolParam() = 0;
//Returns device parameter interface pointers for the framework to call, and each protocol service object instance may contain multiple devices
virtual IDeviceParam* getIDeviceParam(char const* deviceCode) = 0;
//Returns a channel parameter interface pointer for the framework to call, and each device contains multiple channels
virtual IChannelParam* getIChannelParam(char const* deviceCode, int channelNo) = 0;
//initialize
virtual bool ProtoSvrInit(InterfaceDeviceConfig* p) = 0;
//Executed after the device configuration is loaded
virtual bool OnAfterLoadDeviceConfig(Device* pDevice) = 0;
//unload
virtual bool ProtoSvrUnInit() = 0;
//Destruct
virtual ~IProtocol() {}
//The working function, which is activated by a timer, will not exit if the function does its own loop internally, and the activation action has no practical effect
virtual void protocol_process_job() = 0;
//Timer function, which is executed before the worker is activated
virtual void protocol_process_timer_job() = 0;
private:
map<string, map<int, IChannelParam*> > m_device_channelparams;
};
All undefined virtual functions must be implemented.
The three-level parameters: protocol, device, and channel, are defined by IProtocolParam, IDeviceParam, and IChannelParam, respectively, and the interface structure is similar, mainly including a loading configuration interface and an output text description interface. IChannelParam also includes an interface for output to tables.
The framework's handling of custom parameters consists only of loading the configuration and outputting the text (for console or show command display).
Protocol parameter interfaces, as defined below:
struct IProtocolParam
{
//Output of custom parameters
virtual std::string _ToString()const { return ""; }
//Loaded from the configuration json
virtual bool LoadConfig(InterfaceDeviceConfig* pInterface, cJSON* cjson_resolvePropertyMap) { return true; }
//general
int rate = 0; This value cannot be higher than the value set for all channels every millisecond of acquisition
int _rate = 0; The value to be used is set by DAQ
std::string ToString()const
{
std::stringstream ss;
ss << " rate:" << rate << " _rate:" << _rate << std::endl;
ss << _ToString() << std::endl;
return ss.str();
}
};
When configuring the loading, pass in the device interface object pointer to read or modify the properties of the object, and the json passed in corresponds to the "resolvePropertyMap" object of the configuration file.
If the protocol handler does its own loop processing and does not exit, it can ignore the common parameter rate. The common parameters have been processed by the framework, so please note that the names of the configuration items used by the custom parameters should not conflict with the common parameters.
The device parameters are as follows:
class IDeviceParam
{
public:
//Output of custom parameters
virtual std::string _ToString()const { return ""; }
//Loaded from the configuration 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();
}
};
It's simpler, and there's not even a common parameter.
The parameter passed in when loading the configuration is the device object pointer, and json is a member of the corresponding confTabs array.
The channel parameter interface is as follows:
class IChannelParam
{
public:
virtual ~IChannelParam() {}
//Output of custom parameters
virtual std::string _ToString()const = 0;
//Loaded from the configuration json
virtual bool LoadConfig(string const& protocolCode, cJSON* cjson_resolveParamConfigVOList_item) = 0;
//Output table header
virtual void AddHtmlTalbeCols(string const& protocolCode, CHtmlDoc::CHtmlTable2& table, bool bEdit) = 0;
//Output tabular data
virtual void AddToHtmlTableLine(string const& protocolCode, CHtmlDoc::CHtmlTable2& table, bool bEdit) = 0;
};
The configuration loading interface only passes in the protocol code, which can be appropriately distinguished according to this, and the json passed in corresponds to the parameters of the channel.
The interface for outputting tables is used for table output, the framework first calls AddHtmlTalbeCols to output table headers, that is, add the corresponding columns, and then calls AddToHtmlTableLine line line by row to output column data, note that the number of table headers and output data must be strictly consistent.