Let's start by writing the logic of our plugin. This is done by editing plugin.h and plugin.cpp files of the project. Much like in C++ Script, these files contain classes with methods that Dewesoft automatically calls whenever appropriate (e.g. when measuring is started, when measuring is stopped, when setup is saved,...).
Let's first create the two input channels required by our plugin. Unlike in C++ Script where we were able to just click on the user interface to add the channels, we have to create these "by hand" in Processing plugin. We do this by making changes to the plugin.h and plugin.cpp files. In the plugin.h file we define the two input channels as public variables of the Module class.
class LatchMathModule : public Dewesoft::Processing::Api::Advanced::Module
{
public:
ScalarInputChannel criteriaChannelIn;
ScalarInputChannel inputChannelIn;
};
Note that the types of the channels are ScalarInputChannel, exactly like in C++ Script. This is not by accident. In fact, channels in Processing plugin have very similar interfaces as the ones defined there, so if you are familiar with C++ Script, it shouldn't be such a jump to master Processing plugin channels.
In the plugin.cpp file we now reserve two input slots (top left panel on the settings window) that will hold our channels and connect them with the variables we just defined. We do this by modifying the connectInputChannels()
method:
void LatchMathModule::connectInputChannels(InputChannelSlots& slots)
{
slots.connectChannel("Input channel", &inputChannelIn, ChannelTimebase::Synchronous);
slots.connectChannel("Criteria channel", &criteriaChannelIn, ChannelTimebase::Synchronous);
}
With this, we specified that our two input slots will be called "Input channel" and "Criteria channel", and their timebases will be synchronous.
By specifying the type of our input channels as ScalarInputChannel, we have told Dewesoft that we expect our channels to hold scalar values. That is why Dewesoft will automatically filter out all channels that are not scalar when letting the user choose a channel for a particular slot. If our processing plugin only worked with e.g. complex vectors, all we would have to do is set the type of the channel as ComplexVectorInputChannel, and Dewesoft would take care of the rest for us.
The last thing we need to do to get the code to compile is to edit the getPluginProperties()
method. Here we can change the general behaviour of our plugin - how many channels it accepts, where in Dewesoft it appears, if it has a settings form or not, etc. For our purposes, the only thing that really matters is the inputSlotsMode
property which will allow our plugin to accept multiple inputs.
void LatchMathModule::getPluginProperties(PluginProperties& props)
{
props.name = "Latch math - scalar";
props.description = "Pro Tutorial example.";
props.pluginType = PluginType::application;
props.hasProjectSettings = false;
props.inputSlotsMode = InputSlotsMode::multiple;
}
Compile the project and you should be able to find our plugin under "More..." menu. Adding it you can see the two input slots we prepared for our plugin:
Image 9: Compile the project and add your plugin inside More... tab Next, let's prepare variables that will control the behaviour of our latch math. We will do this in the plugin.h file where we define the public variables for the criteria limit and for the edge type. We will also create a custom enum for edge type so the code will be more descriptive.
enum edgeTypes
{
RisingEdge = 0,
FallingEdge = 1
};
class LatchMathModule : public Dewesoft::Processing::Api::Advanced::Module
{
public:
double criteriaLimit = 0;
edgeTypes edgeType = RisingEdge;
};
mountChannels
Next, let's add some output channels to our plugin. Again, in C++ Script this was done by clicking on the UI, but here we have to do it all by hand. First let's add a variable that will hold our output channel to plugin.h:
class LatchMathModule : public Dewesoft::Processing::Api::Advanced::Module
{
public:
ScalarOutputChannel outputChannel;
};
To actually register the channel with Dewesoft, we use a special method called mountChannels()
. Here we set basic channel properties like the name of the channel that is seen by the end-user, the channel index, which should always be unique. The type of value that the channel will output is determined from the definition of the channel in the plugin.h file, and since we defined our channel as ScalarOutputChannel
it means that our channel will output scalar values.
We also need to set the timebase of the output channel. We do this by mounting the channel with the mountSyncChannel()
function if we want the timebase to be synchronous, mountAsyncChannel()
for asynchronous timebase or mountSingleValueChannel()
if we want the timebase to be a single value. In our example, the output channel should be of an asynchronous type because we do not know exactly when a new sample will be added.
As you can see from the code snippet below we can mount channel as fixed or dynamic channel. The difference between the two is that Fixed must always contain a constant number of channels, while Dynamic may contain a different number of channels every time the mountChannel()
method is called.
void LatchMathModule::mountChannels(OutputChannels& fixed, OutputChannels& dynamic)
{
fixed.mountAsyncChannel("Latch", 0, &outputChannel);
}
configure
The configure()
method gets called every time before the measurement is started. This is the place to set the final settings dependent on and regarding channels.
Here we set the output channels' expectedAsyncRate
and can also change the output channels' name, unit and description.
Another important thing to be set here is the properties of the resampler.
void LatchMathModule::configure()
{
resampler.blockSizeInSamples = 1;
resampler.samplingRate = ResamplerSamplingRate::Synchronous;
resampler.futureSamplesRequiredForCalculation = 1;
outputChannel.setExpectedAsyncRate(5);
}
To make sense of the code above we will take a detour and take a closer look at the resampler and its properties:
resampler
The C++ Processing Plugin works so that all the input channels of the plugin get resampled to the same sample rate, which means that all the input channels have samples at the same timestamps. In C++ Script this is done automatically, with every input channel getting resampled to the timebase of the first assigned input channel. In Processing plugin we get a lot more control over the behaviour of the resampler.
The sampling rate can be:
- Synchronous - all the samples will be spaced equidistantly based on the acquisition sample rate;
- SingleValue - the input channels only have one sample as an input, and the calculate() function will get called once every couple hundred milliseconds; and
- AsynchronousSingleMaster - the samples get resampled to the asynchronous rate of the master channel. To set the master channel we need to call
setMasterChannel()
function like so: resampler.setMasterChannel(&inputChannelIn);
We also need to set the number of samples we receive in every calculate()
call. This is set with the blockSizeInSamples
property, if we choose it to be 1 then the calculation will be sample-based and we only receive one sample per calculate()
call, but if we set it higher then we receive a block of samples every time.
Sometimes we need access to previous or future samples in order to correctly process our signal, in this case, we use the futureSamplesRequiredForCalculation
or pastSamplesRequiredForCalculation
which allows us to access these samples.
calculate
This method is called repeatedly during Measure mode. Here we can safely read from and write to the channels. In this method, we add the actual logic for our plugin:
void LatchMathModule::calculate()
{
float currentSampleCriteriaChannel = criteriaChannelIn.getScalar(0);
float nextSampleCriteriaChannel = criteriaChannelIn.getScalar(1);
bool crossedRisingEdgeCriteria = currentSampleCriteriaChannel <= criteriaLimit && nextSampleCriteriaChannel >= criteriaLimit;
bool crossedFallingEdgeCriteria = currentSampleCriteriaChannel >= criteriaLimit && nextSampleCriteriaChannel <= criteriaLimit;
if ((crossedFallingEdgeCriteria && edgeType == FallingEdge) || (crossedRisingEdgeCriteria && edgeType == RisingEdge))
{
float value = inputChannelIn.getScalar(1);
outputChannel.addScalar(value, inputChannelIn.getTime(1));
}
}
Calculate function should be as fast as possible, otherwise, it might cause Dewesoft to lose data!
Writing to output channels and reading from input channels is only valid inside ::calculate() procedure, and only from the main thread! Attempting to do so from any other function or any other thread might result in your plugin crashing!
We have now implemented all the logic required by our latch math plugin, but we have a big problem: the users are not able to change any settings of our module, so the criteria limit is always going to be 0 and we are always going to be looking for the rising edge! Let's create a user interface so they get a chance to modify these values.