Plugins

Mundo is extendable using application plugins. These plugins can be loaded and initialized during runtime by placing them in a plugins subdirectory in your configuration directory. Each plugin must be a directory, prefixed with mundo_, and should contain a plugin.json, an empty __init__.py, and one Python file entrypoint.

For example, on Linux, creating a new plugin can be done like this:

cd ~/.config/mundo/plugins
mkdir mundo_verycoolplugin
cd mundo_verycoolplugin
touch __init__.py main.py plugin.json

To be able to load your plugin, the plugin.json file must have at least the following value:

{
  "entrypoint": "main.py",
  "name": "My very cool plugin"
}

but it is recommended to also include the following:

{
  "entrypoint": "main.py",
  "name": "myverycoolplugin",
  "version": "0.0.1",
  "description": "My description"
}

The entrypoint tells Mundo where to find your plugin class. For the plugin to be initialized, your Python entrypoint (defined in your plugin.json file), should define a new class that extends the BasePlugin class, i.e:

from mundo.api.plugins import BasePlugin
from mundo.api.context import ApplicationContext # for typings

class MyPlugin(BasePlugin): # must extend BasePlugin
    pass

You are free to use more than one Python file in your plugin. Your plugin directory is automatically added to the Python path once it is initialized. This means that you can import files from your plugin directory using:

from myverycoolplugin.module1 import foo
from myverycoolplugin.module2 import bar

Rendering properties

Plugins are rendered in the sidebar of the application. There are two types of plugins; default and custom. Custom plugins are rendered at the bottom of the sidebar, whereas default plugins are rendered at the top. Plugins can define if where they want to be rendered using the default field of their plugin.json, i.e.

{
  "entrypoint": "main.py",
  "name": "myverycoolplugin",
  "version": "0.0.1",
  "description": "My description",
  "default": true
}

It is also possible to automatically expand the plugin dropdown on startup using the expand_on_start field in the plugin.json, i.e.

{
  "entrypoint": "main.py",
  "name": "myverycoolplugin",
  "version": "0.0.1",
  "description": "My description",
  "expand_on_start": true
}

The default property and the expand_on_start property can be used both in conjuction with each other, and separately.

Adding support for a new controller

Plugins allow you to support any kind of motor controller by implementing the common methods of the BaseController class. To register your controller for usage in the application, create a plugin as described in the section above and register it in the plugins constructor. For example, registering a new controller might look something like this:

from mundo.api.controller import BaseController
from mundo.api.context import ApplicationContext

class MyVeryCoolController(BaseController):
    # Custom implementations for all required methods
    ...

class MyPlugin(BasePlugin):
    def __init__(self, context: ApplicationContext) -> None:
        super().__init__(context)
        self.context.register_controller("MyControllerName", MyVeryCoolController)

Rendering custom widgets

Plugins can also render custom Qt widgets in the application GUI. To enable rendering, your plugin class must implement the should_render property and the render() method:

from typing import Callable
from PySide2.QtWidgets import QWidget
from mundo.api.context import ApplicationContext

class MyPlugin(BasePlugin):
    ...

    @property
    def should_render(self) -> bool:
        # This is False by default
        return True

    def render(self, parent: QWidget, expand: Callable, collapse: Callable) -> None:
        # Render widget inside the parent, setup event
        # handlers, etc.
        #
        # You can expand and collapse the plugin's dropdown using the
        # expand and collapse methods, i.e. expand(), or collapse().
        ...

Saving and loading plugin configurations

One important aspect of plugins is the ability to save and load data, such as configuration or connection parameters set by a user. These parameters may be global, or specific to a single session, or to a certain type of sampling platform. To allow plugins to easily save and load data in these contexts, the application implements three different types of configurations:

  1. SystemConfig (global)

  2. SessionConfig (for a single sampling session)

  3. PlatformConfig (for a certain sampling platform)

Each configuration is extended from the base config BaseConfig, and is loaded in different contexts. Because of this, it is highly important that your plugin uses the correct configuration type for saving and loading data.

The global system configuration is always available via the application context:

# Somewhere in your plugin class
self.context.system.<field>

Warning

You should at all costs avoid changing any data in any of the configurations directly, i.e. do not use any of the setters in the configuration instances. This can lead to unexpected behaviour.

In order for your plugin to save data in one of these three contexts, you can implement one of the serialization methods in your plugin class:

Each serialize method should return a dictionary with strings as keys and a value of any type. If no data should be saved, the method should return None or an empty dictionary (this is the default if you choose not to implement these methods). The serialization methods will be automatically called and handled appropriately by the application. You do not have to - and should not - call them manually!

Once your plugin has saved data in one of the contexts, you can easily access global system configuration data using the system property and your plugins unique :

# Anywhere in your plugin class
system_config = self.context.system_config.plugins.get(self.identifier)

However, some contexts are loaded on-demand, which means that they are not immediately available on startup. To load your configuration from these contexts, you should use the ON_SESSION_LOAD() and ON_PLATFORM_LOAD(). These events are broadcasted right after a session or platform has been loaded. This means that they are now accessible via the session property and platform property, respectively. These events will be called everytime a session or platform is loaded, even if the session or platform does not change.

For example, a plugin that uses both a session and platform config could look like this:

from mundo.api.plugins import BasePlugin

# For typing
from mundo.api.serialize import SerializedData
from mundo.api.context import ApplicationContext

class MyPlugin(BasePlugin):
    def __init__(self, context: ApplicationContext) -> None:
        super().__init__(context)

        # Create dictionaries to store plugin configuration in
        self.__my_global_config: SerializedData = {}
        self.__my_session_config: SerializedData = {}
        self.__my_platform_config: SerializedData = {}

        # Fetch the global system plugin configuration, if any
        system_config = self.context.system_config.get(self.identifier)
        if system_config is not None:
            self.__my_global_config = system_config

    def on_session_load(self) -> None:
        # Load the plugins config for the current session
        session_config = self.context.session.plugins.get(
            self.identifier
        )

        if session_config is None:
            # No saved configuration for this session
            pass
        else:
            # We have a saved configuration for this session
            self.__my_session_config = session_config

    def on_platform_load(self) -> None:
        # Load the plugins config for the current platform
        platform_config = self.context.platform.plugins.get(
            self.identifier
        )

        if platform_config is None:
            # No saved configuration for this platform
            pass
        else:
            # We have a saved configuration for this platform
            pass
            self.__my_platform_config = platform_config

   # We want to save our configuration data
   def serialize_session(self) -> SerializedData:
       return self.__session_config

   def serialize_platform(self) -> SerializedData:
       return self.__platform_config

Plugin API

class mundo.api.plugins.BasePlugin(
context: mundo.api.context.ApplicationContext
)

Base plugin interface. All plugins should extend this class.

__init__(
context: mundo.api.context.ApplicationContext
)

Initializes the plugin.

It is recommended to always call this constructor in your own plugins, even if you implement your own.

Parameters

context – Instance of the ApplicationContext class

property context: mundo.api.context.ApplicationContext

The application context.

Returns

The current application context

property identifier: str

The unique identifier of the plugin. This will be set to the name of the plugin directory, i.e. mundo_<name>. This unique identifier should be used to e.g. fetch plugin-specific configurations from the context.

Returns

The name of the plugin directory, including the mundo_ prefix

property should_render: bool

If the plugin should be rendered by the application GUI. This property must be implemented and return True in every plugin that wants to be rendered.

Returns

True if plugin want to render a custom widget. False otherwise

render(
parent: QWidget,
expand: Callable,
collapse: Callable
) None

Render the plugins GUI inside the parent widget. This method will be called once in order for the plugin to setup the appropriate event handlers and render its widget.

In order for this method to be called, should_render must return True. This property is False by default and must be implemented in the plugin in order for the rendering to occur.

Parameters
  • parent – The parent Qt widget that the plugin should render in.

  • expand – Method used to expand the plugin’s dropdown

  • collapse – Method used to collapse the plugin’s dropdown

serialize() Optional[Dict[str, Any]]

Configuration parameters for the plugin that should be saved to the global configuration file. If no configuration parameters should be saved, this method should return either and empty dictionary, or None.

The data returned by this method will be automatically loaded and passed as a parameter to the plugin constructor the next time the application starts. Note that this method will be called automatically by the application at the appropriate time.

If you want to save configuration parameters for a specific session, see serialize_session(), or for platform configuration parameters serialize_platform().

Returns

A JSON-encodable dictionary with configuration parameters, or None if nothing should be saved

serialize_session() Optional[Dict[str, Any]]

Configuration parameters for the plugin that should be saved to a session configuration file. If no configuration parameters should be saved, this method should return either and empty dictionary, or None.

