Documentation

This page hosts the documentation of the SmartCGMS software architecture and all of its components.

The documentation is being updated to acommodate the recent improvements. Please, be patient, we are working on it...


Filter

A filter is a "top-level" entity in SmartCGMS configuration. It may manage other entity types, depending on its purpose. A single filter is configured as a single link in SmartCGMS configuration.

Descriptor

Every filter is described by its descriptor (scgms::TFilter_Descriptor):

struct TFilter_Descriptor {
    const GUID id;
    const NFilter_Flags flags;
    const wchar_t *description;
    const size_t parameters_count;
    const NParameter_Type* parameter_type;
    const wchar_t** ui_parameter_name;
    const wchar_t** config_parameter_name;
    const wchar_t** ui_parameter_tooltip;
};

  • id is an unique GUID assigned to this filter. The filter is created using this identifier.
  • flags represents a filter flags; this field is currently unused.
  • description is a string representing a name of the filter; it must not be nullptr and must contain a valid zero-terminated string
  • parameters_count represents a count of parameters (and a size of following 4 arrays)
  • parameter_type is an array of size parameters_count, containing parameter types from scgms::NParameter_Type enumerator (see below)
  • ui_parameter_name is an array of size parameters_count, containing the human-readable parameter names for each parameter
  • config_parameter_name is an array of size parameters_count, containing keys used for each parameter in configuration file
    • such a key should consist solely of alphanumeric characters, dash and underscore
    • using this key, the filter is able to read the configuration parameter from the container passed to Configure method (see below)
  • ui_parameter_tooltip is an array of size parameters_count, containing extended (human-readable) descriptions of parameters

A scgms::NParameter_Type enumerator contains a set of pre-defined constants, which are used to represent either a data-type of given parameter, and also a parameter semantic. Currently, the enumerator holds the following values:

  • ptNull - "empty" parameter, not interpreted, UI may choose to ignore it; currently the gpredict3-desktop frontend uses this value to create a visual separator
  • ptWChar_Array - string parameter (internally represented as a vector of characters to allow interoperability)
  • ptInt64_Array - an array of integers
  • ptDouble - a double-precision floating point value
  • ptRatTime - same as ptDouble, but interpreted as a time; the UI may display a date/time picker control
  • ptInt64 - a single 64-bit (long) integer
  • ptBool - a boolean (true/false) value; internally represented as a single byte
  • ptSignal_Model_Id - a GUID of a signal model; the UI may display a combobox with known signal models
  • ptDiscrete_Model_Id - a GUID of discrete model; the UI may display a combobox with known discrete models
  • ptMetric_Id - a GUID of a metric; the UI may display a combobox with known metrics
  • ptSolver_Id - a GUID of a solver; the UI may display a combobox with known solvers
  • ptModel_Produced_Signal_Id - a GUID of a signal produced by a model (discrete or signal); the UI may display a combobox with known signals - if the configuration contains a model selector, the combobox may contain only selected model-produced signals
  • ptSignal_Id - a GUID of a signal; the UI may display a combobox with known signals (not necessarily produced by any known model)
  • ptDouble_Array - an array of double-precision floating point values
  • ptSubject_Id - same as ptInt64, but interpreted as a subject ID; this ID may come from a database

A shared object exporting a filter must:

  • export a do_get_filter_descriptors function
    • this function returns a continuous array of filter descriptors identified by its first and one-after-last element
  • create this filter, when requested by do_create_filter call

An example of do_get_filter_descriptors may be as follows:

const std::array filter_descriptors = { { descriptor_1, descriptor_2 } };

HRESULT IfaceCalling do_get_filter_descriptors(scgms::TFilter_Descriptor **begin, scgms::TFilter_Descriptor **end) {
    *begin = filter_descriptors.data();
    *end = filter_descriptors.data() + filter_descriptors.size();
    return S_OK;
}

An example of do_create_filter may be as follows:

HRESULT IfaceCalling do_create_filter(const GUID *id, scgms::IFilter *output, scgms::IFilter **filter) {
	if (*id == descriptor_1.id)
		return Manufacture_Object<CMy_Filter>(filter, output);

	return E_NOTIMPL;
}

