A modular Node.js driver for Waveshare E-Paper displays that supports multiple display models with different resolutions and color modes.
- Modular architecture: Easy to extend with new display models
- Multiple display support: Currently supports 8+ display models with more easily added
- Color mode support: Monochrome, grayscale and color modes where supported by hardware
- Common API: Unified interface across all display models
- Hardware abstraction: Base class handles common SPI/GPIO operations
- PNG image loading: Built-in support for loading and displaying PNG images
Not all devices have been tested in the field. Please create a GitHub issue if you have confirmed one of the untested platforms working.
| Model | Resolution | Color Modes | Description | Status |
|---|---|---|---|---|
| 2in13 | 122 × 250 | Monochrome | 2.13" black/white | Untested |
| 2in7 | 176 × 264 | Mono, 4-grayscale | 2.7" with grayscale support | Untested |
| 2in7b | 176 × 264 | 3-color | 2.7" black/white/red or yellow | Untested |
| 7in5 | 640 × 384 | Monochrome | 7.5" black/white | Confirmed working |
| 7in3f | 800 × 480 | 7-color | 7.3" full color (7 colors) | Untested |
| 13in3k | 960 × 680 | Mono, 4-grayscale | 13.3" with grayscale support | Confirmed working |
| 13in3b | 960 × 680 | 3-color | 13.3" black/white/red or yellow | Untested |
| 13in3gray | 1600 × 1200 | 16-grayscale | 13.3" 16-level grayscale (IT8951) | Experimental — not functional |
Note: the 13in3gray driver is gated behind an experimental: true option
because the IT8951's 16-bit command protocol is not yet implemented —
constructing it without that option throws. Contributions welcome.
npm install waveshare-epaperThe canvas module is an optional extra — install it separately
(npm install canvas) if you want to render with drawCanvas(); everything
else (including PNG loading) works without it.
By default GPIO pins are toggled by spawning libgpiod's gpioset/gpioget
CLI tools — a process per toggle. Installing the optional native binding
makes pin toggles native calls instead, which speeds up refreshes
substantially:
sudo apt install libgpiod-dev
npm install node-libgpiodNo code changes needed — the library detects node-libgpiod and uses it
automatically, falling back to the CLI tools when it isn't available. You can
check which backend is active with epd.gpio.constructor.name
(LibgpiodGpio vs CliGpio).
For security, add your user to the gpio group instead of running as root:
# Add current user to gpio group
sudo usermod -a -G gpio $USER
# Log out and back in, then verify group membership
groupsAfter logging back in, you can run GPIO programs without sudo.
const { createDisplay } = require('waveshare-epaper');
(async () => {
// Create display instance (13.3" 4-grayscale)
const epd = createDisplay('13in3k', '4gray', {
rstPin: 17,
dcPin: 25,
busyPin: 24,
pwrPin: 18
});
// Initialize and use
await epd.init();
await epd.clear();
// Draw with different gray levels (0=black, 1=dark gray, 2=light gray, 3=white)
epd.drawRect(10, 10, 100, 50, 0, true); // Black filled rectangle
epd.drawRect(120, 10, 100, 50, 1, true); // Dark gray rectangle
epd.drawRect(230, 10, 100, 50, 2, true); // Light gray rectangle
epd.drawLine(10, 80, 330, 80, 0); // Black line
// Update display
await epd.display();
await epd.sleep();
})();
⚠️ Experimental: this driver does not yet speak the IT8951's actual protocol and will not drive the panel. The example shows the intended API; it requiresexperimental: trueto run.
const { createDisplay } = require('waveshare-epaper');
(async () => {
const epd = createDisplay('13in3gray', '16gray', {
rstPin: 17,
dcPin: 25,
busyPin: 24,
pwrPin: 18,
vcom: -2.30, // Adjust according to your display
experimental: true // Driver is not yet functional; see note above
});
await epd.init();
await epd.clear();
// Draw with 16 different gray levels (0=black, 15=white)
const cellWidth = 100;
for (let i = 0; i < 16; i++) {
const x = (i % 8) * cellWidth;
const y = Math.floor(i / 8) * 60;
epd.drawRect(x, y, cellWidth - 2, 58, i, true);
}
// Display with high quality GC16 mode
await epd.display('GC16');
await epd.sleep();
})();Requires: npm install canvas
const { createDisplay } = require('waveshare-epaper');
(async () => {
const { createCanvas } = require('canvas');
const epd = createDisplay('13in3k', '4gray', {
rstPin: 17, dcPin: 25, busyPin: 24, pwrPin: 18
});
await epd.init();
// Create a simple canvas
const canvas = createCanvas(400, 200);
const ctx = canvas.getContext('2d');
// White background
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw text and shapes
ctx.fillStyle = 'black';
ctx.font = '36px Arial';
ctx.fillText('Canvas Demo', 50, 100);
// Draw a rectangle around the text
ctx.strokeStyle = 'gray';
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, 380, 180);
// Draw to display
await epd.drawCanvas(canvas, 100, 100);
await epd.display();
await epd.sleep();
})();Creates a display instance for the specified model.
model(string): Display model ('2in13', '2in7', '2in7b', '7in5', '7in3f', '13in3k', '13in3b', '13in3gray')colorMode(string): Color mode ('mono', '4gray', '16gray', '3color', '7color') - must be supported by the displayoptions(object): Configuration options
Options:
rstPin(number): Reset GPIO pin (default: 17)dcPin(number): Data/Command GPIO pin (default: 25)busyPin(number): Busy GPIO pin (default: 24)pwrPin(number): Power control GPIO pin (default: 18)busyTimeoutMs(number): How long to wait for the BUSY pin before throwing (default: 10000)maxImagePixels(number): Reject PNGs larger than this many pixels (default: ~16.7M)gpioChip(string): GPIO chip name (default: 'gpiochip0')busNumber(number): SPI bus number (default: 0)deviceNumber(number): SPI device number (default: 0)maxSpeedHz(number): SPI max speed (default: 4000000)
Note: there is no csPin option — chip select is driven by the SPI
controller's CE line (wire the panel's CS to CE0, or CE1 with
deviceNumber: 1), not by a GPIO.
accentColor(string): For 3-color displays, specify 'red' or 'yellow' accent colorvcom(number): VCOM voltage for IT8951 displays (default: -2.30)gpio(object): Custom GPIO backend (see Hardware Backends below)spi(object): Custom SPI backend (see Hardware Backends below)
GPIO and SPI access go through small injectable backends (see hal.js), so
you can swap in your own implementation — for unit testing without hardware,
or to use a different GPIO stack (e.g. a native libgpiod binding):
const epd = createDisplay('13in3k', 'mono', {
gpio: {
write: async (pin, value) => { /* drive a pin */ },
read: async (pin) => 0, // return 0 or 1
release: async () => { /* optional: free lines on cleanup() */ }
},
spi: {
open: async () => { /* open the bus */ },
transfer: async (buffer) => { /* write buffer to the bus */ },
close: () => { /* close the bus */ }
}
});The default GPIO backend is LibgpiodGpio (native node-libgpiod binding,
lines requested once and held) when that module is installed, otherwise
CliGpio (shells out to libgpiod's gpioset/gpioget via execFile with
input validation — no shell interpolation). The default SPI backend uses the
spi-device module, loaded only when actually used, so the library also runs
on machines without SPI support (e.g. in CI).
Returns array of supported models with their specifications.
await epd.init()- Initialize the displayawait epd.clear()- Clear display to background colorawait epd.display()- Update the display with current bufferawait epd.sleep()- Put display into low power modeawait epd.cleanup()- Put the panel into deep sleep, power it down and release SPI/GPIO resources
await epd.powerOn()- Turn on display power (automatically called during init)await epd.powerOff()- Turn off display power (automatically called during cleanup)
epd.setPixel(x, y, color)- Set individual pixelepd.drawLine(x0, y0, x1, y1, color)- Draw lineepd.drawRect(x, y, width, height, color, filled)- Draw rectangleawait epd.drawPNG(filePath, x, y)- Load and draw PNG imageawait epd.drawCanvas(canvas, x, y)- Draw HTML5 Canvas object
7-color displays:
epd.setPixelColor(x, y, colorName)- Set pixel using color nameepd.drawColorLine(x0, y0, x1, y1, colorName)- Draw colored lineepd.drawColorRect(x, y, w, h, colorName, filled)- Draw colored rectangleawait epd.show7Block()- Display all 7 colors in blocks
3-color displays:
epd.drawBlackRect(x, y, w, h, filled)- Draw black rectangleepd.drawRedRect(x, y, w, h, filled)- Draw accent color rectangleawait epd.show3ColorTest()- Display 3-color test pattern
- Monochrome mode:
0= black,1= white - 4-grayscale mode:
0= black,1= dark gray,2= light gray,3= white - 16-grayscale mode:
0= black,1-14= varying gray levels,15= white - 3-color mode:
0= black,1= white,2= accent color (red or yellow, configurable) - 7-color mode:
0= black,1= white,2= green,3= blue,4= red,5= yellow,6= orange
For 7-color displays, you can use color names:
epd.setPixelColor(x, y, 'RED');
epd.drawColorRect(10, 10, 100, 50, 'BLUE', true);
epd.drawColorLine(0, 0, 100, 100, 'GREEN');Available colors: BLACK, WHITE, RED, GREEN, BLUE, YELLOW, ORANGE
├── index.js # Main entry point and factory functions
├── EPDBase.js # Base class: SPI/GPIO protocol, drawing, image import
├── hal.js # Hardware abstraction layer (GPIO/SPI backends)
├── formats.js # Pixel format strategies (one class per color mode)
├── displays/
│ ├── index.js # Driver registry built from each driver's meta
│ ├── EPD2in13.js # 2.13" monochrome display driver
│ ├── EPD2in7.js # 2.7" mono/4-grayscale display driver
│ ├── EPD2in7b.js # 2.7" 3-color display driver
│ ├── EPD7in5.js # 7.5" monochrome display driver
│ ├── EPD7in3f.js # 7.3" 7-color display driver
│ ├── EPD13in3k.js # 13.3" mono/4-grayscale display driver
│ ├── EPD13in3b.js # 13.3" 3-color display driver
│ └── EPD13in3Gray.js # 13.3" 16-grayscale display driver (IT8951)
├── examples/ # Usage examples for each feature
└── README.md # This file
To add support for a new display model:
-
Create a new file in
displays/(e.g.,EPDNewModel.js) -
Extend
EPDBaseclass:const EPDBase = require('../EPDBase'); class EPDNewModel extends EPDBase { constructor(options = {}) { super(options); this.width = 200; // Set display width this.height = 200; // Set display height this.colorMode = options.colorMode || 'mono'; this.bitsPerPixel = this.colorMode === '4gray' ? 2 : 1; this.initializeBuffer(); } async initDisplay() { // Implement display-specific initialization } async displayImage() { // Implement display-specific image update } }
-
Export
metaandcreatefrom the module so the registry picks it up — the factory lookup andgetSupportedModels()are both derived from this:module.exports = { EPDNewModel, meta: { model: 'newmodel', aliases: ['n.m'], size: '200x200', colorModes: ['mono'], description: 'New 200x200 panel' }, create: (colorMode, options) => new EPDNewModel(options) };
-
Add the module to the
moduleslist indisplays/index.js
Pixel packing, clear values and RGB quantization come from the color mode's
format class in formats.js — a new panel with an existing color mode needs
no pixel code at all. A new color mode means adding one format class there.
- Raspberry Pi with SPI enabled
gpiodtools installed (apt install gpiod)- Node.js 14+ (recommended: Node.js 18+)
| Model | GPIO Chip | Pin Layout | SPI Interface | Initialization Notes |
|---|---|---|---|---|
| Pi 1 Model A/B | gpiochip0 |
26-pin (original) | /dev/spidev0.0 |
Limited pins, use adapted wiring |
| Pi 1 Model A+/B+ | gpiochip0 |
40-pin | /dev/spidev0.0 |
Full compatibility |
| Pi 2 Model B | gpiochip0 |
40-pin | /dev/spidev0.0 |
Full compatibility |
| Pi 3 Model A+/B+ | gpiochip0 |
40-pin | /dev/spidev0.0 |
Full compatibility |
| Pi 4 Model B | gpiochip0 |
40-pin | /dev/spidev0.0 |
Full compatibility |
| Pi 5 | gpiochip4 |
40-pin | /dev/spidev0.0 |
Requires gpioChip option |
Critical: Pi 5 uses gpiochip4 instead of gpiochip0. You must specify this in your configuration or initialization will fail:
const epd = createDisplay('13in3k', 'mono', {
rstPin: 17,
dcPin: 25,
busyPin: 24,
pwrPin: 18,
gpioChip: 'gpiochip4' // Required for Pi 5
});Original Pi 1 Model A/B have only 26 GPIO pins. Use this pin mapping:
- RST: GPIO 17 ✓ (available)
- DC: GPIO 25 ✓ (available)
- CS: GPIO 22 ✓ (available)
- BUSY: GPIO 24 ✓ (available)
- PWR: GPIO 18 ✓ (available)
The following GPIO pins are required for proper operation:
| Function | Default Pin | Description |
|---|---|---|
| RST | GPIO 17 | Reset signal (output) |
| DC | GPIO 25 | Data/Command signal (output) |
| CS | GPIO 22 | SPI Chip Select (output) |
| BUSY | GPIO 24 | Busy status signal (input) |
| PWR | GPIO 18 | Power control (output) - Critical for cold boot operation |
Important: The power pin (PWR) is essential for reliable operation, especially when cold-booting the Raspberry Pi. Many Waveshare e-paper displays require explicit power control to function properly. Without this pin, the display may:
- Not respond to commands after a cold boot
- Work only after running other display programs first
- Show inconsistent initialization behavior
The driver uses SPI interface with these default settings:
- SPI Bus:
/dev/spidev0.0(bus 0, device 0) - Speed: 4 MHz (configurable)
- Mode: SPI Mode 0 (CPOL=0, CPHA=0)
sudo raspi-config
# Navigate to: Interfacing Options → SPI → Enable
# Or add to /boot/config.txt:
echo "dtparam=spi=on" | sudo tee -a /boot/config.txt
sudo rebootSymptoms: Display appears to initialize (no errors) but screen doesn't update, or works only after running manufacturer's example programs.
Cause: Missing power pin configuration. Many Waveshare displays have internal power management that requires explicit control.
Solution: Always specify the power pin in your configuration:
const epd = createDisplay('13in3k', 'mono', {
rstPin: 17,
dcPin: 25,
busyPin: 24,
pwrPin: 18
});Permission errors: Make sure you're running with appropriate permissions or add your user to the gpio group:
sudo usermod -a -G gpio $USER
# Then log out and back inGPIO already in use: If you see "Device or resource busy" errors, check what's using the GPIO pins:
# Check what's using GPIO pins
sudo lsof /dev/gpiomem
# Or check specific pins
gpioinfo | grep -E "(17|18|24|25)"If you're unsure which GPIO chip your Pi uses, run:
gpioinfo | head -n 1For Pi 5, you'll see: gpiochip4 - 54 lines
For older Pi models: gpiochip0 - 54 lines (or similar)
You can also set the GPIO chip via environment variable:
export GPIO_CHIP=gpiochip4 # For Pi 5
node your-epd-program.jsThen check for this variable in your code:
const epd = createDisplay('13in3k', 'mono', {
gpioChip: process.env.GPIO_CHIP || 'gpiochip0'
});If while loading canvas you get this error:
Error: libcairo.so.2: ELF load command address/offset not page-aligned
Tru removing canvas and re-installing by compiling from source.
rm -fr node_modules/canvas/
npm install canvas --build-from-source
This can happen if you compile Node.js from source because the precompiled ciaro library that comes with canvas might not be compatible with what you have.
See the examples/ directory for working examples of each feature. Each example demonstrates a specific capability and can be copied directly into your projects.
const epd = createDisplay('7in3f', '7color');
await epd.init();
// Show all 7 colors in blocks
await epd.show7Block();
// Draw using color names
epd.drawColorRect(50, 50, 100, 80, 'RED', true);
epd.drawColorLine(0, 100, 800, 100, 'BLUE');
epd.setPixelColor(10, 10, 'GREEN');
await epd.display();// Black/White/Red display
const epd = createDisplay('13in3b', 'red');
// Or Black/White/Yellow display
const epd = createDisplay('13in3b', 'yellow');
await epd.init();
// Draw in different colors
epd.drawBlackRect(50, 50, 200, 100, true); // Black rectangle
epd.drawRedRect(300, 50, 200, 100, true); // Accent color rectangle
// White is the background color
// Show test pattern
await epd.show3ColorTest();The drawCanvas() method allows rendering HTML5 Canvas objects directly to the display. This enables dynamic text rendering with TrueType fonts, graphics drawing, and complex layouts:
const { createCanvas, registerFont } = require('canvas');
// Register a TrueType font
registerFont('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', { family: 'MyFont' });
// Create and configure canvas
const canvas = createCanvas(800, 400);
const ctx = canvas.getContext('2d');
// White background
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw text with custom font
ctx.fillStyle = 'black';
ctx.font = '48px MyFont';
ctx.textAlign = 'center';
ctx.fillText('Hello E-Paper!', canvas.width / 2, canvas.height / 2);
// Draw graphics
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
// Render to display
await epd.drawCanvas(canvas, 0, 0);Canvas Features:
- TrueType font rendering with
canvaspackage - Text, shapes, gradients, and complex graphics
- Automatic color conversion for all display modes
- Transparency support (transparent pixels become background color)
- Full HTML5 Canvas API compatibility
Requirements:
Install the canvas package for Canvas support:
npm install canvasSee canvas-example.js for a complete working example.
The driver automatically converts PNG images to the appropriate color format:
// 7-color display will convert RGB to nearest of 7 colors
await epd.drawPNG('colorful-image.png', 0, 0);
// 3-color display will detect red/yellow regions and convert others to black/white
await epd.drawPNG('mixed-color-image.png', 0, 0);The original display classes are still available for backward compatibility:
const { EPD13in3k } = require('waveshare-epaper');
const epd = new EPD13in3k();However, using the new factory function is recommended:
const { createDisplay } = require('waveshare-epaper');
const epd = createDisplay('13in3k', 'mono');To publish a new version to npm:
npm version patch # or minor/major
npm publishMIT License - see LICENSE file for details.
Based on Waveshare example code.