Device Protocol Development Guide

directory

1 Basic design... 1

1.1 Device Interface and Protocol Pointers... 1

1.2 Discovery and Loading of Custom Protocols... 1

1.3 Sample Code... 2

2 References... 2

2.1 Configuration structure of the framework... 2

2.1.1       InterfaceDeviceConfig. 2

2.1.2 ProtocolSvr 3

2.1.3       Device. 4

2.1.4       Channel 8

2.2 IProtocol protocol interface... 15

2.2.1 Overview... 15

2.2.2 Custom parameters... 16

2.2.3       IProtocolParam... 17

2.2.4       IDeviceParam... 18

2.2.5       IChannelParam... 18

 

 

1 Basic design

1.1 Device Interface and Protocol Pointers

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.

1.2 Discovery and Loading of Custom Protocols

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.

1.3 Sample Code

Below the gwprocotol directory is an example protocol that functions the same as a tunneling protocol.

Graphical user interface, text, applications

The AI-generated content may not be correct.

Refer to or modify this code to implement a custom protocol.

2 References

2.1 Configuration structure of the framework

2.1.1  InterfaceDeviceConfig

A configuration of a device interface that contains multiple devices. The structure is as follows:

struct InterfaceDeviceConfig

{

string protocolCode; //MODBUSOPCUADLT645S7

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.

2.1.2 ProtocolSvr

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.

2.1.3  Device

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.

2.1.4  Channel

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.

2.2 IProtocol protocol interface

2.2.1 Overview

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.

2.2.2 Custom parameters

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).

2.2.3  IProtocolParam

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.

2.2.4  IDeviceParam

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.

2.2.5  IChannelParam

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.