disguise developers

Designer Plugins

Plugins for Disguise Designer software.

Python Execution API

Overview

This is a set of API endpoints allowing Python 2.7 scripts to be executed in a Designer session remotely. This new functionality allows programmers to control and automate Designer using Python and the d3 module, opening up a wide range of possibilities for show control, automation and integration with other systems.

The API consists of two endpoints:

The execute endpoint is the primary method for running code in Designer however when more complex scripts are required, the registermodule endpoint can be used to preload modules into the environment that can be referenced in the execute endpoint.

This API has not been optimised for performance-related tasks such as executing on every frame or providing telemetry metrics, users are encouraged to use other tools such as the Live Update API for these tasks.

For the avoidance of doubt, this endpoint MUST NOT be polled, and is for functional changes such as object creation.

Architecture

Plugins python request

The above diagram shows a high level overview of the Python execution architecture.

When a request is made to the execute endpoint, the stringified Python 2.7 code is unpacked into a internal runner function. The runner function is then executed with the Designer embedded python in a specific module namespace if a moduleName is provided, or in a default namespace if no module name is provided. More information on these namespaces can be found in the following sections.

The return value of this runner function is then converted to JSON and returned in the response body along with any logging information.

The wrapping of the user code into a runner function means that all code is executed in a function scope, this means that global variables cannot be created in the execute endpoint. This also allows for some additional guardrails to be put in place to prevent stability issues such as:

However some statements are not valid in the context of a function body, such as from [package] import * and users should avoid using these statements in their code.

Module namespaces

The registermodule endpoint allows users to register a Python module that can be used in subsequent calls to the execute endpoint. This is useful for more complex scripts that require multiple functions or classes to be defined but also acts as a namespace to contain plugins from each other.

These registered modules are managed by Designer and when used in the execute endpoint are used as the execution namespace, meaning all variables, methods and classes defined in the module are immediately accessible without needing an import statement.

Default namespace

When no moduleName is provided in the execute endpoint, the code is executed in the default plugin namespace. This namespace can’t be added to but has some additional utility functions to help with common tasks. More information on these utility functions can be found on the Execution Utility Functions page.

API

execute

POST
api/session/python/execute
Execute a block of python code, no state is preserved after execution

Request

The request body is structured as follows:

{
  "moduleName": "string", # Optional
  "script": "string"
}

moduleName - The registered module the script will be executed in. For one-off scripts, this field can be omitted or an empty string.

script - The Python code to be executed. This needs to be valid Python 2.7 code, using the correct indentation and stringified.

Response

{
  "status": {
    "code": 0,
    "message": "string",
    "details": [
      {
        "type_url": "string",
        "value": "string"
      }
    ]
  },
  "d3Log": "string",
  "pythonLog": "string",
  "returnValue": "string"
}

Note that calling this endpoint too frequently or during a show is not a supported workflow - this is intended for show programming tasks, not during production.

An example of using the execute endpoint is shown below using Python:

import requests

response = requests.post(
    url="http://localhost/api/session/python/execute", 
    json={
        "script": "print('Hello Disguise Designer!')"
    }
)

The response to this statement would look as follows:

{
    "status": {
        "code": 0,
        "message": "",
        "details": []
    },
    "d3Log": "Hello Disguise Designer!\nPython script took 37.352000000000004ms\n",
    "pythonLog": "Hello Disguise Designer!\n",
    "returnValue": "null"
}

registermodule

POST
api/session/python/registermodule
Register a python module to be used in later execute calls.

Request

{
  "moduleName": "string",
  "contents": "string",
}

moduleName - This is the name the module will be referred to in Python. This string will need to be parsed in the execute request in the moduleName field in order to load the module. See the Module naming section for more information on valid module names.

contents - This is the contents of the Python module in a single string. Classes, functions and variables can be defined here to be used in the execute.

Response

{
  "status": {
    "code": 0,
    "message": "string",
    "details": [
      {
        "type_url": "string",
        "value": "string"
      }
    ]
  }
}

An example of registering a Python module using the registermodule endpoint is shown below:

import requests

