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.

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);
				
			

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;
				
			

4) Running Your First Script

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

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

				
					string jobString = File.ReadAllText(@"C:\Jobs\testJob.job");
				
			
  1. Deserialize the Script Parse and compile the script by calling the Deserialize() extension function:
				
					WitJob job = jobString.Deserialize();
				
			
If there’s a compilation error, an exception will be thrown, indicating which specific block failed to parse.
  1. Execute the Script Run the script by passing the job object to the ProcessAsync() function:
				
					WitEngine.Instance.ProcessAsync(job);
				
			

This function won’t block the thread. In a real environment with actual hardware, the script might run for several minutes or even longer. You can track the script’s execution through the connected callbacks.

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);
}
				
			

Let’s walk through the lines of the script:

  1. 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.

  2. 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)

				
			
  1. Block Definition The {} braces define the block of logic.
  2. 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.
  3. Return Statement The Return(value); statement sends the value outside the script:
    • Return: Sends a value via the ProcessingReturnValue callback. Unlike typical return statements (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 ProcessingReturnValue callback.
    • (): Encapsulates the values being returned. In this case, only value is returned, but multiple values can be passed, separated by commas.
    • ;: Ends the statement.
  4. 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
}

				
			

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
}

				
			

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
}

				
			

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.

While the class doesn’t need much else, defining a proper ToString implementation helps with debugging, as this text will appear in any exception message during script compilation or execution.

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)
        };
    }
}

				
			

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 DeserializeVariable function:

				
					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);
}

				
			
  • 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, valueStr would contain the direct value of the variable. For example:

				
					Int:val=2;

				
			

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();
				
			

This deeper parsing is done by:

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

				
			

SerializeVariableValue

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

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

				
					private void RegisterAdapters(IWitControllerManager controllers)
{
    controllers.RegisterVariable(new WitVariableAdapterColor());
}

				
			

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; }
}

				
			

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")]

				
			

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

				
					Color:val = WitColor(1, 2, 3);
				
			

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"];
}

				
			

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);
}

				
			
  • 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);
}

				
			

Finally, register the activity adapter:

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

				
			

Now, you can run a script like this:

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

				
			

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());
    }
}

				
			

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);
}

				
			

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:

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.

Leave a Reply

Your email address will not be published. Required fields are marked *