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

@multiviewer/led-panel-driver

v0.1.0

Published

TypeScript driver for BLE RGB LED matrix panels (LED_BLE_*, 32x32 and 64x16). Works in Node, Electron main and renderer (Web Bluetooth).

Readme

led-panel-driver

TypeScript driver for Bluetooth LE RGB LED matrix panels that advertise as LED_BLE_* (sold as BK Light, among other brands). It shows images, animations, native scrolling text and a clock, and controls brightness, power and orientation.

Built for Electron: use it from the main process (Node, through noble) or from the renderer (Web Bluetooth). It also works in plain Node and in Chromium browsers.

| Panel | Device type | Size | Checked on hardware | | ------- | ----------- | ----- | ------------------- | | ACT1026 | 0x81 | 32x32 | yes | | ACT1025 | 0x83 | 64x16 | yes |

Other sizes in the same family are in the device table, but nobody has tested them yet.

Install

pnpm add @multiviewer/led-panel-driver

For the main-process or Node route, also add the Bluetooth library:

pnpm add @stoprocent/noble

Types: ./node needs @types/node, ./web needs @types/web-bluetooth, ./electron and ./electron/preload need the types that ship with the electron package.

The package has a led-panel command-line tool (see "Command line"). In a project that depends on the package, run it with pnpm exec led-panel.

Install warnings

  • On macOS, pnpm install can report that @stoprocent/bluetooth-hci-socket failed to build. That is harmless: it is noble's Linux-only HCI backend, and macOS does not use it.
  • In this repository, the example app can warn that it cannot link the led-panel bin. The bin file only exists after the library is built; the warning goes away after pnpm --filter @multiviewer/led-panel-driver build.

Entry points

| Import | Runs in | What it adds | | ------------------------------------------------ | -------------------------- | ------------------------------------------------------------------------------ | | @multiviewer/led-panel-driver | anywhere | client, protocol, image and text helpers, mock transport | | @multiviewer/led-panel-driver/node | Node, Electron main | NobleTransport, findPanel, scanForPanels, shutdownNoble, PNG decoding | | @multiviewer/led-panel-driver/web | browser, Electron renderer | WebBluetoothTransport, requestPanel | | @multiviewer/led-panel-driver/electron | Electron main | registerLedPanelIpc, enableWebBluetooth, shutdownNoble | | @multiviewer/led-panel-driver/electron/preload | Electron preload | exposeLedPanelApi, LedPanelApi and the Bridge* types (BridgePanelInfo) |

Electron, main-process route (recommended)

The main process owns the panels. A sandboxed, context-isolated renderer drives them over IPC.

// main.ts
import { app, BrowserWindow, ipcMain } from "electron";
import { registerLedPanelIpc } from "@multiviewer/led-panel-driver/electron";

app.whenReady().then(() => {
  registerLedPanelIpc(ipcMain, {
    app, // releases the panels and stops noble on quit
    // Only your own pages may use the panels. Without isAllowedSender, every sender is accepted.
    isAllowedSender: (event) => event.senderFrame?.url.startsWith(APP_URL) ?? false,
  });
  new BrowserWindow({ webPreferences: { preload: PRELOAD_PATH, sandbox: true } });
});

isAllowedSender runs for every IPC call and restricts which pages may use the panels. The default accepts every sender, so set it when the app can show pages you do not control.

// preload.ts (bundle it into one CommonJS file, see "Electron notes")
import { exposeLedPanelApi } from "@multiviewer/led-panel-driver/electron/preload";
exposeLedPanelApi(); // window.ledPanel
// renderer.ts
import type { LedPanelApi } from "@multiviewer/led-panel-driver/electron/preload";
declare global {
  interface Window {
    ledPanel: LedPanelApi;
  }
}

const [first] = await window.ledPanel.scan({ timeoutMs: 5000 });
if (!first) throw new Error("no panel found");
const panel = await window.ledPanel.connect(first.id);

await window.ledPanel.showText(panel.id, "HELLO", { color: "#ffaa00", animation: "scrollLeft" });