response = requests.post(
    url="http://localhost/api/session/python/registermodule", 
    json={
        "moduleName": "screen_utils",
        "contents": """
def createLEDScreen():
    # Create a new LED screen resource
    screen = resourceManager.loadOrCreate(
        'objects/ledscreen/ledscreen_1.apx',
        LedScreen
    )

    # Set the physical size in meters
    screen.scale = Vec(1, 2, 0)

    # Set the resolution in pixels
    screen.resolution = Vec(1920, 1080)

    # Center the screen at origin
    screen.offset = Vec(0, 0, 0)

    return screen
"""
    }
)

Giving the following response:

{
    "status": {
        "code": 0,
        "message": "",
        "details": []
    }
}

If errors are experienced whilst compiling the given module, errors will be given in the message field.

To use the above module in the execute, the module name must be passed into the moduleName field, the request would look as follows:

import requests

response = requests.post(
    url="http://localhost/api/session/python/execute", 
    json={
        "moduleName": "screen_utils",
        "script": "createLEDScreen()"
    }
)

The d3 package

The d3 package an interface to Designer object types, resources and session state. A full breakdown of the different classes and functions can be found on the Python Reference page.

All of the d3 package is always imported into the execute environment, therefore a d3 import statement is not required.

Some elements of the d3 package are not available through the plugin system, such as GUI elements, to prevent stability issues. For more information, please read the Python Restrictions page.

Returning values

It is possible to return values from the execute endpoint. As mentioned in the Architecture section, all execution code is run within the runner function, by adding a return statement the script will return values from the runner function.

The returned values will be serialised as JSON using the Python json package.

An example of returning a value is shown below

import requests

response = requests.post(
    url="http://localhost/api/session/python/execute", 
    json={
        "script": "return {'a': [], 'b':2}"
    }
)

print(response.json())

Gives the response:

{
    "status": {
        "code": 0,
        "message": "",
        "details": []
    },
    "d3Log": "Python script took 3.1447ms\n",
    "pythonLog": "",
    "returnValue": "{\"a\": [], \"b\": 2}"
}

Logging

As always logging is important but it is even more crucial when running commands remotely. To help with this, two logging fields are provided in the response.

Run time and timeouts

The execution of Python scripts through the execute occurs on the main application thread and causing the application to pause whilst the script executes. To avoid cases where scripts run longer than expected, a timeout interrupt has been implemented. The length of this timeout is dictated by the experimental option pythonApiExecutionTimeout which can be set through the d3Manager window. When a script exceeds this value, a KeyboardInterrupt is executed, and the Python execution stops and returns a TimeoutError in the response.

Writing out scripts

Scripts sent to the execute endpoint can be written out using the experimental pythonApiOutputRequestsToProject flag through d3Manager. When this is set to True all scripts sent to the execute endpoint will be written to the project internal\\PythonApiHistory directory. This can be useful for debugging issues as well as keeping a record of the scripts executed in a project. However, this should be used with caution as executing many requests will cause the project size to grow quickly.

Undo

Undo is supported when executing scripts through the execute endpoint.

Module naming

As plugins are imported as Python modules, plugin names need to follow the same naming conventions:

Using modules in other modules

Registering a Python plugin adds the defined module to the internal Designer Python system, meaning these modules can be used inside other Python plugin modules. Modules, when registered, are prepended with user_module_ to protect core modules. To use a registered module in another module, this would need to be added to the module name.

Here’s an example of creating another plugin module:

import requests

response = requests.post(
    url="http://localhost/api/session/python/registermodule", 
    json={
        "moduleName": "serialise_helpers",
        "contents": """
def serialiseLED(ledScreen):
    return {'name':ledScreen.path , 'uid': ledScreen.uid}
"""
    }
)

This could then be used when executing in another module namespace by importing the module:

import requests

response = requests.post(
    url="http://localhost/api/session/python/execute", 
    json={
        "moduleName": "screen_utils",
        "script": """
from user_module_serialise_helpers import serialiseLED
return serialiseLED(createLEDScreen())
"""
    }
)

Loading modules on startup

Another option for loading modules is to have the modules registered when the Designer application is started. This can be done by adding the modules to the project {project_root}/plugins directory. These will be loaded using the following rules: