capen_inkplate/inkplate7.py

565 lines
16 KiB
Python

# MicroPython driver for Inkplate 7
# By Soldered Electronics
# Based on the original contribution by https://github.com/tve
import time
import os
from machine import ADC, I2C, SPI, Pin, SDCard
from micropython import const
from shapes import Shapes
from PCAL6416A import *
from gfx import GFX
from gfx_standard_font_01 import text_dict as std_font
# Connections between ESP32 and color Epaper
EPAPER_RST_PIN = const(19)
EPAPER_DC_PIN = const(33)
EPAPER_CS_PIN = const(27)
EPAPER_BUSY_PIN = const(32)
EPAPER_CLK = const(18)
EPAPER_DIN = const(23)
pixelMaskLUT = [0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80]
pixelMaskGLUT = [0xF, 0xF0]
LUT_IP7 = [0b0011, 0b0000, 0b0100]
# ePaper resolution
E_INK_HEIGHT = 384
E_INK_WIDTH = 640
E_INK_NUM_PIXELS = E_INK_HEIGHT * E_INK_WIDTH
E_INK_BUFFER_SIZE = E_INK_NUM_PIXELS // 2
busy_timeout_ms = 30000
class Inkplate:
# Colors
WHITE = 0b00000000
BLACK = 0b00000001
RED = 0b00000010
_width = E_INK_WIDTH
_height = E_INK_HEIGHT
rotation = 0
textSize = 1
_panelState = False
_framebuf = bytearray([0x33] * E_INK_BUFFER_SIZE)
@classmethod
def begin(self):
self.wire = I2C(0, scl=Pin(22), sda=Pin(21))
self.spi = SPI(2)
self.clearDisplay()
self.GFX = GFX(
E_INK_WIDTH,
E_INK_HEIGHT,
self.writePixel,
self.writeFastHLine,
self.writeFastVLine,
self.writeFillRect,
None,
None,
)
# Wake the panel and init it
if not (self.setPanelDeepSleepState(False)):
return False
# Put it back to sleep
self.setPanelDeepSleepState(True)
# Set i2c
self._i2c = I2C(0, scl=Pin(22), sda=Pin(21))
# Init gpio expander
self._PCAL6416A = PCAL6416A(self._i2c)
# Init SD card
self.SD_ENABLE = gpioPin(self._PCAL6416A, 10, modeOUTPUT)
self.SD_ENABLE.digitalWrite(1) # Initially disable the SD card
# Set battery enable pin
self.VBAT_EN = gpioPin(self._PCAL6416A, 9, modeOUTPUT)
self.VBAT_EN.digitalWrite(0) # Initially disable the battery read
# Set battery read pin
self.VBAT = ADC(Pin(35))
self.VBAT.atten(ADC.ATTN_11DB)
self.VBAT.width(ADC.WIDTH_12BIT)
self.setRotation(0)
return True
@classmethod
def getPanelDeepSleepState(self):
return self._panelState
@classmethod
def setPanelDeepSleepState(self, state):
# False wakes the panel up
# True puts it to sleep
if not state:
self.spi.init(baudrate=40000000, firstbit=SPI.MSB,
polarity=0, phase=0)
self.EPAPER_BUSY_PIN = Pin(EPAPER_BUSY_PIN, Pin.IN)
self.EPAPER_RST_PIN = Pin(EPAPER_RST_PIN, Pin.OUT)
self.EPAPER_DC_PIN = Pin(EPAPER_DC_PIN, Pin.OUT)
self.EPAPER_CS_PIN = Pin(EPAPER_CS_PIN, Pin.OUT)
time.sleep_ms(10)
self.resetPanel()
# Reinit the panel
self.sendCommand(b"\x12")
_timeout = time.ticks_ms()
while not self.EPAPER_BUSY_PIN.value() and (time.ticks_ms() - _timeout) < busy_timeout_ms:
pass
self.sendCommand(b"\x65")
self.sendData(b"\x01")
self.sendCommand(b"\xAB")
self.sendCommand(b"\x65")
self.sendData(b"\x00")
self.sendCommand(b"\x01")
self.sendData(b"\x37")
self.sendData(b"\x00")
self.sendCommand(b"\x00")
self.sendData(b"\xCF")
self.sendData(b"\x08")
self.sendCommand(b"\x06")
self.sendData(b"\xc7")
self.sendData(b"\xcc")
self.sendData(b"\x28")
self.sendCommand(b"\x30")
self.sendData(b"\x3c")
self.sendCommand(b"\x41")
self.sendData(b"\x00")
self.sendCommand(b"\x50")
self.sendData(b"\x77")
self.sendCommand(b"\x60")
self.sendData(b"\x22")
self.sendCommand(b"\x61")
self.sendData(b"\x02")
self.sendData(b"\x80")
self.sendData(b"\x01")
self.sendData(b"\x80")
self.sendCommand(b"\x82")
self.sendData(b"\x1E")
self.sendCommand(b"\xe5")
self.sendData(b"\x03")
self.sendCommand(b"\x04")
self._panelState = True
return True
else:
# Put the panel to sleep
self.sendCommand(b"\x65")
self.sendData(b"\x01")
self.sendCommand(b"\xB9")
self.sendCommand(b"\x65")
self.sendData(b"\x00")
self.sendCommand(b"\x02")
# Wait for ePaper
_timeout = time.ticks_ms()
while not self.EPAPER_BUSY_PIN.value() and (time.ticks_ms() - _timeout) < busy_timeout_ms:
pass
self.sendCommand(b"\07")
self.sendData(b"\xA5")
time.sleep_ms(1)
# Turn off SPI
self.spi.deinit()
self.EPAPER_BUSY_PIN = Pin(EPAPER_BUSY_PIN, Pin.IN)
self.EPAPER_RST_PIN = Pin(EPAPER_RST_PIN, Pin.IN)
self.EPAPER_DC_PIN = Pin(EPAPER_DC_PIN, Pin.IN)
self.EPAPER_CS_PIN = Pin(EPAPER_CS_PIN, Pin.IN)
self._panelState = False
return False
@classmethod
def resetPanel(self):
self.EPAPER_RST_PIN.value(0)
time.sleep_ms(15)
self.EPAPER_RST_PIN.value(1)
time.sleep_ms(15)
@classmethod
def sendCommand(self, command):
self.EPAPER_DC_PIN.value(0)
self.EPAPER_CS_PIN.value(0)
self.spi.write(command)
self.EPAPER_CS_PIN.value(1)
@classmethod
def sendData(self, data):
self.EPAPER_CS_PIN.value(0)
self.EPAPER_DC_PIN.value(1)
self.spi.write(data)
self.EPAPER_CS_PIN.value(1)
time.sleep_ms(1)
@classmethod
def clearDisplay(self):
self._framebuf = bytearray(([0x33] * E_INK_BUFFER_SIZE))
@classmethod
def display(self):
# Wake the display
self.setPanelDeepSleepState(False)
# Write b/w pixels
self.sendCommand(b"\x10")
self.sendData(self._framebuf)
# EPD update
self.sendCommand(b"\x12")
time.sleep_ms(10)
_timeout = time.ticks_ms()
while not self.EPAPER_BUSY_PIN.value() and (time.ticks_ms() - _timeout) < busy_timeout_ms:
pass
# Put the display back to sleep
self.setPanelDeepSleepState(True)
@classmethod
def width(self):
return self._width
@classmethod
def height(self):
return self._height
# Arduino compatibility functions
@classmethod
def setRotation(self, x):
self.rotation = x % 4
if self.rotation == 0 or self.rotation == 2:
self.GFX.width = E_INK_WIDTH
self.GFX.height = E_INK_HEIGHT
self._width = E_INK_WIDTH
self._height = E_INK_HEIGHT
elif self.rotation == 1 or self.rotation == 3:
self.GFX.width = E_INK_HEIGHT
self.GFX.height = E_INK_WIDTH
self._width = E_INK_HEIGHT
self._height = E_INK_WIDTH
@classmethod
def getRotation(self):
return self.rotation
@classmethod
def drawPixel(self, x, y, c):
self.startWrite()
self.writePixel(x, y, c)
self.endWrite()
@classmethod
def startWrite(self):
pass
@classmethod
def writePixel(self, x, y, c):
if x > self.width() - 1 or y > self.height() - 1 or x < 0 or y < 0:
return
if (c > 2):
return
if self.rotation == 3:
x = self.width() - x - 1
x, y = y, x
elif self.rotation == 0:
x = self.width() - x - 1
y = self.height() - y - 1
elif self.rotation == 1:
y = self.height() - y - 1
x, y = y, x
elif self.rotation == 2:
pass
_x = x // 2
_x_sub = x % 2
_position = E_INK_WIDTH // 2 * y + _x
# Clear both black and red frame buffer
# Clear the old pixel in the buffer
self._framebuf[_position] &= pixelMaskGLUT[_x_sub]
self._framebuf[_position] |= (LUT_IP7[c] << ((1 - _x_sub) * 4))
@classmethod
def writeFillRect(self, x, y, w, h, c):
for j in range(w):
for i in range(h):
self.writePixel(x + j, y + i, c)
@classmethod
def writeFastVLine(self, x, y, h, c):
for i in range(h):
self.writePixel(x, y + i, c)
@classmethod
def writeFastHLine(self, x, y, w, c):
for i in range(w):
self.writePixel(x + i, y, c)
@classmethod
def writeLine(self, x0, y0, x1, y1, c):
self.GFX.line(x0, y0, x1, y1, c)
@classmethod
def endWrite(self):
pass
@classmethod
def drawFastVLine(self, x, y, h, c):
self.startWrite()
self.writeFastVLine(x, y, h, c)
self.endWrite()
@classmethod
def drawFastHLine(self, x, y, w, c):
self.startWrite()
self.writeFastHLine(x, y, w, c)
self.endWrite()
@classmethod
def fillRect(self, x, y, w, h, c):
self.startWrite()
self.writeFillRect(x, y, w, h, c)
self.endWrite()
@classmethod
def fillScreen(self, c):
self.fillRect(0, 0, self.width(), self.height(), c)
@classmethod
def drawLine(self, x0, y0, x1, y1, c):
self.startWrite()
self.writeLine(x0, y0, x1, y1, c)
self.endWrite()
@classmethod
def drawRect(self, x, y, w, h, c):
self.GFX.rect(x, y, w, h, c)
@classmethod
def drawCircle(self, x, y, r, c):
self.GFX.circle(x, y, r, c)
@classmethod
def fillCircle(self, x, y, r, c):
self.GFX.fill_circle(x, y, r, c)
@classmethod
def drawTriangle(self, x0, y0, x1, y1, x2, y2, c):
self.GFX.triangle(x0, y0, x1, y1, x2, y2, c)
@classmethod
def fillTriangle(self, x0, y0, x1, y1, x2, y2, c):
self.GFX.fill_triangle(x0, y0, x1, y1, x2, y2, c)
@classmethod
def drawRoundRect(self, x, y, q, h, r, c):
self.GFX.round_rect(x, y, q, h, r, c)
@classmethod
def fillRoundRect(self, x, y, q, h, r, c):
self.GFX.fill_round_rect(x, y, q, h, r, c)
@classmethod
def setTextSize(self, s):
self.textSize = s
@classmethod
def setFont(self, f):
self.GFX.font = f
@classmethod
def printText(self, x, y, s, c=BLACK):
self.GFX._very_slow_text(x, y, s, self.textSize, c)
@classmethod
def drawBitmap(self, x, y, data, w, h, c=BLACK):
byteWidth = (w + 7) // 8
byte = 0
self.startWrite()
for j in range(h):
for i in range(w):
if i & 7:
byte <<= 1
else:
byte = data[j * byteWidth + i // 8]
if byte & 0x80:
self.writePixel(x + i, y + j, c)
self.endWrite()
@classmethod
def gpioExpanderPin(self, pin, mode):
return gpioPin(self._PCAL6416A, pin, mode)
@classmethod
def initSDCard(self):
self.SDCardWake()
try:
os.mount(
SDCard(
slot=3,
miso=Pin(12),
mosi=Pin(13),
sck=Pin(14),
cs=Pin(15)),
"/sd"
)
except Exception as e:
print("Sd card could not be read", str(e))
@classmethod
def SDCardSleep(self):
self.SD_ENABLE.digitalWrite(1)
time.sleep_ms(25)
@classmethod
def SDCardWake(self):
self.SD_ENABLE.digitalWrite(0)
time.sleep_ms(25)
@classmethod
def drawImageFile(self, x, y, path, invert=False):
with open(path, "rb") as f:
header14 = f.read(14)
if header14[0] != 0x42 or header14[1] != 0x4D:
return 0
header40 = f.read(40)
w = int(
(header40[7] << 24)
+ (header40[6] << 16)
+ (header40[5] << 8)
+ header40[4]
)
h = int(
(header40[11] << 24)
+ (header40[10] << 16)
+ (header40[9] << 8)
+ header40[8]
)
dataStart = int((header14[11] << 8) + header14[10])
depth = int((header40[15] << 8) + header40[14])
totalColors = int((header40[33] << 8) + header40[32])
rowSize = 4 * ((depth * w + 31) // 32)
if totalColors == 0:
totalColors = 1 << depth
palette = None
if depth <= 8:
palette = [0 for i in range(totalColors)]
p = f.read(totalColors * 4)
for i in range(totalColors):
palette[i] = (
54 * p[i * 4] + 183 * p[i * 4 + 1] + 19 * p[i * 4 + 2]
) >> 14
# print(palette)
f.seek(dataStart)
for j in range(h):
# print(100 * j // h, "% complete")
buffer = f.read(rowSize)
for i in range(w):
val = 0
if depth == 1:
px = int(
invert
^ (palette[0] < palette[1])
^ bool(buffer[i >> 3] & (1 << (7 - i & 7)))
)
val = palette[px]
elif depth == 4:
px = (buffer[i >> 1] & (0x0F if i & 1 == 1 else 0xF0)) >> (
0 if i & 1 else 4
)
val = palette[px]
if invert:
val = 3 - val
elif depth == 8:
px = buffer[i]
val = palette[px]
if invert:
val = 3 - val
elif depth == 16:
px = (buffer[(i << 1) | 1] << 8) | buffer[(i << 1)]
r = (px & 0x7C00) >> 7
g = (px & 0x3E0) >> 2
b = (px & 0x1F) << 3
val = (54 * r + 183 * g + 19 * b) >> 14
if invert:
val = 3 - val
elif depth == 24:
r = buffer[i * 3]
g = buffer[i * 3 + 1]
b = buffer[i * 3 + 2]
val = (54 * r + 183 * g + 19 * b) >> 14
if invert:
val = 3 - val
elif depth == 32:
r = buffer[i * 4]
g = buffer[i * 4 + 1]
b = buffer[i * 4 + 2]
val = (54 * r + 183 * g + 19 * b) >> 14
if invert:
val = 3 - val
val >>= 1
self.drawPixel(x + i, y + h - j, val)
@classmethod
def read_battery(self):
self.VBAT_EN.digitalWrite(1)
# Probably don't need to delay since Micropython is slow, but we do it anyway
time.sleep_ms(5)
value = self.VBAT.read()
self.VBAT_EN.digitalWrite(0)
result = (value / 4095.0) * 1.1 * 3.548133892 * 2
return result
@classmethod
def drawColorImage(self, x, y, w, h, buf):
scaled_w = int(-(-(w / 4.0) // 1))
for i in range(h):
for j in range(scaled_w):
self.writePixel(4 * j + x, i + y, (buf[scaled_w * i + j] & 0xC0) >> 6)
if 4 * j + x + 1 < w:
self.writePixel(4 * j + x + 1, i + y, (buf[scaled_w * i + j] & 0x30) >> 4)
if 4 * j + x + 2 < w:
self.writePixel(4 * j + x + 2, i + y, (buf[scaled_w * i + j] & 0x0C) >> 2)
if 4 * j + x + 3 < w:
self.writePixel(4 * j + x + 3, i + y, (buf[scaled_w * i + j] & 0x03))