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
180 changes: 25 additions & 155 deletions EPDBase.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const fs = require('fs');
const { PNG } = require('pngjs');
const { createDefaultGpio, SpiDeviceBackend, validatePin } = require('./hal');
const { createFormat } = require('./formats');

class EPDBase {
constructor(options = {}) {
Expand Down Expand Up @@ -68,12 +69,24 @@ class EPDBase {
};
}

// Pixel format strategy for the current color mode (see formats.js).
// Created lazily because subclasses set colorMode after super() runs.
// Null for modes without a format (the experimental 16gray driver
// overrides the pixel operations itself).
get format() {
if (this._formatMode !== this.colorMode) {
this._format = createFormat(this.colorMode, this.colors);
this._formatMode = this.colorMode;
}
return this._format;
}

initializeBuffer() {
const totalBits = this.width * this.height * this.bitsPerPixel;
this.imageBuffer = Buffer.alloc(Math.ceil(totalBits / 8));

// Initialize color buffer for 3-color displays
if (this.colorMode === '3color') {
if (this.format && this.format.usesColorBuffer) {
this.colorBuffer = Buffer.alloc(Math.ceil(this.width * this.height / 8));
}
}
Expand Down Expand Up @@ -200,17 +213,8 @@ class EPDBase {
}

async clear() {
if (this.colorMode === 'mono') {
this.imageBuffer.fill(0xFF);
} else if (this.colorMode === '4gray') {
// For 4-gray mode, white = 0xC0 -> top 2 bits = 11
// 4 white pixels per byte = 11 11 11 11 = 0xFF
this.imageBuffer.fill(0xFF);
} else if (this.colorMode === '3color') {
this.imageBuffer.fill(0xFF); // White background
if (this.colorBuffer) this.colorBuffer.fill(0x00); // No color
} else if (this.colorMode === '7color') {
this.imageBuffer.fill(0x11); // White pixels (all pixels set to WHITE = 1)
if (this.format) {
this.format.clear({ image: this.imageBuffer, color: this.colorBuffer });
}
await this.display();
}
Expand Down Expand Up @@ -247,76 +251,8 @@ class EPDBase {
return;
}

if (this.colorMode === 'mono') {
const byteIndex = Math.floor((x + y * this.width) / 8);
const bitIndex = 7 - ((x + y * this.width) % 8);

if (color === 0) {
// Black
this.imageBuffer[byteIndex] &= ~(1 << bitIndex);
} else {
// White
this.imageBuffer[byteIndex] |= (1 << bitIndex);
}
} else if (this.colorMode === '4gray') {
// For 4-gray mode, store pixels in format expected by C encoding algorithm
const pixelIndex = x + y * this.width;
const byteIndex = Math.floor(pixelIndex / 4);
const pixelPos = pixelIndex % 4;
const bitShift = 6 - (pixelPos * 2); // Top 2 bits first: 6,4,2,0

// Based on your actual test results (mapping appears inverted):
let rawValue;
switch (color) {
case 0: rawValue = 0x00; break; // Black -> 00 (darkest available)
case 1: rawValue = 0x01; break; // Dark gray -> 01
case 2: rawValue = 0x02; break; // Light gray -> 10
case 3: rawValue = 0x03; break; // White -> 11
default: rawValue = 0x03; break; // Default to white
}

// Clear the 2 bits for this pixel and set new value
const mask = 0x03 << bitShift; // Create mask for 2 bits at correct position
this.imageBuffer[byteIndex] &= ~mask; // Clear the bits
this.imageBuffer[byteIndex] |= (rawValue << bitShift); // Set the bits
} else if (this.colorMode === '3color') {
// For 3-color displays, color parameter can be:
// 0 = black, 1 = white, 2 = red/yellow (accent color)
const byteIndex = Math.floor((x + y * this.width) / 8);
const bitIndex = 7 - ((x + y * this.width) % 8);

if (color === 2) {
// Accent color (red/yellow) - set in color buffer, clear in main buffer
this.imageBuffer[byteIndex] |= (1 << bitIndex); // White in main buffer
if (this.colorBuffer) {
this.colorBuffer[byteIndex] |= (1 << bitIndex); // Set color bit
}
} else if (color === 0) {
// Black - clear in both buffers
this.imageBuffer[byteIndex] &= ~(1 << bitIndex);
if (this.colorBuffer) {
this.colorBuffer[byteIndex] &= ~(1 << bitIndex);
}
} else {
// White - set in main buffer, clear in color buffer
this.imageBuffer[byteIndex] |= (1 << bitIndex);
if (this.colorBuffer) {
this.colorBuffer[byteIndex] &= ~(1 << bitIndex);
}
}
} else if (this.colorMode === '7color') {
// For 7-color displays, pack 2 pixels per byte (4 bits each, but only 3 bits used)
const pixelIndex = x + y * this.width;
const byteIndex = Math.floor(pixelIndex / 2);
const pixelPos = pixelIndex % 2;

if (pixelPos === 0) {
// First pixel (upper 4 bits)
this.imageBuffer[byteIndex] = (this.imageBuffer[byteIndex] & 0x0F) | ((color & 0x07) << 4);
} else {
// Second pixel (lower 4 bits)
this.imageBuffer[byteIndex] = (this.imageBuffer[byteIndex] & 0xF0) | (color & 0x07);
}
if (this.format) {
this.format.setPixel({ image: this.imageBuffer, color: this.colorBuffer }, this.width, x, y, color);
}
}

Expand Down Expand Up @@ -382,57 +318,7 @@ class EPDBase {

// Convert RGB color to display format based on color mode
rgbToColor(r, g, b) {
if (this.colorMode === 'mono') {
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
return gray < 128 ? 0 : 1; // Black or white
} else if (this.colorMode === '4gray') {
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
// Map to 4 levels: 0=black, 1=dark gray, 2=light gray, 3=white
if (gray < 64) return 0;
else if (gray < 128) return 1;
else if (gray < 192) return 2;
else return 3;
} else if (this.colorMode === '3color') {
// Simple color detection for 3-color displays
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);

// Check if it's predominantly red or yellow
if (r > g + 50 && r > b + 50 && r > 150) return 2; // Red-ish
if (r > 150 && g > 150 && b < 100) return 2; // Yellow-ish

// Otherwise black or white based on brightness
return gray < 128 ? 0 : 1;
} else if (this.colorMode === '7color') {
// Advanced color detection for 7-color displays
const maxComponent = Math.max(r, g, b);
const minComponent = Math.min(r, g, b);

// Very dark colors
if (maxComponent < 50) return this.colors.BLACK;

// Very bright colors
if (minComponent > 200) return this.colors.WHITE;

// Color detection based on dominant component
if (r > g + 30 && r > b + 30) {
// Red-dominant
if (g > 150) return this.colors.ORANGE; // Red + Green = Orange
return this.colors.RED;
} else if (g > r + 30 && g > b + 30) {
// Green-dominant
if (r > 150) return this.colors.YELLOW; // Red + Green = Yellow
return this.colors.GREEN;
} else if (b > r + 30 && b > g + 30) {
// Blue-dominant
return this.colors.BLUE;
}

// Fallback to grayscale
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
return gray < 128 ? this.colors.BLACK : this.colors.WHITE;
}

return 0;
return this.format ? this.format.rgbToColor(r, g, b) : 0;
}

// Keep backward compatibility
Expand Down Expand Up @@ -468,17 +354,9 @@ class EPDBase {
const a = this.data[idx + 3];

// Handle transparency - treat transparent as white/background
let pixelValue;
if (a < 128) {
// Transparent pixels become background color
if (self.colorMode === 'mono') pixelValue = 1; // White
else if (self.colorMode === '4gray') pixelValue = 3; // White
else if (self.colorMode === '3color') pixelValue = 1; // White
else if (self.colorMode === '7color') pixelValue = self.colors.WHITE;
else pixelValue = 1;
} else {
pixelValue = self.rgbToColor(r, g, b);
}
const pixelValue = (a < 128)
? (self.format ? self.format.background : 1)
: self.rgbToColor(r, g, b);

imageData.pixels[y * this.width + x] = pixelValue;
}
Expand Down Expand Up @@ -538,17 +416,9 @@ class EPDBase {
const a = imageData.data[dataIndex + 3];

// Handle transparency - treat transparent as white/background
let pixelValue;
if (a < 128) {
// Transparent pixels become background color
if (this.colorMode === 'mono') pixelValue = 1; // White
else if (this.colorMode === '4gray') pixelValue = 3; // White
else if (this.colorMode === '3color') pixelValue = 1; // White
else if (this.colorMode === '7color') pixelValue = this.colors.WHITE;
else pixelValue = 1;
} else {
pixelValue = this.rgbToColor(r, g, b);
}
const pixelValue = (a < 128)
? (this.format ? this.format.background : 1)
: this.rgbToColor(r, g, b);

this.setPixel(screenX, screenY, pixelValue);
}
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,11 @@ Available colors: `BLACK`, `WHITE`, `RED`, `GREEN`, `BLUE`, `YELLOW`, `ORANGE`
### File Structure
```
├── index.js # Main entry point and factory functions
├── EPDBase.js # Base class with common functionality
├── 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 # Display module exports
│ ├── 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
Expand Down Expand Up @@ -338,8 +339,20 @@ To add support for a new display model:
}
```

3. Add to `displays/index.js` exports
4. Add case to `createDisplay()` function
3. Export `meta` and `create` from the module so the registry picks it up —
the factory lookup and `getSupportedModels()` are both derived from this:
```javascript
module.exports = {
EPDNewModel,
meta: { model: 'newmodel', aliases: ['n.m'], size: '200x200', colorModes: ['mono'], description: 'New 200x200 panel' },
create: (colorMode, options) => new EPDNewModel(options)
};
```
4. Add the module to the `modules` list in `displays/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.

## Hardware Requirements

Expand Down
11 changes: 10 additions & 1 deletion displays/EPD13in3Gray.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,5 +336,14 @@ class EPD13in3Gray extends EPDBase {

module.exports = {
EPD13in3Gray,
create16Gray: (options) => EPD13in3Gray.create16Gray(options)
create16Gray: (options) => EPD13in3Gray.create16Gray(options),
meta: {
model: '13in3gray',
aliases: ['13.3gray', '13in3_16gray', '13.3_16gray'],
size: '1600x1200',
colorModes: ['16gray'],
description: '13.3" 16-level grayscale (IT8951) - EXPERIMENTAL, driver not yet functional',
experimental: true
},
create: (colorMode, options) => EPD13in3Gray.create16Gray(options)
};
5 changes: 4 additions & 1 deletion displays/EPD13in3b.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,8 @@ module.exports = {
EPD13in3b,
create3Color: (accentColor = 'red', options) => EPD13in3b.create(accentColor, options),
createRed: (options) => EPD13in3b.create('red', options),
createYellow: (options) => EPD13in3b.create('yellow', options)
createYellow: (options) => EPD13in3b.create('yellow', options),
meta: { model: '13in3b', aliases: ['13.3b'], size: '960x680', colorModes: ['3color'], description: '13.3" black/white/red' },
// The colorMode argument doubles as the accent color for 3-color panels
create: (colorMode, options) => EPD13in3b.create(colorMode || 'red', options)
};
4 changes: 3 additions & 1 deletion displays/EPD13in3k.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,5 +216,7 @@ class EPD13in3k extends EPDBase {
module.exports = {
EPD13in3k,
createMono: (options) => EPD13in3k.create('mono', options),
create4Gray: (options) => EPD13in3k.create('4gray', options)
create4Gray: (options) => EPD13in3k.create('4gray', options),
meta: { model: '13in3k', aliases: ['13.3k'], size: '960x680', colorModes: ['mono', '4gray'], description: '13.3" mono/4-grayscale' },
create: (colorMode, options) => EPD13in3k.create(colorMode === '4gray' ? '4gray' : 'mono', options)
};
4 changes: 3 additions & 1 deletion displays/EPD2in13.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,7 @@ class EPD2in13 extends EPDBase {

module.exports = {
EPD2in13,
createMono: (options) => new EPD2in13(options)
createMono: (options) => new EPD2in13(options),
meta: { model: '2in13', aliases: ['2.13'], size: '122x250', colorModes: ['mono'], description: '2.13" monochrome' },
create: (colorMode, options) => new EPD2in13(options)
};
4 changes: 3 additions & 1 deletion displays/EPD2in7.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,5 +215,7 @@ class EPD2in7 extends EPDBase {
module.exports = {
EPD2in7,
createMono: (options) => EPD2in7.create('mono', options),
create4Gray: (options) => EPD2in7.create('4gray', options)
create4Gray: (options) => EPD2in7.create('4gray', options),
meta: { model: '2in7', aliases: ['2.7'], size: '176x264', colorModes: ['mono', '4gray'], description: '2.7" mono/4-grayscale' },
create: (colorMode, options) => EPD2in7.create(colorMode === '4gray' ? '4gray' : 'mono', options)
};
5 changes: 4 additions & 1 deletion displays/EPD2in7b.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,8 @@ class EPD2in7b extends EPDBase {

module.exports = {
EPD2in7b,
create3Color: (accentColor = 'red', options) => EPD2in7b.create(accentColor, options)
create3Color: (accentColor = 'red', options) => EPD2in7b.create(accentColor, options),
meta: { model: '2in7b', aliases: ['2.7b'], size: '176x264', colorModes: ['3color'], description: '2.7" black/white/red' },
// The colorMode argument doubles as the accent color for 3-color panels
create: (colorMode, options) => EPD2in7b.create(colorMode || 'red', options)
};
26 changes: 3 additions & 23 deletions displays/EPD7in3f.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,28 +113,6 @@ class EPD7in3f extends EPDBase {
await this.display();
}

// Override setPixel for optimized 7-color handling
setPixel(x, y, color) {
if (x >= this.width || y >= this.height || x < 0 || y < 0) {
return;
}

const pixelIndex = x + y * this.width;
const byteIndex = Math.floor(pixelIndex / 2);
const pixelPos = pixelIndex % 2;

// Ensure color is in valid range
const validColor = Math.max(0, Math.min(7, color));

if (pixelPos === 0) {
// First pixel (upper 4 bits)
this.imageBuffer[byteIndex] = (this.imageBuffer[byteIndex] & 0x0F) | ((validColor & 0x0F) << 4);
} else {
// Second pixel (lower 4 bits)
this.imageBuffer[byteIndex] = (this.imageBuffer[byteIndex] & 0xF0) | (validColor & 0x0F);
}
}

// Convenience methods for color drawing
drawColorRect(x, y, width, height, colorName, filled = false) {
const color = this.colors[colorName] !== undefined ? this.colors[colorName] : this.colors.WHITE;
Expand All @@ -160,5 +138,7 @@ class EPD7in3f extends EPDBase {

module.exports = {
EPD7in3f,
create7Color: (options) => EPD7in3f.create(options)
create7Color: (options) => EPD7in3f.create(options),
meta: { model: '7in3f', aliases: ['7.3f'], size: '800x480', colorModes: ['7color'], description: '7.3" full color (7 colors)' },
create: (colorMode, options) => EPD7in3f.create(options)
};
4 changes: 3 additions & 1 deletion displays/EPD7in5.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,7 @@ class EPD7in5 extends EPDBase {

module.exports = {
EPD7in5,
createMono: (options) => new EPD7in5(options)
createMono: (options) => new EPD7in5(options),
meta: { model: '7in5', aliases: ['7.5'], size: '640x384', colorModes: ['mono'], description: '7.5" monochrome' },
create: (colorMode, options) => new EPD7in5(options)
};
Loading
Loading