Add simple fpt_mono.py demo with support for black and white e-paper screen

pull/62/head
richardhannagan 2024-04-09 14:44:29 +10:00
rodzic 90fe6b81bd
commit 84297d2e52
2 zmienionych plików z 339 dodań i 0 usunięć

Wyświetl plik

@ -0,0 +1,290 @@
# ePaper2in13V4.py nanogui driver for Pico-ePpaper-2.13
# Tested with RPi Pico
# EPD is subclassed from framebuf.FrameBuffer for use with Writer class and nanogui.
# Optimisations to reduce allocations and RAM use.
# Released under the MIT license see LICENSE
# Thanks to @Peter for a great micropython-nano-gui: https://github.com/peterhinch/micropython-nano-gui
# -----------------------------------------------------------------------------
# * | File : ePaper2in13V4.py
# * | Author : Waveshare team
# * | Function : Electronic paper driver
# * | This version: V1.0
# * | Date : 2023-08-12
# -----------------------------------------------------------------------------
import framebuf
import uasyncio as asyncio
from time import sleep_ms, ticks_ms, ticks_us, ticks_diff
class EPD(framebuf.FrameBuffer):
# A monochrome approach should be used for coding this. The rgb method ensures
# nothing breaks if users specify colors.
@staticmethod
def rgb(r, g, b):
return int((r > 127) or (g > 127) or (b > 127))
def __init__(self, spi, cs, dc, rst, busy, landscape=False, asyn=False, full=True):
self._spi = spi
self._cs = cs # Pins
self._dc = dc
self._rst = rst
self._busy = busy
self._lsc = landscape
self._asyn = asyn
self._as_busy = False # Set immediately on start of task. Cleared when busy pin is logically false (physically 1).
self._full = full
self._updated = asyncio.Event()
# Public bound variables required by nanogui.
self.width = 250 if landscape else 128
self.height = 128 if landscape else 250
self.demo_mode = False # Special mode enables demos to run
self._buffer = bytearray(self.height * self.width // 8)
self._mvb = memoryview(self._buffer)
mode = framebuf.MONO_VLSB if landscape else framebuf.MONO_HLSB
super().__init__(self._buffer, self.width, self.height, mode)
if self._full:
self.init()
else:
self.init_partial()
def _command(self, command, data=None):
self._dc(0)
self._cs(0)
self._spi.write(command)
self._cs(1)
if data is not None:
self._data(data)
def _data(self, data, buf1=bytearray(1)):
self._dc(1)
for b in data:
self._cs(0)
buf1[0] = b
self._spi.write(buf1)
self._cs(1)
def init(self):
# Hardware reset
self._rst(1)
sleep_ms(20)
self._rst(0)
sleep_ms(5)
self._rst(1)
sleep_ms(20)
# Initialisation
self.wait_until_ready()
self._command(b'\x12') # SWRESET
self.wait_until_ready()
self._command(b'\x01') # Driver output control
self._data(b'\xF9')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x11') # data entry mode
self._data(b'\x03')
self._command(b'\x44')
self._data(b'\x00')
self._data(b'\x0F')
self._command(b'\x45')
self._data(b'\x00')
self._data(b'\x00')
self._data(b'\xF9')
self._data(b'\x00')
self._command(b'\x4E')
self._data(b'\x00')
self._command(b'\x4F')
self._data(b'\x00')
self._data(b'\x00')
self._command(b'\x3C') # BorderWaveform
self._data(b'\x05')
self._command(b'\x21') # Display update control
self._data(b'\x00')
self._data(b'\x80')
self._command(b'\x18') # Read built-in temperature sensor
self._data(b'\x80')
self.wait_until_ready()
print('Init Done.')
def displayPartial(self, image):
# Hardware reset
self._rst(1)
sleep_ms(200)
self._rst(0)
sleep_ms(20) # 5ms in Waveshare code
self._rst(1)
sleep_ms(200)
# Initialisation
cmd = self._command
cmd(b'\x3C', b'\x80') # BorderWavefrom
scmd(b'\01', b'\xF9\x00\x00') # Driver output control
cmd(b'\x01', b'\x03') # data entry mode
cmd(b'\x44', b'\x00\x0F')
cmd(b'\x45', b'\x00\x00\x0F\x00')
cmd(b'\x4E', b'\x00')
cmd(b'\x4F', b'\x00\x00')
self.wait_until_ready()
print('Init Partial Done.')
def wait_until_ready(self):
sleep_ms(50)
t = ticks_ms()
while not self.ready():
sleep_ms(100)
dt = ticks_diff(ticks_ms(), t)
print('wait_until_ready {}ms {:5.1f}mins'.format(dt, dt/60_000))
async def wait(self):
await asyncio.sleep_ms(0) # Ensure tasks run that might make it unready
while not self.ready():
await asyncio.sleep_ms(100)
# Pause until framebuf has been copied to device.
async def updated(self):
await self._updated.wait()
# For polling in asynchronous code. Just checks pin state.
# 1 == busy.
def ready(self):
return not(self._as_busy or (self._busy() == 1)) # 1 == busy
async def _as_show(self, buf1=bytearray(1)):
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
t = ticks_ms()
if self._lsc: # Landscape mode
wid = self.width
tbc = self.height // 8 # Vertical bytes per column
iidx = wid * (tbc - 1) # Initial index
idx = iidx # Index into framebuf
vbc = 0 # Current vertical byte count
hpc = 0 # Horizontal pixel count
for i in range(len(mvb)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
else:
for i, b in enumerate(mvb):
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if not(i & 0x1f) and (ticks_diff(ticks_ms(), t) > 20):
await asyncio.sleep_ms(0)
t = ticks_ms()
self._updated.set() # framebuf has now been copied to the device
self._updated.clear()
if self._full:
print('sync full refresh')
cmd(b'\x22', b'\xF7') # DISPLAY_REFRESH
cmd(b'\x20')
else:
print('sync partial refresh')
cmd(b'\x22', b'\xFF') # DISPLAY_REFRESH
cmd(b'\x20')
await asyncio.sleep(1)
while self._busy() == 1:
await asyncio.sleep_ms(200) # Don't release lock until update is complete
self._as_busy = False
# draw the current frame memory. Blocking time ~180ms
def show(self, buf1=bytearray(1)):
if self._asyn:
if self._as_busy:
raise RuntimeError('Cannot refresh: display is busy.')
self._as_busy = True
asyncio.create_task(self._as_show())
return
t = ticks_us()
mvb = self._mvb
send = self._spi.write
cmd = self._command
cmd(b'\x24')
self._dc(1)
# Necessary to deassert CS after each byte otherwise display does not
# clear down correctly
if self._lsc: # Landscape mode
wid = self.width
tbc = self.height // 8 # Vertical bytes per column
iidx = wid * (tbc - 1) # Initial index
idx = iidx # Index into framebuf
vbc = 0 # Current vertical byte count
hpc = 0 # Horizontal pixel count
for _ in range(len(mvb)):
self._cs(0)
buf1[0] = ~mvb[idx] # INVERSION HACK ~data
send(buf1)
self._cs(1)
idx -= self.width
vbc += 1
vbc %= tbc
if not vbc:
hpc += 1
idx = iidx + hpc
else:
for b in mvb:
self._cs(0)
buf1[0] = ~b # INVERSION HACK ~data
send(buf1)
self._cs(1)
if self._full:
print('sync full refresh')
cmd(b'\x22', b'\xF7') # DISPLAY_REFRESH
cmd(b'\x20')
else:
print('sync partial refresh')
cmd(b'\x22', b'\xFF') # DISPLAY_REFRESH
cmd(b'\x20')
te = ticks_us()
print('show time', ticks_diff(te, t)//1000, 'ms')
if not self.demo_mode:
# Immediate return to avoid blocking the whole application.
# User should wait for ready before calling refresh()
return
self.wait_until_ready()
sleep_ms(2000) # Give time for user to see result
# to wake call init()
def sleep(self):
self._as_busy = False
self.wait_until_ready()
self._command(b'\x10')
self._data(b'\x01')
self._rst(0) # According to schematic this turns off the power

Wyświetl plik

@ -0,0 +1,49 @@
# fpt_mono.py Test/demo program for framebuf plot. Cross-patform
# Tested on Raspberry Pi Pico with E-Paper display:
# Waveshare 2.13inch E-Paper Module for Raspberry Pi Pico 250*122 (Black/White): https://www.waveshare.com/pico-epaper-2.13.htm
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2018-2020 Peter Hinch
# Initialise hardware and framebuf before importing modules.
from color_setup import ssd # Create a display instance
import cmath
import math
import utime
import uos
from gui.core.writer import Writer
from gui.core.fplot import PolarGraph, PolarCurve, CartesianGraph, Curve, TSequence
from gui.core.nanogui import refresh
from gui.widgets.label import Label
refresh(ssd, True)
# Fonts
import gui.fonts.arial10 as arial10
import gui.fonts.freesans20 as freesans20
from gui.core.colors import *
Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it
wri = Writer(ssd, arial10, verbose=False)
wri.set_clip(True, True, False)
def seq():
print('Time sequence test - sine and cosine.')
refresh(ssd, True) # Clear any prior image
# y axis at t==now, with gridlines and border
g = CartesianGraph(wri, 6, 0, xorigin = 10, height=120, width=248, fgcolor=None,
gridcolor=None, bdcolor=False)
ts1 = TSequence(g, None, 50)
ts2 = TSequence(g, None, 50)
for t in range(100):
g.clear()
ts1.add(0.9*math.sin(t/10))
ts2.add(0.4*math.cos(t/10))
refresh(ssd)
utime.sleep_ms(100)
print('Test runs to completion.')
seq()
utime.sleep(1.5)