DEV Community

Dmitry
Dmitry

Posted on • Originally published at ratner.io

WitEngine: Building Modular Controllers for Script Automation

Automation is at the heart of modern software and hardware systems. Whether you’re managing complex hardware interactions or streamlining repetitive tasks, having a flexible and modular approach to scripting can save both time and effort. That’s where WitEngine comes in.

I created WitEngine to address the challenges I faced in projects that required seamless control of multiple devices and systems. The ability to quickly modify scripts and add new functionalities without having to overhaul the entire setup is a game-changer, especially in environments where time and precision are critical.

For a deeper dive into the features and capabilities of WitEngine, check out the WitEngine project page. Here, I’ll guide you through getting started with WitEngine, including how to create a controller, run processes, and automate tasks.

Table of Contents

  1. What is WitEngine?
  2. Getting Started with WitEngine
  3. The Script Structure
  4. Creating Controllers in WitEngine
  5. Defining the Controller Module
  6. Conclusion

What is WitEngine?

At its core, WitEngine is a modular API designed to help you build flexible interpreters for simple scripts. It allows you to manage complex tasks by breaking them down into independent modules called controllers. These controllers define specific variables, functions, and processes, which the main interpreter (or host) loads from a designated folder. This modular approach makes WitEngine highly extensible, as you can easily add new controllers without modifying the entire system.

For example, in a hardware setup like a photobox, you could have one controller for handling the rotation of a table, another for changing background colors, and yet another for taking snapshots with cameras. Each of these controllers can be developed and tested independently, then combined to execute a seamless process.

What makes WitEngine powerful is that it allows you to automate interactions between hardware devices or systems, while keeping the logic clean and easy to maintain. Scripts can freely reference variables and functions from multiple controllers, giving you the flexibility to define workflows as needed.

Getting Started with WitEngine

Getting started with WitEngine in your project is straightforward. (You can find the examples and a pre-configured demo project here: GitHub repository).

Here’s how to begin:

1) Set the Controllers Folder

WitEngine needs a folder where the controllers will be located. By default, the system will look for controllers in a folder named @Controllers, located in the same directory as the running .exe file. However, you can specify any other path if needed.

2) Reload Controllers

To load controllers from the designated folder, call the following function:

WitEngine.Instance.Reload(null, null, null);
Enter fullscreen mode Exit fullscreen mode

The Reload function takes three parameters:

  • Local resource access interface – for now, set it to null.
  • Logger interface – this can also be null.
  • Path to the controllers foldernull will use the default path. After calling this function, the controllers located in the selected folder will be loaded into the system.

3) Add Event Handlers

Next, you’ll need to add handlers to respond to different stages of WitEngine’s processing:

WitEngine.Instance.ProcessingProgressChanged += OnProcessingProgressChanged;
WitEngine.Instance.ProcessingCompleted += OnProcessingCompleted;
WitEngine.Instance.ProcessingPaused += OnProcessingPaused;
WitEngine.Instance.ProcessingReturnValue += OnProcessingReturnValue;
WitEngine.Instance.ProcessingStarted += OnProcessingStarted;
Enter fullscreen mode Exit fullscreen mode

4) Running Your First Script

Now, you’re ready to run your first script. Here’s how:

Load the Script
Load the script text into a variable, for example:

string jobString = File.ReadAllText(@"C:\Jobs\testJob.job");
Enter fullscreen mode Exit fullscreen mode

Deserialize the Script
Parse and compile the script by calling the Deserialize() extension function:

WitJob job = jobString.Deserialize();
Enter fullscreen mode Exit fullscreen mode

If there’s a compilation error, an exception will be thrown, indicating which specific block failed to parse.
Execute the Script
Run the script by passing the job object to the ProcessAsync() function:

WitEngine.Instance.ProcessAsync(job);
Enter fullscreen mode Exit fullscreen mode

The Script Structure

Here’s an example of a minimal WitEngine script:

~This is a minimal test job~
Job:TestJob()
{
    Int:value= 2;
    Return(value);
}
Enter fullscreen mode Exit fullscreen mode

Let’s walk through the lines of the script:

Comments
The first line (~This is a minimal test job~) is a comment, which is ignored by the parser. Comments can be placed anywhere in the script, and they behave as expected from typical comments in other languages.

