rvl-node
v9.0.0
Published
A Node.js implementation of the RVL* library family
Readme
RVL Node
A Node.js implementation of rvl's sending side. It joins the fleet's network, follows the fleet clock, and schedules scenes for the boards to show, so a program on a laptop or a Pi can drive the lights the way a controller board does.
For working examples, see the two demos in this repository: demo/waves.ts, which cycles through the built-in animations, and demo/scenes.ts, which schedules palette shifts on a bar grid at a fixed tempo.
Requirements
- One RVL Node per host. It binds UDP port 4979 and sends on every channel, so a second instance has nothing to add, and
initreports an error if one is already running.
Installation
Install using npm:
npm install rvl-nodeUsage
The code below connects to the fleet and shows solid red on every board listening on channel 1, dissolving in over half a second.
import {
createAnimationParameters,
createSolidColorAnimation,
init,
on,
setAnimationParameters,
} from 'rvl-node';
init();
on('connected', () => {
setAnimationParameters(
1, // Channel 1
createAnimationParameters(
createSolidColorAnimation(0, 255, 255) // Hue 0, full saturation, full alpha: solid red
)
);
});How it works
Scenes
Everything the fleet shows is a scene: an animation, or off, with a start frame and a fade. When a scene starts, every board dissolves from whatever it is showing into the new scene over the fade, so a change lands on all of them at the same instant and looks the same everywhere. Every scene fades, and the shortest fade is 16 frames, about half a second.
setAnimationParameters and setOff each schedule a scene on a channel. Without a startFrame, RVL Node defaults to five frames ahead if the channel's last scene has finished fading, and otherwise it holds the request until that fade ends. A newer request replaces a held one and whatever the boards end up showing is always the latest thing you asked for.
The fleet clock and frames
The fleet shares an animation clock, a millisecond count the boards agree on through the coordinator's reference broadcasts, and RVL Node follows it. A frame is 32 ms long (31.25 frames per second) and scene starts and fades are counted in frames. Frame numbers wrap every 2^27 frames, so compare them with subtractFrames rather than <.
The fleet clock is available a few seconds after init, and connected waits for it as well as for a device ID. Until then the clock accessors throw, scenes without a start are held, and scenes with a start can't be placed, so setAnimationParameters and setOff throw for those.
Configuring boards
A board's channel, brightness, pixel count, color order, status LED brightness and nickname can be set over the wire, and the board stores them, so they survive a reboot. getNodes asks every board to report itself and returns what came back: each board's settings and which of the six properties it honors, since a board without a status LED ignores that one and a board with physical controls reads channel and brightness from them. configureNode sets one property on one board and resolves with the board's report once it has applied it. identifyNode makes a board flash white for five seconds, strip and status LED, so you can tell which physical board an ID is.
Boards answer with a broadcast addressed to this node, which is lost a few percent of the time like any broadcast, so a listing asks twice and a targeted request asks once more before rejecting.
API
Setup
init
Joins the fleet: binds the socket, starts requesting a device ID from the coordinator, and starts following the fleet clock. It returns at once and the rest happens in the background; connected says when the fleet can be driven.
Signature:
interface RVLInitOptions {
networkInterface?: string;
}
function init(options?: RVLInitOptions): void;Arguments:
options.networkInterface(optional): the network interface to send and receive RVL packets on, e.g. "wlan0". Without it, RVL Node uses the first interfacegetAvailableInterfaceslists, and throws if there is none. A named interface doesn't need an address yet, so name it if you're starting before the network is up.
Returns: nothing. It throws if it has already been called, or if no interface is usable. The bind finishes in the background: if port 4979 is already in use, most likely by another RVL Node on the host, an error event says so, and init may be called again after it.
Every other function below, except on, the interface helpers, subtractFrames and the animation builders, throws until init has been called.
on
Subscribes to an event.
Signature:
function on(event: 'connected' | 'disconnected', listener: () => void): void;
function on(event: 'error', listener: (error: Error) => void): void;connected is emitted once this node has a device ID from the coordinator and the fleet clock, in whichever order they arrive, and so can drive the fleet. disconnected is emitted when the ID is lost, which happens when the network interface loses its address; connected is emitted again when it's reassigned, since the clock stays synced. error carries a failed bind. With no listener for it, the error surfaces as an uncaught exception, since Node's emitters throw an unhandled error.
getDeviceId
Signature:
function getDeviceId(): number | undefined;Returns: the device ID this node appears as to the fleet, or undefined if the coordinator hasn't assigned one yet. The ID is dropped when the interface loses its address, and may be different when it's reassigned.
isConnected
Signature:
function isConnected(): boolean;Returns: whether this node has a device ID and the fleet clock, and so can drive the fleet.
getNetworkInterface
Signature:
function getNetworkInterface(): string;Returns: the name of the interface RVL Node is sending on.
getAvailableInterfaces
Gets the list of available network interfaces that RVL Node can use. To be considered valid, an interface must have an IPv4 address that isn't self-assigned (169.254.x.x), and its name must start with en, eth, wlan, Wi-Fi, or Ethernet.
Signature:
function getAvailableInterfaces(): string[];Returns: a list of interface names that can be used with RVL Node.
getDefaultInterface
Gets the network interface init will use when none is named.
Signature:
function getDefaultInterface(): string | undefined;Returns: the name of the interface that will be used, or undefined if there is no suitable interface.
Scenes
setAnimationParameters
Schedules an animation on a channel. You can craft animation parameters by hand, but it's recommended to use the helper functions exported by this package, such as createAnimationParameters and createSimpleWaveParameters, instead. Crafting parameters by hand is a pain.
Signature:
function setAnimationParameters(
channel: number,
parameters: AnimationParameters,
options?: SceneOptions
): Promise<void>;Arguments:
channel: the channel to schedule the animation on, from 0 to 7.parameters: the animation. Throws if there are more than 4 layers, or if a value is out of range for its field:timePeriodanddistancePeriodare 1 to 255,aandbare 0 to 255, andw_t,w_xandphiare -128 to 127.options.startFrame(optional): the fleet frame the scene starts on, an integer from 0 to 2^32 - 1, at or aftergetEarliestStartFrame. The check allows one frame boundary to pass between that read and this call, so a start read just before is still taken, four frames ahead at the least; a second boundary and it throws. Also throws ifgetEarliestStartFrameisundefined, or before the first clock sync. Without it the scene is scheduled as described under Scenes.options.fadeFrames(optional): how many frames the boards take to dissolve from what they are showing into this scene, an integer from 16 to 255 (about 0.5 to 8 s). Defaults to 32, a second.
Returns: a promise that resolves once the channel has settled: no request is being held for it, and the last scene scheduled on it has been sent twice, at once and again four frames later as insurance against a lost packet. That can be some seconds after the call when the scene was held behind a running fade. Before the first clock sync it resolves at once, with the request held. The promise never rejects: without a device ID the sends are skipped but still count, and the scene goes out once the coordinator assigns one.
setOff
Turns off the lights on a channel. Off is a scene like any other: it fades in the same way and takes the same options, and setAnimationParameters turns the channel back on.
Signature:
function setOff(channel: number, options?: SceneOptions): Promise<void>;Arguments:
channel: the channel to turn off, from 0 to 7.options(optional): the same options assetAnimationParameters.
Returns: a promise that resolves once the channel has settled, as setAnimationParameters's does.
getEarliestStartFrame
Gets the earliest frame an explicit startFrame can take on a channel right now: five frames ahead, or the end of the running fade if that is later.
Signature:
function getEarliestStartFrame(channel: number): number | undefined;Returns: that frame, or undefined while a scene you scheduled is still ahead or a request is being held, in which case a call with a startFrame would throw. Read it again a frame later. A program scheduling on a grid should read it rather than keep its own copy of the channel's state, which goes stale when the fleet clock steps. Throws before the first clock sync.
The fleet clock
getAnimationClock
Signature:
function getAnimationClock(): number;Returns: the fleet's animation clock now, in milliseconds, which wraps at 2^32. Throws before the first clock sync.
toAnimationClock
Signature:
function toAnimationClock(localTime: number): number;Returns: the animation clock at a local time, as performance.now() gives it. Throws before the first clock sync.
toLocalTime
Signature:
function toLocalTime(animationClock: number): number;Returns: the local time, as performance.now() gives it, nearest to now that maps to the animation clock value; subtract performance.now() from it for a setTimeout delay. Throws before the first clock sync.
FRAME_PERIOD
The length of a frame in animation clock milliseconds, 32. A frame number is the animation clock divided by it, rounded down.
subtractFrames
Signature:
function subtractFrames(a: number, b: number): number;Returns: the signed number of frames from b to a, correct across the 2^27 frame wrap for differences of up to 2^26 frames, about 25 days.
Configuration
getNodes
Asks every board on the network to report itself.
Signature:
function getNodes(): Promise<NodeStatus[]>;Returns: a promise for the boards that answered within a second, sorted by device ID. A board that is off or not on the network is absent, and so is one whose reply was lost twice, so a missing board is worth a second call. Throws before connected.
configureNode
Sets one property on one board.
Signature:
function configureNode<P extends keyof NodeConfiguration>(
deviceId: number,
property: P,
value: NodeConfiguration[P]
): Promise<NodeStatus>;Arguments:
deviceId: the board, 0 to 239, asgetNodesreports it.propertyandvalue:channel0 to 7;brightness0 to 255;pixelCount1 toMAX_PIXEL_COUNT, the number of pixels the board renders and drives;colorOrder, the order the strip reads its three bytes in,GRBfor a WS2812B;statusLedBrightness0 to 255, for boards that have one;nickname, up to 16 printable ASCII characters. Throws for a value out of range.
Returns: a promise for the board's status carrying the new value, so a status the board sent for some other reason in the meantime isn't mistaken for the answer. A property the board doesn't honor, per capabilities in its status, is ignored, and the first status shows it unchanged. The board stores the change a second later. Rejects if the board hasn't answered after two attempts, about two seconds. Throws before connected.
identifyNode
Flashes a board white for five seconds, its strip and, on a board that has one, its status LED, so you can pick it out.
Signature:
function identifyNode(deviceId: number): Promise<NodeStatus>;Returns: a promise for the board's status, which confirms it heard. Rejects if the board hasn't answered after two attempts. Throws before connected.
MAX_PIXEL_COUNT
The most pixels a board drives, 300, which is the size of the firmware's strip buffers. A pixelCount past it throws.
NUM_CHANNELS
The number of channels, 8. A channel is 0 to NUM_CHANNELS - 1.
NICKNAME_LENGTH
The most characters a nickname holds, 16.
Animation builders
createAnimationParameters
Builds a complete parameter set from up to four layers. The layers are composited from the fourth up to the first, each blended over the result by its alpha.
Signature:
function createAnimationParameters(
animation1?: AnimationLayer,
animation2?: AnimationLayer,
animation3?: AnimationLayer,
animation4?: AnimationLayer
): AnimationParameters;Returns: parameters with the layers given and empty layers for the rest.
createEmptyAnimation
Signature:
function createEmptyAnimation(): AnimationLayer;Returns: a layer with every field 0, which renders as transparent black.
createSolidColorAnimation
Signature:
function createSolidColorAnimation(
h: number,
s: number,
a: number
): AnimationLayer;Arguments: hue, saturation and alpha, each 0 to 255.
createColorCycleAnimation
A layer whose hue cycles through the spectrum, the same color along the whole strip.
Signature:
function createColorCycleAnimation(rate: number, a: number): AnimationLayer;Arguments: a rate from 1 to 32 and an alpha from 0 to 255.
createMovingAnimation
A solid color whose alpha sweeps along the strip.
Signature:
function createMovingAnimation(
h: number,
s: number,
rate: number,
spacing: number
): AnimationLayer;Arguments: hue and saturation from 0 to 255, a rate from 0 to 32, and a spacing from 1 to 16.
createPulsingAnimation
A solid color whose alpha pulses, the same along the whole strip.
Signature:
function createPulsingAnimation(
h: number,
s: number,
rate: number
): AnimationLayer;Arguments: hue and saturation from 0 to 255 and a rate from 1 to 32.
createRainbowAnimation
A rainbow that moves along the strip.
Signature:
function createRainbowAnimation(a: number, rate: number): AnimationLayer;Arguments: an alpha from 0 to 255 and a rate from 1 to 32.
createSimpleWaveParameters
The controller boards' Wave preset without its foreground layer: a wave in one hue moving over a background in another. The strip's average color holds still while the pattern moves, so a scene change reads as a clean color shift.
Signature:
function createSimpleWaveParameters(options: {
rate: number;
waveHue: number;
backgroundHue: number;
brightness: number;
}): AnimationParameters;Arguments: a rate from 0 to 32, the two hues from 0 to 255, and a brightness from 0 to 255 applied to both layers. With one hue on both layers nothing moves, so keep them apart.
Returns: a complete parameter set, the wave in the first layer and the background in the third, as the preset lays them out.
Types
interface AnimationColorParameters {
a: number; // Amplitude, 0 to 255
b: number; // Offset, 0 to 255
w_t: number; // Rate over time, -128 to 127
w_x: number; // Rate along the strip, -128 to 127
phi: number; // Phase, -128 to 127
}
interface AnimationLayer {
h: AnimationColorParameters;
s: AnimationColorParameters;
v: AnimationColorParameters;
a: AnimationColorParameters;
}
interface AnimationParameters {
timePeriod?: number; // 1 to 255, default 255
distancePeriod?: number; // 1 to 255, default 32
animations: AnimationLayer[];
}
interface SceneOptions {
startFrame?: number;
fadeFrames?: number;
}
type ColorOrder = 'RGB' | 'RBG' | 'GRB' | 'GBR' | 'BRG' | 'BGR';
interface NodeConfiguration {
channel: number; // 0 to 7
brightness: number; // 0 to 255
pixelCount: number; // 1 to MAX_PIXEL_COUNT
colorOrder: ColorOrder;
statusLedBrightness: number; // 0 to 255
nickname: string; // up to 16 printable ASCII characters
}
// Which of the properties a board honors
type NodeCapabilities = { [P in keyof NodeConfiguration]: boolean };
interface NodeStatus extends NodeConfiguration {
deviceId: number;
capabilities: NodeCapabilities;
}Each color component of a layer is b + a × sin(w_t·t + w_x·x + phi) with the sine scaled 0 to 1, so it runs from b to a + b. t runs over timePeriod tenths of a second and x over distancePeriod pixels, so w_t and w_x count cycles per period. The layer's h, s and v make its color, and a is its alpha over the layers beneath it.
License
Copyright (c) Bryan Hughes [email protected]
This file is part of RVL Node.
RVL Node is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
RVL Node is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with RVL Node. If not, see http://www.gnu.org/licenses/.
