====== 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 [[blocks:drivers:|device drivers]]. You must have sufficient programming experience to feel comfortable with all the concepts described below. Scripts are written using the [[https://www.typescriptlang.org|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 [[https://nodejs.org/en/|node.js]]. In particular, it shares the following properties:
* All code //must// be written in a [[https://www.codeschool.com/blog/2014/10/30/understanding-node-js/|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 [[https://scotch.io/tutorials/understanding-javascript-promises-pt-i-background-basics|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 [[blocks:drivers:tools|here]].
* An up-to-date set of files describing the scripting environment. These are available from PIXILAB's [[https://github.com/pixilab/blocks-script|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 [[https://www.typescriptlang.org/docs/handbook/classes.html|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 [[https://www.typescriptlang.org/docs/handbook/decorators.html|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 [[https://www.typescriptlang.org/docs/handbook/classes.html#accessors|setter/getter accessor]] managing the property value. The decorator takes two optional parameters:
- **description?: string** is a brief, textual description of the property, shown in the user interface when selecting the property.
- **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//
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:
- The first parameter, if specified, limits who can call the resource from the outside, and accepts the same values as the @roleRequired annotation.
- 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:server_configuration_file#top_level_apikeys_item|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("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 [[blocks:drivers:troubleshooting|here]].