How to write a generator using Python?

A ModbusPal project may require very specific generators in order to mimic a real-world device. The approach of ModbusPal is to let the user create its own generators thanks to scripts.

Important notice

Minimalist generator

  1. Import the class PythonGenerator from the modbuspal.script package.
  2. Create a Python class that inherits PythonGenerator.
  3. Override the getValue() function (the default implementation only returns zeroes).

Then the new generator should be registered into the project, so that it can be used from the Automation Editor of any automation in the current project:

  1. Create an instance of the generator.
  2. Register this instance by calling the ModbusPal.addGeneratorInstantiator() function.
MinimalistGenerator.py
from modbuspal.script import PythonGenerator

class MinimalistGenerator(PythonGenerator):

  def getValue(self,time):
    return time;

mg = MinimalistGenerator();
ModbusPal.addGeneratorInstantiator(mg);
      

Why does the generator have a strange name?

The above example will create a new generator with a rather strange name: View of a generator with a strange name

This is because, by default, the generator is named after the Java classname. But in the case of a class created by a Python script, the resulting classname is cryptic.

To solve this problem, the generator must implement the getClassName() function in order to return a better name.

It is highly recommanded to implement the getClassName() function.

from modbuspal.script import PythonGenerator

class MinimalistGenerator(PythonGenerator):

  def getValue(self,time):
    return time;

  def getClassName(self):
    return "MinimalistGenerator";

mg = MinimalistGenerator();
ModbusPal.addGeneratorInstantiator(mg);
      

How to initialize the generator?

Some generators will require to initialize variables when they are instanciated. Normally, this would be done in the constructor of the Python class, the __init__() function.

But, due to the way ModbusPal operates, the constructor is not always called. So, in order to initialize the internal variables of the generator, the user should implement the init() function instead.

All initializations that are required by the generator should be made into the init() function. The following sample of code illustrates how:

class MyGenerator(PythonGenerator):
  def init(self):
    self.myVar = 5;
          

How to change the icon of the generator?

By default, the genetor has the following icon: Default icon for a new generator But it can be modified, replaced by any other 64x64 PNG or JPEG image.

To replace the icon of a generator, call the setIcon() function. This is usually performed in the init() function described above. When using the setIcon function, only absolute paths should be used.

The following snippet illustrates how to use the setIcon() function in order to use an PNG image that is located in the same directory as the script:

class MyGenerator(PythonGenerator):
  def init(self):
    self.setIcon(mbp_script_directory+"/my_icon.png");
          

How to create a configuration panel for the generator?

It is a natural thing that a generator lets the user modify some parameters. But this task implies that a graphical interface is available.

When writing a PythonGenerator, a graphical interface can be provided to ModbusPal so that the parameters of the generator are modified by the user.

To do so, simply implement the getControlPanel() function; it should return an object of the javax.swing.JPanel class.

The following example creates a dummy user interface. There is actually no input, but only a text in a JLabel:

class MyGenerator(PythonGenerator):

  def init(self):
    self.controlPanel = JPanel();
    self.controlPanel.setLayout( BorderLayout() );
    self.controlPanel.add( JLabel("Hello, world!") );

  def getControlPanel(self):
    return self.controlPanel;
          

How to save and load the settings of the generator?

If the generator requires the user to provide some parameters, usually thanks to a control panel, then it becomes necessary to save those parameters into the project file. And then, of course, it is necessary to be able to load those parameters from the project file.

Save

To save the parameters of the generator, implement the saveGeneratorSettings() function. This function has one important parameter: it is the OutputStream to write into.

The project file is an XML formatted file, so its best if the generator uses also XML.

The following example saves the value of paramA and paramB into the project file:

class MyGenerator(PythonGenerator):

  def init(self):
    self.paramA = 5;
    self.paramB = 7;

  def saveGeneratorSettings(self,outputStream):
    outputStream.write("<paramA value=\""+ str(self.paramA) +"\" />\r\n");
    outputStream.write("<paramB value=\""+ str(self.paramB) +"\" />\r\n");
          

Load

In order to load the settings of a generator, the loadGeneratorSettings() function has to be implemented.

The following example will load the settings saved by the previous example. If the settings have been saved using the XML format, then the user can use the powerful APIs of Java, as well as the toolkit class provided by ModbusPal, modbuspal.toolkit.XMLTools. The input parameter nodeListis an instance of org.w3c.dom.NodeList.

class MyGenerator(PythonGenerator):

  def init(self):
    self.paramA = 5;
    self.paramB = 7;

  def loadGeneratorSettings(self,nodeList):
    nodeParamA = XMLTools.getNode(nodeList,"paramA");
    if nodeParamA is not None:
        self.paramA = int( XMLTools.getAttribute("value",nodeParamA) );
    nodeParamB = XMLTools.getNode(nodeList,"paramB");
    if nodeParamB is not None:
        self.paramB = int( XMLTools.getAttribute("value",nodeParamB) );
          

API

Please consult the Javadoc of ModbusPal in order to get more information on all the classes introduced in this page.

Full example

The following script illustrates all the aspects of a fully customized generator:

AdvancedGenerator.py
from modbuspal.script import PythonGenerator
from modbuspal.toolkit import NumericTextField
from modbuspal.toolkit import XMLTools
from java.awt import *
from javax.swing import *

class AdvancedGenerator(PythonGenerator):

  # Init function:
  # - set generator icon
  # - create the control panel
  def init(self):

    self.setIcon(mbp_script_directory+"/CustomGenerator.png");
    self.createCtrlPane();

  # Returns the real class name of this generator
  def getClassName(self):
    return "AdvancedGenerator";

  # This function will create a control panel using Java Swing components.
  # The control panel will appear in the middle of the generator panel,
  # in the automation editor.
  def createCtrlPane(self):

    self.ctrlPane = JPanel();
    self.ctrlPane.setLayout( FlowLayout() );

    self.ctrlPane.add( JLabel("A=") );
    self.aTextField = NumericTextField(1.0);
    self.ctrlPane.add( self.aTextField );


  # Override the getControlPanel function so that the
  # control panel created in the init function is returned
  def getControlPanel(self):

    return self.ctrlPane;


  # Return the generated value, f(x)=ax+b
  # where a is defined by the user (in the control panel)
  # and b is the initial value of the generator (that is the
  # current value of the automation when the generator starts).
  def getValue(self,x):

    a = float( self.aTextField.getDouble() );
    b = self.getInitialValue();
    return a*x+b;


  # Save the parameters of this generator with XML formatting into
  # the provided output stream.
  def saveSettings(self, out):

    out.write("<a value=\""+ self.aTextField.getText() +"\" />\r\n");


  # Load the parameters of this generator from the provided DOM structure.
  def loadSettings(self,nodes):

    node = XMLTools.getNode(nodes,"a");
    if not (node is None) :
      value = XMLTools.getAttribute("value",node);
      self.aTextField.setText(value);

genInstance = AdvancedGenerator();
ModbusPal.addGeneratorInstantiator(genInstance);