Top-level Function Definition
The second line defines the top-level function, which serves as the entry point:

  • Job: A keyword indicating the type (top-level function).
  • :: Separates the type from the name.
  • TestJob: The name of the function (can be any name).
  • (): This script doesn’t accept parameters, but you can pass parameters when calling ProcessAsync() if needed:
public void ProcessAsync(WitJob job, params object[] parameters)
Enter fullscreen mode Exit fullscreen mode

Block Definition
The {} braces define the block of logic.

Variable Definition and Assignment
The line Int:value= 2; defines a variable and assigns it a value:

  • Int: Variable type.
  • :: Separator between the type and the name.
  • value: Variable name.
  • = 2: Assigns the value 2.
  • ;: Marks the end of the statement.

Return Statement
The Return(value); statement sends the value outside the script:

  • Return: Sends a value via the ProcessingReturnValuecallback. Unlike typical returnstatements (e.g., in C#), this does not terminate the script’s execution. It can be called multiple times during script execution, and each result can be caught via the ProcessingReturnValuecallback.
  • (): Encapsulates the values being returned. In this case, only valueis returned, but multiple values can be passed, separated by commas.
  • ;: Ends the statement.

End of Script
The closing brace } indicates the end of the logic block and the script.
When this script runs, the ProcessingReturnValue callback will receive the value 2 as an integer:

private void OnProcessingReturnValue(object[] value)
{
    // Handle the returned value
}
Enter fullscreen mode Exit fullscreen mode

Creating Controllers in WitEngine

Each controller in **WitEngine **can define variables (Variables), functions (Activities), the logic for parsing and serializing these elements, as well as the core business logic that will be accessible via the defined variables and functions.

Defining Variables

Let’s assume we have a class that describes a color using three components:

public class WitColor : ModelBase
{
    #region Constructors
    public WitColor(int red, int green, int blue)
    {
        Red = red;
        Green = green;
        Blue = blue;
    }
    #endregion

    #region Functions
    public override string ToString()
    {
        return $"{Red}, {Green}, {Blue}";
    }
    #endregion

    #region ModelBase
    public override bool Is(ModelBase modelBase, double tolerance = DEFAULT_TOLERANCE)
    {
        if (!(modelBase is WitColor color))
            return false;

        return Red.Is(color.Red) &&
               Green.Is(color.Green) &&
               Blue.Is(color.Blue);
    }

    public override ModelBase Clone()
    {
        return new WitColor(Red, Green, Blue);
    }
    #endregion

    #region Properties
    public int Red { get; }
    public int Green { get; }
    public int Blue { get; }
    #endregion
}
Enter fullscreen mode Exit fullscreen mode

Now, to use such objects in a WitEngine script, we need to define a corresponding variable. Here’s how to add that variable definition:

[Variable("Color")]
public class WitVariableColor : WitVariable<WitColor>
{
    public WitVariableColor(string name) : base(name) { }

    #region Functions
    public override string ToString()
    {
        var value = Value?.ToString() ?? "NULL";
        return $"{Name} = {value}";
    }
    #endregion
}
Enter fullscreen mode Exit fullscreen mode

Two important points to note:

  1. The variable class implements the abstract generic class WitVariable, with the object type (WitColor) as the parameter.
  2. The class is marked with the Variable attribute, where the parameter specifies the word to represent the variable in the script. In this case, it’s Color.

Creating a Variable Adapter

Next, we need to create an adapter that helps in parsing the script and interacting with the variable:

public class WitVariableAdapterColor : WitVariableAdapter<WitVariableColor>
{
    public WitVariableAdapterColor() : base(ServiceLocator.Get.ControllerManager) { }

    protected override WitVariableColor DeserializeVariable(string name, string valueStr, IWitJob job)
    {
        if (string.IsNullOrEmpty(valueStr) || valueStr == "NULL")
            return new WitVariableColor(name);

        Manager.Deserialize($"{name}={valueStr};", job);
        return new WitVariableColor(name);
    }

    protected override string SerializeVariableValue(WitVariableColor variable)
    {
        return "NULL";
    }

