divoom-timesgate-sdk
v0.1.0
Published
A fully type-safe Node.js & TypeScript SDK for controlling the Divoom Times Gate over your local network — high-resolution image & animation pipeline, complete command coverage, and zero-guesswork docs.
Maintainers
Readme
divoom-timesgate-sdk
A fully type-safe Node.js & TypeScript SDK for controlling the Divoom Times Gate over your local network — with a high-resolution image & animation pipeline, complete command coverage, and zero-guesswork docs.
Highlights
- 🧊 Every documented command, fully typed — system settings, dial/channel control, built-in tools, animations, and the image pipeline.
- 🖼️ High-resolution image pipeline — push crisp 128×128 JPEG frames (LANCZOS3 + q95, 4:4:4 chroma) via
sharp. Album-art styling, accent extraction, designed "now-playing" panels, and multi-panel panoramas. - 🧠 Ergonomic + safe — grouped client API (
client.system,client.draw, …), argument validation with descriptive errors, and a typed error hierarchy. - 🔌 Modern packaging — dual ESM/CJS, first-class types, tree-shakeable. The image module is a separate entry point so core-only users never pull in
sharp. - 🔎 LAN discovery + cloud helpers — find your device's IP, and browse dials/images/fonts from Divoom's cloud.
- ✅ Tested & documented — extensive unit tests and TSDoc on every public symbol.
Table of contents
- Install
- Quick start
- Getting your
LocalToken - Finding your device
- Core concepts
- The image pipeline
- Command reference
- Cloud discovery
- Error handling
- Configuration
- Examples
- Development
Install
npm install divoom-timesgate-sdk
# or
pnpm add divoom-timesgate-sdk
# or
yarn add divoom-timesgate-sdkRequires Node.js 18+ (uses the global fetch). sharp is included for the image pipeline and ships prebuilt binaries for common platforms.
Quick start
import { TimesGateClient } from 'divoom-timesgate-sdk';
import { encodePanel } from 'divoom-timesgate-sdk/image';
const client = new TimesGateClient({
host: '192.168.1.50', // your Times Gate's LAN IP
localToken: 229930, // from the Divoom app → device → Settings (see below)
});
// Is it reachable?
if (!(await client.ping())) throw new Error('Times Gate not reachable');
// Set the brightness
await client.system.setBrightness(80);
// Push album art (or any image — file path, URL, or Buffer) to panel 0
const frame = await encodePanel('https://example.com/cover.jpg');
await client.draw.sendImage(0, frame.data);Getting your LocalToken
Many control commands (such as Channel/SetBrightness) are authenticated with a LocalToken — a stable, per-device number.
⚠️ The current public API docs don't list
LocalToken, but real devices still use it. You can read it in the Divoom mobile app → your Times Gate → Settings. It looks like a number, e.g.229930.
Pass it once when you create the client and the SDK injects it into every request:
const client = new TimesGateClient({ host: '192.168.1.50', localToken: 229930 });Read-only commands generally work without it, so for quick experiments you can omit it (it defaults to 0). If a command comes back with a DivoomDeviceError, a missing/incorrect LocalToken is the usual cause.
Finding your device
If you don't know the device's IP, discover it from the same network:
import { discoverDevices, TimesGateClient } from 'divoom-timesgate-sdk';
const devices = await discoverDevices();
// → [{ name: 'Times Gate', id: 300038420, ip: '192.168.1.50', mac: 'a8032aff46b1' }]
const gate = devices.find((d) => d.name.toLowerCase().includes('times gate'));
const client = new TimesGateClient({ host: gate!.ip, localToken: 229930 });Discovery goes through Divoom's cloud (it reports devices seen from your public IP), so it needs outbound internet and won't work across different networks.
Core concepts
- 5 panels. The Times Gate has five LCD panels, indexed
0–4(PanelIndex), each 128×128. - Panel masks. Some commands act on multiple panels at once via an
LcdArray— a 5-element mask like[1, 0, 0, 0, 1](1= act,0= skip). The SDK builds these for you from panel indices, with helperspanelToLcdArray/panelsToLcdArrayif you need them. - PicIDs. The device caches image frames by a strictly-increasing
PicID.client.drawmanages this automatically (you can override or read the device's value withgetPicId()). - Hardware versions. Hardware 400 uses
http://IP:80/post(the default). Hardware 402 useshttp://IP:9000/divoom_api— passport: 9000, path: '/divoom_api'.
The image pipeline
The image helpers live in the divoom-timesgate-sdk/image entry point (so projects that only send pre-encoded bytes never load sharp). Every helper returns an EncodedFrame whose .data is the base64 string you hand to client.draw.sendImage(...).
import {
encodePanel,
prepareAlbumArt,
renderTextPanel,
splitImageAcrossPanels,
getAccentColor,
solidFrame,
} from 'divoom-timesgate-sdk/image';Resize & encode any image
const frame = await encodePanel('./photo.png', { fit: 'cover' }); // 128×128 JPEG q95
await client.draw.sendImage(0, frame.data);encodePanel accepts a file path, an http(s) URL, a Buffer/Uint8Array, or a sharp instance.
Album art (smooth or pixel)
const art = await prepareAlbumArt(coverUrl, { style: 'smooth' }); // or 'pixel'
const accent = await getAccentColor(coverUrl); // vivid color for theming → '#E94F37'
await client.draw.sendImage(0, art.data);Designed "now-playing" text panel
Crisp, auto-wrapped text rendered with Pango — no reliance on host system fonts:
const panel = await renderTextPanel({
eyebrow: 'Now Playing',
title: 'Bohemian Rhapsody',
subtitle: 'Queen',
accent: '#E94F37',
progress: 0.42, // draws a progress bar
});
await client.draw.sendImage(1, panel.data);One image across all five panels
const tiles = await splitImageAcrossPanels('./wide-banner.png'); // panorama
for (const { panel, frame } of tiles) {
await client.draw.sendImage(panel, frame.data);
}Animations
// Build frames however you like (here: a color cycle) and play them.
const frames = await Promise.all(
['#FF0066', '#FF9900', '#33CC66', '#3399FF'].map((c) => solidFrame(c)),
);
await client.draw.sendAnimation(
[0],
frames.map((f) => f.data),
{ picSpeed: 120 },
);Command reference
Everything hangs off a TimesGateClient, grouped by area. A few highlights:
client.system
| Method | Command | Notes |
| --------------------------------------------- | ----------------------- | -------------------------- |
| setBrightness(0–100) | Channel/SetBrightness | |
| getAllConfig() | Channel/GetAllConf | brightness, formats, flags |
| setScreen(on) | Channel/OnOffScreen | |
| setTimeZone('GMT-5') | Sys/TimeZone | |
| setSystemTime(date) | Device/SetUTC | Date or epoch seconds |
| setWeatherLocation(lon, lat) | Sys/LogAndLat | |
| getWeather() | Device/GetWeatherInfo | |
| setTemperatureMode('celsius'\|'fahrenheit') | Device/SetDisTempMode | |
| setMirrorMode(enabled) | Device/SetMirrorMode | |
| setHourMode(use24Hour) | Device/SetTime24Flag | |
| getDeviceTime() | Device/GetDeviceTime | |
client.draw (image pipeline)
await client.draw.sendImage(panel, frame.data, { picSpeed: 1000 });
await client.draw.sendImageToPanels([0, 2, 4], frame.data);
await client.draw.sendAnimation([0], frames, { picSpeed: 100 });
await client.draw.resetCache(); // clear stuck frames / reset the PicID counter
const id = await client.draw.getPicId();client.dial (what each panel shows)
await client.dial.selectWholeDial(7); // one dial across all 5 panels
await client.dial.setChannelMode('independent', 978);
await client.dial.selectSubDial(0, 10, 978); // panel 0 → dial 10
const { SelectIndex } = await client.dial.getIndex();client.tool
await client.tool.setCountdown(1, 30); // 1:30 countdown, start
await client.tool.setStopwatch('start'); // 'start' | 'stop' | 'reset'
await client.tool.setScoreboard(3, 5); // red, blue
await client.tool.setNoiseMeter(true);
await client.tool.playBuzzer({ activeMs: 200, offMs: 100, totalMs: 900 });client.animation
await client.animation.playGifUrls([0], ['http://f.divoom-gz.com/64_64.gif']);
await client.animation.playStoredGif([0], fileId); // FileId from the cloud client
await client.animation.sendText(0, 'Hello!', { align: 'center', color: '#FFD400' });
await client.animation.sendItemList(1, items, { backgroundGif });client.batch
await client.batch.run([
{ Command: 'Channel/SetBrightness', Brightness: 100 },
{ Command: 'Channel/OnOffScreen', OnOff: 1 },
]);Escape hatch
For anything not yet wrapped, send a raw payload (still typed, LocalToken still injected):
const res = await client.send<{ error_code: number; PicId: number }>({
Command: 'Draw/GetHttpGifId',
});Cloud discovery
Two cloud-backed helpers (HTTPS to app.divoom-gz.com) complement the on-device API:
discoverDevices(options?)→Promise<DiscoveredDevice[]>— find Times Gates on your network and read each one's IP, id, and MAC. See Finding your device for a full example.DivoomCloudClient— browse dials, stored images, and fonts; their ids then drive on-device commands.
import { DivoomCloudClient } from 'divoom-timesgate-sdk';
const cloud = new DivoomCloudClient();
const { ClockList } = await cloud.getWholeDialList(1);
await client.dial.selectWholeDial(ClockList[0].ClockId);
const { DialTypeList } = await cloud.getDialTypes();
const { DialList } = await cloud.getDialList(DialTypeList[0], 1);
const { FontList } = await cloud.getFontList();Error handling
Every error extends DivoomError, so you can catch the whole family or narrow to a specific cause:
import {
DivoomError,
DivoomDeviceError,
DivoomTimeoutError,
DivoomValidationError,
} from 'divoom-timesgate-sdk';
try {
await client.system.setBrightness(150); // invalid → throws before any request
} catch (err) {
if (err instanceof DivoomValidationError) console.error('Bad argument:', err.message);
else if (err instanceof DivoomTimeoutError) console.error('Device too slow');
else if (err instanceof DivoomDeviceError) console.error('Device said no:', err.errorCode);
else if (err instanceof DivoomError) console.error('Divoom error:', err.message);
}| Error | When |
| ----------------------- | ------------------------------------------- |
| DivoomValidationError | Bad argument, caught before sending. |
| DivoomConnectionError | Device unreachable. |
| DivoomTimeoutError | Request exceeded the timeout. |
| DivoomHttpError | Non-2xx HTTP response. |
| DivoomDeviceError | Device returned a non-zero error_code. |
| DivoomCloudError | Cloud API returned a non-zero ReturnCode. |
Configuration
const client = new TimesGateClient({
host: '192.168.1.50',
localToken: 229930,
port: 80, // 9000 for hardware version 402
path: '/post', // '/divoom_api' for hardware version 402
protocol: 'http',
timeoutMs: 8000, // per-request timeout
retries: 2, // automatic retries for transient failures
retryDelayMs: 300, // exponential backoff base
fetch: customFetch, // inject your own fetch (proxies, tests, …)
});Transient failures (network errors, timeouts, 5xx) are retried with exponential backoff; device-level and 4xx errors are surfaced immediately.
Examples
Runnable example apps live in examples/:
| Example | What it shows |
| --------------------------- | ------------------------------------------------------ |
| 01-quickstart | Connect, ping, set brightness, push a frame. |
| 02-spotify-now-playing | Album art + live now-playing panel (the flagship). |
| 03-system-and-weather | Configure the device and read weather/config. |
| 04-tools | Countdown, stopwatch, scoreboard, noise meter, buzzer. |
| 05-panorama | One image spanning all five panels. |
| 06-animation | Multi-frame animations and hosted GIFs. |
| 07-text-and-notifications | Designed text panels and scrolling text. |
pnpm install
pnpm --filter @divoom-timesgate-examples/01-quickstart startSecurity notes
- Your LAN is the trust boundary. The device API is plain HTTP and unauthenticated on the local network aside from the per-device
LocalToken. The optional cloud helpers (discoverDevices,DivoomCloudClient) use HTTPS toapp.divoom-gz.com. - Image sources. The
imagehelpers treat a string source as anhttp(s)URL or a local file path. Don't pass untrusted/attacker-influenced strings — a URL can trigger a server-side request (SSRF) and a path can read local files. URL downloads are bounded by a timeout and a size cap; for untrusted input, validate the source yourself or pass aBufferyou fetched under your own controls. - Device-side fetches.
animation.playGifUrls/playGifPerPanel/sendItemList({ backgroundGif })andbatch.useCommandSourcemake the device fetch a URL you supply (anduseCommandSourceexecutes the returned commands). Only pass URLs you trust; allowlist hosts if end users can influence them.
See SECURITY.md for reporting and more detail.
Development
pnpm install
pnpm build # tsup → dual ESM/CJS + d.ts
pnpm test # vitest
pnpm test:coverage # coverage report
pnpm typecheck # tsc --noEmit
pnpm lint # eslint
pnpm format # prettier --write
pnpm check # typecheck + lint + format:check + test
pnpm docs # typedoc → ./docsSee CONTRIBUTING.md for the contribution workflow (we use Changesets for versioning).
Acknowledgements
Command shapes are transcribed from Divoom's official ShowDoc API. This project is not affiliated with or endorsed by Divoom.
License
MIT © Jason Praful
