Advanced Scripting

While binding buttons and other controls directly to properties provides basic functionality for many control needs, more complex actions call for more powerful solutions. Some examples are

  • Multi-step actions.
  • Actions that need to be spread out in time.
  • Parameters that need to be calculated or based on some external input or property, such as the state of some device, time of day or day of week.
  • To make an action, or parts of it, conditional on such an external property.

In such cases, Blocks' Task feature often fits the bill, as it can do all the above things, and more.

However, for very complex scenarios, where you need to deal with numerous variables, create entirely new classes of objects, or for some reason prefer to write your logic in a more structured or object oriented way, the advanced scripting capabilities of Blocks can come in handy.

Things You Must Know

Advanced scripts share most concepts and requirements with device drivers. You must have sufficient programming experience to feel comfortable with all the concepts described below. Scripts are written using the TypeScript programming language, which can be considered a superset of JavaScript. Thus, if you have experience with modern, class-based JavaScript, you should feel right at home.

While the execution environment is specific to Blocks, it has some similarities with node.js. In particular, it shares the following properties:

  • All code must be written in a non-blocking way. In particular, this means you can't use any long-running loops or similar code constructs.
  • Potentially time-consuming operations typically return a promise, to be resolved or rejected once the operation finishes, thereby allowing subsequent operations to proceed.

Things You Must Have

  • Development tools and code, as described here.
  • An up-to-date set of files describing the scripting environment. These are available from PIXILAB's github page.
  • A Mac, Window or Linux computer you can use for development purposes, with its own Blocks license.

:!: This computer should not be running a mission-critical Blocks installation, as your development process may interfere with the normal operation of the server.

Install the files downloaded from Github above into your development Blocks server by copying all relevant files to the PIXILAB-Blocks-root/script folder under your home directory. When installing the files, take care if copying into an existing PIXILAB-Blocks-root/script folder, as some operating systems will replace entire folders with new ones, rather than merging only the new files. You may want to copy files into folders one by one, rather than replacing your entire script folder with the downloaded one.

Important

It will take you some time getting familiar with Blocks scripting. Thus, you must be prepared to spend some time to gain proficiency.

:!: With great power comes great responsibility! The scripting layer of Blocks provides a great deal of power and flexibility, while also offering plenty of "rope to hang yourself" (or "shoot yourself in the foot" – pick your metaphor). An incorrectly written, or poorly tested, script can seriously interfere with Blocks operation, or even make your Blocks system lock up completely.

Anatomy of a Script

This section provides a brief overview of what's inside a script. You may want to install the required tools, download the code and open the user/ClassyScript.ts sample to follow along.

Script Class

A user script must have an exported TypeScript class, derived from the Script base-class (found in system_lib/Script.ts). Here's an example of what this class declaration may look like:

export class ClassyScript extends Script {

This class (here named ClassyScript) must be stored in a file named ClassyScript.ts, which compiles to a file named ClassyScript.js. Both these files must be located in the PIXILAB-Blocks-root/script/user folder, under your home directory.

:!: While only the .js file is required to use the script, you typically keep the .ts "source code" file in the same directory to simplify future changes.

Constructor Function

A new instance of the script will be created automatically when Blocks starts the script. This is done by calling the constructor function, passing it an object of the ScriptEnv class. Note that you must pass the ScriptEnv parameter to the Script base class using the super(env) call in the constructor.

public constructor(env : ScriptEnv) {
	super(env);

Decorators

TypeScript decorators (aka "annotations") are used to mark and embellish certain features of the script. Those decorators are imported from the system_lib/Metadata module:

import {callable, parameter, property} from "system_lib/Metadata";

@property

This decorator exposes a property that can be accessed by panel items and tasks, just like properties of Spots and other built-in Blocks objects. This decorator is applied to the setter/getter accessor managing the property value. The decorator takes two optional parameters:

  1. description?: string is a brief, textual description of the property, shown in the user interface when selecting the property.
  2. readOnly?: boolean set to true to mark the property as read-only.

An alternative to marking a property as read-only using the decorator is to only provide a getter. Use the decorator readOnly parameter if you want to make the property read-only from the outside, while still providing a setter for internal use.

You should generally provide a setter for updating the property values, and then always set the value by assigning through the setter. Always going through the setter ensures that clients will be notified of the change. An alternative is to manually notify clients by calling this.changed, as shown halfway down in the example script.

:!: The getter may be called frequently. Avoid doing any heavy computation on each call. Preferably, you should just return the value of a variable here. If you need to do extensive computations to obtain the value, then cache it in a variable for use by subsequent calls.

@min and @max

Use these decorators on numeric values to specify the allowable range. This allows sliders and other controls to be scaled accordingly. They both take a number, specifying the desired minimum and maximum value of the property. If defined, values set (through the corresponding setter) will automatically be clipped to this range.

@callable

Marks a function as accessible from Tasks. The optional parameter provides a textual description of the function.

@parameter

An optional decorator that can be applied to parameters to callable functions, providing a textual description of the parameter.

@roleRequired

Optional decorator for the user script's class, specifying the minimum role required to set properties exposed by the script. Its parameter must be one of the following: "Admin", "Manager", "Creator", "Editor", "Contributor", "Staff" or "Spot". If not applied, the script's properties can be set regardless of role.

@resource

Apply this decorator to a public function, making it accessible from a web client under

/rest/script/invoke/<user-script-name>/<method-name>

using a HTTP request with an optional JSON body payload, which (if specified) is deserialized and passed to the method as an Object. An object or string returned from the method will be serialized as JSON data and returned to the web client. May return a promise eventually resolving with the result value. The decorator accepts the following (optional) parameters:

  1. The first parameter, if specified, limits who can call the resource from the outside, and accepts the same values as the @roleRequired annotation.
  2. The second parameter lets you specify the HTTP verb used for calling the resource, accepting the values 'GET' or 'POST', with 'POST' being the default verb if not specified.

:!: NOTE: See script/system_lib/Metadata.ts in your server's Blocks root for more details on the resource decorator.

@apiKey

This decorator can be used alongside the @resource decorator to protect custom API end-points using an API key specified in Blocks' configuration file.

Programmatic Properties

In some cases, it may be desirable to define properties programmatically at runtime. This allows properties to be created and named as the script is started, according to information that may not have been available before (e.g., when the script was written). You typically define such properties in the constructor function, as you see in the example script.

this.property<boolean>("dynProp1", {type: Boolean}, (sv) => {
    if (sv !== undefined) {
        if (this.mDynPropValue !== sv) {
            this.mDynPropValue = sv;
            console.log("dynProp1", sv);
        }
    }
    return this.mDynPropValue;
});

Unlike statically declared properties, programmatic ones use a single function acting as both the setter and getter. When called with a value (i.e., the value is not undefined), it acts as a setter. It always returns the current value of the property.

:!: When changing the value of a programmatic property, you must notify the system of such changes manually, by calling the changed function on the script/driver, passing it the name of the changed property. This is generally not necessary for declarative properties (i.e., those defined using @property) when assigning to the property (i.e., through its setter), as doing so automatically triggers a change notification.

Services Available to Scripts

Scripts can use the system-defined classes found in script/system. Those provide direct access to objects defined in Block, such as Spots, basic Network I/O, DMX lighting, web services and WATCHOUT.

Using Libraries

Each script will appear as a single entity in Blocks. While you may add any number of properties or functions to a script, it is sometimes preferable to split unrelated functionality into separate scripts to make your system more modular. Here, you may want to make some utility classes and functions available to several scripts, rather than duplicating that code in each script.

To facilitate this, create a separate typescript file in the script/lib directory, containing the shared functionality. Then import that library module into each script where you want access to its features.

In a similar way, the script/system_lib directory contains shared code, such as the Script base class required by all scripts.

:!: Do not add custom scripts to the script/system_lib directory. it is reserved for PIXILAB code only.

Hot Swapping of Scripts

While developing scripts, you frequently make changes and test the functionality in an iterative process. By default, scripts are only loaded when the server is started. Thus, to test any changes, you'd need to restart the server after every change.

As an alternative, you can enable automatic script re-loading by adding the following two lines to a file named config.yml located in your PIXILAB-Blocks-root:

scripting:
  watchFiles: true

For more information about this file, please see appendix A in the Blocks manual.

Debugging Scripts

Use console.log statements to display the value of variables and various events throughout your script. Data output using console.log, console.warn and console.error appears in the logs/latest.log file. Any errors encountered while starting or running your script will also appear in this log file. Use the tail terminal command to keep an eye on this log file while testing scripts. Learn more about logging here.