    protected override WitVariableColor Clone(WitVariableColor variable)
    {
        return new WitVariableColor(variable.Name)
        {
            Value = variable.Value == null
                ? null
                : new WitColor(variable.Value.Red, variable.Value.Green, variable.Value.Blue)
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

This class implements the abstract generic class WitVariableAdapter, with our defined variable type as the parameter.

The key functions here are:

  • DeserializeVariable: Parses the script to create a variable.
  • SerializeVariableValue: Handles serialization of the variable.

DeserializeVariable

Let’s break down the DeserializeVariablefunction:

protected override WitVariableColor DeserializeVariable(string name, string valueStr, IWitJob job)
{
    if (string.IsNullOrEmpty(valueStr) || valueStr == "NULL")
        return new WitVariableColor(name);

    Manager.Deserialize($"{name}={valueStr};", job);
    return new WitVariableColor(name);
}
Enter fullscreen mode Exit fullscreen mode
  • name: The name of the variable.
  • valueStr: The expression after the “=” symbol that leads to the creation of this variable.
  • job: The current script’s compilation tree.

In simpler cases, such as an int, valueStrwould contain the direct value of the variable. For example:

Int:val=2;
Enter fullscreen mode Exit fullscreen mode

In this case, the name parameter would receive “val”, and valueStr would be “2”. But more complex cases, such as function calls, require deeper parsing:

Int:val=DoSomeAction();
Enter fullscreen mode Exit fullscreen mode

This deeper parsing is done by:

Manager.Deserialize($"{name}={valueStr};", job);
Enter fullscreen mode Exit fullscreen mode

SerializeVariableValue

Unlike DeserializeVariable, this function handles only final values, meaning it provides default values like NULLfor complex objects such as WitColor.

Now, let’s register the adapter. In your controller module, you’ll inject IWitControllerManagerand register the adapter:

private void RegisterAdapters(IWitControllerManager controllers)
{
    controllers.RegisterVariable(new WitVariableAdapterColor());
}
Enter fullscreen mode Exit fullscreen mode

Defining Functions

Now that we have the variable, we need a function to create it. We’ll define a simple “constructor”-like function to create a WitColor object in the script.

First, declare the function definition:

[Activity("WitColor")]
public class WitActivityColor : WitActivity
{
    public WitActivityColor(string color, int red, int green, int blue)
    {
        Color = color;
        Red = red;
        Green = green;
        Blue = blue;
    }

    public string Color { get; }
    public int Red { get; }
    public int Green { get; }
    public int Blue { get; }
}
Enter fullscreen mode Exit fullscreen mode

This class implements WitActivity and stores everything necessary for the function to operate—in this case, the variable name (Color) and the three components of color (Red, Green, Blue).

Like with variables, the function name in the script is defined by the attribute:

[Activity("WitColor")]
Enter fullscreen mode Exit fullscreen mode

Thus, a WitColor object can be created in the script like this:

Color:val = WitColor(1, 2, 3);
Enter fullscreen mode Exit fullscreen mode

Creating a Function Adapter

Next, we need an adapter to guide WitEngine on how to process this function:

public class WitActivityAdapterColor : WitActivityAdapterReturn<WitActivityColor>
{
    public WitActivityAdapterColor() :
        base(ServiceLocator.Get.ProcessingManager, ServiceLocator.Get.Logger, ServiceLocator.Get.Resources) { }

    protected override void ProcessInner(WitActivityColor action, WitVariableCollection pool, ref string message)
    {
        pool[action.Color].Value = new WitColor(action.Red, action.Green, action.Blue);
    }

    protected override string[] SerializeParameters(WitActivityColor activity)
    {
        return new[]
        {
            $"{activity.Color}",
            $"{activity.Red}",
            $"{activity.Green}",
            $"{activity.Blue}"
        };
    }

    protected override WitActivityColor DeserializeParameters(string[] parameters)
    {
        if (parameters.Length == 4)
            return new WitActivityColor(parameters[0], int.Parse(parameters[1]), int.Parse(parameters[2]), int.Parse(parameters[3]));

        throw new ExceptionOf<WitActivityColor>(Resources.IncorrectInput);
    }

    protected override WitActivityColor Clone(WitActivityColor activity)
    {
        return new WitActivityColor(activity.Color, activity.Red, activity.Green, activity.Blue);
    }

    protected override string Description => Resources["ColorDescription"];
    protected override string ErrorMessage => Resources["ColorErrorMessage"];
}
Enter fullscreen mode Exit fullscreen mode

Key Functions

  • DeserializeParameters: Parses the function parameters from the script.
protected override WitActivityColor DeserializeParameters(string[] parameters)
{
    if (parameters.Length == 4)
        return new WitActivityColor(parameters[0], int.Parse(parameters[1]), int.Parse(parameters[2]), int.Parse(parameters[3]));

    throw new ExceptionOf<WitActivityColor>(Resources.IncorrectInput);
}
Enter fullscreen mode Exit fullscreen mode
  • ProcessInner: Contains the main logic, creating a new WitColor object and assigning it to the pool of variables:
protected override void ProcessInner(WitActivityColor action, WitVariableCollection pool, ref string message)
{
    pool[action.Color].Value = new WitColor(action.Red, action.Green, action.Blue);
}
Enter fullscreen mode Exit fullscreen mode

Finally, register the activity adapter:

private void RegisterAdapters(IWitControllerManager controllers)
{
    controllers.RegisterVariable(new WitVariableAdapterColor());
    controllers.RegisterActivity(new WitActivityAdapterColor());
}
Enter fullscreen mode Exit fullscreen mode

Now, you can run a script like this:

~Test Color Variable Job~
Job:ColorJob()
{
    Color:color = WitColor(1, 2, 3);
    Return(color);
}
Enter fullscreen mode Exit fullscreen mode

Defining the Controller Module

To enable WitEngine to load a controller module, you need to define a class that connects the host with the module:

[Export(typeof(IWitController))]
public class WitControllerVariablesModule : IWitController
{
    public void Initialize(IServiceContainer container)
    {
        InitServices(container);
        RegisterAdapters(ServiceLocator.Get.ControllerManager);
    }

    private void InitServices(IServiceContainer container)
    {
        container.Resolve<IResourcesManager>()
            .AddResourceDictionary(new ResourcesBase<Resources>(Assembly.GetExecutingAssembly()));

        ServiceLocator.Get.Register(container.Resolve<ILogger>());
        ServiceLocator.Get.Register(container.Resolve<IWitResources>());
        ServiceLocator.Get.Register(container.Resolve<IWitControllerManager>());
        ServiceLocator.Get.Register(container.Resolve<IWitProcessingManager>());
    }

    private void RegisterAdapters(IWitControllerManager controllers)
    {
        controllers.RegisterVariable(new WitVariableAdapterColor());
        controllers.RegisterActivity(new WitActivityAdapterColor());
    }
}
Enter fullscreen mode Exit fullscreen mode

This class implements the IWitController interface, and it’s crucial to annotate the class with the [Export] attribute. Without this attribute, WitEngine won’t recognize or load the controller.

The key function in this class is Initialize:

public void Initialize(IServiceContainer container)
{
    InitServices(container);
    RegisterAdapters(ServiceLocator.Get.ControllerManager);
}
Enter fullscreen mode Exit fullscreen mode

The Initialize function takes a service container as its parameter, which the host passes to the modules. One important service it provides is the IWitControllerManager, which is responsible for registering adapters (for variables and activities) within WitEngine.

Conclusion

I’ve created a comprehensive demo project available on GitHub to help you explore the power and flexibility of WitEngine. The project includes the core WitEngine framework, along with two essential controller modules. The first module contains adapters for basic variable types: int, double, string, and WitColor (as discussed above). The second module offers adapters for core operations such as loops, parallel actions, delayed actions, and the Return function (as we saw earlier), along with several others.

In addition to these modules, the project features a GUI designed to test the capabilities of WitEngine and to help you experiment with various scenarios. The GUI gives you a hands-on way to see how your scripts and controllers interact with the system in real time.

Here’s a screenshot of the GUI in action:

Image description

Feel free to explore the demo, test the controllers, and build your own controllers and scripts using WitEngine. I’ll be discussing other capabilities and use cases in future posts, so stay tuned for more detailed tutorials and examples.

You can find the full project and codebase here on GitHub.


Visit my website | Check out my GitHub

Top comments (0)