const ctx = canvas.getContext("2d")!;
function frame() {
  draw(ctx);
  const { width, height, data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
  void window.ledPanel.drawFrame(panel.id, { width, height, data }); // latest frame wins
  requestAnimationFrame(frame);
}
frame();

Electron, renderer route (Web Bluetooth)

// main.ts
import { enableWebBluetooth } from "@multiviewer/led-panel-driver/electron";
enableWebBluetooth(win); // picks the panel in Electron's device chooser event
// renderer.ts, inside a click handler (Web Bluetooth needs a user gesture)
import { LedPanelClient, requestPanel } from "@multiviewer/led-panel-driver/web";

const client = await LedPanelClient.connect(await requestPanel());
await client.showImage(ctx.getImageData(0, 0, 64, 16));

The page must be a secure context (file://, https:// or http://localhost).

Node

import { LedPanelClient, findPanel, shutdownNoble } from "@multiviewer/led-panel-driver/node";

const client = await LedPanelClient.connect(await findPanel({ timeoutMs: 8000 }));
console.log(client.info); // { deviceType: 0x81, width: 32, height: 32, ... }

await client.setBrightness(60);
await client.showText("Hi", { color: "#00ff88" });
await client.resumeStoredLoop(); // back to the images stored on the panel
await client.disconnect();
await shutdownNoble(); // lets the process exit

Client API

| Method | Does | | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | showImage(image, { fit, kernel, background }) | resize to the panel, encode PNG, upload | | drawFrame(image) | fast frame streaming for animation; a newer frame replaces a queued one | | showText(text, { color, background, animation, speed }) | text drawn and animated by the panel itself; returns missing glyphs | | showPng, showGif, showRgb | upload data that is already encoded | | setBrightness(0-100), setPower(on), setOrientation(0-3) | display settings (the panel cannot report them back) | | showClock({ style, hour24, showDate }), setTime(date) | built-in clock | | resumeStoredLoop() | leave live content and go back to the stored images | | on("connect" \| "disconnect" \| "reconnect" \| "notification" \| "error", fn) | events |

Text animations: static, scrollLeft, scrollRight, blink, breathe, snowflake, laser, and scrollUp / scrollDown on 32-pixel-high panels only.

All content goes to slot 0 by default: it shows at once and does not overwrite the images stored on the panel. Slots 1-100 are persistent and overwrite stored content, so only use them on purpose. eraseAll() and deleteSlots() exist but are destructive.

The client reconnects by itself after a link loss (on the next operation) and runs every command through one queue.

Measured frame rates with two panels streaming at the same time: about 10 fps per panel through the main-process bridge and about 12 fps through Web Bluetooth.

Command line

led-panel scan --info
led-panel text "HELLO" --panel all --animation scrollLeft --color "#ffaa00"
led-panel image picture.png --panel LED_BLE_XXXXXXXX --fit cover
led-panel brightness 40 --panel all
led-panel --panel all resume
led-panel --help

Global options (--panel, --timeout, --verbose, --json, --help) work before or after the command. --panel takes an advertised name, a Bluetooth id or all, and repeats.

Electron notes

  • Quit: an Electron main process that loaded noble does not exit on macOS unless noble is stopped. registerLedPanelIpc(ipcMain, { app }) does this on quit; otherwise call shutdownNoble() before quitting.
  • Preload: a sandboxed preload can only require("electron"), so bundle it into one CommonJS file (the example uses tsdown).
  • Images over the bridge: pass { width, height, data }. An ImageData object arrives empty through contextBridge.
  • Errors over the bridge: contextBridge keeps only the message, so bridge errors look like "LedPanelNotFoundError: ...". Check the prefix, not error.name.
  • macOS packaging: add NSBluetoothAlwaysUsageDescription to the app's Info.plist (for electron-builder: mac.extendInfo). A Mac App Store build also needs the com.apple.security.device.bluetooth entitlement. noble ships N-API prebuilds that load in Electron without a rebuild; unpack them from the asar archive.
  • Web Bluetooth: Electron has no device picker. To choose one of several panels, pass its exact name in requestPanel({ namePrefixes: [name] }).

Example app

examples/electron has both routes side by side, with a panel list, a streamed animation, text, a test image, brightness, power and "resume stored images".

pnpm install
pnpm --filter led-panel-electron-example start

Development

pnpm install
pnpm --filter @multiviewer/led-panel-driver test
pnpm --filter @multiviewer/led-panel-driver build
pnpm lint

docs/PROTOCOL.md is the byte-level protocol reference. AGENTS.md has the contributor rules, including the commands that must never be sent to real panels.