-
Notifications
You must be signed in to change notification settings - Fork 6
EFPlugins
SB 2023-07-24.
Enlighten had an existing plugin system before I (SB) got here. I introduced Enlighten Functional plugins to simplify existing code and extend functionality. They were initially designed to as a backwards compatible extension of the existing system.
EFPlugins is a continuation of Enlighten Functional plugins that drops backwards compatibility in an effort to extend simplifications beyond plugins themselves and into the underlying software. As such the minimum release of Enlighten that may include this is 5.0.0. EFPlugins will continue to extend functionality with dynamic widgets and added messaging features.
EFPlugins will be implemented in one file.
(No Longer.)EFPlugins.py
which can be imported by plugins.
EFPluginLoader.py
which is a FeatureObject that manages loading plugins and plugin selection UI.
EFPlugins will do away with the "Connected" vs "Enabled" duality. The Enabled checkbox will take the place of the existing Connected one. When a plugin is enabled or disabled on_enable
and on_disable
events are called accordingly.
EFPlugins will not feature the Process button. This is characteristic of a change in when plugins are allowed to execute code. EFPlugins only execute code when they are enabled. That includes widget generation, which should be done on_enable
.
The process_request
function served two purposes. It could be continuously invoked or be the callback to the 'Process' button. process_request
will also be removed, it's replacement depends on the use-case. Most EFPlugins that need to run continuously should implement the on_spectra
function, which gets called once per frame when a plugin is enabled and a spectrometer is connected. Some cases may call for the on_update
event, which is called once per frame with or without a spectrometer. This is useful for plugins that have dynamic widgets or specify x-values using horizontal lines. Finally, plugins that define discrete, one-off actions such as writing files should do so using buttons and callbacks.
-
on_spectra
takes four or fewer parameters:spectrum
,x_axis
,ctx
, andui
-
spectrum
is anp.array
consisting of tuples(pixel, wavelength, wavenumber, counts)
it is always defined -
x_axis
is anint
in range [0,2] that corresponds to the currently visible x-axis: 0 - pixel, 1 - wavelength, 2 - wavenumber -
ctx
is the instance ofController
. This is usually calledctl
in Enlighten, but it is a context variable not a controller. For now the rename only needs to occur within EFPlugins. -
ui
points to the UI Frame that contains the plugin's buttons, labels, and text inputs.
-
-
on_update
takes the same parameters ason_spectra
, exceptspectrum
may beNone
if no spec is connected. -
on_enable
andon_disable
have the same parameters ason_update
. -
callbacks are connected using the underlying UI library, they will usually pass a parameter indicating which user interface element created the message.
My whiteboard session leading up to this document is here.
Paste of Apple's Garageband Plugin system. Like ours it is organized into groups. It shows a design for having multiple plugins (up to four) in a signal post processing stack.

