-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Included ModelChanged Eventhandling. Fixed update of nodes after reconnection. #368
Changes from all commits
370df35
16ada09
3545052
68c9d0f
d52d8b2
5fe6d56
a77b113
84747cd
397d86e
6e05866
4d13a60
51f8c90
46cd1b0
2e3217a
1ac7267
713e2e9
ed6b945
112ccf8
0d92ec5
af8d443
cc7822e
c4518b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,213 @@ namespace Umati | |
std::shared_ptr<OpcUaTypeReader> pTypeReader) | ||
: m_pDashboardDataClient(pDashboardDataClient), m_pPublisher(pPublisher), m_pTypeReader(pTypeReader) | ||
{ | ||
this->startEventThread(); | ||
} | ||
|
||
DashboardClient::~DashboardClient() { | ||
this->stopEventThread(); | ||
} | ||
|
||
void DashboardClient::startEventThread() { | ||
if (m_eventThreadRunning) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m_eventThreadRunning is
|
||
{ | ||
LOG(INFO) << "EventThread Running"; | ||
return; | ||
} | ||
|
||
auto func = [this]() { | ||
int cnt = 0; | ||
while (this->m_eventThreadRunning) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not protected by mutex There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced by atomic. |
||
if(!m_eventqueue.empty()) { | ||
IDashboardDataClient::StructureChangeEvent sce = m_eventqueue.front(); | ||
m_eventqueue.pop(); | ||
this->m_dynamicNodes.push_back(sce.refreshNode); | ||
if(sce.nodeAdded || sce.referenceAdded) { | ||
this->updateAddDataSet(sce.refreshNode); | ||
this->Publish(); | ||
} | ||
if(sce.nodeDeleted || sce.referenceDeleted) { | ||
this -> updateDeleteDataSet(sce.refreshNode); | ||
if(sce.nodeDeleted == true) { | ||
auto search = browsedSimpleNodes.find(sce.refreshNode); | ||
if(search != browsedSimpleNodes.end()) { | ||
std::shared_ptr<const ModelOpcUa::SimpleNode> simpleNode = search->second; | ||
this->deleteAndUnsubscribeNode(*simpleNode); | ||
} | ||
} | ||
this -> Publish(); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indentation looks suscious. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced by Threadsafe queue! |
||
std::this_thread::sleep_for(std::chrono::milliseconds(100)); | ||
} | ||
}; | ||
m_eventThreadRunning = true; | ||
m_eventThread = std::thread(func); | ||
} | ||
void DashboardClient::stopEventThread() { | ||
m_eventThreadRunning = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m_eventThreadRunning is not protected. Might lead to memory corruption There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced by atomic bool. |
||
if (m_eventThread.joinable()) | ||
{ | ||
m_eventThread.join(); | ||
} | ||
} | ||
void DashboardClient::reloadDataSet(ModelOpcUa::NodeId_t nodeId) { | ||
auto search = browsedSimpleNodes.find(nodeId); | ||
if(search == browsedSimpleNodes.end()){ | ||
return; | ||
} | ||
std::shared_ptr<const ModelOpcUa::SimpleNode> simpleNode = search->second; | ||
auto childNodes = simpleNode->ChildNodes; | ||
if(childNodes.empty()) { | ||
return; | ||
} | ||
auto child = childNodes.front(); | ||
std::shared_ptr<const ModelOpcUa::PlaceholderNode> placeholderNode = std::dynamic_pointer_cast<const ModelOpcUa::PlaceholderNode>(child); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not make data model non-const? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This involves changeing some classes in the model and i the codebase. I want to do this, but maybe it is better to do this in a separate task. |
||
if(placeholderNode == nullptr) { | ||
return; | ||
} | ||
std::shared_ptr<ModelOpcUa::PlaceholderNode> placeholderNodeUnconst = std::const_pointer_cast<ModelOpcUa::PlaceholderNode>(placeholderNode); | ||
for(auto instance : placeholderNode->getInstances()) { | ||
placeholderNodeUnconst->removeInstance(instance); | ||
deleteAndUnsubscribeNode(instance); | ||
} | ||
auto browseResults = m_pDashboardDataClient->Browse(nodeId, child->ReferenceType, child->SpecifiedTypeNodeId); | ||
this->updateAddDataSet(nodeId); | ||
} | ||
|
||
void DashboardClient::updateDeleteDataSet(ModelOpcUa::NodeId_t refreshNodeId) { | ||
auto search = browsedSimpleNodes.find(refreshNodeId); | ||
if( search != browsedSimpleNodes.end()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Invert + Return |
||
std::shared_ptr<const ModelOpcUa::SimpleNode> simpleNode = search->second; | ||
auto childNodes = simpleNode->ChildNodes; | ||
if(!childNodes.empty()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Invert + Return |
||
auto child = childNodes.front(); | ||
std::shared_ptr<const ModelOpcUa::PlaceholderNode> placeholderNode = std::dynamic_pointer_cast<const ModelOpcUa::PlaceholderNode>(child); | ||
if(placeholderNode == nullptr) { return; } | ||
auto browseResults = m_pDashboardDataClient->Browse(refreshNodeId, child->ReferenceType, child->SpecifiedTypeNodeId); | ||
//Check if Instances are still in browsresult | ||
std::list<ModelOpcUa::PlaceholderElement> instances = placeholderNode->getInstances(); | ||
std::list<ModelOpcUa::PlaceholderElement> missingElements; | ||
for(auto& el : instances) { | ||
bool found = false; | ||
ModelOpcUa::NodeId_t nodeId = el.pNode->NodeId; | ||
for(ModelOpcUa::BrowseResult_t browseResult : browseResults) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. std::find_if |
||
if(browseResult.NodeId == nodeId) { | ||
found = true; | ||
break; | ||
} | ||
} | ||
if(!found) { | ||
missingElements.push_back(el); | ||
} | ||
} | ||
//Remove and Unsubscribe the elements | ||
for(auto& el : missingElements) { | ||
std::shared_ptr<ModelOpcUa::PlaceholderNode> placeholderNodeUnconst = std::const_pointer_cast<ModelOpcUa::PlaceholderNode>(placeholderNode); | ||
placeholderNodeUnconst->removeInstance(el); | ||
deleteAndUnsubscribeNode(el); | ||
} | ||
} | ||
} | ||
} | ||
void DashboardClient::deleteAndUnsubscribeNode(ModelOpcUa::PlaceholderElement placeHolderElement) { | ||
std::shared_ptr<const ModelOpcUa::SimpleNode> element = placeHolderElement.pNode; | ||
const std::list<std::shared_ptr<const ModelOpcUa::Node>> children = element->ChildNodes; | ||
for(auto& el : children) { | ||
std::shared_ptr<const ModelOpcUa::Node> node = el; | ||
std::shared_ptr<const ModelOpcUa::SimpleNode> simpleNode = std::dynamic_pointer_cast<const ModelOpcUa::SimpleNode>(node); | ||
if(simpleNode != nullptr) { | ||
const ModelOpcUa::SimpleNode simpleN = *simpleNode; | ||
deleteAndUnsubscribeNode(simpleN); | ||
} else { | ||
std::shared_ptr<const ModelOpcUa::PlaceholderNode> placeHolderNode = std::dynamic_pointer_cast<const ModelOpcUa::PlaceholderNode>(node); | ||
if(placeHolderNode == nullptr) { continue;} | ||
std::list<ModelOpcUa::PlaceholderElement> instances = placeHolderNode->getInstances(); | ||
for(auto& el : instances) { | ||
deleteAndUnsubscribeNode(el); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. std::foreach |
||
} | ||
} | ||
} | ||
browsedNodes.erase(element->NodeId); | ||
browsedSimpleNodes.erase(element->NodeId); | ||
|
||
} | ||
void DashboardClient::deleteAndUnsubscribeNode(const ModelOpcUa::SimpleNode simpleNode) { | ||
const std::list<std::shared_ptr<const ModelOpcUa::Node>> children = simpleNode.ChildNodes; | ||
for(auto node : children) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks identical to the method above? Use a single function for this? |
||
std::shared_ptr<const ModelOpcUa::SimpleNode> simpleNode = std::dynamic_pointer_cast<const ModelOpcUa::SimpleNode>(node); | ||
if(simpleNode != nullptr) { | ||
const ModelOpcUa::SimpleNode simpleN = *simpleNode; | ||
deleteAndUnsubscribeNode(simpleN); | ||
} else { | ||
std::shared_ptr<const ModelOpcUa::PlaceholderNode> placeHolderNode = std::dynamic_pointer_cast<const ModelOpcUa::PlaceholderNode>(node); | ||
if(placeHolderNode != nullptr) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Invert the condition and There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
std::list<ModelOpcUa::PlaceholderElement> instances = placeHolderNode->getInstances(); | ||
for(std::list<ModelOpcUa::PlaceholderElement>::iterator it = instances.begin(); it != instances.end(); it++) { | ||
deleteAndUnsubscribeNode(*it); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. std::foreach |
||
} | ||
|
||
} | ||
} | ||
} | ||
browsedNodes.erase(simpleNode.NodeId); | ||
browsedSimpleNodes.erase(simpleNode.NodeId); | ||
} | ||
void DashboardClient::subscribeEvents() { | ||
auto ecbf = [this](IDashboardDataClient::StructureChangeEvent sce) { | ||
this->m_eventqueue.push(sce); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. std::queue is not thread safe, mutex required. |
||
}; | ||
m_pDashboardDataClient->SubscribeEvent(ecbf); | ||
} | ||
void DashboardClient::updateAddDataSet(ModelOpcUa::NodeId_t refreshNodeId) { | ||
LOG(INFO) << "Update Add DataSet"; | ||
if(m_dataSets.empty()) { | ||
return; | ||
} | ||
std::shared_ptr<Umati::Dashboard::DashboardClient::DataSetStorage_t> dataSet = m_dataSets.front(); | ||
auto search = browsedSimpleNodes.find(refreshNodeId); | ||
if(search == browsedSimpleNodes.end()) { | ||
return; | ||
} | ||
std::shared_ptr<const ModelOpcUa::SimpleNode> simpleNode = search->second; | ||
auto childNodes = simpleNode->ChildNodes; | ||
if(!childNodes.empty()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. invert + return? |
||
auto child = childNodes.front(); | ||
std::shared_ptr<const ModelOpcUa::PlaceholderNode> placeholderNode = std::dynamic_pointer_cast<const ModelOpcUa::PlaceholderNode>(child); | ||
if(placeholderNode == nullptr) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return instead? |
||
} else { | ||
auto browseResults = m_pDashboardDataClient->Browse(refreshNodeId, child->ReferenceType, | ||
child->SpecifiedTypeNodeId); | ||
for (auto &browseResult : browseResults) { | ||
if (browseResult.TypeDefinition.Id == NodeId_BaseObjectType.Id) { | ||
auto ifs = m_pDashboardDataClient->Browse(browseResult.NodeId, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indentation |
||
Dashboard::IDashboardDataClient::BrowseContext_t::HasInterface()); | ||
browseResult.TypeDefinition = ifs.front().NodeId; | ||
LOG(INFO) << "Updated TypeDefinition of " << browseResult.BrowseName.Name << " to " << browseResult.TypeDefinition | ||
<< " because the node implements an interface"; | ||
} | ||
|
||
auto possibleType = m_pTypeReader->m_typeMap->find(browseResult.TypeDefinition); // use subtype | ||
if (possibleType != m_pTypeReader->m_typeMap->end()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. invert + return |
||
{ | ||
if(browsedNodes.find(browseResult.NodeId) == browsedNodes.end()) { | ||
auto sharedPossibleType = possibleType->second; | ||
ModelOpcUa::PlaceholderElement plElement; | ||
plElement.BrowseName = browseResult.BrowseName; | ||
plElement.pNode = TransformToNodeIds(browseResult.NodeId, sharedPossibleType); | ||
plElement.TypeDefinition = browseResult.TypeDefinition; | ||
//Const cast | ||
std::shared_ptr<ModelOpcUa::PlaceholderNode> placeholderNodeUnconst = std::const_pointer_cast<ModelOpcUa::PlaceholderNode>(placeholderNode); | ||
placeholderNodeUnconst->addInstance(plElement); | ||
subscribeValues(plElement.pNode, dataSet->values, dataSet->values_mutex); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
bool DashboardClient::containsNodeId(ModelOpcUa::NodeId_t nodeId) { | ||
return (browsedNodes.find(nodeId) != browsedNodes.end()); | ||
} | ||
|
||
|
||
|
@@ -224,6 +431,8 @@ namespace Umati | |
break; | ||
} | ||
} | ||
} else { | ||
LOG(INFO) << "Not Inserted Node: " << startNode; | ||
} | ||
auto pNode = std::make_shared<ModelOpcUa::SimpleNode>( | ||
startNode, | ||
|
@@ -232,6 +441,7 @@ namespace Umati | |
foundChildNodes); | ||
|
||
pNode->ofBaseDataVariableType = pTypeDefinition->ofBaseDataVariableType; | ||
browsedSimpleNodes.insert({startNode, pNode}); | ||
return pNode; | ||
} | ||
|
||
|
@@ -405,7 +615,7 @@ namespace Umati | |
std::map<std::shared_ptr<const ModelOpcUa::Node>, nlohmann::json> &valueMap, | ||
std::mutex &valueMap_mutex) | ||
{ | ||
// LOG(INFO) << "subscribeValues " << pNode->NodeId.Uri << ";" << pNode->NodeId.Id; | ||
//LOG(INFO) << "subscribeValues " << pNode->NodeId.Uri << ";" << pNode->NodeId.Id; | ||
|
||
// Only Mandatory/Optional variables | ||
if (isMandatoryOrOptionalVariable(pNode)) | ||
|
@@ -506,8 +716,10 @@ namespace Umati | |
try | ||
{ | ||
for(auto value : m_subscribedValues){ | ||
if(value && value.get()->getNodeId() == pNode.get()->NodeId) | ||
return; | ||
if(value && value.get()->getNodeId() == pNode.get()->NodeId) { | ||
//LOG(INFO) << "Allready Subscribed" << value.get()->getNodeId(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you remove the comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll Remove the comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you? |
||
return; | ||
} | ||
} | ||
auto subscribedValue = m_pDashboardDataClient->Subscribe(pNode->NodeId, callback); | ||
m_subscribedValues.push_back(subscribedValue); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,9 @@ | |
#include <map> | ||
#include <set> | ||
#include <mutex> | ||
#include <thread> | ||
#include <queue> | ||
|
||
namespace Umati { | ||
|
||
namespace Dashboard { | ||
|
@@ -42,6 +45,8 @@ namespace Umati { | |
std::shared_ptr<IPublisher> pPublisher, | ||
std::shared_ptr<OpcUaTypeReader> pTypeReader); | ||
|
||
~DashboardClient(); | ||
|
||
void addDataSet( | ||
const ModelOpcUa::NodeId_t &startNodeId, | ||
const std::shared_ptr<ModelOpcUa::StructureNode> &pTypeDefinition, | ||
|
@@ -51,6 +56,13 @@ namespace Umati { | |
void Publish(); | ||
|
||
void Unsubscribe(ModelOpcUa::NodeId_t nodeId); | ||
void subscribeEvents(); | ||
|
||
bool containsNodeId(ModelOpcUa::NodeId_t nodeId); | ||
void updateAddDataSet(ModelOpcUa::NodeId_t nodeId); | ||
void updateDeleteDataSet(ModelOpcUa::NodeId_t nodeId); | ||
void deleteAndUnsubscribeNode(const ModelOpcUa::SimpleNode nodeId); | ||
void reloadDataSet(ModelOpcUa::NodeId_t nodeId); | ||
|
||
|
||
protected: | ||
|
@@ -92,10 +104,19 @@ namespace Umati { | |
std::shared_ptr<OpcUaTypeReader> m_pTypeReader; | ||
|
||
std::set<ModelOpcUa::NodeId_t> browsedNodes; | ||
std::map<const ModelOpcUa::NodeId_t, std::shared_ptr<const ModelOpcUa::SimpleNode>> browsedSimpleNodes; | ||
std::recursive_mutex m_dataSetMutex; | ||
std::list<std::shared_ptr<DataSetStorage_t>> m_dataSets; | ||
std::map<std::string, LastMessage_t> m_latestMessages; | ||
|
||
bool m_eventThreadRunning; | ||
std::thread m_eventThread; | ||
std::queue<IDashboardDataClient::StructureChangeEvent> m_eventqueue; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. std::queue is not threadsafe! Add mutex/lockfree queue or sth There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced by a threadsafe queue. |
||
std::list<ModelOpcUa::NodeId_t> m_dynamicNodes; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Never read? Could be removed? |
||
|
||
void startEventThread(); | ||
void stopEventThread(); | ||
|
||
bool isMandatoryOrOptionalVariable(const std::shared_ptr<const ModelOpcUa::SimpleNode> &pNode); | ||
|
||
void handleSubscribeChildNodes(const std::shared_ptr<const ModelOpcUa::SimpleNode> &pNode, | ||
|
@@ -134,6 +155,8 @@ namespace Umati { | |
|
||
void TransformToNodeIdNodeNotFoundLog(const ModelOpcUa::NodeId_t &startNode, | ||
const std::shared_ptr<ModelOpcUa::StructureNode> &pChild) const; | ||
|
||
void deleteAndUnsubscribeNode(ModelOpcUa::PlaceholderElement placeHolderElement); | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,7 +25,22 @@ namespace Umati | |
class IDashboardDataClient | ||
{ | ||
public: | ||
/// Struct for handling Events | ||
struct StructureChangeEvent{ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can there be more than one true, otherwise use ENUM? |
||
ModelOpcUa::NodeId_t refreshNode; | ||
bool nodeAdded = false; | ||
bool nodeDeleted = false; | ||
bool referenceAdded = false; | ||
bool referenceDeleted = false; | ||
bool dataTypeChanged = false; | ||
}; | ||
|
||
/*! \brief Callback function for new Value Data Callback. | ||
* | ||
* This callback is triggered if a the data of an item changes. | ||
*/ | ||
typedef std::function<void(nlohmann::json value)> newValueCallbackFunction_t; | ||
typedef std::function<void(StructureChangeEvent sce)> eventCallbackFunction_t; | ||
|
||
virtual ~IDashboardDataClient() = default; | ||
|
||
|
@@ -233,22 +248,39 @@ namespace Umati | |
|
||
virtual void buildCustomDataTypes() = 0; | ||
|
||
class EventSubscriptionHandle { | ||
mdornaus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public: | ||
inline EventSubscriptionHandle(int32_t clientHandle, int32_t subscriptionId) : m_clientHandle(clientHandle), m_subscriptionId(subscriptionId){}; | ||
inline ~EventSubscriptionHandle(){}; | ||
inline void unsubscribe() { m_unsubscribed = true; } | ||
inline bool isUnsubscribed() {return m_unsubscribed;} | ||
inline int32_t getClientHandle() { return m_clientHandle;} | ||
inline int32_t getSubscriptionId() { return m_subscriptionId;} | ||
|
||
private: | ||
bool m_unsubscribed = false; | ||
int32_t m_clientHandle; | ||
int32_t m_subscriptionId; | ||
}; | ||
|
||
virtual std::string readNodeBrowseName(const ModelOpcUa::NodeId_t &nodeId) = 0; | ||
|
||
/// \todo Extract from interface! | ||
virtual std::string getTypeName(const ModelOpcUa::NodeId_t &nodeId) = 0; | ||
|
||
virtual std::shared_ptr<ValueSubscriptionHandle> | ||
Subscribe(ModelOpcUa::NodeId_t nodeId, newValueCallbackFunction_t callback) = 0; | ||
|
||
virtual std::shared_ptr<ValueSubscriptionHandle> Subscribe(ModelOpcUa::NodeId_t nodeId, newValueCallbackFunction_t callback) = 0; | ||
virtual void Unsubscribe(std::vector<int32_t> monItemIds, std::vector<int32_t> clientHandles) = 0; | ||
|
||
virtual std::shared_ptr<EventSubscriptionHandle> SubscribeEvent(eventCallbackFunction_t ecbf) = 0; | ||
virtual void UnsubscribeEvent(std::shared_ptr<Dashboard::IDashboardDataClient::EventSubscriptionHandle> eventSubscriptionHandle) = 0; | ||
|
||
virtual std::vector<nlohmann::json> ReadeNodeValues(std::list<ModelOpcUa::NodeId_t> nodeIds) = 0; | ||
|
||
virtual std::vector<std::string> Namespaces() = 0; | ||
|
||
/// Verify that the connection and session are ok | ||
virtual bool VerifyConnection() = 0; | ||
|
||
}; | ||
} // namespace Dashboard | ||
} // namespace Umati |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Best practice is following the rule of tree.
https://en.cppreference.com/w/cpp/language/rule_of_three