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)undefined
blinkstick.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.