npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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.

npm version CI Types License: MIT


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

npm install divoom-timesgate-sdk
# or
pnpm add divoom-timesgate-sdk
# or
yarn add divoom-timesgate-sdk

Requires 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 04 (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 helpers panelToLcdArray / panelsToLcdArray if you need them.
  • PicIDs. The device caches image frames by a strictly-increasing PicID. client.draw manages this automatically (you can override or read the device's value with getPicId()).
  • Hardware versions. Hardware 400 uses http://IP:80/post (the default). Hardware 402 uses http://IP:9000/divoom_api — pass port: 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 start

Security 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 to app.divoom-gz.com.
  • Image sources. The image helpers treat a string source as an http(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 a Buffer you fetched under your own controls.
  • Device-side fetches. animation.playGifUrls / playGifPerPanel / sendItemList({ backgroundGif }) and batch.useCommandSource make the device fetch a URL you supply (and useCommandSource executes 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 → ./docs

See 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