Control your BlinkStick devices from Node.js with a TypeScript-first, Promise-powered API and a powerful animation engine.
async/await everywhere – no legacy callbacks.Animation, AnimationBuilder and helpers.#ff0000, rgb(255,0,0)), tuples… even random.node-hid APIs - you can control devices without blocking the event loop.npm i @ginden/blinkstick-v2
import { findFirstAsync } from '@ginden/blinkstick-v2';
// Find the first connected device (throws if none are found)
const blinkstick = await findFirstAsync();
// Make the LED pulse between black ↔ purple for ~1 s.
// The promise resolves with `undefined` once the pulse is finished.
await blinkstick.pulse('purple');
👉 Need more? Browse the full, searchable API reference at https://ginden.github.io/blinkstick-node-v2.
This project is a fork of the original blinkstick-node library.
It aims to keep the spirit and feature-set of the original while bringing the code base into the modern TypeScript & Promise world.
BlinkStick Node provides an interface to control BlinkStick devices connected to your computer with Node.js.
What is BlinkStick? It's a tiny USB-controlled RGB LED device. Learn more at https://www.blinkstick.com.
AbortSignal (this is only partially supported, your mileage may vary)undefinedblinkstick.animation namespaceAnimation bag class for common animationsAnimationBuilder class for building animationsBlinkStickSync and BlinkStickAsync for sync and async APIs and future specialization
BlinkStickProSync and BlinkStickProAsync, as the Pro device seems to have lots of unusual featuresBREAKING CHANGES:
string when dealing with low-level data - use Buffer instead, we assume that you know what you are doingRead notes on hardware and firmware.
BlinkStick Square devices identify themselves as just BlinkStick. If you try to find base BlinkStick device using findFirst("BlinkStick"), it may find a BlinkStick Square instead. This is unlikely to affect users with only one BlinkStick device connected, but if you have both BlinkStick and BlinkStick Square, you may need to do some workarounds to distinguish them. Look at consts/device-descriptions.ts for detection logic.BlinkStick Flex won't work on Linux due to kernel limitations.If you want to gift or buy me a BlinkStick device for testing purposes, please email me.
Tested:
Should work:
Does not work:
Variable LED count
BlinkStick Flex and BlinkStick Pro come with a variable number of LEDs.
Library by default assumes that you have maximal number of LEDs available.
If not, you can set the number of LEDs using ledCount property on BlinkStick instance:
blinkstick.ledCount = 42;
or write it permanently to the device using setLedCount method:
await blinkstick.setLedCount(42);
Install using npm:
npm install @ginden/blinkstick-v2
Using async APIs is the recommended way. While even sync APIs use Promises, they may block the event loop, which is not a good practice.
Read docs of node-hid for more
information.
Note: under the hood the async flavour wraps node-hid’s HIDAsync class, while the sync API talks to HID.
The async version keeps the Node.js event loop free during USB I/O and is therefore recommended for most real-world applications.
import { BlinkStick, findFirstAsync } from '@ginden/blinkstick-v2';
const blinkstick = await findFirstAsync();
If you are using Async API, you might accidentally let Blinkstick instance to be garbage-collected. This will emit a
warning, because Blinkstick instance holds reference to C API object. To avoid it, just call close or
use explicit resource management.
Direct construction of BlinkStick is not recommended.
import { BlinkStick, findFirst } from '@ginden/blinkstick-v2';
const blinkstick = findFirst();
// Color names are allowed
await blinkstick.pulse('red');
// "random" is also allowed
await blinkstick.pulse('random');
// RGB values are allowed
await blinkstick.pulse(100, 0, 0);
// RGB values as hex string are allowed
await blinkstick.pulse('#ff0000');
// RGB values as hex string are allowed
await blinkstick.pulse('ff0000');
// Well, even rgb(255, 0, 0) is allowed
await blinkstick.pulse('rgb(255, 0, 0)');
await blinkstick.setColor('red');
// Will work only if you have at least 2 LEDs
await blinkstick.led(0).setColor('green');
await blinkstick.led(1).setColor('blue');
// Set color of all LEDs
await blinkstick.leds().setColor('yellow');
If you want to use usb instead of node-hid, you can do so by using functions provided within usb namespace, like that:
import { usb } from '@ginden/blinkstick-v2';
const stick = await usb.findFirst();
See advanced.md.
Let's start with an example:
import { findFirst, Animation } from '@ginden/blinkstick-v2';
// `animationApi` is **your own** module with helper functions;
// it is shown here only to illustrate that animations are just plain objects.
import { animationApi } from './animation-api';
const blinkstick = findFirst();
const animation = Animation.repeat(
  Animation.morphMany(['blue', 'purple', 'red', 'yellow', 'green', 'cyan'], 5000),
  12,
);
// Fire-and-forget – the call resolves immediately, the animation continues in the background
blinkstick.animation.runAndForget(animation);
// Or, let's consider using AnimationBuilder
import { AnimationBuilder } from '@ginden/blinkstick-v2';
const complexAnimation = AnimationBuilder.startWithBlack(50)
  // Add black-red-black pulse over 1 second
  .addPulse('red', 1000)
  // Appends new animation to the end of the current one
  .append(
    AnimationBuilder
      // Starts with white color
      .startWithColor('white', 1000)
      // Pulses to green over 500ms
      .addPulse('green', 500)
      // Pulses to yellow over 500ms
      .addPulse([255, 255, 0], 500)
      // Morphs to red over 500ms
      .morph(
        {
          r: 255,
          g: 0,
          b: 0,
        },
        500,
      )
      // Waits with result of previous steps for 1 second
      .wait(1000)
      .build(),
  )
  // Repeats the whole animation 3 times
  .repeat(3)
  // Wait with last frame for 1 second
  .wait(1000)
  // Morphs to purple over 1 second
  .morphToColor('purple', 1000)
  // This is really advanced feature that allows you to transform each frame
  .transformEachFrame((frame) => frame)
  .build();