The data returned by this method will be automatically loaded and passed as a parameter to the ON_SESSION_LOAD event. Note that this method will be called automatically by the application at the appropriate time.

If you want to save configuration parameters globally, see serialize(), or for platform configuration parameters serialize_platform().

Returns

A JSON-encodable dictionary with configuration parameters, or None if nothing should be saved

serialize_platform() Optional[Dict[str, Any]]

Configuration parameters for the plugin that should be saved to a platform configuration file. If no configuration parameters should be saved, this method should return either and empty dictionary, or None.

The data returned by this method will be automatically loaded and passed as a parameter to the ON_PLATFORM_LOAD event. Note that this method will be called automatically by the application at the appropriate time.

If you want to save configuration parameters globally, see serialize(), or for session configuration parameters serialize_session().

Returns

A JSON-encodable dictionary with configuration parameters, or None if nothing should be saved

on_ready() None

Event called once the application is fully initialized and ready. Plugins should use this event before attempting to access the application context.

on_session_load() None

Event called after a session has been loaded, or closed. This event indicates that the session property has been updated, either to a new config, or to None.

on_platform_load() None

Event called after a platform has been loaded, or closed. This event indicates that the platform property has been updated, either to a new config, or to None.

on_system_save() None

Event called after the system config has been saved. This event indicates that the system property has been updated.

on_session_save() None

Event called after a session config has been saved. This event indicates that the session property has been updated.

on_platform_save() None

Event called after a platform config has been saved. This event indicates that the platform property has been updated.

on_connected() None

Event called after the motors has been connected.

on_disconnected() None

Event called after the motors has been disconnected.

on_start() None

Event called before starting an automation process.

on_stop() None

Event called after stopping an automation process.

on_reset() None

Event called after resetting an automation process.

on_destroy() None

Event called before the plugin is deactivated. Plugins should run their cleanup on this event, if needed. After this event has been executed, the current instance of the plugin will never be used, nor called again.

on_sample_selected(
sample: Optional[mundo.api.sample.Sample],
should_process: bool
) None

Event called when a sample is selected (clicked) by a user. This will be called everytime a sample is clicked, even if an already selected sample is selected again. If the user has not selected any sample, this event will be called with None as argument.

Parameters
  • sample – The selected sample, or None if no sample is selected

  • should_process – Extra parameter used to decide if an plugin or widget should process

the broadcast or not.

on_samples_updated(
samples: List[mundo.api.sample.Sample]
) None

Event called when samples has been updated.

Parameters

samples – A list of all samples that has been updated

on_mode_updated() None

Event called when the processing mode has been updated.

on_processing_sample_updated(
sample: Optional[mundo.api.sample.Sample]
) None

Event called when the processing sample has been updated.

Parameters

samples – The currently processing sample, if any

on_queue_updated() None

Event called when a queue has been updated.

Plugin manager

class mundo.api.plugins.PluginManager

Manager for plugins.

Loads and initializes available plugins, both internal and external. Allows for event broadcasting to initialized events using the broadcast() method.

__init__() None

Creates a new Plugin manager.

property activated_plugins: List[mundo.api.plugins.Plugin]

Returns a list of all activated plugins.

Returns

List of activated extension plugins

load_plugins(
context: mundo.api.context.ApplicationContext
) None

Loads and initializes all available Mundo plugins.

Parameters

context – The application context

reload_plugins(
context: mundo.api.context.ApplicationContext
) None

Reloads all activated plugins.

Parameters

context – The application context

broadcast(
event: mundo.api.events.Event,
*args
) None

Broadcasts an application event to all initialized plugins.

All application events are defined in the Event enumeration. These events allow activated plugins to hook into the application at certain points in time by running custom code.

For example, if the user wants to send a signal to an external measuring instrument before the sampling process is started, this can be done by implementing the on_start() method. This plugin method will be automatically called when the ON_START event is broadcasted.

Parameters
  • event – Event type to broadcast

  • *args – Event arguments, if any

serialize_all(
fn: Callable
) Dict[str, Any]

Serializes all active plugins by calling fn with each plugin. fn should call the serialize method that should be used.

Parameters

fn – The iterator callback. Must accept a plugin instance as parameter, and should return a dictionary of plugin configuration data, or None

Returns

A dictionary of serialized plugin data to save. If no data should be saved, an empty dictionary is returned