Consuming deployed components as Synapse plugins
We have received questions on how to load a deployed component into Synapse as a plugin and use it as a building block for new systems. This was something that was intended from early on, but was cut from the release and until now we have had few questions on the subject.
This post will almost be in the form of a tutorial and it will explain the interfaces needed to produce a plugin and to allow for standard signal flow. First we will create a minimal plugin, then fill it with a deployed component and lastly add some bells and whistles.
Quick hop to a later step:
Step 1A: Create a minimal plugin
Step 1B: Create a minute GUI level for the plugin
Step 2: Loading the plugin into Synapse
Step 3: Make the plugin deployable
Step 4: Encapsulate a deployed component within your plugin
Step 5: Test the plugin in Synapse vs. the original
Step 6: Change the GUI of your plugin
Step 7: Create and display settings
Step 8: The merry-go-round
Conclution + source files
First, to be a Synapse plugin at all, our type must implement two interfaces. Peltarion.Core.Atomic and Peltarion.Core.IPlugin found in the ILibrary.dll assembly. Implementing Atomic is as easy as deriving from Peltarion.Core.Atom.
Most plugins in Synapse comes with a low level and a high, GUI level. The low level should be minimalistic and is meant to be used in deployment, while the GUI level packs extras used only by Synapse and that are not necessary for taking care of business ones the component is deployed. The two levels are created in separate assemblies. If the component is not meant to ever be deployed, you can do just a high level and not have to bother with multiple assemblies. We will later be able to deploy our component so we will use two levels and create two assemblies.
Let's start! To do this, fire up Microsoft Visual Studio (or your editor of choice) and start a new project of type class library. I called mine simply SynapsePlugin and that will also be my default namespace. Start by adding a reference to ILibrary.dll (found in C:\Program Files\Peltarion\Synapse\Bin). Add 'using Peltarion.Core;' and create a class that derives from Atom and implements IPlugin.
IPlugin will have you implement a System.Version get property called RequiredVersion. This is the required version of Synapse. We don't have to worry about it in this case, so just return an empty Version object. IPlugin derives from Peltarion.Core.NamedItem so you must also implement a string get property called Name.
Now you should have something that looks akin to this.
Now if we compile this it would already be a Synapse plugin although it would not do anything useful. From here we have to decide what kind of plugin we wish to create. It could be an update rule or an input format or any other of a number of plugins that Synapse uses. We however will make a plugin that can be part of the signal flow and show up as a component in design mode. For this we need to implement Peltarion.Core.SignalReceiver and Peltarion.Core.SignalEmitter.
Signals are passed as arrays of Peltarion.Core.Signal. The position in the array maps to ports on the component. Most components in Synapse are single port components, and make only use of the first Signal in the array. Further SetSignal in SignalReceiver will be called once as opposed to GetSignal in SignalEmitter that might be called several times. Thus make your calculations from SetSignal and prepare an output to be returned by GetSignal.
Let's start with SignalReceiver. This interface has one void method SetSignal(params Signal signal). Our first test plugin will have one input port and for toy calculation we will calculate the sign of the input signal. We will prepare a Signal that can later be returned by the SignalEmitter interface. To support this we will create a field variable called 'output'.
SignalReceiver also sports a get/set int property Inputs. The int array should have the same length as the number of input ports of the component. The number at each position denoting the feature count expected of the Signal entering on that port. For simplicity we will use a single port and expect only one feature. Thus, in our case, the property will return an array of length one (ports), containing a one (features). Since we do not intend to let the user change this, we will implement the set property as an empty method.
Signal.Data will return a Peltarion.Maths.Matrix. It is paramount not to alter the incoming signal since it can be fed to other components as well depending on the topology. We could have used Signal.Data.Sign() to calculate the sign, but that would have altered the input signal. So in that case we have to use Matrix.MClone() first make a copy.
The SignalEmitter interface has one method called GetSignal() that returns Signal and a get/set int property Outputs. The int array works the same as the Inputs property but concerns output ports. Our plugin will have one output port and one output feature, so the array will be length 1 and will hold 1. Again we set the set property to an empty method.
To create the GUI level we will add a new project to our solution in Microsoft Visual Studio. Use the Windows Control Library template. I, deciding to be original, called mine SynapsePluginGui. This time you will have to add references to ILibrary.dll, GLibrary.dll (also found in C:\Program Files\Peltarion\Synapse\Bin) and to the project just created in step 1A.
Start by deleting the windows control VS creates for you by default and create an empty class called SplugGui instead. Add using commands for the namespaces Peltarion.Core and SynapsePlugin (or whatever namespace you used for the low level).
Let your new class derive from the low level type and add the Peltarion.Core.GuiPlug interface. This interface lets Synapse find the plugin as one that can be used as a building block in design. It has two get properties. One is called Category and is of type string, and the other is called Control and returns a Peltarion.Core.IControl.
Category will state under which categories in the component bar the component should be placed. Let's put it in 'Basic' and a new one: 'Home Grown'. We will not bother with creating a Gui for this plugin just yet, so we will just return a new instance of the bare base class Peltarion.Core.IControl.
That is it! We are now ready to use our plugin in Synapse.
Navigate to the Synapse plugin directory (C:\Program Files\Peltarion\Synapse\Plugins) and create a new folder and call it 'homemade' or something. Compile everything from step 1 and copy the two assemblies into the folder you just created. (With the names used in this post, that would be SynapsePlugin.dll and SynapsePluginGui.dll.)
Now start Synapse!
Go to design mode and YES, there is a new plugin in the component bar called 'Your Plugin'. Drag it on to the canvas/work-area and it should appear as two green vertical stripes. This is because we did not bother with any GUI. Get a Function Source and two plots as well. Connect the Function Source to Your Plugin and also to one of the plots. Connect Your Plugin to the other plot. Go to training and press play.
Not bad for a first plugin. And it wasn't very hard was it? Of course, there is a bit more to it. If you continue to post processing and deployment, you will see that Your Plugin is listed together with the plots as undeployable. How did that happen? Well, we need to override the default XMLSerialazition of Atom for it to be deployable. Let's go back to Visual Studio.
Go to the source of the low level of your plugin. At the top add 'using System.Xml;'. The Peltarion.Core.XmlSerializable interface that is implemented by Atom has two methods: ToXml and FromXml. The first generates an XmlDocument and the second parses the same document at a later time. The idea is that it is your own XML that is returned to you, so that even if a few tags are mandatory (to make it work properly with inherited types and deployment) you can add any supplementary tags of your pleasing.
In your plugin class override XmlDocument ToXml(). To make our work a fair bit simpler we will use the Peltarion.Core.XMLProducer to create the xml document. The XMLProducer derives from System.Xml.XmlTextWriter but has got some extra functionality The XMLProducer has a constructor that looks like so:
XMLProducer(string Guid, Type typeof_not_GetType, bool deployable)
The Guid is the id of the instance of the plugin. This is supplied by Atom so that is that. The type is the type of the plugin. Now use typeof and NOT GetType(), this is important as GetType() will not return the correct type when this method is called in a derived class. The last argument specifies if this component is to be considered as deployable.
Since we overrode the method of the base class, its XML is lost. To include the XML of the base class in your XML use the EncloseBaseXML method of the XMLProducer. Now you can call the close method to generate the XmlDocument to be returned.
On overriding FromXml() we will again use the XMLProducer to extract the base class XML.
Now you can deploy your plugin as well as run it within Synapse.
For this step you will need a new version (220.127.116.11) of the deployment plugin. The old (18.104.22.168) has a bug that prohibits deployed components from reentering Synapse.
To check the version of the one you have:
1) Go to C:\Program Files\Peltarion\Synapse\Plugins
2) Right-Click on Deployment.dll and choose Properties in the pop-up menu.
3) Click on the Version tab and look for File Version at the top
If the version is less than 22.214.171.124 copy this new version of the Deployment plugin to the 'C:\Program Files\Peltarion\Synapse\Plugins' directory, overwriting the existing one.
Since you come this far, given the scope of this post, I expect you already have a System you wish to make a plugin. I will use the police system developed in Tutorial II - Good Cop/Bad Cop.
We will do things just a little bit differently than in the tutorial, and of course we will use the new deployment plugin mentioned just above. For this reason, any system you have deployed and want to use will have to be redeployed with the new plugin.
When you are ready to deploy your solution, go back to Training and set the Batch Length to zero in the Control System pane. Go to post processing and deployment. Make sure to uncheck the 'Single File Deployment'-box. This is important.
To be able to show that this works a little better once brought back into Synapse, select the 'Output' component in the Shortcuts area. (If you are not following the tutorial this does not matter.) In the Unstage area, select 0-None in the stage-info dropdown.
Save the solution and click Deploy!
Bring up your Visual Studio again and for your low level project (SynapsePlugin) and add a reference to the assembly you just deployed from Synapse. For me that would be '...My Documents\Peltarion Synapse\Deployed\GoodCopBadCop\GoodCopBadCop.dll' Don't mind all the other dlls likely to be found in the same directory.
Go to the source of the low level plugin (SPlug.cs) and 'using Peltarion.Deployed;'. Also add a field variable for your deployed type.
As you see the deployed type will have a set of constructors taking a boolean argument standAlone. This should be set false when going back into Synapse. (True is default and should be used otherwise.)
We must now make some changes to to the implementation of SignalReceiver to pipe the incoming signal through the deployed component. We recall that the data unit was called 'CSV' so we find the property Input_CSV and set it to the Peltarion.Maths.Matrix struct extracted from the Data property of the incoming signal.
We use the StepEpoch method to do just that, and now we can set the field variable 'output' to the output from the deployed component, accessed from Output_Port0.
(If you followed the tutorial and cannot find Output_Port0, you probably forgot to change the name of the last function layer, and can use FunctionLayer4_Port0.)
We no longer expect only one input feature. Ten in the case of the police data set. We could hard code that, but there is a way to get it from the MatrixInputFormat. The MatrixInputFormat in turn can be reached via the CSV_Input property of the data unit bag extracted from the CSV property of the Data class accessible from the property with the same name located on the deployed component.
I think you will find that last bit easier in code
Nor do we have only one output feature but two. Let's just hard code that for now.
Since the deployed system has no learning turned on, and no memories, we can consider ourselves done. (If it had either we would have had to override the reset method.)
So let's compile and copy the result into our 'homemade' directory in the Synapse plugin directory. This time we will have to copy three files though. SynapsePlugin.dll, SynapsePluginGui.dll and the deployed component GoodCopBadCop.dll. If the system will not let you overwrite the old ones
because they are in use by another program, try closing Synapse.
Now start Synapse and open the same solution again. In design add Your Plugin to the work area and also drag the csv data unit from the Solution Explorer to the work area.
Select the new data source and go to the settings browser. Scroll down to Scale/Normalize and change the value to preserve. This because the plugin expects unscaled data. Connect the data source to your plugin.
Drag a merger (found under signal flow) and a plot and connect them. Connect the output function layer (just prior to the delta terminator) to one port of the merger. Connect your plugin to the other.
Go to training and switch off learning in the control system pane. Click Play. With the zoom tool click the on the plot. As you see the signals from the two systems completely overlap. Pick the select tool ones more and click the plot again. Now that it is selected its settings will appear in the settings browser. Change the buffering values to LazySample, 100 and Validation. This will show only 100 samples from the validation set.
As it is still impossible to distinguish the two sets of signals, switch learning back on. Slowly you might see one set shifting slightly from the other. Reset the system with the stop button and press play again. Now you will clearly see how the system starts to adapt and converge to something much similar to the solution embedded in the Plugin.
Hit backspace [<--] or Crtl+F to zoom out again. As you might have seen before, and maybe been annoyed about, your plugin is sort of hard to see unless it is selected. This will be remedied in the next step.
Close Synapse and head back to Visual Studio. Go to the source of the high or GUI level of the plugin. At the bottom add a new class, call in Gui and let it inherit Pltarion.Core.IControl. The first thing we will do is to override GetMediumIcon(). This method returns a System.Drawing.Image and this image is what you will see in the component bar in Synapse.
You can return any image you like as long as it is 32x32 pixels. I'm going to use this image. You can download it and use it as well if you like.
To get the image in to the assembly and get easy access to it from your code, go to the Visual Studio Solution Explorer and double-click properties of the SynapsePluginGui project. Go to resources and click the blue link to create a resource file. some sort of grid will appear. Hit Ctrl+2 to change from strings to images. Now you should have a blank white pane. Drag and drop your image on to the pane. Make sure it has a proper name. Mine is called cuffs32. Save and close the properties window.
To return the image in our overridden method write:
We will also have to return the new Gui class instead of the empty IControl. It will not look any different on the Synapsian work area though, yet that we are about to remedy. First though, we will create a constructor on the Gui class so that we can pass a reference to the plugin itself on instantiation. This is good practice since more useful GUIs usually need to display some property of the component.
Create a field variable to hold the reference to the plugin in the Gui class. I called mine 'target'. Create a constructor that take the plugin type as argument and set the field variable.
Create another field variable to hold the reference to the Gui in the plugin class. Call it 'gui'. Modify the Control property get method to:
return gui ?? (gui = new Gui(this));
This is what my code looks like now.
Synapse will display the Gui component that ultimately extends System.Windows.Forms.Control, on the work area. This means we can actually create a fully fledged windows GUI if we like. We will not go quite that far.
There is a type Peltarion.GLib.Gui.StandardControl that will give us a shaded background. Let's use that one. Add it to the Controls collection and set the DockStyle to Fill.
In order to show a different Gui in training mode you can listen to the Gui_WorkModeChanged event found on IControl. I will leave that as an exercise however.
What I will show, is how to make settings and have them appear in the settings browser.
The settings framework is all in the Peltarion.Settings namespace, so start by adding a using Peltarion.Settings; For good measure, add a using Peltarion.Settings.Common; as well since we will use things from there too.
To supply settings our high level plugin class must implement the Peltarion.Settings.SettingProvider interface. To implement this interface we will have to create the method GetSettings that return Peltarion.Settings.SettingTable.
All settings have a category, a name and a value. This is the information that will appear in the settings browser in Synapse. Adding settings to a SettingTable is, at least in appearance, very similar to adding values to a .NET dictionary. You specify the category and the name as keys and set that entry of the SettingTable to the setting.
There are many different types of settings for different needs that take values of different types. Most are type safe so we will usually not have to cast.
We will start by adding a settings to show the number of input and output features of the plugin. For this we will use a SettingInt setting. Settings typically have an arsenal of constructors, many dealing with methods of constraining user input and retrieval of changed values. Since inputs and outputs of our plugin cannot change we will go for the simplest constructor taking only a value and a description as arguments. This way they will automatically be read-only.
The descriptions has been cut a little to for it all to fit.
To show a bit more complex use of settings (without adding too much support code) we will add a setting to change the BorderStyle property of the Gui class created earlier. This is possibly as useful as having a plugin that can tell good and bad Dutch policemen apart, but it serves its purpose.
Since the BorderStyle property is an enum, we will use the generic SettingEnum setting. It Too has several constructors and we will use one that looks like this:
SettingEnum(T value, string description, SingleEventHandler eventHandler)
The event handler is a delegate that will be fired as the user changes the value of the setting. We will take use of anonymous methods and create a delegate on the fly, but you can of course pass a delegate to an actual method just as well.
SettingEnum is generic (that is what the T means). The T is to be replaced by the enum type we are interested in. For this example it will be System.Windows.Forms.BorderStyle. This means our event handler must also take System.Windows.Forms.BorderStyle to be compatible with the setting. (Take a look at the constructor and in your mind replace both 'T' for System.Windows.Forms.BorderStyle.)
While we are at it, let's add a really annoying one as well.
I think we have done quite enough damage about now. Lets compile this and see how it looks in Synapse. Don't forget to copy the GUI level of the plugin (SynapsePluginGui.dll) to the homemade folder in Synapse plugin directory.
Start Synapse and drag your plugin on to the canvas. Zoom in on the plugin and see how the text changes with the size.
Select the plugin and check the settings browser. And don't forget to try the popup setting.
As an exercise, deploy a solution with the plugin you just created, then jump back to step 1a and make a plugin out of it. Come on, it's great fun! You can do this all night
Now you can write a Synapse plugin with a GUI that can display settings. Even better, you can get deployed models back into Synapse and use them as building blocks for future solutions.
- We started off by creating a minimal shell that would load as a Synapse plugin.
- we had a look at the XML serialization, realizing that we could save anything in a custom tag and get it back on deserialization.
- We enclosed a deployed synapse model and piped the plugin signal flow through it, thus being able to use deployed models as Synapse plugins.
- We learned how to make the plugin distinguishable and look a little nicer. Using the same principles we can let our plugin show information of what it is doing and interact with the user via a full scale Windows Forms GUI, making user communication very powerful.
- We learned how to expose settings so that they will appear in the Synapse settings browser. By Intelligent use of the existing rather general setting classes and with the possibility to extend them and create new ones, should need arise, we can make settings suitable for quite complex situations relatively easy.
Here you can get the c# source files used for this post
Hope you enjoyed this post, and I hope you will be even more productive with Synapse in the future.
Mons / Peltarion