This page describes the basics of Matricks plugins. Plugins may be developed in any language that is supported by the Extism PDK. Much of the information on this page applies to plugins in all compatible languages, but all example code given here will be in Rust. If you’re interested in examples of plugin development in other languages, check out the examples on the GitHub.

Prerequisites

Install Matricks

To run your plugins on a Raspberry Pi, download and install Matricks. See installation instructions for Matricks here.

Install Simtricks (Optional)

To run your plugins on non-Raspberry Pi devices, download and install Simtricks, a simulator for Matricks plugin testing and development. For installation instructions and usage information, check out Simtricks.

Install Rust and the wasm32-wasi target

If you don’t have Rust installed already, you can do so by visiting the rustup website. Once you have Rust installed, use rustup to install the wasm32-wasi toolchain:

rustup target add wasm32-wasi

Create a new plugin from a template (Optional)

You can use cargo-generate to create a new empty Matricks plugin. The Matricks plugin template sets up the update and setup functions, as well as the Matricks logging functions. First, install cargo-generate:

cargo install cargo-generate

Next, use cargo-generate to make a new project:

cargo generate --git https://github.com/wymcg/matricks_plugin_template.git

Once you have generated your new project, you can navigate to the new project and confirm that it builds:

cargo build --release --target wasm32-wasi

Plugin structure

Matricks plugins have two parts: the setup function, and the update function.

The setup function

The setup function is called once when the plugin is instantiated. In this function, you may perform any initialization your plugin needs. In simple plugins, this function may be empty.

In Rust, the setup function is declared as follows:

#[plugin_fn]
pub fn setup(_: ()) -> FnResult<()> {
    // Setup your plugin here!
    
    Ok(())
}

The update function

The update function is called once per frame, and provides the next state of the matrix to Matricks. The next matrix state is returned as a JSON string containing a two-dimensional array of BGRA color values. For example, a 3x3 matrix showing the color blue on all LEDs would be represented as:

[
  [[255, 0, 0, 255], [255, 0, 0, 255], [255, 0, 0, 255]],
  [[255, 0, 0, 255], [255, 0, 0, 255], [255, 0, 0, 255]],
  [[255, 0, 0, 255], [255, 0, 0, 255], [255, 0, 0, 255]]
]

If the update function returns null instead, Matricks will stop requesting new updates from this plugin.

In Rust, a setup function that returns a blue 3x3 matrix state as shown above might look like the following:

#[plugin_fn]
pub fn update(_: ()) -> FnResult<Json<Option<Vec<Vec<[u8; 4]>>>>> {
    // --snip--
    
    // Return a JSON-format string containing 3x3 matrix of all blue
    Ok(Json(Some(vec![vec![[255, 0, 0, 255]; 3]; 3])))
    
    // Or, if you want to stop this plugin, return JSON null instead
    Ok(Json(None))
}

Retrieving configuration info

Matricks provides information about the connected LED matrix using a key-value store. The following keys are available:

Key Description
width Width of the LED matrix, in number of LEDs
height Height of the LED matrix, in number of LEDs
target_fps The target framerate of the matrix
brightness The brightness of the matrix, from 0-255

The values stored in this key-value store are strings, and must be converted to the desired type before use.

The method for accessing the config key-value store differs between supported languages. Consult the Extism PDK documentation for information on your desired language. In Rust, pulling the width of the matrix from the config might look like this:

let width: usize = config::get("width")     // Option<String>
    .unwrap()                               // String
    .parse()                                // Result<usize, Err>
    .unwrap();                              // usize

Logging from a plugin

Several host functions are available which allow plugins to make logs through Matricks:

Name Description
matricks_debug Make a debug log through Matricks
matricks_info Make an info log through Matricks
matricks_warn Make a warn log through Matricks
matricks_error Make an error log through Matricks

The method for calling host functions differs between supported languages. Consult the Extism PDK documentation for information on your desired language. In Rust, declaring and calling the logging functions might look like this:

// Declare the debug functions
#[host_fn]
extern "ExtismHost" {
    fn matricks_debug(msg: &str);
    fn matricks_info(msg: &str);
    fn matricks_warn(msg: &str);
    fn matricks_error(msg: &str);
}

// --snip--

// Call the log functions
unsafe { // Foreign functions in Rust must be called within "unsafe" blocks!
    matricks_debug("This is a debug message!")?;
    matricks_info("This is an info message!")?;
    matricks_warn("This is a warning message!")?;
    matricks_error("This is an error message!")?;
}

Maintaining plugin state

It is very common to want to maintain some sort of plugin state between calls to the setup and update functions. For example, you might want to initialize a counter in the setup function, and increment it every time the update function is called.

In some supported languages, it is possible to declare state variables globally so that they can be accessed from the setup and update functions. For example, in Rust, you may use the lazy_static crate to do the following:

use lazy_static::lazy_static;
use std::ops::DerefMut;
use std::sync::{Arc, Mutex};

lazy_static! {
    static ref COUNTER: Arc<Mutex<u8>> = Arc::new(Mutex::new(0usize));
}

// --snip--

#[plugin_fn]
pub fn update(_: ()) -> FnResult<Json<Option<Vec<Vec<[u8; 4]>>>>> {
    // --snip--
    
    // Grab a mutable reference to the counter
    let mut counter = COUNTER.lock().unwrap();
    let counter = counter.deref_mut();
    
    // Increment the counter
    *counter += 1;
    
    // --snip--
}

Alternatively, Extism provides a key-value store for plugins. The method for accessing this key-value store differs between supported languages. Complex data structures can be difficult to use with this key-value store, so it is generally recommended to avoid this method if possible.

A note on WASM/WASI compatibility

You may find that when using a Rust crate in your plugin project, features may be broken or your project may have compilation issues. WASM (and especially WASI) are still very new, and so certain libraries may have compatibility issues that may not be addressed. In these cases, seek out alternative crates; some crates may specifically advertise WASM compatibility.

WASM/WASI compatibility issues are becoming rarer as the platform matures, but issues do occasionally appear. Because of this, it is recommended to make a small test plugin to confirm that the libraries you would like to use in your plugin work as expected.

Conclusion

Hopefully, you’ve been able to get your hands dirty with some Matricks plugin development! If you have a question that hasn’t been answered by this document, here’s a few links that might help.