UNPKG

waveshare-epaper

Version:

A modular Node.js driver for Waveshare E-Paper displays that supports multiple display models with different resolutions and color modes

570 lines (489 loc) 20 kB
const spi = require('spi-device'); const fs = require('fs'); const { PNG } = require('pngjs'); class EPDBase { constructor(options = {}) { // SPI configuration this.busNumber = options.busNumber || 0; this.deviceNumber = options.deviceNumber || 0; this.spiOptions = { maxSpeedHz: options.maxSpeedHz || 4000000, mode: spi.MODE0, bitsPerWord: 8 }; // GPIO pins (using gpiod for RPi5) this.pins = { RST: options.rstPin || 17, DC: options.dcPin || 25, CS: options.csPin || 22, BUSY: options.busyPin || 24, PWR: options.pwrPin || 18 }; this.gpioChip = options.gpioChip || 'gpiochip0'; this.spiDevice = null; this.initialized = false; // These will be set by subclasses this.width = 0; this.height = 0; this.colorMode = 'mono'; // 'mono', '4gray', '3color', '7color' this.bitsPerPixel = 1; this.imageBuffer = null; // Color buffer for dual-buffer displays (3-color, etc.) this.colorBuffer = null; // Color constants for 7-color displays this.colors = { BLACK: 0, WHITE: 1, GREEN: 2, BLUE: 3, RED: 4, YELLOW: 5, ORANGE: 6 }; } 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') { this.colorBuffer = Buffer.alloc(Math.ceil(this.width * this.height / 8)); } } async init() { try { // Initialize SPI device this.spiDevice = spi.openSync(this.busNumber, this.deviceNumber, this.spiOptions); // Initialize GPIO pins await this.initGPIO(); // Hardware reset await this.reset(); // Display-specific initialization (implemented by subclasses) await this.initDisplay(); this.initialized = true; } catch (error) { throw new Error(`Failed to initialize EPD: ${error.message}`); } } async initGPIO() { // Initialize output pins to high (skip BUSY pin which is input) for (const [name, pin] of Object.entries(this.pins)) { if (name !== 'BUSY') { await this.writeGPIO(pin, 1); } } } async reset() { await this.writeGPIO(this.pins.RST, 1); await this.delay(200); await this.writeGPIO(this.pins.RST, 0); await this.delay(2); await this.writeGPIO(this.pins.RST, 1); await this.delay(200); } async writeGPIO(pin, value) { const { exec } = require('child_process'); return new Promise((resolve, reject) => { exec(`gpioset ${this.gpioChip} ${pin}=${value}`, (error, stdout, stderr) => { if (error) { reject(new Error(`Failed to set GPIO ${pin}: ${error.message}`)); } else { resolve(); } }); }); } async readGPIO(pin) { const { exec } = require('child_process'); return new Promise((resolve, reject) => { exec(`gpioget ${this.gpioChip} ${pin}`, (error, stdout, stderr) => { if (error) { reject(new Error(`Failed to read GPIO ${pin}: ${error.message}`)); } else { resolve(parseInt(stdout.trim())); } }); }); } async sendCommand(command) { await this.writeGPIO(this.pins.DC, 0); return new Promise((resolve, reject) => { this.spiDevice.transfer([{ sendBuffer: Buffer.from([command]), receiveBuffer: Buffer.alloc(1), byteLength: 1 }], (error) => { if (error) reject(error); else resolve(); }); }); } async sendData(data) { await this.writeGPIO(this.pins.DC, 1); const buffer = Array.isArray(data) ? Buffer.from(data) : Buffer.from([data]); return new Promise((resolve, reject) => { this.spiDevice.transfer([{ sendBuffer: buffer, receiveBuffer: Buffer.alloc(buffer.length), byteLength: buffer.length }], (error) => { if (error) reject(error); else resolve(); }); }); } async waitUntilIdle() { let timeout = 0; const maxTimeout = 100; // 10 seconds max wait while (await this.readGPIO(this.pins.BUSY) === 1) { await this.delay(100); timeout++; if (timeout >= maxTimeout) { console.log('Warning: Display busy timeout - continuing anyway'); break; } } } async setWindow(xStart, yStart, xEnd, yEnd) { // Set RAM X address window await this.sendCommand(0x44); await this.sendData([ xStart & 0xFF, (xStart >> 8) & 0x03, xEnd & 0xFF, (xEnd >> 8) & 0x03 ]); // Set RAM Y address window await this.sendCommand(0x45); await this.sendData([ yStart & 0xFF, (yStart >> 8) & 0x03, yEnd & 0xFF, (yEnd >> 8) & 0x03 ]); } async setCursor(x, y) { // Set RAM X address counter await this.sendCommand(0x4E); await this.sendData([x & 0xFF, (x >> 8) & 0x03]); // Set RAM Y address counter await this.sendCommand(0x4F); await this.sendData([y & 0xFF, (y >> 8) & 0x03]); } async clear() { if (this.colorMode === 'mono') { this.imageBuffer.fill(0xFF); } else if (this.colorMode === '4gray') { this.imageBuffer.fill(0xFF); // All white pixels (3 = white in 4gray mode) } 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) } await this.display(); } async display() { if (!this.initialized) { throw new Error('Display not initialized. Call init() first.'); } // Display-specific implementation (implemented by subclasses) await this.displayImage(); } // Abstract methods to be implemented by subclasses async initDisplay() { throw new Error('initDisplay() must be implemented by subclass'); } async displayImage() { throw new Error('displayImage() must be implemented by subclass'); } // Common pixel manipulation methods setPixel(x, y, color) { if (x >= this.width || y >= this.height || x < 0 || y < 0) { 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') { const pixelIndex = x + y * this.width; const byteIndex = Math.floor(pixelIndex / 4); const pixelPos = pixelIndex % 4; const bitShift = (3 - pixelPos) * 2; // Clear the 2 bits for this pixel this.imageBuffer[byteIndex] &= ~(0x03 << bitShift); // Set the new value this.imageBuffer[byteIndex] |= ((color & 0x03) << bitShift); } 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); } } } // Set pixel with color name for 7-color displays setPixelColor(x, y, colorName) { if (this.colorMode === '7color' && this.colors[colorName] !== undefined) { this.setPixel(x, y, this.colors[colorName]); } else { // For other modes, convert color names to appropriate values const colorMap = { 'BLACK': 0, 'WHITE': 1, 'RED': this.colorMode === '3color' ? 2 : 4, 'YELLOW': this.colorMode === '3color' ? 2 : 5, 'GREEN': 2, 'BLUE': 3, 'ORANGE': 6 }; const colorValue = colorMap[colorName] !== undefined ? colorMap[colorName] : 1; this.setPixel(x, y, colorValue); } } drawLine(x0, y0, x1, y1, color) { const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0); const sx = x0 < x1 ? 1 : -1; const sy = y0 < y1 ? 1 : -1; let err = dx - dy; let x = x0; let y = y0; while (true) { this.setPixel(x, y, color); if (x === x1 && y === y1) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } } } drawRect(x, y, width, height, color, filled = false) { if (filled) { for (let i = 0; i < height; i++) { this.drawLine(x, y + i, x + width - 1, y + i, color); } } else { this.drawLine(x, y, x + width - 1, y, color); this.drawLine(x, y, x, y + height - 1, color); this.drawLine(x + width - 1, y, x + width - 1, y + height - 1, color); this.drawLine(x, y + height - 1, x + width - 1, y + height - 1, color); } } // 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; } // Keep backward compatibility rgbToGrayscale(r, g, b) { return this.rgbToColor(r, g, b); } // Load PNG file and convert to display format async loadPNG(filePath) { const self = this; return new Promise((resolve, reject) => { fs.createReadStream(filePath) .pipe(new PNG()) .on('parsed', function() { const imageData = { width: this.width, height: this.height, pixels: new Array(this.width * this.height) }; // Convert RGBA pixels to display format for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { const idx = (this.width * y + x) << 2; const r = this.data[idx]; const g = this.data[idx + 1]; const b = this.data[idx + 2]; 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); } imageData.pixels[y * this.width + x] = pixelValue; } } resolve(imageData); }) .on('error', reject); }); } // Draw PNG image at specified coordinates async drawPNG(filePath, x = 0, y = 0) { const imageData = await this.loadPNG(filePath); console.log(`Drawing PNG: ${imageData.width}x${imageData.height} at (${x}, ${y})`); // Draw each pixel, checking bounds for (let py = 0; py < imageData.height; py++) { for (let px = 0; px < imageData.width; px++) { const screenX = x + px; const screenY = y + py; // Skip pixels outside display bounds if (screenX >= this.width || screenY >= this.height || screenX < 0 || screenY < 0) { continue; } const pixelValue = imageData.pixels[py * imageData.width + px]; this.setPixel(screenX, screenY, pixelValue); } } } // Draw Canvas object at specified coordinates async drawCanvas(canvas, x = 0, y = 0) { const ctx = canvas.getContext('2d'); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); console.log(`Drawing Canvas: ${canvas.width}x${canvas.height} at (${x}, ${y})`); // Draw each pixel, checking bounds for (let py = 0; py < canvas.height; py++) { for (let px = 0; px < canvas.width; px++) { const screenX = x + px; const screenY = y + py; // Skip pixels outside display bounds if (screenX >= this.width || screenY >= this.height || screenX < 0 || screenY < 0) { continue; } const dataIndex = (py * canvas.width + px) * 4; const r = imageData.data[dataIndex]; const g = imageData.data[dataIndex + 1]; const b = imageData.data[dataIndex + 2]; 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); } this.setPixel(screenX, screenY, pixelValue); } } } async powerOn() { await this.writeGPIO(this.pins.PWR, 1); await this.delay(100); } async powerOff() { await this.writeGPIO(this.pins.PWR, 0); await this.delay(100); } async sleep() { await this.sendCommand(0x10); await this.sendData(0x01); } async cleanup() { if (this.spiDevice) { try { this.spiDevice.closeSync(); } catch (error) { // Ignore errors during cleanup } } // Power down the display try { await this.powerOff(); } catch (error) { // Ignore errors during cleanup } } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } module.exports = EPDBase;