Serving Rigs
Running rigs using slipway run
is useful for testing,
but when deployed we want to serve our rigs over the internet, or a local network,
to our devices.
The Slipway CLI comes with a built in HTTP server for doing exactly this, and the slipway serve
command
is the basis of this functionality. Running the following command will display the help:
slipway serve --help
All slipway serve
commands and sub-commands take a path as the first argument.
This path is the base directory of the server, from which all configuration files are discovered.
If you are running slipway serve
commands from the base directory, you can simply use .
as the path.
We will assume this is the case in the examples that follow.
Init
If you are creating a new server then then the init
sub-command is useful for setting up
the basic directory structure and configuration files:
slipway serve . init
Running this in an empty folder will produce the following output:
Adding config: "./slipway_serve.json"
Adding dockerfile: "./Dockerfile"
Adding folder: "./components"
Adding folder: "./rigs"
Adding folder: "./playlists"
Adding folder: "./devices"
Adding folder: "./fonts"
Running the init
sub-command in a folder which is not empty will only create the files and folders which do not already exist.
We'll go though each of the items it created in turn.
./slipway_serve.json
The slipway_serve.json
file is the primary file through which the server is configured.
The init
command will produce a basic version of this file which will look something like this:
{
"log_level": "info",
"registry_urls": [
"file:./components/{publisher}.{name}.{version}.tar",
"file:./components/{publisher}.{name}"
],
"timezone": "Europe/London",
"locale": "en-GB",
"rig_permissions": {},
"hashed_api_keys": {}
}
We will go through each of these settings.
log_level
This configures the logging level of the server and any Components which are run.
This is the equivalent of the --log-level
argument to the slipway run
command.
The valid options are, from most verbose to least:
trace
debug
info
(the default level if the setting is omitted)warn
error
registry_urls
The list of Component Registry URLs which should be checked, in order, before falling back to the Slipway Component Registry.
See the Component Registries page for more information.
timezone
The timezone which you want to use when evaluating playlists, and which is made available to Components
in the TZ
environment variable.
The timezone should be a string
identifier such as Europe/London
.
The init
command defaults this to the current system timezone.
See here for more information.
locale
The locale, which is made available to Components in the LC
or LC_ALL
environment variables.
The local should be a string identifier such as en-US
.
The init
command defaults this to the current system locale.
See here for more information.
rig_permissions
A mapping of Rig name to an array of permissions for that Rig.
For example, if there was a Rig rigs/my_rig.json
then you might change this to something like:
{
"rig_permissions": {
"my_rig": {
"allow": [
{ "permission": "fonts" },
{ "permission": "files", "within": "./data" },
{ "permission": "registry_components" },
],
"deny": [
{ "permission": "files", "within": "./data/secrets" },
]
}
}
}
See the Permissions page for more detail about possible permission configuration values.
port
An optional port to serve on. Defaults to 8080
.
hashed_api_keys
A mapping of key names to hashed API keys.
API keys provide security by requiring that all devices requesting dashboards provide an API key contained
in the slipway_serve.json
file. Any requests which do not provide a valid key are rejected.
You can easily add a new random API key by calling the add-api-key
sub-command.
For example:
slipway serve . add-api-key --name my_new_key
See the API Keys section below for more details.
show_api_keys
This parameter is not added by default, but has the following possible values:
"show_api_keys": "never"
"show_api_keys": "new"
"show_api_keys": "always"
If omitted this defaults to never
and slipway serve
will never output incoming API keys to the console.
This is the default for security reasons: If the logs were to be persisted then we would absolutely
not want any credentials stored in them.
However sometimes you do need to discover the API key used by a device, perhaps so it can be stored in a password manager.
Setting show_api_keys
to new
will allow Slipway to output API keys to the console whenever
a new API key is generated for a device (using the TRMNL API), or whenever a device tries to use
the API with an unrecognized API key.
Setting show_api_keys
to always
will always show the device API keys whenever a request is received
by the device. This is useful if you need to find out the API key of a device which is already
configured to use your Slipway server. It is recommended to always put show_api_keys
back to new
or never
once you've gotten the API key you need.
Note that slipway serve . add-api-key
will always show the API key irrespective of this setting.
./Dockerfile
A Dockerfile which will create a deployable container image of your server, with all referenced Components downloaded locally, and all WASM Components ahead-of-time compiled for maximum performance.
This makes it super easy to deploy your server on services such as fly.io, which will
automatically detect and use the Dockerfile when you run fly deploy
.
./components
This folder can contain any local Components, either as folders or TAR files.
./fonts
This folder can contain any fonts you want to make available to Components, in addition to system fonts.
./rigs
This folder contains any Rig files. See the next section for more details.
./playlists
This folder contains any Playlist files. See the next section for more details.
./devices
This folder contains any Device files. See the next section for more details.
Rigs
We've already talked about Rigs. A Rig renders a dashboard by composing together Components.
You can use the add-rig
sub-command to help with creating a basic Rig file in the correct location.
For example:
slipway serve . add-rig --name "example_rig"
Playlists
Sometimes you want to display a different Rig depending on the time or the day, or have a Rig auto-refresh at certain intervals. This is what Playlists are for.
You can use the add-playlist
sub-command to help with creating a basic Playlist file in the correct location.
For example:
slipway serve . add-playlist --name "example_playlist" --rig "example_rig"
Here is an example of a playlist:
{
"schedule": [
{
"time": {
"from": "07:00",
"to": "09:00"
},
"days": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
]
"refresh": {
"minutes": 30,
},
"rig": "school_calendar"
},
{
"time": {
"from": "22:00",
"to": "04:00"
},
"refresh": {
"hours": 3
},
"rig": "house_energy_usage"
},
{
"refresh": {
"cron": "0 * * * *"
},
"rig": "house_energy_usage"
}
]
}
Playlists contain a schedule
.
When a playlist is requested, each item in the schedule is checked in order.
The first item which is found to be "active" is used.
An item is active if the current date and time,
as calculated using the timezone
specified in the slipway_serve.json
file,
falls within the specified time range and on the specified days.
days
The days
field takes an array of days on which the playlist item can be active.
If no days are specified, then all days are considered valid.
time
The time
field contains an object which can contain either a from
field or a to
field or both.
The from
and to
fields are times formatted as HH:mm
.
An object with neither from
nor to
is not valid; instead the entire time
field should be removed
or set to null
.
If no time is specified, then all times are considered valid.
refresh
The refresh interval while this Playlist item is active.
This can be specified in seconds:
"refresh": {
"seconds": 30,
}
Or in minutes:
"refresh": {
"minutes": 15,
}
Or in hours:
"refresh": {
"hours": 2,
}
Or as a Cron schedule. For example this specifies a refresh on the hour every hour:
"refresh": {
"cron": "0 * * * *"
}
rig
The rig
field specifies the name of the Rig that should be run while this Playlist item
is active.
Example Revisited
Looking back at the example playlist above we can now see that:
-
Monday to Friday between 7am and 9am the
school_calendar
Rig should be displayed, and refreshed every 30 minutes. -
Otherwise, on any day between 10pm and 4am the
house_energy_usage
Rig should be displayed, and refreshed every 3 hours. -
Otherwise the
house_energy_usage
Rig should be displayed and refreshed on the hour every hour.
Devices
A Device represents a physical device which wants to display a Rig.
You can use the add-device
sub-command to help with creating a basic Device file in the correct location.
For example:
slipway serve . add-device --name "example_device" --playlist "example_playlist"
At its simplest, a device simply references a playlist:
{
"playlist": "example_playlist"
}
If your physical device is a TRMNL device, or will be using the TRMNL API, then the device file also contains the information required to identify and authenticate the TRMNL device.
The add-trmnl-device
sub-command is used to add this additional information to a Device file.
See this page for more information.
Device Context
Devices can also specify context
, which can be used to supply arbitrary data to Rigs about the device,
and can in turn be used to alter the Rig's behavior.
{
"playlist": "example_playlist",
"context": {
"width": 800,
"height": 480,
"color": false
}
}
In your Rig you can access the current device context and pass it through to Components by using the $.context.device
query.
Serving
To start the server, simply run:
slipway serve .
By default the server runs on port 8080
.
When the server is running, Rigs, Playlists and Devices can be accessed at the following URLs:
/rigs/<rig_name>
/playlists/<playlist_name>
/devices/<device_name>
Where <rig_name>
, <playlist_name>
, and <device_name>
are the names of the files in the
rigs
, playlists
, and devices
folder with the .json
extension removed.
So if you had a Rig located at:
rigs/my_rig.json
And your server was running at:
http://localhost:8080/
Then you could view this rig by visiting:
http://localhost:8080/rigs/my_rig
TRMNL API
In addition to the above endpoints, the server also supports the TRMNL API for compatibility with any device running the TRMNL firmware.
The TRMNL API has its own security system, where each device is assigned an API key by the server. See the TRMNL documentation for more information.
Query String Arguments
Rig, Playlist and Device URLs all take a standard set of query string arguments:
format
How the server should format the result. This can be one of:
image
: Format the result as an image. This is the default.json
: Return the raw JSON result from the output Component of the Rig.html
: Return the result as an HTML page containing a URL which generates the image.html_embed
: Return the result as an HTML page containing the image as an embeddeddata
URL.
image_format
How the server should format the image. This can be one of:
png
: Format the image as a PNG. This is the default.jpeg
: Format the image as a JPEG.bmp_1bit
: Format the image as a 1 bit Bitmap.
authorization
The Rig, Device and Playlist calls require that the requests contain an authorization
header,
the value of which should match one of the API keys added to the slipway_serve.json
.
However if the device making the request is unable to add the API key as a header, it can
instead be specified using the authorization
query string parameter.
Passing the authorization string as a query string parameter is not as secure as passing it as a header, and should only be used when adding a header is impractical or impossible.
API Keys
New API keys can be easily added to the slipway_serve.json
using the add-api-key
sub-command.
For example:
slipway serve . add-api-key --name my_new_key
This command will generate a long and random key, and add the hashed version of the key to the slipway_serve.json
file
with the name my_new_key
.
This is secure, as it is computationally infeasible to work out the unhashed key from the hashed key. The hashing algorithm used is SHA256.
The add-api-key
command will output the unhashed key in the console for you to save somewhere securely.
It is the unhashed key which should be supplied by devices in the authorization
header,
not the hashed key.
The unhashed keys should be stored somewhere secure, such as in a password manager. Anyone with access to an unhashed key is able to call your Slipway server endpoints.
Keys can be revoked simply by removing them from the slipway_serve.json
and re-deploying the server.
If you wish to add a custom key, rather than a randomly generated one, you can use the slipway hash
command
to generate the hash, and add the hashed key to the slipway_serve.json
manually.
You can add as many or as few keys as you like. So for example you could have one key for all devices, or one key per device, or anything in between.
If some devices have to pass their API key as a query string parameter, rather than as a header, it may be worth using a separate key so it can be easily revoked if it leaks, without affecting other devices.
Consolidate
The consolidate
sub-command will download every referenced Component to a local cache, to improve
cold start latency when the server is run for the first time.
slipway serve . consolidate
This is done automatically by the Dockerfile (as part of the aot-compile
sub-command) when deploying.
Ahead-Of-Time Compilation
When running WASM Components often the biggest overhead is compiling the WASM Component to machine code.
The Dockerfile therefore runs the aot-compile
sub-command during deployment to compile all WASM Components
ahead of time.
The aot-compile
sub-command automatically calls the consolidate
sub-command internally.
slipway serve . aot-compile
By default the current machine's architecture is used for AOT compilation, however this can be overridden with the
---target
argument:
slipway serve . aot-compile --target x86_64-unknown-linux-gnu
Even after running aot-compile
the server will not automatically use the resulting artifacts.
You must specify the --aot
argument when running the server to actually use the AOT artifacts.
slipway serve . --aot
The reason for this is that using AOT compiled artifacts is a more complicated workflow, with addition complexity around machine architectures and the staleness of the AOT artifacts. My making the use of AOT artifacts explicit when starting the server it is hoped that potential confusion will be reduced.