You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
965 lines
32 KiB
Python
965 lines
32 KiB
Python
5 years ago
|
"""ILI9341 LCD/Touch module."""
|
||
|
from time import sleep
|
||
|
from math import cos, sin, pi, radians
|
||
|
from sys import implementation
|
||
|
import ustruct
|
||
|
|
||
|
|
||
|
def color565(r, g, b):
|
||
|
"""Return RGB565 color value.
|
||
|
|
||
|
Args:
|
||
|
r (int): Red value.
|
||
|
g (int): Green value.
|
||
|
b (int): Blue value.
|
||
|
"""
|
||
|
return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3
|
||
|
|
||
|
|
||
|
class Display(object):
|
||
|
"""Serial interface for 16-bit color (5-6-5 RGB) IL9341 display.
|
||
|
|
||
|
Note: All coordinates are zero based.
|
||
|
"""
|
||
|
|
||
|
# Command constants from ILI9341 datasheet
|
||
|
NOP = const(0x00) # No-op
|
||
|
SWRESET = const(0x01) # Software reset
|
||
|
RDDID = const(0x04) # Read display ID info
|
||
|
RDDST = const(0x09) # Read display status
|
||
|
SLPIN = const(0x10) # Enter sleep mode
|
||
|
SLPOUT = const(0x11) # Exit sleep mode
|
||
|
PTLON = const(0x12) # Partial mode on
|
||
|
NORON = const(0x13) # Normal display mode on
|
||
|
RDMODE = const(0x0A) # Read display power mode
|
||
|
RDMADCTL = const(0x0B) # Read display MADCTL
|
||
|
RDPIXFMT = const(0x0C) # Read display pixel format
|
||
|
RDIMGFMT = const(0x0D) # Read display image format
|
||
|
RDSELFDIAG = const(0x0F) # Read display self-diagnostic
|
||
|
INVOFF = const(0x20) # Display inversion off
|
||
|
INVON = const(0x21) # Display inversion on
|
||
|
GAMMASET = const(0x26) # Gamma set
|
||
|
DISPLAY_OFF = const(0x28) # Display off
|
||
|
DISPLAY_ON = const(0x29) # Display on
|
||
|
SET_COLUMN = const(0x2A) # Column address set
|
||
|
SET_PAGE = const(0x2B) # Page address set
|
||
|
WRITE_RAM = const(0x2C) # Memory write
|
||
|
READ_RAM = const(0x2E) # Memory read
|
||
|
PTLAR = const(0x30) # Partial area
|
||
|
VSCRDEF = const(0x33) # Vertical scrolling definition
|
||
|
MADCTL = const(0x36) # Memory access control
|
||
|
VSCRSADD = const(0x37) # Vertical scrolling start address
|
||
|
PIXFMT = const(0x3A) # COLMOD: Pixel format set
|
||
|
FRMCTR1 = const(0xB1) # Frame rate control (In normal mode/full colors)
|
||
|
FRMCTR2 = const(0xB2) # Frame rate control (In idle mode/8 colors)
|
||
|
FRMCTR3 = const(0xB3) # Frame rate control (In partial mode/full colors)
|
||
|
INVCTR = const(0xB4) # Display inversion control
|
||
|
DFUNCTR = const(0xB6) # Display function control
|
||
|
PWCTR1 = const(0xC0) # Power control 1
|
||
|
PWCTR2 = const(0xC1) # Power control 2
|
||
|
PWCTRA = const(0xCB) # Power control A
|
||
|
PWCTRB = const(0xCF) # Power control B
|
||
|
VMCTR1 = const(0xC5) # VCOM control 1
|
||
|
VMCTR2 = const(0xC7) # VCOM control 2
|
||
|
RDID1 = const(0xDA) # Read ID 1
|
||
|
RDID2 = const(0xDB) # Read ID 2
|
||
|
RDID3 = const(0xDC) # Read ID 3
|
||
|
RDID4 = const(0xDD) # Read ID 4
|
||
|
GMCTRP1 = const(0xE0) # Positive gamma correction
|
||
|
GMCTRN1 = const(0xE1) # Negative gamma correction
|
||
|
DTCA = const(0xE8) # Driver timing control A
|
||
|
DTCB = const(0xEA) # Driver timing control B
|
||
|
POSC = const(0xED) # Power on sequence control
|
||
|
ENABLE3G = const(0xF2) # Enable 3 gamma control
|
||
|
PUMPRC = const(0xF7) # Pump ratio control
|
||
|
|
||
|
ROTATE = {
|
||
|
0: 0x88,
|
||
|
90: 0xE8,
|
||
|
180: 0x48,
|
||
|
270: 0x28
|
||
|
}
|
||
|
|
||
|
def __init__(self, spi, cs, dc, rst,
|
||
|
width=240, height=320, rotation=0):
|
||
|
"""Initialize OLED.
|
||
|
|
||
|
Args:
|
||
|
spi (Class Spi): SPI interface for OLED
|
||
|
cs (Class Pin): Chip select pin
|
||
|
dc (Class Pin): Data/Command pin
|
||
|
rst (Class Pin): Reset pin
|
||
|
width (Optional int): Screen width (default 240)
|
||
|
height (Optional int): Screen height (default 320)
|
||
|
rotation (Optional int): Rotation must be 0 default, 90. 180 or 270
|
||
|
"""
|
||
|
self.spi = spi
|
||
|
self.cs = cs
|
||
|
self.dc = dc
|
||
|
self.rst = rst
|
||
|
self.width = width
|
||
|
self.height = height
|
||
|
if rotation not in self.ROTATE.keys():
|
||
|
raise RuntimeError('Rotation must be 0, 90, 180 or 270.')
|
||
|
else:
|
||
|
self.rotation = self.ROTATE[rotation]
|
||
|
|
||
|
# Initialize GPIO pins and set implementation specific methods
|
||
|
if implementation.name == 'circuitpython':
|
||
|
self.cs.switch_to_output(value=True)
|
||
|
self.dc.switch_to_output(value=False)
|
||
|
self.rst.switch_to_output(value=True)
|
||
|
self.reset = self.reset_cpy
|
||
|
self.write_cmd = self.write_cmd_cpy
|
||
|
self.write_data = self.write_data_cpy
|
||
|
else:
|
||
|
self.cs.init(self.cs.OUT, value=1)
|
||
|
self.dc.init(self.dc.OUT, value=0)
|
||
|
self.rst.init(self.rst.OUT, value=1)
|
||
|
self.reset = self.reset_mpy
|
||
|
self.write_cmd = self.write_cmd_mpy
|
||
|
self.write_data = self.write_data_mpy
|
||
|
self.reset()
|
||
|
# Send initialization commands
|
||
|
self.write_cmd(self.SWRESET) # Software reset
|
||
|
sleep(.1)
|
||
|
self.write_cmd(self.PWCTRB, 0x00, 0xC1, 0x30) # Pwr ctrl B
|
||
|
self.write_cmd(self.POSC, 0x64, 0x03, 0x12, 0x81) # Pwr on seq. ctrl
|
||
|
self.write_cmd(self.DTCA, 0x85, 0x00, 0x78) # Driver timing ctrl A
|
||
|
self.write_cmd(self.PWCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02) # Pwr ctrl A
|
||
|
self.write_cmd(self.PUMPRC, 0x20) # Pump ratio control
|
||
|
self.write_cmd(self.DTCB, 0x00, 0x00) # Driver timing ctrl B
|
||
|
self.write_cmd(self.PWCTR1, 0x23) # Pwr ctrl 1
|
||
|
self.write_cmd(self.PWCTR2, 0x10) # Pwr ctrl 2
|
||
|
self.write_cmd(self.VMCTR1, 0x3E, 0x28) # VCOM ctrl 1
|
||
|
self.write_cmd(self.VMCTR2, 0x86) # VCOM ctrl 2
|
||
|
self.write_cmd(self.MADCTL, self.rotation) # Memory access ctrl
|
||
|
self.write_cmd(self.VSCRSADD, 0x00) # Vertical scrolling start address
|
||
|
self.write_cmd(self.PIXFMT, 0x55) # COLMOD: Pixel format
|
||
|
self.write_cmd(self.FRMCTR1, 0x00, 0x18) # Frame rate ctrl
|
||
|
self.write_cmd(self.DFUNCTR, 0x08, 0x82, 0x27)
|
||
|
self.write_cmd(self.ENABLE3G, 0x00) # Enable 3 gamma ctrl
|
||
|
self.write_cmd(self.GAMMASET, 0x01) # Gamma curve selected
|
||
|
self.write_cmd(self.GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E,
|
||
|
0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00)
|
||
|
self.write_cmd(self.GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31,
|
||
|
0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F)
|
||
|
self.write_cmd(self.SLPOUT) # Exit sleep
|
||
|
sleep(.1)
|
||
|
self.write_cmd(self.DISPLAY_ON) # Display on
|
||
|
sleep(.1)
|
||
|
self.clear()
|
||
|
|
||
|
def block(self, x0, y0, x1, y1, data):
|
||
|
"""Write a block of data to display.
|
||
|
|
||
|
Args:
|
||
|
x0 (int): Starting X position.
|
||
|
y0 (int): Starting Y position.
|
||
|
x1 (int): Ending X position.
|
||
|
y1 (int): Ending Y position.
|
||
|
data (bytes): Data buffer to write.
|
||
|
"""
|
||
|
self.write_cmd(self.SET_COLUMN, *ustruct.pack(">HH", x0, x1))
|
||
|
self.write_cmd(self.SET_PAGE, *ustruct.pack(">HH", y0, y1))
|
||
|
|
||
|
self.write_cmd(self.WRITE_RAM)
|
||
|
self.write_data(data)
|
||
|
|
||
|
def cleanup(self):
|
||
|
"""Clean up resources."""
|
||
|
self.clear()
|
||
|
self.display_off()
|
||
|
self.spi.deinit()
|
||
|
print('display off')
|
||
|
|
||
|
def clear(self, color=0):
|
||
|
"""Clear display.
|
||
|
|
||
|
Args:
|
||
|
color (Optional int): RGB565 color value (Default: 0 = Black).
|
||
|
"""
|
||
|
w = self.width
|
||
|
h = self.height
|
||
|
# Clear display in 1024 byte blocks
|
||
|
if color:
|
||
|
line = color.to_bytes(2, 'big') * (w * 8)
|
||
|
else:
|
||
|
line = bytearray(w * 16)
|
||
|
for y in range(0, h, 8):
|
||
|
self.block(0, y, w - 1, y + 7, line)
|
||
|
|
||
|
def contrast(self, level):
|
||
|
"""Set display contrast to specified level.
|
||
|
|
||
|
Args:
|
||
|
level (int): Contrast level (0 - 15).
|
||
|
Note:
|
||
|
Can pass list to specifiy
|
||
|
"""
|
||
|
assert(0 <= level < 16)
|
||
|
self.write_cmd(self.CONTRAST_MASTER, level)
|
||
|
|
||
|
def display_off(self):
|
||
|
"""Turn display off."""
|
||
|
self.write_cmd(self.DISPLAY_OFF)
|
||
|
|
||
|
def display_on(self):
|
||
|
"""Turn display on."""
|
||
|
self.write_cmd(self.DISPLAY_ON)
|
||
|
|
||
|
def draw_circle(self, x0, y0, r, color):
|
||
|
"""Draw a circle.
|
||
|
|
||
|
Args:
|
||
|
x0 (int): X coordinate of center point.
|
||
|
y0 (int): Y coordinate of center point.
|
||
|
r (int): Radius.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
f = 1 - r
|
||
|
dx = 1
|
||
|
dy = -r - r
|
||
|
x = 0
|
||
|
y = r
|
||
|
self.draw_pixel(x0, y0 + r, color)
|
||
|
self.draw_pixel(x0, y0 - r, color)
|
||
|
self.draw_pixel(x0 + r, y0, color)
|
||
|
self.draw_pixel(x0 - r, y0, color)
|
||
|
while x < y:
|
||
|
if f >= 0:
|
||
|
y -= 1
|
||
|
dy += 2
|
||
|
f += dy
|
||
|
x += 1
|
||
|
dx += 2
|
||
|
f += dx
|
||
|
self.draw_pixel(x0 + x, y0 + y, color)
|
||
|
self.draw_pixel(x0 - x, y0 + y, color)
|
||
|
self.draw_pixel(x0 + x, y0 - y, color)
|
||
|
self.draw_pixel(x0 - x, y0 - y, color)
|
||
|
self.draw_pixel(x0 + y, y0 + x, color)
|
||
|
self.draw_pixel(x0 - y, y0 + x, color)
|
||
|
self.draw_pixel(x0 + y, y0 - x, color)
|
||
|
self.draw_pixel(x0 - y, y0 - x, color)
|
||
|
|
||
|
def draw_ellipse(self, x0, y0, a, b, color):
|
||
|
"""Draw an ellipse.
|
||
|
|
||
|
Args:
|
||
|
x0, y0 (int): Coordinates of center point.
|
||
|
a (int): Semi axis horizontal.
|
||
|
b (int): Semi axis vertical.
|
||
|
color (int): RGB565 color value.
|
||
|
Note:
|
||
|
The center point is the center of the x0,y0 pixel.
|
||
|
Since pixels are not divisible, the axes are integer rounded
|
||
|
up to complete on a full pixel. Therefore the major and
|
||
|
minor axes are increased by 1.
|
||
|
"""
|
||
|
a2 = a * a
|
||
|
b2 = b * b
|
||
|
twoa2 = a2 + a2
|
||
|
twob2 = b2 + b2
|
||
|
x = 0
|
||
|
y = b
|
||
|
px = 0
|
||
|
py = twoa2 * y
|
||
|
# Plot initial points
|
||
|
self.draw_pixel(x0 + x, y0 + y, color)
|
||
|
self.draw_pixel(x0 - x, y0 + y, color)
|
||
|
self.draw_pixel(x0 + x, y0 - y, color)
|
||
|
self.draw_pixel(x0 - x, y0 - y, color)
|
||
|
# Region 1
|
||
|
p = round(b2 - (a2 * b) + (0.25 * a2))
|
||
|
while px < py:
|
||
|
x += 1
|
||
|
px += twob2
|
||
|
if p < 0:
|
||
|
p += b2 + px
|
||
|
else:
|
||
|
y -= 1
|
||
|
py -= twoa2
|
||
|
p += b2 + px - py
|
||
|
self.draw_pixel(x0 + x, y0 + y, color)
|
||
|
self.draw_pixel(x0 - x, y0 + y, color)
|
||
|
self.draw_pixel(x0 + x, y0 - y, color)
|
||
|
self.draw_pixel(x0 - x, y0 - y, color)
|
||
|
# Region 2
|
||
|
p = round(b2 * (x + 0.5) * (x + 0.5) +
|
||
|
a2 * (y - 1) * (y - 1) - a2 * b2)
|
||
|
while y > 0:
|
||
|
y -= 1
|
||
|
py -= twoa2
|
||
|
if p > 0:
|
||
|
p += a2 - py
|
||
|
else:
|
||
|
x += 1
|
||
|
px += twob2
|
||
|
p += a2 - py + px
|
||
|
self.draw_pixel(x0 + x, y0 + y, color)
|
||
|
self.draw_pixel(x0 - x, y0 + y, color)
|
||
|
self.draw_pixel(x0 + x, y0 - y, color)
|
||
|
self.draw_pixel(x0 - x, y0 - y, color)
|
||
|
|
||
|
def draw_hline(self, x, y, w, color):
|
||
|
"""Draw a horizontal line.
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
w (int): Width of line.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
if self.is_off_grid(x, y, x + w - 1, y):
|
||
|
return
|
||
|
line = color.to_bytes(2, 'big') * w
|
||
|
self.block(x, y, x + w - 1, y, line)
|
||
|
|
||
|
def draw_image(self, path, x=0, y=0, w=320, h=240):
|
||
|
"""Draw image from flash.
|
||
|
|
||
|
Args:
|
||
|
path (string): Image file path.
|
||
|
x (int): X coordinate of image left. Default is 0.
|
||
|
y (int): Y coordinate of image top. Default is 0.
|
||
|
w (int): Width of image. Default is 320.
|
||
|
h (int): Height of image. Default is 240.
|
||
|
"""
|
||
|
x2 = x + w - 1
|
||
|
y2 = y + h - 1
|
||
|
if self.is_off_grid(x, y, x2, y2):
|
||
|
return
|
||
|
with open(path, "rb") as f:
|
||
|
chunk_height = 1024 // w
|
||
|
chunk_count, remainder = divmod(h, chunk_height)
|
||
|
chunk_size = chunk_height * w * 2
|
||
|
chunk_y = y
|
||
|
if chunk_count:
|
||
|
for c in range(0, chunk_count):
|
||
|
buf = f.read(chunk_size)
|
||
|
self.block(x, chunk_y,
|
||
|
x2, chunk_y + chunk_height - 1,
|
||
|
buf)
|
||
|
chunk_y += chunk_height
|
||
|
if remainder:
|
||
|
buf = f.read(remainder * w * 2)
|
||
|
self.block(x, chunk_y,
|
||
|
x2, chunk_y + remainder - 1,
|
||
|
buf)
|
||
|
|
||
|
def draw_letter(self, x, y, letter, font, color, background=0,
|
||
|
landscape=False):
|
||
|
"""Draw a letter.
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
letter (string): Letter to draw.
|
||
|
font (XglcdFont object): Font.
|
||
|
color (int): RGB565 color value.
|
||
|
background (int): RGB565 background color (default: black).
|
||
|
landscape (bool): Orientation (default: False = portrait)
|
||
|
"""
|
||
|
buf, w, h = font.get_letter(letter, color, background, landscape)
|
||
|
# Check for errors (Font could be missing specified letter)
|
||
|
if w == 0:
|
||
|
return w, h
|
||
|
|
||
|
if landscape:
|
||
|
y -= w
|
||
|
if self.is_off_grid(x, y, x + h - 1, y + w - 1):
|
||
|
return 0, 0
|
||
|
self.block(x, y,
|
||
|
x + h - 1, y + w - 1,
|
||
|
buf)
|
||
|
else:
|
||
|
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
|
||
|
return 0, 0
|
||
|
self.block(x, y,
|
||
|
x + w - 1, y + h - 1,
|
||
|
buf)
|
||
|
return w, h
|
||
|
|
||
|
def draw_line(self, x1, y1, x2, y2, color):
|
||
|
"""Draw a line using Bresenham's algorithm.
|
||
|
|
||
|
Args:
|
||
|
x1, y1 (int): Starting coordinates of the line
|
||
|
x2, y2 (int): Ending coordinates of the line
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
# Check for horizontal line
|
||
|
if y1 == y2:
|
||
|
if x1 > x2:
|
||
|
x1, x2 = x2, x1
|
||
|
self.draw_hline(x1, y1, x2 - x1 + 1, color)
|
||
|
return
|
||
|
# Check for vertical line
|
||
|
if x1 == x2:
|
||
|
if y1 > y2:
|
||
|
y1, y2 = y2, y1
|
||
|
self.draw_vline(x1, y1, y2 - y1 + 1, color)
|
||
|
return
|
||
|
# Confirm coordinates in boundary
|
||
|
if self.is_off_grid(min(x1, x2), min(y1, y2),
|
||
|
max(x1, x2), max(y1, y2)):
|
||
|
return
|
||
|
# Changes in x, y
|
||
|
dx = x2 - x1
|
||
|
dy = y2 - y1
|
||
|
# Determine how steep the line is
|
||
|
is_steep = abs(dy) > abs(dx)
|
||
|
# Rotate line
|
||
|
if is_steep:
|
||
|
x1, y1 = y1, x1
|
||
|
x2, y2 = y2, x2
|
||
|
# Swap start and end points if necessary
|
||
|
if x1 > x2:
|
||
|
x1, x2 = x2, x1
|
||
|
y1, y2 = y2, y1
|
||
|
# Recalculate differentials
|
||
|
dx = x2 - x1
|
||
|
dy = y2 - y1
|
||
|
# Calculate error
|
||
|
error = dx >> 1
|
||
|
ystep = 1 if y1 < y2 else -1
|
||
|
y = y1
|
||
|
for x in range(x1, x2 + 1):
|
||
|
# Had to reverse HW ????
|
||
|
if not is_steep:
|
||
|
self.draw_pixel(x, y, color)
|
||
|
else:
|
||
|
self.draw_pixel(y, x, color)
|
||
|
error -= abs(dy)
|
||
|
if error < 0:
|
||
|
y += ystep
|
||
|
error += dx
|
||
|
|
||
|
def draw_lines(self, coords, color):
|
||
|
"""Draw multiple lines.
|
||
|
|
||
|
Args:
|
||
|
coords ([[int, int],...]): Line coordinate X, Y pairs
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
# Starting point
|
||
|
x1, y1 = coords[0]
|
||
|
# Iterate through coordinates
|
||
|
for i in range(1, len(coords)):
|
||
|
x2, y2 = coords[i]
|
||
|
self.draw_line(x1, y1, x2, y2, color)
|
||
|
x1, y1 = x2, y2
|
||
|
|
||
|
def draw_pixel(self, x, y, color):
|
||
|
"""Draw a single pixel.
|
||
|
|
||
|
Args:
|
||
|
x (int): X position.
|
||
|
y (int): Y position.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
if self.is_off_grid(x, y, x, y):
|
||
|
return
|
||
|
self.block(x, y, x, y, color.to_bytes(2, 'big'))
|
||
|
|
||
|
def draw_polygon(self, sides, x0, y0, r, color, rotate=0):
|
||
|
"""Draw an n-sided regular polygon.
|
||
|
|
||
|
Args:
|
||
|
sides (int): Number of polygon sides.
|
||
|
x0, y0 (int): Coordinates of center point.
|
||
|
r (int): Radius.
|
||
|
color (int): RGB565 color value.
|
||
|
rotate (Optional float): Rotation in degrees relative to origin.
|
||
|
Note:
|
||
|
The center point is the center of the x0,y0 pixel.
|
||
|
Since pixels are not divisible, the radius is integer rounded
|
||
|
up to complete on a full pixel. Therefore diameter = 2 x r + 1.
|
||
|
"""
|
||
|
coords = []
|
||
|
theta = radians(rotate)
|
||
|
n = sides + 1
|
||
|
for s in range(n):
|
||
|
t = 2.0 * pi * s / sides + theta
|
||
|
coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)])
|
||
|
|
||
|
# Cast to python float first to fix rounding errors
|
||
|
self.draw_lines(coords, color=color)
|
||
|
|
||
|
def draw_rectangle(self, x, y, w, h, color):
|
||
|
"""Draw a rectangle.
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
w (int): Width of rectangle.
|
||
|
h (int): Height of rectangle.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
x2 = x + w - 1
|
||
|
y2 = y + h - 1
|
||
|
self.draw_hline(x, y, w, color)
|
||
|
self.draw_hline(x, y2, w, color)
|
||
|
self.draw_vline(x, y, h, color)
|
||
|
self.draw_vline(x2, y, h, color)
|
||
|
|
||
|
def draw_sprite(self, buf, x, y, w, h):
|
||
|
"""Draw a sprite (optimized for horizontal drawing).
|
||
|
|
||
|
Args:
|
||
|
buf (bytearray): Buffer to draw.
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
w (int): Width of drawing.
|
||
|
h (int): Height of drawing.
|
||
|
"""
|
||
|
x2 = x + w - 1
|
||
|
y2 = y + h - 1
|
||
|
if self.is_off_grid(x, y, x2, y2):
|
||
|
return
|
||
|
self.block(x, y, x2, y2, buf)
|
||
|
|
||
|
def draw_text(self, x, y, text, font, color, background=0,
|
||
|
landscape=False, spacing=1):
|
||
|
"""Draw text.
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
text (string): Text to draw.
|
||
|
font (XglcdFont object): Font.
|
||
|
color (int): RGB565 color value.
|
||
|
background (int): RGB565 background color (default: black).
|
||
|
landscape (bool): Orientation (default: False = portrait)
|
||
|
spacing (int): Pixels between letters (default: 1)
|
||
|
"""
|
||
|
for letter in text:
|
||
|
# Get letter array and letter dimensions
|
||
|
w, h = self.draw_letter(x, y, letter, font, color, background,
|
||
|
landscape)
|
||
|
# Stop on error
|
||
|
if w == 0 or h == 0:
|
||
|
print('Invalid width {0} or height {1}'.format(w, h))
|
||
|
return
|
||
|
|
||
|
if landscape:
|
||
|
# Fill in spacing
|
||
|
if spacing:
|
||
|
self.fill_hrect(x, y - w - spacing, h, spacing, background)
|
||
|
# Position y for next letter
|
||
|
y -= (w + spacing)
|
||
|
else:
|
||
|
# Fill in spacing
|
||
|
if spacing:
|
||
|
self.fill_hrect(x + w, y, spacing, h, background)
|
||
|
# Position x for next letter
|
||
|
x += (w + spacing)
|
||
|
|
||
|
# # Fill in spacing
|
||
|
# if spacing:
|
||
|
# self.fill_vrect(x + w, y, spacing, h, background)
|
||
|
# # Position x for next letter
|
||
|
# x += w + spacing
|
||
|
|
||
|
def draw_vline(self, x, y, h, color):
|
||
|
"""Draw a vertical line.
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
h (int): Height of line.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
# Confirm coordinates in boundary
|
||
|
if self.is_off_grid(x, y, x, y + h):
|
||
|
return
|
||
|
line = color.to_bytes(2, 'big') * h
|
||
|
self.block(x, y, x, y + h - 1, line)
|
||
|
|
||
|
def fill_circle(self, x0, y0, r, color):
|
||
|
"""Draw a filled circle.
|
||
|
|
||
|
Args:
|
||
|
x0 (int): X coordinate of center point.
|
||
|
y0 (int): Y coordinate of center point.
|
||
|
r (int): Radius.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
f = 1 - r
|
||
|
dx = 1
|
||
|
dy = -r - r
|
||
|
x = 0
|
||
|
y = r
|
||
|
self.draw_vline(x0, y0 - r, 2 * r + 1, color)
|
||
|
while x < y:
|
||
|
if f >= 0:
|
||
|
y -= 1
|
||
|
dy += 2
|
||
|
f += dy
|
||
|
x += 1
|
||
|
dx += 2
|
||
|
f += dx
|
||
|
self.draw_vline(x0 + x, y0 - y, 2 * y + 1, color)
|
||
|
self.draw_vline(x0 - x, y0 - y, 2 * y + 1, color)
|
||
|
self.draw_vline(x0 - y, y0 - x, 2 * x + 1, color)
|
||
|
self.draw_vline(x0 + y, y0 - x, 2 * x + 1, color)
|
||
|
|
||
|
def fill_ellipse(self, x0, y0, a, b, color):
|
||
|
"""Draw a filled ellipse.
|
||
|
|
||
|
Args:
|
||
|
x0, y0 (int): Coordinates of center point.
|
||
|
a (int): Semi axis horizontal.
|
||
|
b (int): Semi axis vertical.
|
||
|
color (int): RGB565 color value.
|
||
|
Note:
|
||
|
The center point is the center of the x0,y0 pixel.
|
||
|
Since pixels are not divisible, the axes are integer rounded
|
||
|
up to complete on a full pixel. Therefore the major and
|
||
|
minor axes are increased by 1.
|
||
|
"""
|
||
|
a2 = a * a
|
||
|
b2 = b * b
|
||
|
twoa2 = a2 + a2
|
||
|
twob2 = b2 + b2
|
||
|
x = 0
|
||
|
y = b
|
||
|
px = 0
|
||
|
py = twoa2 * y
|
||
|
# Plot initial points
|
||
|
self.draw_line(x0, y0 - y, x0, y0 + y, color)
|
||
|
# Region 1
|
||
|
p = round(b2 - (a2 * b) + (0.25 * a2))
|
||
|
while px < py:
|
||
|
x += 1
|
||
|
px += twob2
|
||
|
if p < 0:
|
||
|
p += b2 + px
|
||
|
else:
|
||
|
y -= 1
|
||
|
py -= twoa2
|
||
|
p += b2 + px - py
|
||
|
self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color)
|
||
|
self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color)
|
||
|
# Region 2
|
||
|
p = round(b2 * (x + 0.5) * (x + 0.5) +
|
||
|
a2 * (y - 1) * (y - 1) - a2 * b2)
|
||
|
while y > 0:
|
||
|
y -= 1
|
||
|
py -= twoa2
|
||
|
if p > 0:
|
||
|
p += a2 - py
|
||
|
else:
|
||
|
x += 1
|
||
|
px += twob2
|
||
|
p += a2 - py + px
|
||
|
self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color)
|
||
|
self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color)
|
||
|
|
||
|
def fill_hrect(self, x, y, w, h, color):
|
||
|
"""Draw a filled rectangle (optimized for horizontal drawing).
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
w (int): Width of rectangle.
|
||
|
h (int): Height of rectangle.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
|
||
|
return
|
||
|
chunk_height = 1024 // w
|
||
|
chunk_count, remainder = divmod(h, chunk_height)
|
||
|
chunk_size = chunk_height * w
|
||
|
chunk_y = y
|
||
|
if chunk_count:
|
||
|
buf = color.to_bytes(2, 'big') * chunk_size
|
||
|
for c in range(0, chunk_count):
|
||
|
self.block(x, chunk_y,
|
||
|
x + w - 1, chunk_y + chunk_height - 1,
|
||
|
buf)
|
||
|
chunk_y += chunk_height
|
||
|
|
||
|
if remainder:
|
||
|
buf = color.to_bytes(2, 'big') * remainder * w
|
||
|
self.block(x, chunk_y,
|
||
|
x + w - 1, chunk_y + remainder - 1,
|
||
|
buf)
|
||
|
|
||
|
def fill_rectangle(self, x, y, w, h, color):
|
||
|
"""Draw a filled rectangle.
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
w (int): Width of rectangle.
|
||
|
h (int): Height of rectangle.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
|
||
|
return
|
||
|
if w > h:
|
||
|
self.fill_hrect(x, y, w, h, color)
|
||
|
else:
|
||
|
self.fill_vrect(x, y, w, h, color)
|
||
|
|
||
|
def fill_polygon(self, sides, x0, y0, r, color, rotate=0):
|
||
|
"""Draw a filled n-sided regular polygon.
|
||
|
|
||
|
Args:
|
||
|
sides (int): Number of polygon sides.
|
||
|
x0, y0 (int): Coordinates of center point.
|
||
|
r (int): Radius.
|
||
|
color (int): RGB565 color value.
|
||
|
rotate (Optional float): Rotation in degrees relative to origin.
|
||
|
Note:
|
||
|
The center point is the center of the x0,y0 pixel.
|
||
|
Since pixels are not divisible, the radius is integer rounded
|
||
|
up to complete on a full pixel. Therefore diameter = 2 x r + 1.
|
||
|
"""
|
||
|
# Determine side coordinates
|
||
|
coords = []
|
||
|
theta = radians(rotate)
|
||
|
n = sides + 1
|
||
|
for s in range(n):
|
||
|
t = 2.0 * pi * s / sides + theta
|
||
|
coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)])
|
||
|
# Starting point
|
||
|
x1, y1 = coords[0]
|
||
|
# Minimum Maximum X dict
|
||
|
xdict = {y1: [x1, x1]}
|
||
|
# Iterate through coordinates
|
||
|
for row in coords[1:]:
|
||
|
x2, y2 = row
|
||
|
xprev, yprev = x2, y2
|
||
|
# Calculate perimeter
|
||
|
# Check for horizontal side
|
||
|
if y1 == y2:
|
||
|
if x1 > x2:
|
||
|
x1, x2 = x2, x1
|
||
|
if y1 in xdict:
|
||
|
xdict[y1] = [min(x1, xdict[y1][0]), max(x2, xdict[y1][1])]
|
||
|
else:
|
||
|
xdict[y1] = [x1, x2]
|
||
|
x1, y1 = xprev, yprev
|
||
|
continue
|
||
|
# Non horizontal side
|
||
|
# Changes in x, y
|
||
|
dx = x2 - x1
|
||
|
dy = y2 - y1
|
||
|
# Determine how steep the line is
|
||
|
is_steep = abs(dy) > abs(dx)
|
||
|
# Rotate line
|
||
|
if is_steep:
|
||
|
x1, y1 = y1, x1
|
||
|
x2, y2 = y2, x2
|
||
|
# Swap start and end points if necessary
|
||
|
if x1 > x2:
|
||
|
x1, x2 = x2, x1
|
||
|
y1, y2 = y2, y1
|
||
|
# Recalculate differentials
|
||
|
dx = x2 - x1
|
||
|
dy = y2 - y1
|
||
|
# Calculate error
|
||
|
error = dx >> 1
|
||
|
ystep = 1 if y1 < y2 else -1
|
||
|
y = y1
|
||
|
# Calcualte minimum and maximum x values
|
||
|
for x in range(x1, x2 + 1):
|
||
|
if is_steep:
|
||
|
if x in xdict:
|
||
|
xdict[x] = [min(y, xdict[x][0]), max(y, xdict[x][1])]
|
||
|
else:
|
||
|
xdict[x] = [y, y]
|
||
|
else:
|
||
|
if y in xdict:
|
||
|
xdict[y] = [min(x, xdict[y][0]), max(x, xdict[y][1])]
|
||
|
else:
|
||
|
xdict[y] = [x, x]
|
||
|
error -= abs(dy)
|
||
|
if error < 0:
|
||
|
y += ystep
|
||
|
error += dx
|
||
|
x1, y1 = xprev, yprev
|
||
|
# Fill polygon
|
||
|
for y, x in xdict.items():
|
||
|
self.draw_hline(x[0], y, x[1] - x[0] + 2, color)
|
||
|
|
||
|
def fill_vrect(self, x, y, w, h, color):
|
||
|
"""Draw a filled rectangle (optimized for vertical drawing).
|
||
|
|
||
|
Args:
|
||
|
x (int): Starting X position.
|
||
|
y (int): Starting Y position.
|
||
|
w (int): Width of rectangle.
|
||
|
h (int): Height of rectangle.
|
||
|
color (int): RGB565 color value.
|
||
|
"""
|
||
|
if self.is_off_grid(x, y, x + w - 1, y + h - 1):
|
||
|
return
|
||
|
chunk_width = 1024 // h
|
||
|
chunk_count, remainder = divmod(w, chunk_width)
|
||
|
chunk_size = chunk_width * h
|
||
|
chunk_x = x
|
||
|
if chunk_count:
|
||
|
buf = color.to_bytes(2, 'big') * chunk_size
|
||
|
for c in range(0, chunk_count):
|
||
|
self.block(chunk_x, y,
|
||
|
chunk_x + chunk_width - 1, y + h - 1,
|
||
|
buf)
|
||
|
chunk_x += chunk_width
|
||
|
|
||
|
if remainder:
|
||
|
buf = color.to_bytes(2, 'big') * remainder * h
|
||
|
self.block(chunk_x, y,
|
||
|
chunk_x + remainder - 1, y + h - 1,
|
||
|
buf)
|
||
|
|
||
|
def is_off_grid(self, xmin, ymin, xmax, ymax):
|
||
|
"""Check if coordinates extend past display boundaries.
|
||
|
|
||
|
Args:
|
||
|
xmin (int): Minimum horizontal pixel.
|
||
|
ymin (int): Minimum vertical pixel.
|
||
|
xmax (int): Maximum horizontal pixel.
|
||
|
ymax (int): Maximum vertical pixel.
|
||
|
Returns:
|
||
|
boolean: False = Coordinates OK, True = Error.
|
||
|
"""
|
||
|
if xmin < 0:
|
||
|
print('x-coordinate: {0} below minimum of 0.'.format(xmin))
|
||
|
return True
|
||
|
if ymin < 0:
|
||
|
print('y-coordinate: {0} below minimum of 0.'.format(ymin))
|
||
|
return True
|
||
|
if xmax >= self.width:
|
||
|
print('x-coordinate: {0} above maximum of {1}.'.format(
|
||
|
xmax, self.width - 1))
|
||
|
return True
|
||
|
if ymax >= self.height:
|
||
|
print('y-coordinate: {0} above maximum of {1}.'.format(
|
||
|
ymax, self.height - 1))
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def load_sprite(self, path, w, h):
|
||
|
"""Load sprite image.
|
||
|
|
||
|
Args:
|
||
|
path (string): Image file path.
|
||
|
w (int): Width of image.
|
||
|
h (int): Height of image.
|
||
|
Notes:
|
||
|
w x h cannot exceed 2048
|
||
|
"""
|
||
|
buf_size = w * h * 2
|
||
|
with open(path, "rb") as f:
|
||
|
return f.read(buf_size)
|
||
|
|
||
|
def reset_cpy(self):
|
||
|
"""Perform reset: Low=initialization, High=normal operation.
|
||
|
|
||
|
Notes: CircuitPython implemntation
|
||
|
"""
|
||
|
self.rst.value = False
|
||
|
sleep(.05)
|
||
|
self.rst.value = True
|
||
|
sleep(.05)
|
||
|
|
||
|
def reset_mpy(self):
|
||
|
"""Perform reset: Low=initialization, High=normal operation.
|
||
|
|
||
|
Notes: MicroPython implemntation
|
||
|
"""
|
||
|
self.rst(0)
|
||
|
sleep(.05)
|
||
|
self.rst(1)
|
||
|
sleep(.05)
|
||
|
|
||
|
def scroll(self, y):
|
||
|
"""Scroll display vertically.
|
||
|
|
||
|
Args:
|
||
|
y (int): Number of pixels to scroll display.
|
||
|
"""
|
||
|
self.write_cmd(self.VSCRSADD, y >> 8, y & 0xFF)
|
||
|
|
||
|
def set_scroll(self, top, bottom):
|
||
|
"""Set the height of the top and bottom scroll margins.
|
||
|
|
||
|
Args:
|
||
|
top (int): Height of top scroll margin
|
||
|
bottom (int): Height of bottom scroll margin
|
||
|
"""
|
||
|
if top + bottom <= self.height:
|
||
|
middle = self.height - (top + bottom)
|
||
|
print(top, middle, bottom)
|
||
|
self.write_cmd(self.VSCRDEF,
|
||
|
top >> 8,
|
||
|
top & 0xFF,
|
||
|
middle >> 8,
|
||
|
middle & 0xFF,
|
||
|
bottom >> 8,
|
||
|
bottom & 0xFF)
|
||
|
|
||
|
def write_cmd_mpy(self, command, *args):
|
||
|
"""Write command to OLED (MicroPython).
|
||
|
|
||
|
Args:
|
||
|
command (byte): ILI9341 command code.
|
||
|
*args (optional bytes): Data to transmit.
|
||
|
"""
|
||
|
self.dc(0)
|
||
|
self.cs(0)
|
||
|
self.spi.write(bytearray([command]))
|
||
|
self.cs(1)
|
||
|
# Handle any passed data
|
||
|
if len(args) > 0:
|
||
|
self.write_data(bytearray(args))
|
||
|
|
||
|
def write_cmd_cpy(self, command, *args):
|
||
|
"""Write command to OLED (CircuitPython).
|
||
|
|
||
|
Args:
|
||
|
command (byte): ILI9341 command code.
|
||
|
*args (optional bytes): Data to transmit.
|
||
|
"""
|
||
|
self.dc.value = False
|
||
|
self.cs.value = False
|
||
|
# Confirm SPI locked before writing
|
||
|
while not self.spi.try_lock():
|
||
|
pass
|
||
|
self.spi.write(bytearray([command]))
|
||
|
self.spi.unlock()
|
||
|
self.cs.value = True
|
||
|
# Handle any passed data
|
||
|
if len(args) > 0:
|
||
|
self.write_data(bytearray(args))
|
||
|
|
||
|
def write_data_mpy(self, data):
|
||
|
"""Write data to OLED (MicroPython).
|
||
|
|
||
|
Args:
|
||
|
data (bytes): Data to transmit.
|
||
|
"""
|
||
|
self.dc(1)
|
||
|
self.cs(0)
|
||
|
self.spi.write(data)
|
||
|
self.cs(1)
|
||
|
|
||
|
def write_data_cpy(self, data):
|
||
|
"""Write data to OLED (CircuitPython).
|
||
|
|
||
|
Args:
|
||
|
data (bytes): Data to transmit.
|
||
|
"""
|
||
|
self.dc.value = True
|
||
|
self.cs.value = False
|
||
|
# Confirm SPI locked before writing
|
||
|
while not self.spi.try_lock():
|
||
|
pass
|
||
|
self.spi.write(data)
|
||
|
self.spi.unlock()
|
||
|
self.cs.value = True
|