One obvious complication is the second_graph
capability. This diagram shows the soup of complexity that can arise from combining stackable plugins with optional additional graphs.
I think it would be better to let the user choose how many graphs there are instead of the plugin. Then each graph can have 0 or more plugins applied to it. Plugins used by a graph should be listed in the legend.
2023-10-16
The "User Concept" is a technique for building an API where you write the program as if the API already exists. In this case I am writing this several months since last looking at my design for EFPlugins.
# - we don't need an import because Enlighten functions are passed into `ctl`
# - the program body is populated with several easy to remember event functions
# This design of plugin can be easily stacked with other plugins
# the event functions would be called in series, defined by the plugin order
# and the `ctl.plugin` variable would be context-bound to the currently executing
# plugin
# we trust our plugin programmers can use existing libraries (or copy paste our examples)
import pyqt5
"""
This design makes it easier to have multiple plugins running at the same time,
though that is not a requirement or purpose. Multiple plugins may look like this:
Outside of this, instead of Connected and Enabled like before, plugins can be Added or Removed.
There is a sequence active plugins, and they act in series.
+----------------+
| Active Plugins |
+----------------+
* Duplicate Graph
* Scale [x2]
[ Add ] [ Clear ]
This would make two graphs, one scaled and one original.
Here it first duplicates the graph, then applies the Y-axis scale
factor of two to one of them. If the plugin order were swapped, then
both graphs would be scaled, because first the scaling is applied then
a duplicate is made.
Creating combined plugins can be done by loading two or more
other plugins in a predetermined order.
"""
def on_gui(ctl):
"""
Called once, when plugin is added, to configure GUI
"""
# plugins use the same unmodified GUI API that we do
cb = pyqt5.CheckBox()
cb.name = "dup"
# `util.add_label` extends Qt gui. It takes in a widget and a string
# and it returns a HorizontalLayout containing a label and the original widget.
# (This replaces the functionality added by PluginFieldWidgets, without additional state)
cb_with_label = ctl.util.add_label(cb, "Show Duplicate Graph")
ctl.plugin.addWidget(cb_with_label)
def on_get_spectra(ctl, spectra, wavelengths):
"""
called once per spectral acquisition
"""
# ctl parameter contains many live instances from Enlighten
# spectra contains an array or np.array containing peak height information (pixels)
# wavelengths is an array of the same size as spectra containing x-axis information
wavenumbers = ctl.util.wavelengths_to_wavenumbers(wavelengths)
# we can replace spectra with a function call
ctl.set_spectra_pixels(spectra) # No-OP
# we can also produce the secondary graph with a function call
if ctl.plugin.get_widget_by_name("dup").checked:
ctl.set_secondary_spectra_pixels(spectra) # duplicate
# set_secondary_spectra_pixels must be called once per spectral acquisition for the graph to show
# otherwise the graph will hide.
# This means that as soon as the checkbox is unchecked, the graph will disappear, without having to
# to be explicitly destroyed.
# Events include:
# - on_gui
# - on_gui_changed
# - on_get_spectra
# - on_update
For this demo combined plugin, we imagine two fictional plugins. One that shows the duplicate graph and one that scales the spectra on the Y-Axis. The combined plugin shows both an original and scaled graphs.
def on_gui(ctl):
# first define the contents
ctl.plugins.load("DuplicateGraph")
ctl.plugins.load("ScaleY")
# then define a front-end interface
sf = pyqt5.NumericInput()
sf.name = "scale_factor"
sf_with_label = ctl.util.add_label(cb, "Scale Factor")
ctl.plugin.addWidget(sf_with_label)
def on_get_spectra(ctl):
# only the widgets added by this combined plugin will be visible to the user
# but widgets added by the child plugins are still accessible by the combined plugin's API
# in this case we will use that to only show the dup graph when the scale factor is not equal to 1.
showDup = ctl.plugin.get_widget("scale_factor").value != 1
# combined plugins can interact with the invisible GUI of the plugins they are combining:
ctl.plugin.get_widget("dup").checked = showDup
def on_gui(ctl):
if len(ctl.plugins.all_active) > 1:
if msgboxYesNo("This plugin must be run by itself. Okay to stop other plugins?"):
ctl.plugins.stop_others()
Under Enlighten settings, we may have a "persist plugins after restart" option, when enabled the active plugin list is saved and loaded through configuration.
The complications mentioned earlier were inadvertently solved by the user concept.
Any configuration of plugins being applied to primary and secondary graphs can be applied using a few basic building blocks.
- Plugins that modify the graph modify the main graph
- The second graph can be presented by duplicating the main graph
- Plugins may override the main graph
- Unmodified spectra is available anywhere in the post-processing stack
To create an arbitrary configuration in runtime, you would first apply all plugins you want on the secondary graph, then you apply a "duplicate graph" plugin, then apply a "reset graph" plugin, then apply all the plugins you want on the primary graph.
In the screenshot it shows a sequence of [A][Duplicate][B][C][Reset]D. Which results in A, B, and C applied to the primary graph and only A applied to the secondary graph. One step further shows D only applied to the primary graph and A applied to the secondary graph. The screenshot also shows that there is a hardcoded limit of two graphs--although that is not strictly necessary, since a single sequence of plugins can be distributed among any amount of graphs using Duplicate and Reset.
That being said, the solution I suggested above, where the user decides how many graphs there are and which plugins are applied explicitly, is much better UX--although it sounds harder to engineer. That's a judgement call.
Written on 2023-10-16. Planned for unspecified.
- Create a branch sbee-efplugins
- Create a series of dummy plugins that are easy to test in EFPlugin format, under
pluginExamples/EFPlugins
- Write out the API, meeting both user concept and design in the middle, successfully test on all EFPlugins
- Rewrite all existing plugins to use EFPlugin format
- Remove
pluginExamples/EFPlugins
from the source tree (possibly keeping any that are still useful)