In this example, Manufacture_Object is a SDK function which creates an instance of IReferenced object and initializes its reference counter. The function is called with GUID of desired filter, output filter pointer and target memory, in which the pointer to newly created entity should be stored. Note the output filter pointer gets passed here - the filter itself is responsible for passing the device events to next filters in chain.

Typically, the filter chain is configured through the SDK class CFilter_Executor (and its RAII reference counted wrapper, SFilter_Executor). Thus, the filter factory function gets automatically called with the succeeding filter. The end of the chain should contain a terminal filter, which should properly deallocate device events passed through the filter chain. The SDK executor includes the default terminal filter, which drops the reference count of device event in Execute method call, returning S_OK, indicating, that the event passed through the whole chain without an error.

Interface

Every filter entity must implement the scgms::IFilter interface. The interface is defined as an abstract C++ class as follows:

class IFilter : public virtual refcnt::IReferenced {
public:
    virtual HRESULT IfaceCalling Configure(IFilter_Configuration* configuration, refcnt::wstr_list *error_description) = 0;
    virtual HRESULT IfaceCalling Execute(scgms::IDevice_Event *event) = 0;	
};

The Configure method is called prior the full operation mode. This method passes the configuration parameters to the filter and configures it to operational state. The return value may indicate a success (S_OK), success with warning (S_FALSE) or a fatal error (any other non-success code). Yielding a faulty result code leads to configuration failure and fails the whole process of configuring the filter chain.

Once in operational state, the outer code (often wrapped in a filter executor or similar code) may choose to call the Execute method. This invokes the actual filter's control loop. The device event passed to this call is passed with a move semantic - the device event is now owned by this filter. The Execute method must either pass the event to the next filter, or call Release to properly deallocate the device event memory.

One may choose to implement this interface on his own. However, SmartCGMS SDK provides the developer with base class CBase_Filter, implementing the repeating parts of code and wrapping all pointers into its reference-counted RAII wrappers. This class implements the interface and defines protected pure virtual methods Do_Configure and Do_Execute. An example of a filter implemented using the wrapper is included at the bottom of this page.

Operation

The filter has basically three states, in which it may operate:

  • Created - the filter just got instantiated and waits for the Configure call.
  • Operational - the filter is configured and is able to process device events in Execute method. The Execute method is called exclusively in this state.
  • Terminated - the filter received the Shut_Down device event code, deallocated all its resources and waits for its deallocation by outer code. Any further Configure and Execute call on this filter instance is invalid.
The figure below illustrates the filter lifecycle.

Filter states

The filter may be partially reset to its initial state by sending Warm_Reset device code through the chain. By using this code, the filter may "remember" useful information from its previous runs, but is should reset to its initial state. An example may include parameter optimalization - such a filter may remember newly obtained parameters, but should discard all data that the parameters are based on.

Events passing

Device events are processed synchronously in the filter chain. This means, that when the code calls Execute on any filter in the chain, the call does not return until the last device event reaches the last filter. In other words, the call to the Execute method is recursive. When using the standard way of working with filter chains (via scgms library), the execution is guarded by a recursive mutex. Thus, only a single device event may pass through the filter chain at one time.

If the filter creates a new thread, the Execute calls (or Send calls in case of using the SDK) are all synchronized. The developer should bear in mind, that this synchronization may occur in order to avoid deadlocks.

Example implementation

An example filter implemented using SmartCGMS SDK:

// the following SDK header files are required
#include <iface/DeviceIface.h>
#include <iface/FilterIface.h>
#include <rtl/FilterLib.h>

class CExample_Filter : public scgms::CBase_Filter {

private:
    double mMy_Var = std::numeric_limits::quiet_NaN();

protected:
    virtual HRESULT Do_Configure(scgms::SFilter_Configuration configuration, refcnt::Swstr_list& error_description) override final {

        // read Example_Config_Var (defined in descriptor, potentially contained in config.ini file)
        // the rsExample_Config_Var constant is defined in an example descriptor block below
        // if it is not found in configuration, use default value of 3.0
        mMy_Var = configuration.Read_Double(rsExample_Config_Var, 3.0);

        // maybe do some validation; do not forget to indicate an error, so the configuration process fails
        if (mMy_Var < 0.0) {
            error_description.push(L"Invalid Example_Config_Var value! Use positive values");
            return E_INVALIDARG;
        }

        // everything is configured correctly
        return S_OK;
    }

    virtual HRESULT Do_Execute(scgms::UDevice_Event event) override final {

        if (event.device_code() == scgms::NDevice_Event_Code::Shut_Down) {
            // close all files...
            // terminate all threads...
            // deallocate all memory...
        }

        // ...

        // pass the event further
        // this is not mandatory - if you do not wish to propagate events to next filters, you may just return S_OK, the UDevice_Event wrapper deallocates the device event automatically
        // and the call will unroll properly; most situations, however, requires you to pass the events to the next filter in chain:
        return mOutput.Send(event);
    }
    
public:
    CExample_Filter(scgms::IFilter *output);
    virtual ~CExample_Filter();
};

The implementation above demonstrates the base code for a filter. We will use this filter as an example in the following code.

Every filter should have its own GUID and filter descriptor. The general recommendation in SmartCGMS code is to use namespaces to contain a specific entity info and descriptions. An example of header definition follows:

namespace example_filter {

    constexpr GUID filter_id = { 0x904410ca, 0xb0aa, 0x4fb1, { 0x8f, 0x76, 0x74, 0x68, 0x80, 0x13, 0x82, 0xab } }; // { 904410CA-B0AA-4FB1-8F76-7468801382AB }

    extern const wchar_t* rsExample_Config_Var;
}

This code may be shared between the filter code and descriptor code, and thus may be placed in a reachable header file.

The descriptor block example follows:

// we will need all of the following includes
#include <iface/DeviceIface.h>       // TFilter_Descriptor
#include <iface/FilterIface.h>       // filter flags
#include <rtl/FilterLib.h>           // filter parameters
#include <rtl/manufactory.h>         // Manufacture_Object
#include <utils/descriptor_utils.h>  // do_get_descriptors

namespace example_filter {

    constexpr size_t param_count = 1;

    const scgms::NParameter_Type param_type[param_count] = {
        scgms::NParameter_Type::ptDouble
    };

    const wchar_t* ui_param_name[param_count] = {
        L"Some multiplier (example)"
    };

    const wchar_t* rsExample_Config_Var = L"Example_Config_Var";

    const wchar_t* config_param_name[param_count] = {
        rsExample_Config_Var
    };

    const wchar_t* ui_param_tooltips[param_count] = {
        L"This is just an example of some parameter going to the filter in configure method"
    };

    const wchar_t* filter_name = L"My example filter";

    const scgms::TFilter_Descriptor descriptor = {
        filter_id,
        scgms::NFilter_Flags::None,
        filter_name,
        param_count,
        param_type,
        ui_param_name,
        config_param_name,
        ui_param_tooltips
    };
}

Now, we have to export the descriptor and the filter itself in the do_get_filter_descriptors and do_create_filter functions. An example of these functions follows:

// we often summarize known descriptors in an array or vector (continuous memory container)
const std::array<scgms::TFilter_Descriptor, 1> filter_descriptors = { { example_filter::descriptor } };

extern "C" HRESULT IfaceCalling do_get_filter_descriptors(scgms::TFilter_Descriptor **begin, scgms::TFilter_Descriptor **end) {

    // do_get_descriptors is SDK function
	return do_get_descriptors(filter_descriptors, begin, end);
}

extern "C" HRESULT IfaceCalling do_create_filter(const GUID *id, scgms::IFilter *output, scgms::IFilter **filter) {

    // is this our filter? If yes, instantiate it!
	if (*id == example_filter::descriptor.id) {
		return Manufacture_Object<CExample_Filter>(filter, output);
	}

    // we do not know how to instantiate such filter
	return E_NOTIMPL;
}