/q-sys-plugin-guide

Samples and documentation for building and deploying QSC Q-Sys Plugins

MIT LicenseMIT

QSC Q-Sys Plugin Development Guide

Development Template & Guides for building and deploying QSC Q-Sys Community Plugins.

MIT license LinkedIn

A resource for development of plugins for the Q-Sys platform.



Getting Started

Quickstart for Use

  • Open Q-Sys Designer with the /dev command line argument
  • Under Asset Manager there will now be a cog icon active at the top right
  • Add the community server https://q-sys.soloworks.co.uk/q-sys-community-plugins/
  • Note: Entries may double up on first time, refresh view with 🔄 to clear

QuickStart for Development

Some knowledge of Git based workflows is required, to submit changes and improvements please follow standard fork/branch workflow and submit a Pull Request when finished.

Clone a plugin repository to somewhere (we use Documents/GitHub/repo-name)

git clone github.com/q-sys-community/q-sys-plugin-<reponame>

User Defined plugins are defined and placed in the folowing folder. Copy the .qplug file from the content folder to here

%userprofile%\Documents\QSC\Q-Sys Designer\Plugins

This will now appear under user plugins as version 0.0.0.0-master. When finished with changes, copy back to the repo and commit.

Note: Please only submit tested and working files, always ensure they have been linted with the correct extensions (see below) to ensure only code changes are submitted and not syntax and spacing. We are opinionated with code in order to debug logic, not spacing.


Development

Overview

Q-Sys Plugins are single file LUA scripts built around a specific framework. They are delivered using NuGet based system.

Plugins are managed in your file system by NuGet using the ID and version in the format id.0.0.0.0, and by Q-Sys Designer by the PluginInfo.Id value. This means you can have multiple versions of a plugin installed, but if they are not given unique ids you may only see one.

Do not work on plugins directly from the installed files, instead make a copy of the plugin from the Repo and work in your plugins folder. Always delete and re-load a plugin after making changes to anything except the control code body, and even then if you encounter behaviour that you don't understand, in the first instance re-load designer and re-drag your plugin.

Plugins consist of two clear sections, which need to be understood to avoid confusion:

Setup

This section consists of all Plugin configuration:

  • PluginInfo - System infomation and MetaData
  • Properties - Configurable values presented in Properties Pane
  • Controls - Input / Output GUI objects and/or PINs
  • GUI Layout - Look and Feel of the presented UI pages

When a plugin is added to a schematic, the above areas are compiled into your program at that point in time. Any changes will not be reflected until either deletion of the control, or possibly a re-load of designer, and will result in very unpredictable results.

Control Code

This section consists of all control code, setting and reading Controls and Properties. This is where the actual moving parts go, changes to this section are picked up on every re-compile or emulation.

Programming Environment (IDE)

For consistency of coding structure, Visual Studio Code should be used for development with the following extensions:

  • Markdown All in One Extension (Search String: yzhang.markdown-all-in-one)
  • markdownlint (Search String: davidanson.vscode-markdownlint)
  • vscode-lua (Search String: trixnz.vscode-lua)

Plugin files with extension .qplug can be associated with Visual Studio Code by editing settings.json:

{
    "files.associations": {
        "*.qplug": "lua"
    }
}

When saving .lua files, always automatically format the file, either by right-clicking the code and chosing Format Document or with the shortcut (Shift-Alt-F). This will help to prevent git code changes flagged based on whitespace or code style.

Deployment

All plugins are open source, which means you are free to dig in and contribute. To submit changes, you can fork a branch and when done submit a pull request. Once accepted, a plugin will be tagged with a new version number (in the format x.x.x.x), and will be automatically packaged and pushed to the server from which it can be installed and used.

Source Control

Use the following guides when making commits to maintain a standard structure and keep automation easier:

In summary, the rules to adhere to are:

  • <type>: <description in 50 chars>
  • <type> can be one of:
    • fix
    • feat
    • docs
    • style
    • refactor
    • chore
  • Do not end with a "."
  • Use the imperative mood (Spoken or written as if giving a command or instruction) e.g.
    • Update document to show...
    • Stop function returning...
    • Alter workflow for control of...
  • Commit message body is optional, but should be sensible, concise and descriptive if used

Plugin File Structure (.lua as .qplug)

Plugin files are single file LUA scripts with a specific Q-Sys flavour framework. They can have the extension .lua, but convention is to use .qplug.

They consist of the following user editable elements:

  • Local Table: PluginInfo
  • Global Table: Controls
  • Function: GetProperties() returns Table
  • Global Table: Properties
  • Function: RectifyProperties(Table) returns Table
  • Function: GetPrettyName(Table) returns String
  • Function: GetControls(Table) returns Table
  • Function: GetPages(Table) returns Table
  • Function: GetControlLayout(Table) returns Table,Table
  • Function: GetComponents(Table) returns Table
  • Body: User control code, wrapped in if Controls

Local Table: PluginInfo

Contains infomation instructing Q-Sys Designer on how to handle the plugin.

Example:

PluginInfo = {
  -- Name of plugin in tree
  -- `~` allows for folder structure
  Name = "FolderName~PluginName",
  -- Version String (unused directly by Q-Sys Designer)
  Version = "VersionString",      // Version String
  -- Id is a UID for this plugin within the context of Designer
  Id = "qsysc.make.model.version",
  -- Description
  Description = "DescriptionString",
  -- Author (Used to put into group)
  -- QSC = QSC Managed
  -- Default: User
  Author = "AuthorString"
  -- Show Debug Window
  ShowDebug = true | false,
}

Function: GetProperties() returns Table

Function to build and store the Properties table (see below)

function GetProperties()
  props = {
    -- Build as per Properties Table spec below
  }

  return props
end

Function: RectifyProperties(Table) returns Table

Function to change properties table based on other properties. Called on any change of properties values.

  function RectifyProperties(props)
    -- Amend as per Properties Table spec below
    return props
  end

Global Table: Properties

Properties = {
  { -- for integer property types
    Name = "MyIntegerName",
    Type = "integer",
    Min = 0,        -- Minimum Value
    Max = 10,       -- Maximum Value
    Value = 5,      -- Default Value
  },
  { -- for boolean property types
    Name = "MyBooleanName",
    Type = "boolean",
    Value = true | false, -- Default State
  },
  { -- for enum property types
    Name = "property_name",
    Type = "enum",
    Choices = {     -- List of options
      "Option_01",
      "Option_02"
      },
    Value = "Option_01",  -- Default Option
  },
}

Function: GetPrettyName(Table) returns String

Return the string to display on the plugin control on the schamtic canvas. If not present will default to Id.

function GetPrettyName()
  return "My Friendly Name String V1.0.0.2"
end

Function: GetControls(Table) returns Table

Return the Controls table as per Controls table spec below

function GetControls(props)
  ctls = {
    -- Build as per Controls Table spec below
  }
  return ctls
end

Global Table: Controls

Controls = {
  {
    -- Button
    {
    Name = "control_name"
    ControlType = "Button"
    ButtonType = "Momentary" | "Toggle" | "Trigger",
    Count = integer, -- if > 0 this will make the control an array
    PinStyle = "Input" | "Output" | "Both",
    UserPin = true | false
    },
    -- Knob
    {
    Name = "control_name",
    ControlType = "Knob",
    ControlUnit = "dB" | "Hz" | "Float" | "Integer" | "Pan" | "Percent" | "Position" | "Seconds",
    Min = value,
    Max = value,
    Count = integer, -- if > 0 this will make the control an array
    PinStyle = "Input" | "Output" | "Both",
    UserPin = true | false
    },
    -- Indicator
    {
    Name = "control_name",
    ControlType = "Indicator",
    IndicatorType = "Led" | "Meter" | "Text" | "Status",
    Count = integer, -- if > 0 this will make the control an array
    PinStyle = "Input" | "Output" | "Both",
    UserPin = true | false
    },
    -- Text
    {
    Name = "control_name",
    ControlType = "Text",
    Count = integer, -- if > 0 this will make the control an array
    PinStyle = "Input" | "Output" | "Both",
    UserPin = true | false
    },
  }
}

Function: GetPages(Table) returns Table

Return table of pages, allowing UI to be tabbed.

local pagenames = {"Mixer","Video Switcher"}

function GetPages(props)
  pages = {}
  if props["Model"].Value=="Model 1" then
    .insert(pages, {name = pagenames[1]}) -- Only the "Mixer" pages shows for Model 1
  else
    for ix,name in ipairs(pagenames) do
      table.insert(pages,{ name = pagenames[ix] }) -- All pages in 'pagenames' show for Model 2
    end
  end
  return pages
end

Function: GetComponents(Table) returns Table

Store data here that isn't lost at runtime? (ToDo: Requires testing to see how it behaves)

function GetComponents(props)  
  return {}
end

Function: GetControlLayout(Table) returns Table,Table

Define how the Controls are presented and behave on UI and Pins. This section represents the main chunk of work in the plugin presentation, and will be the location of most errors.

Returns two tables:

  • Layout - Interactive controls
  • Graphics - Aesthetic controls

Graphics Table: Images

Images can be included by encoding Svg, Png or Jpeg as a base64 string using a site such as:

For example of the encoded string provided, examine the raw source of this file. The image below is embeded as:

data:image/png;base64,<base64EncodedTextHere>

and produces this image

Hello World

Example:

MyBase64EncodedImage = "iVBORw0KGgoAAAANS..." -- Truncated Base64

{
    Type = "Svg",   -- SVG
    --Type = "Image", -- PNG,JPEG
    Image = MyBase64EncodedImage,
    Position = {x, y},
    Size = {x, y}
}

Graphics Table: Text Field

Display static text

Example:

{
    Style = "Text",
    HTextAlign = "Left",
    Padding = i,
    StrokeWidth = i,
    Position = {x, y},
    Size = {x, y}
}

NuSpec File Structure

MetaData

Author

  • "QSC" will cause plugin to appear in "QSC Managed" tree
  • Anything else will appear in "User" tree

Description

The description field is populated by the description.md file in the root of the plugin repo.

NuGet supports most simple markdown, and the deploy process will convert relative image links from markdown to the slightly different NuGet supported format so that you can create and preview the file in VS Code.

Release Notes

To be included as part of the build at a later date, build up from Git Commit notes.