JeVois  1.20
JeVois Smart Embedded Machine Vision Toolkit
Share this page:

#include <jevois/Component/Parameter.H>

template<class Param, class ... Tail>
class jevois::Parameter< Param, Tail ... >

A set of Parameters attached to a component.

This variadic template class is just for the convenience of adding several parameters to a Component in one statement.

The way in which we have implemented Parameter in the JeVois framework may seem unorthodox at first, but is the best way we have found so far in terms of minimizing burden when writing new components with lots of parameters. In our earlier framework, the iLab Neuromorphic Vision Toolkit (iNVT) started in 1995, parameters were included in components as member variables. The burden to programmers was so high that often they just did not include parameters and hardwired values instead, just to avoid that burden. The burden comes from the requirements:

  • we want to be able to support parameters of any type
  • we want each parameter to have a name, description, default value, specification of valid values
  • we want parameters to appear in related groups in the help message
  • we want to support callbacks, i.e., functions that are called when one tries to change the parameter value
  • we typically want the callback to be a member function of the Component that owns a given parameter, since changing that parameter value will typically trigger some re-organization in that Component (otherwise the callback might not be needed).

Possible implementation using class data members for parameters (similar to what we used in iNVT), here shown for a sample int parameter to specify the size of a queue held in a class MyComp that derives from Component:

ParamDef<int> sizeparamdef("size", "Queue size", 5, Range<int>(1, 100), categ);
class MyComp : public jevois::Component
{
public:
Param<int> sizeparam; // ouch
void sizeParamCallback(int newval) { myqueue.resize(newval); }
MyComp(std::string const & instance) :
jevois::Component(instance),
sizeparam(sizeparamdef) // ouch
{
sizeparam.setCallback(&MyComp::sizeParamCallback); // ouch
}
};

So we basically end up with 3 names that people have no idea what to do with and will just use confusing names for (sizeparamdef, sizeparam, sizeParamCallback), and we have to 1) specify the definition of name, description, etc somewhere using some arbitrary name (here sizeparamdef), then add the member variable for the param to the component using some other name (here sizeparam), then construct the param which would typically require linking it to its definition so we can get the default value and such, and finally hook the callback up (note how MyComp is not fully constructed yet when we construct sizeparam hence referencing sizeParamCallback() at that time is dubious at best). In reality, things are even worse since typically the paramdef, component class declaration, and component implementation, should be in 3 different files.

The approach we developed for the Neuromorphic Robotics Toolkit (NRT) and refined for JeVois works as follows:

  • each parameter is a unique new class type. We create that type once with one name, and it holds the parameter value and the definition data. This is further facilitated by the JEVOIS_DECLARE_PARAMETER(ParamName, ParamType, ...) variadic macro.
  • for parameters with callbacks, their class type includes a pure virtual onParamChange(param, value) function that will need to be implemented by the host component. This is facilitated by the JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(ParamName, ParamType, ...) variadic macro. The first argument of onParamChange() is the parameter class type, so that a host component with many parameters will have many different onParamChange() functions, one per parameter that has a callback.
  • components inherit from their parameters using variadic templates to make inheriting from multiple parameters short and easy.
  • each parameter exposes simple functions get(), set(), etc (see ParameterCore and ParameterBase). In a component that has many parameters, accessing parameters is achieved by disambiguating on which base class (i.e., which parameter) one wants to access the get(), set(), etc function, which is achieved by calling paramx::get() vs paramy::get(), etc
  • No need to declare parameter member variables (we inherit from them instead).
  • No need to do anything at construction of the component.
  • No need to manually hook the callback function in the component host class to the parameter.
  • Strong compile-time checking that the programmer did not forget to write the callback function for each parameter that was declared as having a callback.
  • Only one name used throughout for that parameter and all its associated machinery (definition, callback).
  • It is easy to write scripts that search the source tree for information about all the parameters of a component, since those are always all specified in the Parameter< ... > inheritance statement.

Remaining caveat: it is often desirable to use short names for parameters, such as "size", "length", "dims", etc and those names may clash between several components as the .H files for these components are included when building a more complex component that uses those. This is not an issue for Module, which is a terminal entity and is typically written as a single .C file with no .H file. For components intended for broad use, we currently recommend putting all the parameters in a namespace that is the lowercase version of the component class name.

Below is the resulting implementation in Manager.H. We start with declaring the parameters, and we inherit from them when declaring the Manager class:

namespace jevois
{
namespace manager
{
//! Parameter category \relates jevois::Manager
static ParameterCategory const ParamCateg("General Options");
//! Parameter \relates jevois::Manager
JEVOIS_DECLARE_PARAMETER(help, bool, "Print this help message", false, ParamCateg);
#ifdef JEVOIS_LDEBUG_ENABLE
//! Enum for Parameter \relates jevois::Manager
JEVOIS_DEFINE_ENUM_CLASS(LogLevel, (fatal) (error) (info) (debug));
#else
//! Enum for Parameter \relates jevois::Manager
JEVOIS_DEFINE_ENUM_CLASS(LogLevel, (fatal) (error) (info));
#endif
//! Parameter \relates jevois::Manager
JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(loglevel, LogLevel, "Set the minimum log level to display",
LogLevel::info, LogLevel_Values, ParamCateg);
//! Parameter \relates jevois::Manager
JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(tracelevel, unsigned int, "Set the minimum trace level to display",
0, ParamCateg);
//! Parameter \relates jevois::Manager
JEVOIS_DECLARE_PARAMETER(nickname, std::string, "Nickname associated with this camera, useful when multiple "
"JeVois cameras are connected to a same USB bus", "jevois", ParamCateg);
}
// ######################################################################
//! Manager of a hierarchy of Component objects
/*! A Manager should be the top-level Component of any hierarchy of Components. It is primarily responsible for
handling the setting of Parameter values via the command-line or otherwise.
Users should only need to construct a Manager (including Engine, which derives from Manager), add any Component to
it, and then call init() on the Manager, which will parse all command line options, bind them to the relevant
Parameters, and call init() on all subComponents (which in turn calls init() on all of their subComponents,
etc.). See the documentation of Component for more information about the init() flow.
The parameter \p nickname is not internally used by the Manager. It can be set, for example, in \b initscript.cfg
to a different value for each camera, in systems that use multiple JeVois cameras connected to a single USB bus.
\ingroup component */
class Manager : public Component,
public Parameter<manager::help, manager::loglevel, manager::tracelevel, manager::nickname>
{
public:

For the parameters that we declared as having a callback, we further include in our definition of the Manager class overrides for the pure virtual onParamChange() functions that they added to our manager class. Note the signatures of these functions: The first argument is a const reference to the parameter for which this callback is, and its main role is to disambiguate between the different onParamChange() functions a component may have. The second argument is the proposed new parameter value. The onParamChange() function should examine the candidate new value and (1) if it does not like it, throw and std::range_error with a descriptive message of why the value is rejected, (2) otherwise, it is assumed that the value is accepted and the callback can then allocate resources or do other work with that value (the actual modification of the Parameter object is handled upstream and the callback does not need to worry about it: if it returns without throwing, the proposed value will become the new value of the Parameter). The Parameter is locked-up for writing as long as the callback is running, to avoid destruction of the parameter and/or concurrent parameter value changes by several different threads. Thus, callbacks should try to execute quickly, and should not call set(), etc on the parameter as this will always deadlock (get() is allowed if your callback needs to know the current value of the parameter).

//! Parameter callback
void onParamChange(manager::loglevel const & param, manager::LogLevel const & newval) override;
//! Parameter callback
void onParamChange(manager::tracelevel const & param, unsigned int const & newval) override;

There is nothing to do in the constructor, destructor, etc of Manager. The only thing that remains to be done is to implement the onParamChange() functions in Manager.C. Note how we use the JEVOIS_UNUSED_PARAM(x) macro to avoid compiler warnings about our callbacks not using the "param" function parameter:

// ######################################################################
void jevois::Manager::onParamChange(jevois::manager::loglevel const & JEVOIS_UNUSED_PARAM(param),
jevois::manager::LogLevel const & newval)
{
switch(newval)
{
case jevois::manager::LogLevel::fatal: jevois::logLevel = LOG_CRIT; break;
case jevois::manager::LogLevel::error: jevois::logLevel = LOG_ERR; break;
case jevois::manager::LogLevel::info: jevois::logLevel = LOG_INFO; break;
#ifdef JEVOIS_LDEBUG_ENABLE
case jevois::manager::LogLevel::debug: jevois::logLevel = LOG_DEBUG; break;
#endif
}
}
// ######################################################################
void jevois::Manager::onParamChange(jevois::manager::tracelevel const & JEVOIS_UNUSED_PARAM(param),
unsigned int const & newval)
{
if (newval)
{
#if !defined(JEVOIS_TRACE_ENABLE) || !defined(JEVOIS_LDEBUG_ENABLE)
LERROR("Debug trace has been disabled at compile-time, re-compile with -DJEVOIS_LDEBUG_ENABLE=ON and "
"-DJEVOIS_TRACE_ENABLE=ON to see trace info");
#endif
}
}

For completeness, if you wonder what JEVOIS_DECLARE_PARAMETER(ParamName, ParamType, ...) and JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(ParamName, ParamType, ...) exactly do, here they are in ParameterHelpers.H and reproduced here:

#ifndef JEVOIS_DOXYGEN
// Convenience macro to define a Parameter type. All the ... args are passed to ParameterDef
#define JEVOIS_DECLARE_PARAMETER(ParamName, ParamType, ...) \
struct ParamName : public virtual jevois::ParameterRegistry, public jevois::ParameterCore<ParamType> \
{ \
typedef ParamType type; \
\
inline ParamName() : jevois::ParameterCore<ParamType>(jevois::ParameterDef<ParamType>(#ParamName, __VA_ARGS__)) \
{ jevois::ParameterRegistry::addParameter(this); } \
\
inline virtual ~ParamName() \
{ jevois::ParameterRegistry::removeParameter(this); } \
\
inline virtual jevois::Component const * owner() const override \
{ return dynamic_cast<jevois::Component const *>(static_cast<jevois::ParameterRegistry const *>(this)); } \
};
// Convenience macro to define a Parameter type with callback. All the ... args are passed to ParameterDef
#define JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(ParamName, ParamType, ...) \
struct ParamName : public virtual jevois::ParameterRegistry, public jevois::ParameterCore<ParamType> \
{ \
typedef ParamType type; \
\
virtual void onParamChange(ParamName const & param, ParamType const & newval) = 0; \
\
inline ParamName() : jevois::ParameterCore<ParamType>(jevois::ParameterDef<ParamType>(#ParamName, __VA_ARGS__)) \
{ setCallback([this](ParamType const & newval) { this->onParamChange(*this, newval); }); \
jevois::ParameterRegistry::addParameter(this); } \
\
inline virtual ~ParamName() \
{ jevois::ParameterRegistry::removeParameter(this); } \
\
inline virtual jevois::Component const * owner() const override \
{ return dynamic_cast<jevois::Component const *>(static_cast<jevois::ParameterRegistry const *>(this)); } \
};
#endif

Also see Engine.H or the many components in the jevoisbase library.

Definition at line 382 of file Parameter.H.

Inheritance diagram for jevois::Parameter< Param, Tail ... >:
Collaboration diagram for jevois::Parameter< Param, Tail ... >:

The documentation for this class was generated from the following file:
jevois::Manager::onParamChange
void onParamChange(manager::loglevel const &param, manager::LogLevel const &newval) override
Parameter callback.
JEVOIS_DECLARE_PARAMETER
JEVOIS_DECLARE_PARAMETER(thresh1, double, "First threshold for hysteresis", 50.0, ParamCateg)
jevois::Component
A component of a model hierarchy.
Definition: Component.H:181
jevois::traceLevel
int traceLevel
Current trace level.
Definition: Log.C:30
LERROR
#define LERROR(msg)
Convenience macro for users to print out console or syslog messages, ERROR level.
Definition: Log.H:211
jevois
Definition: Concepts.dox:1
JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK
JEVOIS_DECLARE_PARAMETER_WITH_CALLBACK(l2grad, bool, "Use more accurate L2 gradient norm if true, L1 if false", false, ParamCateg)
onParamChange
void onParamChange(manager::loglevel const &param, manager::LogLevel const &newval) override
Parameter callback.
jevois::logLevel
int logLevel
Current log level.
Definition: Log.C:29
jevois::JEVOIS_DEFINE_ENUM_CLASS
JEVOIS_DEFINE_ENUM_CLASS(CameraSensor,(any)(imx290)(os08a10)(ar0234))
Enum for different sensor models.