RobotHealth.cpp
Go to the documentation of this file.
1 /*
2  * This file is part of ArmarX.
3  *
4  * ArmarX is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License version 2 as
6  * published by the Free Software Foundation.
7  *
8  * ArmarX is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  *
16  * @package RobotAPI::ArmarXObjects::RobotHealth
17  * @author Simon Ottenhaus ( simon dot ottenhaus at kit dot edu )
18  * @date 2018
19  * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
20  * GNU General Public License
21  */
22 
23 #include "RobotHealth.h"
29 
30 #include <SimoxUtility/algorithm/get_map_keys_values.h>
31 #include <SimoxUtility/algorithm/string/string_tools.h>
32 #include <algorithm>
33 
34 #include <boost/regex.hpp>
35 
38 #include <mutex>
39 #include <optional>
40 
41 namespace armarx
42 {
43  inline const std::string PROPERTY_REQUESTER_ID = "required_default";
44 
45  void
47  {
49  monitorUpdateHealthTask =
50  new PeriodicTask<RobotHealth>(this, &RobotHealth::monitorHealthUpdateTaskClb, 10);
51 
52  defaultMaximumCycleTimeWarn =
53  armarx::core::time::Duration::MilliSeconds(p.maximumCycleTimeWarnMS);
54  defaultMaximumCycleTimeErr =
55  armarx::core::time::Duration::MilliSeconds(p.maximumCycleTimeErrMS);
56 
57  const std::vector<std::string> requiredTags = simox::alg::split(p.requiredTags, ",");
58  // a special requester: cannot be changed during runtime.
59  tagsPerRequester[PROPERTY_REQUESTER_ID] = std::set<std::string>(requiredTags.begin(), requiredTags.end());
60 
62  }
63 
64  void
66  {
68 
69  monitorUpdateHealthTask->start();
70  }
71 
72  void
73  RobotHealth::monitorHealthUpdateTaskClb()
74  {
75  const std::scoped_lock lock(updateMutex);
76 
79 
80  for (auto & e : updateEntries)
81  {
83  const std::scoped_lock elock(e.mutex);
84 
85  if (!e.enabled)
86  {
87  continue;
88  }
89 
90  if(e.history.empty())
91  {
92  continue;
93  }
94 
95  auto deltaToArrival = now - e.history.back().arrivalTime;
96 
98 
99  if (deltaToArrival > e.maximumCycleTimeErr)
100  {
101  ARMARX_TRACE;
102 
103  if (e.state != HealthError) // not already in error state
104  {
105  ARMARX_TRACE;
106  ARMARX_ERROR << deactivateSpam(0.1, e.name) << "Component " << e.name
107  << " has died.";
108  e.state = HealthError;
109  }
110  }
111  else if (deltaToArrival > e.maximumCycleTimeWarn)
112  {
113  ARMARX_TRACE;
114  ARMARX_WARNING << deactivateSpam(0.1, e.name) << "Component " << e.name
115  << " is running too slow.";
116 
117  e.state = HealthWarning;
118  }
119  else
120  {
121  // If everything is OK (again), update health
122  e.state = HealthOK;
123  }
124  }
125 
126  // update aggregated health
127  RobotHealthState overallHealthState = RobotHealthState::HealthOK;
128 
129 
130  // get aggregated status
131  for (auto & e : updateEntries)
132  {
133  ARMARX_TRACE;
134  std::scoped_lock elock(e.mutex);
135 
136  // trim history
137  while (e.history.size() > 20)
138  {
139  e.history.pop_front();
140  }
141 
142  if (e.required)
143  {
144  if (e.state == HealthWarning && overallHealthState != HealthError)
145  {
146  overallHealthState = RobotHealthState::HealthWarning;
147  }
148  else if (e.state == HealthError)
149  {
150  overallHealthState = RobotHealthState::HealthError;
151  }
152  }
153  else
154  {
155  if (e.state != HealthOK && overallHealthState != HealthError)
156  {
157  overallHealthState = RobotHealthState::HealthWarning;
158  }
159  }
160  }
161 
162  if (overallHealthState == HealthError)
163  {
164  ARMARX_TRACE;
165  ARMARX_INFO << deactivateSpam(3) << "Requesting emergency stop";
166  ARMARX_CHECK_NOT_NULL(p.emergencyStopTopicPrx);
167  p.emergencyStopTopicPrx->reportEmergencyStopState(
168  EmergencyStopState::eEmergencyStopActive);
169  }
170  ARMARX_TRACE;
171  ARMARX_CHECK_NOT_NULL(p.aggregatedRobotHealthTopicPrx);
172  p.aggregatedRobotHealthTopicPrx->aggregatedHeartbeat(overallHealthState);
173 
174  reportDebugObserver();
175  }
176 
177  void
179  {
180  //robotUnitPrx = nullptr;
181  monitorUpdateHealthTask->stop();
182  }
183 
184  void
186  {
187  }
188 
191  {
192  auto defs = armarx::PropertyDefinitionsPtr(
194 
195 
196  defs->topic(p.emergencyStopTopicPrx, "EmergencyStop",
197  "The name of the topic over which changes of the emergencyStopState are sent.");
198  defs->topic<RobotHealthInterfacePrx>(p.robotHealthTopicName, "RobotHealthTopic",
199  "Name of the RobotHealth topic");
200 
201  defs->topic(p.aggregatedRobotHealthTopicPrx, "AggregatedRobotHealthTopic", "Name of the AggregatedRobotHealthTopic");
202 
203  defs->optional(p.maximumCycleTimeWarnMS, "MaximumCycleTimeWarnMS",
204  "Default value of the maximum cycle time for warnings");
205  defs->optional(p.maximumCycleTimeErrMS, "MaximumCycleTimeErrMS",
206  "Default value of the maximum cycle time for error");
207 
208  defs->optional(p.requiredTags, "RequiredTags", "Tags that should be requested.");
209 
210  return defs;
211  }
212 
213  RobotHealth::UpdateEntry*
214  RobotHealth::findUpdateEntry(const std::string& name)
215  {
216  ARMARX_TRACE;
217 
218  for (auto & updateEntrie : updateEntries)
219  {
220  if (updateEntrie.name == name)
221  {
222  return &updateEntrie;
223  }
224  }
225 
226  return nullptr;
227  }
228 
229  std::pair<bool, RobotHealth::UpdateEntry&>
230  RobotHealth::findOrCreateUpdateEntry(const std::string& name)
231  {
232  ARMARX_TRACE;
233  {
234  for (auto & updateEntrie : updateEntries)
235  {
236  if (updateEntrie.name == name)
237  {
238  return {false, updateEntrie};
239  }
240  }
241  }
242 
243  ARMARX_INFO << "registering for heartbeat: '" << name << "'";
244 
245  updateEntries.emplace_back(name, defaultMaximumCycleTimeWarn, defaultMaximumCycleTimeErr);
246 
247  return {true, updateEntries.back()};
248  }
249 
250  void
251  RobotHealth::signUp(const RobotHealthHeartbeatArgs& args, const Ice::Current& current)
252  {
253  ARMARX_TRACE;
254  std::scoped_lock lockU(updateMutex);
255 
256  auto e = findOrCreateUpdateEntry(args.identifier);
257 
258  {
259  std::scoped_lock lock(e.second.mutex);
260 
261  if (e.first)
262  {
263  ARMARX_INFO << "Newly registering component " << args.identifier << " for updates.";
264  }
265  // else no messsage as components may spam sign up for legacy reasons (when there was no sign up)
266 
267  armarx::core::time::fromIce(args.maximumCycleTimeWarning, e.second.maximumCycleTimeWarn);
268  armarx::core::time::fromIce(args.maximumCycleTimeError, e.second.maximumCycleTimeErr);
269 
270  e.second.tags = args.tags;
271  // e.second.required = args.requiredByDefault; // FIXME
272  e.second.description = args.description;
273  e.second.enabled = true;
274  e.second.state = HealthOK;
275  e.second.history.clear();
276  }
277 
278  updateRequiredElements();
279  }
280 
281  void
282  RobotHealth::heartbeat(const std::string& identifier,
283  const core::time::dto::DateTime& referenceTime,
284  const Ice::Current& current)
285  {
286  ARMARX_TRACE;
287  ARMARX_VERBOSE << "Finding update entry";
288 
289  const DateTime arrivalTimestamp = Clock::Now();
290 
291 
292  // We hold a reference to 'o' which is an element in a vector.
293  // If we don't lock until the end of this scope, the vector size might change and 'o' will be invalidated.
294  std::scoped_lock lockU(updateMutex);
295  auto* const entry = findUpdateEntry(identifier);
296 
297  if (entry == nullptr)
298  {
299  ARMARX_WARNING << deactivateSpam() << "Attention. Component `" << identifier
300  << "` was not signed up for heartbeat. Ignoring heartbeat for now...";
301  return;
302  }
303 
304  ARMARX_CHECK_NOT_NULL(entry);
305 
306  std::scoped_lock lock(entry->mutex);
307 
308  if (not entry->enabled)
309  {
310  ARMARX_WARNING << deactivateSpam() << "Attention. Component `" << identifier
311  << "` was not enabled. The heartbeat is ignored... ";
312  return;
313  }
314 
315  ARMARX_VERBOSE << "Extending history";
316  // auto now = armarx::core::time::DateTime::Now();
317 
318 
320  fromIce(referenceTime, refTime);
321  entry->history.push_back(UpdateEntry::TimeInfo{.referenceTime = refTime, .arrivalTime = arrivalTimestamp});
322  }
323 
324  void
325  armarx::RobotHealth::unregister(const std::string& identifier, const Ice::Current&)
326  {
327  ARMARX_TRACE;
328  std::scoped_lock lock(updateMutex);
329 
330  bool found = false;
331  {
332  for (auto & e : updateEntries)
333  {
334  if (e.name == identifier)
335  {
336  std::scoped_lock elock(e.mutex);
337  e.required = false;
338  e.enabled = false;
339  found = true;
340  break;
341  }
342  }
343  // Todo upgrade lock?
344  }
345 
346  if (found)
347  {
348  ARMARX_INFO << "Component " << identifier << " disabled from heartbeat";
349  }
350  else
351  {
352  ARMARX_INFO << "Could not unregister component " << identifier << ". Not found.";
353  }
354 
355  updateRequiredElements();
356  }
357 
358  void
359  armarx::RobotHealth::addRequiredTags(const std::string& requesterIdentifier,
360  const std::vector<std::string>& tags,
361  const Ice::Current& current)
362  {
363  std::scoped_lock lock(updateMutex);
364 
365  // add newly requested tags
366  for(const auto& tag : tags)
367  {
368  tagsPerRequester[requesterIdentifier].insert(tag);
369  }
370 
371  updateRequiredElements();
372  }
373 
374  std::set<std::string>
375  armarx::RobotHealth::requestedTags() const
376  {
377  // obtain a list of all tags that are relevant / requested at the moment
378  std::set<std::string> allRequestedTags;
379  for(const auto& tgs: simox::alg::get_values(tagsPerRequester))
380  {
381  for(const auto& tag : tgs)
382  {
383  allRequestedTags.insert(tag);
384  }
385  }
386  return allRequestedTags;
387  }
388 
389  // checks whether any element in 'search' is in 'values'
390  bool containsAnyOf(const std::set<std::string>& values, const std::vector<std::string>& searchKeys)
391  {
392  return std::any_of(searchKeys.begin(), searchKeys.end(), [&values](const auto& key){
393  return values.count(key) > 0;
394  });
395  }
396 
397  void armarx::RobotHealth::updateRequiredElements()
398  {
399  ARMARX_IMPORTANT << "updateRequiredElements";
400  const std::set<std::string> allRequestedTags = requestedTags();
401 
402  for (auto &e : updateEntries) {
403  ARMARX_INFO << e.name;
404 
405  e.required = false; // if required, will be set in the following
406 
407  std::unique_lock lock(e.mutex);
408 
409  if (containsAnyOf(allRequestedTags, e.tags)) {
410  ARMARX_INFO << e.name << " is required.";
411  e.required = true;
412  if (not e.enabled) {
413  ARMARX_WARNING << "You are requiring the disabled component "
414  << e.name << ". Enabling it...";
415  e.enabled = true;
416  }
417  }
418  }
419  }
420 
421  void
422  armarx::RobotHealth::removeRequiredTags(const std::string& requesterIdentifier,
423  const std::vector<std::string>& tags,
424  const Ice::Current& current)
425  {
426  std::scoped_lock lock(updateMutex);
427 
428  // update the required tags list / map
429  for(const auto& tag : tags)
430  {
431  tagsPerRequester[requesterIdentifier].erase(tag);
432  }
433 
434  updateRequiredElements();
435  }
436 
437  void
438  armarx::RobotHealth::resetRequiredTags(const Ice::Current& current)
439  {
440  std::scoped_lock lock(updateMutex);
441 
442  // treat the special provider (see onInitComponent()) appropriately
443  ARMARX_CHECK(tagsPerRequester.count(PROPERTY_REQUESTER_ID) > 0);
444  const auto propertyProviderTags = tagsPerRequester.at(PROPERTY_REQUESTER_ID);
445 
446  // clear everything except the special provider
447  tagsPerRequester.clear();
448  tagsPerRequester.emplace(PROPERTY_REQUESTER_ID, propertyProviderTags);
449  }
450 
451  RobotHealthInfo
452  RobotHealth::getSummary(const Ice::Current&)
453  {
454  std::scoped_lock lock(updateMutex);
455 
456  ARMARX_TRACE;
457  RobotHealthInfo ret;
458 
460 
463 
464  for (const auto& e : updateEntries)
465  {
466  RobotHealthInfoEntry healthEntry;
467 
468  for (size_t i = 1; i < e.history.size(); i++)
469  {
470  auto later = e.history[i];
471  auto pre = e.history[i - 1];
472 
473  auto delta = later.arrivalTime - pre.arrivalTime;
474 
475  if (minDelta > delta)
476  {
477  minDelta = delta;
478  }
479 
480  if (maxDelta < delta)
481  {
482  maxDelta = delta;
483  }
484  }
485 
486  const Duration timeSinceLastUpdateArrival = e.history.empty() ? Duration() : Clock::Now() - e.history.back().arrivalTime;
487  const Duration timeSinceLastUpdateReference = e.history.empty() ? Duration() : Clock::Now() - e.history.back().referenceTime;
488 
489  const DateTime lastReferenceTime = e.history.empty() ? armarx::core::time::DateTime::Invalid() : e.history.back().referenceTime;
490 
491  const Duration timeSyncDelayAndIce = e.history.empty() ? armarx::core::time::Duration() : e.history.back().arrivalTime - e.history.back().referenceTime;
492 
493  healthEntry.identifier = e.name;
494  healthEntry.state = e.state;
495  healthEntry.enabled = e.enabled;
496  healthEntry.required = e.required;
497  toIce(healthEntry.minDelta, minDelta);
498  toIce(healthEntry.maxDelta,maxDelta);
499  toIce(healthEntry.lastReferenceTimestamp, lastReferenceTime);
500  toIce(healthEntry.timeSinceLastArrival, timeSinceLastUpdateArrival);
501  toIce(healthEntry.timeSyncDelayAndIce, timeSyncDelayAndIce);
502  toIce(healthEntry.timeSinceLastUpdateReference, timeSinceLastUpdateReference);
503  toIce(healthEntry.maximumCycleTimeWarning, e.maximumCycleTimeWarn);
504  toIce(healthEntry.maximumCycleTimeError, e.maximumCycleTimeErr);
505  healthEntry.tags = e.tags;
506 
507  ret.entries[e.name] = healthEntry;
508  }
509 
510  const auto tags = requestedTags();
511  ret.activeTags = std::vector<std::string>(tags.begin(), tags.end());
512 
513  return ret;
514  }
515 
516  std::string
517  RobotHealth::getTopicName(const Ice::Current& current)
518  {
519  return p.robotHealthTopicName;
520  }
521 
522  void
523  RobotHealth::reportDebugObserver()
524  {
525  for (const auto& entry : updateEntries)
526  {
527  if (entry.history.size() > 1)
528  {
529  auto later = entry.history[entry.history.size() - 1];
530  auto pre = entry.history[entry.history.size() - 2];
531  const Duration delta = later.arrivalTime - pre.arrivalTime;
532 
533  const Duration timeSinceLastArrival = Clock::Now() - entry.history.back().arrivalTime;
534  const Duration timeToLastReference = Clock::Now() - entry.history.back().referenceTime;
535  const Duration timeSyncDelay = entry.history.back().arrivalTime - entry.history.back().referenceTime;
536 
537  setDebugObserverDatafield("RobotHealth_" + entry.name + "_lastDelta",
538  delta.toMilliSecondsDouble());
539  setDebugObserverDatafield("RobotHealth_" + entry.name + "_timeSinceLastArrival",
540  timeSinceLastArrival.toMilliSecondsDouble());
541  setDebugObserverDatafield("RobotHealth_" + entry.name + "_timeToLastReference",
542  timeToLastReference.toMilliSecondsDouble());
543  setDebugObserverDatafield("RobotHealth_" + entry.name + "_timeSyncDelayAndIce",
544  timeSyncDelay.toMilliSecondsDouble());
545  setDebugObserverDatafield("RobotHealth_" + entry.name + "_maximumCycleTimeWarn",
546  entry.maximumCycleTimeWarn.toMilliSecondsDouble());
547  setDebugObserverDatafield("RobotHealth_" + entry.name + "_maximumCycleTimeErr",
548  entry.maximumCycleTimeErr.toMilliSecondsDouble());
549  }
550  }
551 
553  }
554 } // namespace armarx
ARMARX_VERBOSE
#define ARMARX_VERBOSE
Definition: Logging.h:180
armarx::aron::ret
ReaderT::InputType T & ret
Definition: rw.h:21
armarx::RobotHealth::createPropertyDefinitions
armarx::PropertyDefinitionsPtr createPropertyDefinitions() override
Definition: RobotHealth.cpp:190
armarx::core::time::fromIce
void fromIce(const dto::ClockType::ClockTypeEnum &dto, ClockType &bo)
Definition: ice_conversions.cpp:12
ARMARX_IMPORTANT
#define ARMARX_IMPORTANT
Definition: Logging.h:183
armarx::RobotHealth::onDisconnectComponent
void onDisconnectComponent() override
Definition: RobotHealth.cpp:178
armarx::RobotHealth::addRequiredTags
void addRequiredTags(const std::string &requesterIdentifier, const std::vector< std::string > &tags, const Ice::Current &current) override
Definition: RobotHealth.cpp:359
armarx::core::time::DateTime::Now
static DateTime Now()
Definition: DateTime.cpp:55
armarx::DebugObserverComponentPluginUser::setDebugObserverBatchModeEnabled
void setDebugObserverBatchModeEnabled(bool enable)
Definition: DebugObserverComponentPlugin.cpp:118
DateTime.h
ARMARX_CHECK_NOT_NULL
#define ARMARX_CHECK_NOT_NULL(ptr)
This macro evaluates whether ptr is not null and if it turns out to be false it will throw an Express...
Definition: ExpressionException.h:206
RobotHealth.h
trace.h
ProsthesisInterface.values
values
Definition: ProsthesisInterface.py:190
Duration.h
armarx::core::time::Duration::toMilliSecondsDouble
double toMilliSecondsDouble() const
Returns the amount of milliseconds.
Definition: Duration.cpp:76
ice_conversions.h
armarx::RobotHealth::UpdateEntry::TimeInfo::referenceTime
armarx::core::time::DateTime referenceTime
Definition: RobotHealth.h:147
armarx::containsAnyOf
bool containsAnyOf(const std::set< std::string > &values, const std::vector< std::string > &searchKeys)
Definition: RobotHealth.cpp:390
armarx::RobotHealth::getSummary
RobotHealthInfo getSummary(const Ice::Current &current) override
Definition: RobotHealth.cpp:452
armarx::RobotHealth::unregister
void unregister(const std::string &identifier, const Ice::Current &) override
Definition: RobotHealth.cpp:325
armarx::RobotHealth::onExitComponent
void onExitComponent() override
Definition: RobotHealth.cpp:185
ARMARX_CHECK
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
Definition: ExpressionException.h:82
armarx::RobotHealth::heartbeat
void heartbeat(const std::string &identifier, const core::time::dto::DateTime &referenceTime, const Ice::Current &current) override
Definition: RobotHealth.cpp:282
Clock.h
armarx::toIce
void toIce(std::map< IceKeyT, IceValueT > &iceMap, const boost::container::flat_map< CppKeyT, CppValueT > &cppMap)
Definition: ice_conversions_boost_templates.h:15
ARMARX_TRACE
#define ARMARX_TRACE
Definition: trace.h:69
armarx::RobotHealth::signUp
void signUp(const RobotHealthHeartbeatArgs &args, const Ice::Current &current) override
Definition: RobotHealth.cpp:251
armarx::RobotHealth::removeRequiredTags
void removeRequiredTags(const std::string &requesterIdentifier, const std::vector< std::string > &tags, const Ice::Current &current) override
Definition: RobotHealth.cpp:422
ARMARX_ERROR
#define ARMARX_ERROR
Definition: Logging.h:189
armarx::fromIce
void fromIce(const std::map< IceKeyT, IceValueT > &iceMap, boost::container::flat_map< CppKeyT, CppValueT > &cppMap)
Definition: ice_conversions_boost_templates.h:26
armarx::RobotHealth::onConnectComponent
void onConnectComponent() override
Definition: RobotHealth.cpp:65
ExpressionException.h
armarx::core::time::DateTime
Represents a point in time.
Definition: DateTime.h:24
armarx::Component::getConfigIdentifier
std::string getConfigIdentifier()
Retrieve config identifier for this component as set in constructor.
Definition: Component.cpp:74
armarx::RobotHealth::getTopicName
std::string getTopicName(const Ice::Current &current) override
Definition: RobotHealth.cpp:517
armarx::ComponentPropertyDefinitions
Default component property definition container.
Definition: Component.h:70
ARMARX_INFO
#define ARMARX_INFO
Definition: Logging.h:174
armarx::PROPERTY_REQUESTER_ID
const std::string PROPERTY_REQUESTER_ID
Definition: RobotHealth.cpp:43
armarx::DebugObserverComponentPluginUser::sendDebugObserverBatch
void sendDebugObserverBatch()
Definition: DebugObserverComponentPlugin.cpp:122
IceUtil::Handle< class PropertyDefinitionContainer >
armarx::RobotHealth::resetRequiredTags
void resetRequiredTags(const Ice::Current &current) override
Definition: RobotHealth.cpp:438
armarx::core::time::Duration
Represents a duration.
Definition: Duration.h:17
armarx::core::time::Clock::Now
static DateTime Now()
Current time on the virtual clock.
Definition: Clock.cpp:97
armarx::Logging::deactivateSpam
SpamFilterDataPtr deactivateSpam(float deactivationDurationSec=10.0f, const std::string &identifier="", bool deactivate=true) const
disables the logging for the current line for the given amount of seconds.
Definition: Logging.cpp:92
armarx::PeriodicTask
Definition: ArmarXManager.h:70
Logging.h
ARMARX_WARNING
#define ARMARX_WARNING
Definition: Logging.h:186
armarx::PropertyDefinitionsPtr
IceUtil::Handle< class PropertyDefinitionContainer > PropertyDefinitionsPtr
PropertyDefinitions smart pointer type.
Definition: forward_declarations.h:34
armarx::armem::Duration
armarx::core::time::Duration Duration
Definition: forward_declarations.h:14
armarx::core::time::DateTime::Invalid
static DateTime Invalid()
Definition: DateTime.cpp:60
armarx
This file offers overloads of toIce() and fromIce() functions for STL container types.
Definition: ArmarXTimeserver.cpp:28
armarx::core::time::Duration::MilliSeconds
static Duration MilliSeconds(std::int64_t milliSeconds)
Constructs a duration in milliseconds.
Definition: Duration.cpp:55
armarx::RobotHealth::onInitComponent
void onInitComponent() override
Definition: RobotHealth.cpp:46
armarx::RobotHealth::UpdateEntry::TimeInfo
Definition: RobotHealth.h:144
armarx::DebugObserverComponentPluginUser::setDebugObserverDatafield
void setDebugObserverDatafield(Ts &&...ts) const
Definition: DebugObserverComponentPlugin.h:97
armarx::split
std::vector< std::string > split(const std::string &source, const std::string &splitBy, bool trimElements=false, bool removeEmptyElements=false)
Definition: StringHelpers.cpp:36