White Rabbit Ticker is a modular Node.js framework for driving a WLED 2D matrix as a scrolling text ticker.
It doesn’t care where the text comes from. Each module returns a string; the framework turns it into pixels and shoves it at WLED over UDP.
Built-in example modules:
- Home Assistant – date + indoor temps
- MPD – “Now Playing”
- Weather via wttr.in
- Local time (12-hour)
- Lobsters RSS headlines
- Plain text file
- Octoprint status (new 12/2/2025)
Follow this in order.
-
Power up your WLED controller with the matrix attached.
-
Open the WLED web UI (hostname like
wled-xxxx.localor the IP). -
In LED Preferences:
- Set total LEDs to
256for a 32×8 panel (or whateverWIDTH * HEIGHTwill be). - Enable 2D and set it to
32columns ×8rows (or your actual size).
- Set total LEDs to
-
Note the hostname or IP. Example:
wled-2f1f50.localor192.168.88.50.
In a terminal on the machine that will run the ticker:
node -v- If it prints v18.x or v20.x, good.
- If it says “command not found” or some ancient version, install a current Node.js (nvm, distro packages, etc.).
Example:
mkdir -p ~/white-rabbit-ticker && cd ~/white-rabbit-ticker
# put matrix.mjs and all the *.mjs modules in this folderYou should end up with something like:
matrix.mjshadata.mjsmpddata.mjswttr.mjstime.mjsfiledata.mjslobsters.mjs
Inside the project folder:
npm init -y && npm install node-fetchThat creates a package.json and installs node-fetch (used by the HA module).
Open matrix.mjs and set these near the top:
const WLED_IP = "wled-2f1f50.local";
const UDP_PORT = 21324;
const WIDTH = 32;
const HEIGHT = 8;
// ms between column steps (bigger = slower)
const SCROLL_INTERVAL_MS = 40;Change:
WLED_IPto your actual WLED hostname or IP.WIDTH/HEIGHTif your matrix isn’t 32×8.SCROLL_INTERVAL_MSif you want faster/slower scroll.
From the project folder:
node matrix.mjsOptional: if your host machine timezone isn’t what you want for the day/night schedule, run with a timezone override:
MATRIX_TZ=America/Chicago node matrix.mjs-
Engine:
matrix.mjs- Opens a UDP socket to WLED (DRGB realtime mode).
- Holds a small 5×7 bitmap font.
- Converts strings → 5-column glyphs → a stream of frames.
- Sends those frames to WLED as
[2, 2] + RGB bytesover UDP. - Walks a list of modules; each module runs once per loop and returns one string.
-
Modules: ES modules that export an object with
idandgetText():export default { id: "example-module", getText: async () => "TEXT TO SCROLL", };
If getText() returns "", the engine skips that module for that pass.
Typical folder contents:
matrix.mjs– core engine / main loophadata.mjs– Home Assistant date + room temperaturesmpddata.mjs– MPD “Now Playing”wttr.mjs– weather via wttr.intime.mjs– local clock, 12-hourfiledata.mjs– scroll a local text filelobsters.mjs– Lobsters RSS titles
Module wiring is at the top of matrix.mjs:
import haData from "./hadata.mjs";
import mpdData from "./mpddata.mjs";
// import lobsters from "./lobsters.mjs";
// import filedata from "./filedata.mjs";
import wttr from "./wttr.mjs";
import time from "./time.mjs";
const MODULES = [
haData,
mpdData,
// lobsters,
// filedata,
wttr,
time,
];- Comment/uncomment imports and entries in
MODULESto turn modules on or off. - Reorder items in
MODULESto change the display order.
The engine supports scheduled color behavior:
- Day mode (color cycling): 07:00 → 18:00
- Night mode (static dim red): 18:00 → 07:00
This is implemented in getCurrentTextColor() by comparing secondsSinceMidnight against:
const dayStart = 7 * 3600; // 07:00
const dayEnd = 18 * 3600; // 18:00Outside that window, it returns NIGHT_COLOR:
const NIGHT_COLOR = { r: 4, g: 0, b: 0 };During day mode, text color cycles smoothly through hue space. The full loop time is:
const DAY_CYCLE_SECONDS = 3600; // example: 1 hour per full color cycleDaytime brightness/saturation are controlled by:
const DAY_SAT = 1.0;
const DAY_VAL = 0.12;If you’re running very dim, low values can look “steppy” on LEDs. There’s an optional temporal dither to smooth the quantization:
const ENABLE_DITHER = 1; // 1 = on, 0 = offBy default, the day/night schedule uses the host’s local time.
If the box running Node has a different timezone than you want, set MATRIX_TZ:
MATRIX_TZ=America/Chicago node matrix.mjsMATRIX_TZ must be an IANA timezone string (e.g. America/Chicago, America/New_York, etc.).
To change the schedule window, edit these in getCurrentTextColor():
const dayStart = 7 * 3600;
const dayEnd = 18 * 3600;Examples:
-
Cycle 06:00 → 22:00
const dayStart = 6 * 3600; const dayEnd = 22 * 3600;
-
Cycle only 09:00 → 17:00
const dayStart = 9 * 3600; const dayEnd = 17 * 3600;
Edit in matrix.mjs unless noted.
const WLED_IP = "wled-xxxx.local"; // or an IP
const UDP_PORT = 21324;
const WIDTH = 32;
const HEIGHT = 8;const SCROLL_INTERVAL_MS = 40;Higher = slower. Lower = faster.
const NIGHT_COLOR = { r: 4, g: 0, b: 0 };
const DAY_CYCLE_SECONDS = 3600;
const DAY_SAT = 1.0;
const DAY_VAL = 0.12;
const ENABLE_DITHER = 1;
// env var (optional)
// MATRIX_TZ=America/ChicagoAnd the window itself is inside getCurrentTextColor():
const dayStart = 7 * 3600;
const dayEnd = 18 * 3600;Top of the file looks like:
const HA_URL = "http://YOURHOMEASSISTANTINSTANCE:8123";
const HA_TOKEN = "YOUR_LONG_LIVED_TOKEN_HERE";
const INSIDE1 = "sensor.master_bedroom_govee_sensor_temperature";
const INSIDE2 = "sensor.bedroom_temperature";
const INSIDE3 = "sensor.living_room_temperature";What to do:
- Set
HA_URLto your Home Assistant base URL. - Create a long-lived access token and paste it as
HA_TOKEN. - Change
INSIDE1/2/3to real entity IDs.
Example output:
Dec 1 Mbr: 72°F BDR: 70°F LR: 71°F
At the top:
const MPD_HOST = "YOURMPDINSTANCEBYIP";
const MPD_PORT = 6600;
const MPD_PASSWORD = "";
const MPD_TIMEOUT_MS = 250;Behavior:
- If MPD is playing: returns
Artist - Title(fallbacks if needed). - If paused/stopped/unreachable: returns
""so the module is skipped.
At the top:
const LOCATION = "YourCity";
const WTTR_URL = "https://wttr.in/YourCity?format=j1";
const CACHE_MS = 15 * 60 * 1000;No real config. Uses system time and returns a 12-hour string:
6:42 PM
At the top:
const FILE_PATH = "/home/youruser/wled-matrix/output.txt";Behavior:
- Reads the file.
- Collapses whitespace/newlines into one long line.
- Returns cleaned content, or
[FILE EMPTY], or[FILE ERROR].
Defaults to https://lobste.rs/rss.
Config options inside that module let you change:
- How many items to include
- Separator between titles
From the project folder:
node matrix.mjsWith a timezone override:
MATRIX_TZ=America/Chicago node matrix.mjsFor long-running:
- Use
tmux/screen, or - Write a small
systemdunit to runnode /path/to/matrix.mjs.
-
The engine normalizes text and looks up each character in a 5×7 font map.
-
Each character becomes 5 columns of bits plus a blank spacer column.
-
All columns are concatenated into a “virtual strip” wider than your matrix.
-
The engine slides a window of width
WIDTHacross that strip. -
For each step:
- It builds a full
WIDTH × HEIGHTRGB frame. - It sends
[2, 2, r, g, b, r, g, b, ...]over UDP. - It waits
SCROLL_INTERVAL_MSand moves one column.
- It builds a full
Pixel index mapping is row-major:
index = y * WIDTH + x
x: 0 → WIDTH-1 (left to right)
y: 0 → HEIGHT-1 (top to bottom)
If it looks mirrored / upside-down, fix the 2D layout in WLED, not the code.
Checklist:
-
Can you reach WLED from the Node box?
ping yourwled.local # or ping 192.168.88.50 -
LED count correct?
- Total LEDs should equal
WIDTH * HEIGHT.
- Total LEDs should equal
-
Realtime allowed?
- In WLED → Sync Interfaces: make sure UDP realtime is enabled/allowed.
-
Firewall:
- Ensure UDP/21324 isn’t being dropped.
- The code assumes straight row-major mapping.
- In WLED’s 2D settings, flip rotation/mirroring/serpentine until it’s correct.
From the Node box:
telnet MPD_HOST MPD_PORTYou should see something like OK MPD 0.22.x.
- Verify
HA_URLand token. - Confirm entity IDs actually exist.
- Open the wttr URL in a browser.
- Lower
CACHE_MSfor faster testing.
Once the basic config is in place and it’s running without errors, you basically leave it alone and let White Rabbit Ticker drag whatever text you throw at it across the LEDs.