Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions EPDBase.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require('fs');
const { PNG } = require('pngjs');
const { CliGpio, SpiDeviceBackend, validatePin } = require('./hal');
const { createDefaultGpio, SpiDeviceBackend, validatePin } = require('./hal');

class EPDBase {
constructor(options = {}) {
Expand Down Expand Up @@ -40,7 +40,7 @@ class EPDBase {

// Hardware backends - injectable for testing or alternate platforms
// (see hal.js for the gpio/spi interfaces)
this.gpio = options.gpio || new CliGpio(this.gpioChip);
this.gpio = options.gpio || createDefaultGpio(this.gpioChip);
this.spi = options.spi || new SpiDeviceBackend(this.busNumber, this.deviceNumber, this.spiOptions);

this.initialized = false;
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ The `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.

### Faster GPIO (Recommended)

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:

```bash
sudo apt install libgpiod-dev
npm install node-libgpiod
```

No 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`).

### GPIO Permissions (Recommended)
For security, add your user to the `gpio` group instead of running as root:

Expand Down Expand Up @@ -203,10 +220,12 @@ const epd = createDisplay('13in3k', 'mono', {
});
```

The default backends shell out to libgpiod's `gpioset`/`gpioget` (with input
validation, no shell interpolation) and use the `spi-device` module. The
`spi-device` module is only loaded when the default SPI backend is used, so
the library also runs on machines without SPI support (e.g. in CI).
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).

#### `getSupportedModels()`
Returns array of supported models with their specifications.
Expand Down
70 changes: 70 additions & 0 deletions hal.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,64 @@ function validateGpioChip(chip) {
return chip;
}

// GPIO backend over the node-libgpiod native binding. Requests each line
// once and holds it, so a pin toggle is a native call instead of a process
// spawn - orders of magnitude faster than CliGpio. Used automatically when
// node-libgpiod is installed (npm install node-libgpiod).
class LibgpiodGpio {
constructor(chip = 'gpiochip0', binding = null) {
this.chipName = validateGpioChip(chip);
this.binding = binding || require('node-libgpiod');
this.chip = new this.binding.Chip(this.chipName);
this.lines = new Map(); // pin -> { line, direction }
}

requestLine(pin, direction) {
const p = validatePin('pin', pin);
let entry = this.lines.get(p);

// Re-request if the pin changes direction (shouldn't happen in
// practice - BUSY is the only input and is never written)
if (entry && entry.direction !== direction) {
entry.line.release();
this.lines.delete(p);
entry = null;
}

if (!entry) {
const line = this.chip.getLine(p);
if (direction === 'out') {
line.requestOutputMode('waveshare-epaper');
} else {
line.requestInputMode('waveshare-epaper');
}
entry = { line, direction };
this.lines.set(p, entry);
}

return entry.line;
}

async write(pin, value) {
this.requestLine(pin, 'out').setValue(value ? 1 : 0);
}

async read(pin) {
return this.requestLine(pin, 'in').getValue();
}

async release() {
for (const { line } of this.lines.values()) {
try {
line.release();
} catch (error) {
// Ignore errors during release
}
}
this.lines.clear();
}
}

// GPIO backend that drives libgpiod's gpioset/gpioget CLI tools. Inputs are
// validated and passed as discrete execFile arguments so nothing ever goes
// through a shell.
Expand Down Expand Up @@ -101,9 +159,21 @@ class SpiDeviceBackend {
}
}

// Pick the best available GPIO backend: the node-libgpiod native binding if
// it is installed and the chip can be opened, otherwise the CLI tools.
function createDefaultGpio(chip) {
try {
return new LibgpiodGpio(chip);
} catch (error) {
return new CliGpio(chip);
}
}

module.exports = {
CliGpio,
LibgpiodGpio,
SpiDeviceBackend,
createDefaultGpio,
validatePin,
validateGpioChip
};
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@
"devDependencies": {
"canvas": "^3.2.0"
},
"peerDependencies": {
"node-libgpiod": ">=0.5.0"
},
"peerDependenciesMeta": {
"node-libgpiod": {
"optional": true
}
},
"keywords": [
"waveshare",
"e-paper",
Expand Down
74 changes: 74 additions & 0 deletions test/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,80 @@ test('waitUntilIdle: UC8176-class (7in5, 7in3f) waits while BUSY reads 0', async
assert.strictEqual(await countBusyPolls('7in3f', [0, 1]), 2);
});

// --- Native GPIO backend --------------------------------------------------

// Fake node-libgpiod binding matching the Chip/Line API of v0.6
function createFakeLibgpiod() {
const state = { values: new Map(), requested: [], released: [] };

class FakeLine {
constructor(pin) { this.pin = pin; }
requestOutputMode() { state.requested.push({ pin: this.pin, direction: 'out' }); }
requestInputMode() { state.requested.push({ pin: this.pin, direction: 'in' }); }
setValue(v) { state.values.set(this.pin, v); }
getValue() { return state.values.get(this.pin) ?? 0; }
release() { state.released.push(this.pin); }
}

class FakeChip {
constructor(identifier) { state.chipIdentifier = identifier; }
getLine(pin) { return new FakeLine(pin); }
}

return { binding: { Chip: FakeChip, Line: FakeLine }, state };
}

test('LibgpiodGpio caches lines and round-trips values', async () => {
const { LibgpiodGpio } = require('../hal');
const { binding, state } = createFakeLibgpiod();
const gpio = new LibgpiodGpio('gpiochip0', binding);

assert.strictEqual(state.chipIdentifier, 'gpiochip0');

await gpio.write(17, 1);
await gpio.write(17, 0);
await gpio.write(17, 1);
// Three writes, but the line is requested only once
assert.deepStrictEqual(state.requested, [{ pin: 17, direction: 'out' }]);
assert.strictEqual(state.values.get(17), 1);

state.values.set(24, 1);
assert.strictEqual(await gpio.read(24), 1);
assert.deepStrictEqual(state.requested[1], { pin: 24, direction: 'in' });
});

test('LibgpiodGpio re-requests on direction change and releases all lines', async () => {
const { LibgpiodGpio } = require('../hal');
const { binding, state } = createFakeLibgpiod();
const gpio = new LibgpiodGpio('gpiochip0', binding);

await gpio.write(5, 1);
await gpio.read(5); // direction change: old line released, new one requested
assert.deepStrictEqual(state.released, [5]);
assert.deepStrictEqual(state.requested.map(r => r.direction), ['out', 'in']);

await gpio.write(6, 0);
await gpio.release();
assert.ok(state.released.includes(6));
assert.strictEqual(gpio.lines.size, 0);
});

test('LibgpiodGpio validates chip name and pins like CliGpio', () => {
const { LibgpiodGpio } = require('../hal');
const { binding } = createFakeLibgpiod();

assert.throws(() => new LibgpiodGpio('gpiochip0; rm -rf /', binding), /Invalid GPIO chip name/);
const gpio = new LibgpiodGpio('gpiochip0', binding);
assert.rejects(() => gpio.write('7; reboot', 1), /Invalid GPIO pin/);
});

test('createDefaultGpio falls back to CliGpio when node-libgpiod is unavailable', () => {
const { createDefaultGpio, CliGpio } = require('../hal');
// node-libgpiod is not installed in this environment
const gpio = createDefaultGpio('gpiochip0');
assert.ok(gpio instanceof CliGpio);
});

// --- Full display cycle ---------------------------------------------------

test('13in3k mono: init + display sends the full framebuffer', async () => {
Expand Down
Loading