Animation bag class is a simple convenience wrapper for several common animations and generates FrameIterable objects.
AnimationBuilder is a more advanced class that allows you to build complex animations.
What is FrameIterable?
type FrameIterable = Iterable<Frame> | AsyncIterable<Frame>;
SimpleFrame is a class of {rgb: RgbTuple, duration: number}. It's used by animation runner to change color of all LEDs at once.
ComplexFrame is a class of {leds: RgbTuple[], duration: number}. It's used by animation runner to change color of each LED separately. Number of LEDs must match the number of LEDs in the device.
WaitFrame is a class of {duration: number}. It's used by animation runner to wait for a given duration.
Most of Animation APIs will throw if you pass a generator. This is there to prevent you from shooting yourself in the foot.
Why?
import { SimpleFrame } from '@ginden/blinkstick-v2';
function* gen() {
  yield SimpleFrame.colorAndDuration('white', 500);
  yield SimpleFrame.colorAndDuration('red', 500);
}
repeat(gen(), 3);
// This would yield only 2 frames - generator doesn't implicitly "fork" when iterated multiple times
All built-in methods will throw if you try to generate animation with FPS higher than 100. As BlinkStick Nano is de facto limited to 75 FPS, it should be enough.
Your custom animation may be "faster" than that, but expect drift and other issues.
could not get feature report from device - this error occurs somewhere in the node-hid library and its dependencies,
and is most likely to occur when calling methods in tight loops. See https://github.com/node-hid/node-hid/issues/561The project is already usable in production, however some pieces of documentation are still scarce. Feel free to open a PR if you can help with any of the items below:
setMode, setChannel helpers and how they relate to the hardware.udev paragraph covers Linux, but Windows quirks and Zadig drivers need love too.Your contributions are highly appreciated! 🙏
If you get an error message on Linux:
Error: cannot open device with path /dev/hidraw0
Please run the following command:
echo "KERNEL==\"hidraw*\", SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"20a0\", ATTRS{idProduct}==\"41e5\", MODE=\"0666\"" | sudo tee /etc/udev/rules.d/85-blinkstick-hid.rules
Then either restart the computer or run the following command to reload udev rules:
sudo udevadm control --reload-rules && sudo udevadm trigger
If you use libusb, you may need slightly different rule:
SUBSYSTEM=="usb", ATTR{idVendor}=="20a0", ATTR{idProduct}=="41e5", MODE="0666"
Open pull requests, you are welcome.
To run tests, you need to have Blinkstick device connected to your computer. This makes it impossible to run tests on CI, and even typical automated testing is rather challenging.
Run npm run test:manual and follow the instructions. You should physically see the device changing colors, and you
will answer yes/no to the questions.
As most interesting parts of the library require a Blinkstick device and human eye to operate (both unavailable in GitHub Actions), we have rather limited automated tests, testing mostly utility functions and frame generation.
Just run npm test and it will run the tests.
You can run npm run repl to start a REPL with the library loaded. This is useful for quick experiments and testing.
npm run repl
Then you can run commands like:
await blinkStickNano.morph('red');
You can track reads and writes to the device by setting certain environment variables.
Eg.
export BLINKSTICK_DEBUG="/tmp/b-%PID.log"
The following variables are interpolated:
%PID – process ID of the current process%RELEASE - release version of the device%SERIAL - serial number of the device%NAME - name of the deviceThen run your script, and it will log all reads and writes to the device to the specified file.
This file is line-delimited JSON, with each line being a single read or write operation.
A proper coverage report would run both manual and automated tests. Feel free to open a PR if you have an idea how to do it.
Copyright (c) 2014 Agile Innovative Ltd and contributors Copyright (c) 2025 Michał Wadas
Released under MIT license.