https://github.com/mrd0ll4r/kaleidoscope
An Environment to Run Lua Programs on top of Submarine
https://github.com/mrd0ll4r/kaleidoscope
lighting lua raspberry-pi rust
Last synced: 5 months ago
JSON representation
An Environment to Run Lua Programs on top of Submarine
- Host: GitHub
- URL: https://github.com/mrd0ll4r/kaleidoscope
- Owner: mrd0ll4r
- License: mit
- Created: 2020-03-29T10:14:19.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2025-01-05T16:41:35.000Z (over 1 year ago)
- Last Synced: 2025-04-07T06:51:20.922Z (about 1 year ago)
- Topics: lighting, lua, raspberry-pi, rust
- Language: Rust
- Homepage:
- Size: 189 KB
- Stars: 2
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Kaleidoscope
An environment to run Lua programs on top of Submarine.
## Description
This is a program that connects to Submarine and executes Lua programs to control Output Devices attached to
the Submarine instance.
## Configuration
Configuration is done via YAML files.
The main configuration file specifies basic properties and a path to a directory containing configuration files for
the individual fixtures:
```yaml
# Address of the AMQP server used to publish status updates.
amqp_server_address: "amqp://192.168.88.30:5672/%2f"
# Address of the Submarine instance to post outputs to.
submarine_http_url: "http://192.168.88.30:3069"
# The address to expose Prometheus metrics on.
prometheus_listen_address: "0.0.0.0:4343"
# The address to expose the HTTP API on.
http_listen_address: "0.0.0.0:3545"
# The path from which to load fixtures and programs.
fixtures_path: "./fixtures"
```
## HTTP API
Kaleidoscope is controlled via a JSON-over-HTTP API.
Currently, these routes are exposed:
```
GET /api/v1/fixtures List fixtures.
GET /api/v1/fixtures/:fixture Get single fixture.
GET /api/v1/fixtures/:fixture/programs List programs for fixture.
POST /api/v1/fixtures/:fixture/set_active_program Set active program by name, provide the name as text in the body.
POST /api/v1/fixtures/:fixture/cycle_active_program Cycle to the next program, skipping MANUAL and EXTERNAL.
GET /api/v1/fixtures/:fixture/programs/:program Get single program.
GET /api/v1/fixtures/:fixture/programs/:program/parameters List parameters for program.
GET /api/v1/fixtures/:fixture/programs/:program/parameters/:parameter Get single parameter.
POST /api/v1/fixtures/:fixture/programs/:program/parameters/:parameter Set parameter value, provide an alloy::program::ParameterSetRequest as JSON in the body.
POST /api/v1/fixtures/:fixture/programs/:program/parameters/:parameter/cycle Cycle discrete parameter value.
```
## The Lua Runtime
We use [mlua](https://crates.io/crates/mlua), which means that our programs are Lua 5.4.
On a high level, the `Runtime` manages a list of `Fixtures`.
Each `Fixture` has a list of `Program`s, of which one is currently active and being executed.
Each `Program` can have a set of discrete and/or continuous `Parameters`.
The Runtime evaluates all Fixtures in parallel.
Each round of executions is called a tick, of which we target `200/s`.
At the end of each tick, all outputs are aggregated into one request and sent to Submarine.
Submarine then processes that request mostly-atomically.
### Fixtures
A Fixture is a set of output addresses controlled by one active Program.
It defines a list of outputs and Programs, and configures other Fixture-wide parameters.
A Fixture definition is itself a valid Lua program.
One Fixture is defined per input file, with the `setup` function setting up the Fixture.
Here's an annotated example:
```lua
SOURCE_VERSION=3
function setup()
-- Set a name for the Fixture.
fixture_name("klo_rgbw")
-- Add outputs.
add_output_alias('klo-r')
add_output_alias('klo-g')
add_output_alias('klo-b')
add_output_alias('klo-w')
-- Whether to disable the builtin MANUAL program.
--disable_manual_program(true)
-- Whether to disable the builtin ON and OFF programs.
--disable_builtin_programs(true)
-- (Optional) programs to load.
add_program("noise", "foo/noise.lua")
end
```
The functions available for Fixture setup are listed in [src/runtime/lua/fixture_builtin.lua](src/runtime/lua/fixture_builtin.lua).
### Builtin Programs
By default, each Fixture has three programs generated for it:
- `OFF`, which sets all outputs of the fixture to `LOW`.
- `ON`, which sets all outputs of the fixture to `ON`.
- `MANUAL`, which generates a continuous parameter for each output of the fixture and sets them according to the
parameter values.
### Programs
Every Program must contain a `setup` function and a `tick` function.
The `setup` function is called once, during Program initialization.
The `tick` function is called in regular intervals if the program is currently active.
During `setup`, a Program defines Parameters, which are mutable through the HTTP API.
Parameter values can then be accessed during the `tick` function.
In the context of `setup()`, a bunch of special functions can be called, which are not available later.
See [src/runtime/lua/program_builtin.lua](src/runtime/lua/program_builtin.lua) for a list.
#### The `tick` Function
At the heart of every program is the `tick(now: f64)` function.
It takes one parameter, the current time in `f64` seconds since an unspecified epoch available as `START`.
For enabled Programs, the Runtime usually calls this function on each tick.
Programs can elect to be handled in "slow mode", which can be useful for outputs that do not require short reaction
times or frequent updates.
Because of this, the `tick` function __must not rely on it being called in a regular interval__.
As an example: Do not increment a counter on each tick and calculate outputs based on it -- use the provided timestamp
to calculate outputs.
The `tick` function can call other functions and do whatever Lua can do, but it should run as fast as possible.
The Runtime keeps track of both the global tick duration and `tick` durations for each program, which might be useful
for debugging.
#### Slow mode
Usually programs are run at every tick.
Slow mode programs are run every 1000 ticks.
The reason for this is that some programs can probably deal with the added
latency, which frees some performance for the programs that need to execute
every tick.
#### Builtins
The Runtime provides a bunch of builtin functions and constants, of which some are written in Rust and some in Lua.
See [src/runtime/lua/program_builtin.lua](src/runtime/lua/program_builtin.lua) for a complete list.
Notable mentions:
- `KALEIDOSCOPE_VERSION: int`, which denotes the version of the Runtime.
- `START: f64` and `NOW: f64` denote the program epoch and current timestamp, both as `f64` seconds.
- `noise2d(f64, f64) -> f64` computes 2D Perlin noise in `[-1,1]`.
This is implemented in Rust and relatively fast.
- `noise3d(f64, f64, f64) -> f64` computes 3D Perlin noise in `[-1,1]`.
This is implemented in Rust and slower than the 2D version.
- `noise4d(f64, f64, f64, f64) -> f64` computes 4D Perlin noise in `[-1,1]`.
This is implemented in Rust and slower than the 3D version.
- `now() -> f64` gets the time in seconds since the program epoch.
- `get_parameter_value(name)` gets the current value of the named parameter.
- `clamp(from: numer, to: number, x: number) -> number` clamps `x` to `[from, to]`.
- `lerp(from: number, to: number, x: number) -> number` interpolates between `from` and `to`.
- `map_range(a_lower: number, a_upper: number, b_lower: number, b_upper: number, x: number) -> number` maps `x` from the
first range to the second.
- `map_to_value(from: number, to: number, x: number) -> u16` maps `x` from `[from,to]` to the 16-bit Submarine value
range.
- `output_alias_to_address(alias: string) -> u16` translates an alias to a numerical address, if it exists.
Raises an error otherwise.
- `set_alias(alias: string, value: u16)` sets the output at `alias` to `value`.
Make sure to call this with integers, probably breaks with non-integers...
## Example Programs
Here's an example program that sets four channels of an `RGBW` output to a Perlin-noise color:
```lua
SOURCE_VERSION=3
-- Constants
local r = 0
local g = 1
local b = 2
local w = 3
local sine_speed = 0.07
local noise_speed = 0.1
-- Parameters
local MODE_BRIGHTNESS_NAME = "brightness"
local MODE_BRIGHTNESS_NIGHT = "night"
local MODE_BRIGHTNESS_DAY = "day"
-- Variables
local current_brightness_mode = MODE_BRIGHTNESS_NIGHT
function setup()
local p_brightness = new_discrete_parameter(MODE_BRIGHTNESS_NAME)
add_discrete_parameter_level(p_brightness, MODE_BRIGHTNESS_NIGHT, "dunkel")
add_discrete_parameter_level(p_brightness, MODE_BRIGHTNESS_DAY, "hell")
declare_discrete_parameter(p_brightness)
end
function compute_white(index, now)
local t = now * sine_speed
if current_brightness_mode == MODE_BRIGHTNESS_DAY then
return map_to_value(0, 1, map_range(-1, 1, 0.9, 1, math.sin(t + (math.pi / 4) * index)))
end
return map_to_value(0, 1, map_range(-1, 1, 0.7, 0.8, math.sin(t + (math.pi / 4) * index)))
end
function compute_color(index, now)
local t = now * noise_speed
if current_brightness_mode == MODE_BRIGHTNESS_DAY then
return map_to_value(0, 1, map_range(-1, 1, 0.8, 1, noise2d(index, t)))
end
return map_to_value(0, 1, map_range(-1, 1, 0.5, 0.9, noise2d(index, t)))
end
function tick(now)
current_brightness_mode = get_parameter_value(MODE_BRIGHTNESS_NAME)
set_alias('klo-w', compute_white(w, now))
set_alias('klo-r', compute_color(r, now))
set_alias('klo-g', compute_color(g, now))
set_alias('klo-b', compute_color(b, now))
end
```
## Compilation & Running
See the [README of Submarine](../submarine/README.md), which explains setup and cross-compilation for Linux on a Raspberry Pi.
In general, while it is not required to run Kaleidoscope on the same machine as Submarine,
we have observed that this benefits lighting performance because of lower latency variance.