The primary API for communication plug-ins leans on the C programming language to ensure binary compatibility across different compiler vendors. It also allows, at least in principle, to write a plug-in in any language that provides a foreign function interface (FFI) to C. However, working directly with the C API can quickly become unwieldy and error-prone. In addition, it is difficult to write tests for such a plug-in.
To avoid these problems, the provided SDK includes a support library which provides a higher level C++ API on top. The support library translates the handle types of the C API (for example, MVValue_t, MVRecipe_t, MVResult_t) either directly into C++ vocabulary types, such as std::variant in case of MVValue_t, or into aggregate types, such as MVPlugin::RecipeData or MVPlugin::Result, which support value semantics. Further, the handling of failures through error codes returned from the C API functions is replaced by exception handling to promote branchless control flow.
The support library is "header-only" and implemented in terms of the underlying C API. Hence, plug-in developers targeting C++17 or above can choose to use it without incurring a dependency on any particular C++ runtime. Consequently, only the version of the support library that is installed as part of the SDK matters when building the plug-in, whereas the runtime compatibility of the resulting plug-in binary with different versions of MERLIC is still exclusively governed by the primary C API.
The support library may undergo API breaks in future versions to incorporate new features or to fix defects. In this case, plug-in developers might need to perform (usually small) changes to their plug-in code when they update to a newer version of the plug-in development SDK.
The C++ support library headers are installed into the subdirectory "include\mvtec\plugin" within the MERLIC installation, for example, "%PROGRAMFILES%\MVTec\MERLIC-26.03", right next to the headers for the C API. Because the support library is header-only and shares the same include path, no additional setup to the build environment is necessary, and no separate CMake target is provided. The support library requires a compiler supporting at least C++17.
The types and functions provided by the support library use the namespace MVPlugin. Consequently, the "MV" prefix, which can be found on all of the C API's names, has been dropped from their support library counterparts.
Optionally, the support library can utilize the {fmt} library for string formatting. Specifically, the MVPlugin::Logger class conditionally supports overloads excepting format strings and format argument packs. Many of the data types provided by the support library already support formatting out-of-the-box which can make quick work of enriching your log messages with the relevant information. To opt into using the {fmt} library, you need to add the following definition to the CMake target of your plug-in and link against the fmt::fmt target.
Both of the example plug-ins, event-logger and action-sender, now use the support library to demonstrate its usage and enhance their readability. They also both make use of the {fmt} library, and its integration into their respective CMake projects can be used as a template.
When using the support library, the life-cycle of the plug-in instance is tied to the existence of an instance of a user-defined C++ "controller" class which we will simply call Plugin. This class should, however, be placed in an appropriately named namespace. The constructor and destructor of this class correspond to the MVOpen and MVClose functions, respectively, whereas the MVStart and MVStop functions are delegated to Plugin::Start and Plugin::Stop member functions. The state of the plug-in instance can be stored in that class as member variables.
The support header PluginCInterfaceHelper.h provides ready-made interface function templates which can be associated with the plug-in in MVInit_V2. As an exception to this rule, it is useful to implement MVOpen separately and call MVPlugin::OpenPlugin from within, as this is a useful customization point to inject dependencies into the Plugin class.
An implementation scaffold might look like this:
The Plugin class can also be unit-tested by following these two steps:
Plugin class has to be injected with "mocked" dependencies.The support library features an MVPlugin::ActionController class which wraps the C APIs provided by mv_action.h and mv_action_def.h, mapping each type of action to one member function (overload set). The parameters of the action are passed as function arguments. The wrapper takes care of initializing the underlying MVAction_t object, setting the appropriate action parameters, and queuing the action at the vision system.
MVPlugin::ActionController implements the MVPlugin::IActionController interface. It is recommended to work with this interface from within the Plugin class and to inject the implementation instance from the outside, as demonstrated in the section Plug-in Controller Class and C Interface Functions. This way, unit tests can inject a mock of the interface to allow setting expectations on the actions being taken by the plug-in.
Receiving events from the vision system directly using the C API defined in mv_event.h and mv_event_def.h entails the following pattern:
MVStartMVEvent_t objectsMVStopThe support library provides an abstraction for this pattern. By deriving from MVPlugin::EventMonitor<Plugin> , the Plugin class can effectively subscribe to certain events from the vision system by defining suitable event callbacks. EventMonitor employs the so called "curiously recurring template pattern (CRTP)" to mix this functionality into the Plugin class, and it will only extract parameters from an event and convert them into their support library counterparts if the Plugin class defines a suitable member function to handle it. Hence, the runtime penalty incurred due to uninteresting events is minimized. The signature of these member functions must match exactly, otherwise the callback is not recognized and the corresponding events are silently ignored.
These callbacks map closely to the Available Events. The Shutdown event is omitted because it is already handled behind the scenes by breaking out of the event loop.
Both the "JobStarted" and "AcquisitionDone" events admit multiple function signatures which add additional information:
For the "JobStarted" event, the original overload, designated (J3), still exists to remain source-compatible and can also act as a fallback when a plug-in built against the present API version is executed in a previous MERLIC version that still uses the API version 2.2.0. The overload (J2) was added in API version 2.2.1 to provide additional information. In API version 2.2.2, the overload (J1) was introduced providing even more (optional) information which is aggregated in the MVPlugin::JobInfo structure. (J1) will also be invoked if the plug-in is executed by a MERLIC versions with API version 2.2.1. You can only define either (J1) or (J2), with (J1) being the recommended choice for new plug-ins. Additionally, (J3) can also be defined as a compatibility fallback. Only one of the overloads will be invoked per event.
For the "AcquisitionDone" event, either the original overload, (A2), or the extended one, (A1), can be defined. If the extended one is defined, it adds information about the image acquisition in the structure MVPlugin::AcquisitionInfo. Unlike with "JobStarted", the extended overload will be called regardless of the MERLIC RTE version executing the plug-in but the additional data may not be available in older versions.
The VisionSystemAvailable callback is a notable addition and indicates that the vision system has been (re-)started and connected. Thus, its internal state and recipe list may have become inconsistent to what the plug-in might expect from monitoring the event stream. The callback lets it react to that. The std::future provided as the second argument is a "lazy" stand-in for the recipe list. If get() is called on it, the vision system will be queried for the current list of recipes.
To use the EventMonitor, the underlying event loop thread has to be started using MVPlugin::EventMonitor::StartEventLoop() in Plugin::Start() and stopped using MVPlugin::EventMonitor::StopEventLoop() in Plugin::Stop(), respectively.
The support library also significantly simplifies the creation of plug-in configuration options through the C APIs provided by mv_plugin_config.h and mv_plugin_config_def.h. To that end, it caters the most common use case where the available user parameters and the constraints imposed upon them are "static" in the sense that they are independent of the actual configuration values. If you need to dynamically change the available user parameters, you need to fall back on the C API for now.
Two data structures need to be defined in your plug-in's namespace. The first one, which we will call Config, contains all the configuration values needed by your plug-in as member variables. We recommend to initialize the members to the desired default values. This data structure will be handed to the Plugin instance in its Start function, at which point it will hold the actual values which the user has set through the "Communication" tab of the MERLIC RTE Setup.
The other data structure, ConfigDescription in this example, should also contain one member per user parameter. These are instantiations of the MVPlugin::Config::ConfigEntry<> template which are created and parametrized using the AddConfigEntry<>() factory function in combination with a "builder pattern".
The ConfigDescription is created statically only once for each plug-in, and its Read() function is called to produce the actual Config data structure based upon this description. A sample implementation of Read() might look like this:
You are free to use Read to perform any conversions from the underlying user parameter types (integers, floating-point numbers, strings) to higher level custom types. Finally, suitable implementations for the MVExpose, MVValidate, and MVStart interface functions need to be provided in MVInit_V2 which consume the ConfigDescription and use it to produce the Config data structure and pass it to Plugin::Start(). The support header PluginCInterfaceHelper.h again provides ready-made interface function templates for this purpose:
The "action-sender" and "save-images" example plug-in demonstrates the use of the configuration feature through the support library.
The support library also enables you to get the image results of a MERLIC Vision App, that is, images that have been specified as an "MVApp result" in the MERLIC Creator. The concept of the image API when using the support library is the same as if the standard C API is used. Therefore, you can refer to Getting Image Results to learn more about the general concept of the image API.
In this section, you can find additional information specific for the use of the support library. The support library provides the types MVPlugin::DataContainerDescriptor and MVPlugin::DataComponent analogous to the ones in the C API. To submit the final data container back to the vision system, you can use the function FetchDataContainer() provided in MVPlugin::ISynchronousQueryController.
To see an example how image results can be retrieved via the support library, take a look at the example plug-in "save-images". When the plug-in is running, it automatically saves the image data contained in to a configurable directory on disk. It also offers a rich set of configuration options to customize the image format, output directory structure, and conditions upon which to output an image. The source code of the plug-in is available in the directory "`examples\communication_plugins\save-image`" within the MERLIC installation directory. Thus, you can take a look at the implementation and how the MVPlugin::DataContainerDescriptor and MVPlugin::DataComponent are used in this example plug-in.
The following code block shows an example how to iterate over the data components in a data container descriptor.
For each data component reference, the default image dimension is queried and then scaled to the half of the original size. Then the modified data container descriptor is submitted back to the vision system.