Add CircuitPython RLCD migration prototype
This commit is contained in:
+10
@@ -1,3 +1,13 @@
|
||||
# Local-only CircuitPython configuration / secrets
|
||||
settings.toml
|
||||
.env
|
||||
|
||||
# Firmware downloads and generated bundles
|
||||
firmware/
|
||||
*.bin
|
||||
*.uf2
|
||||
*.zip
|
||||
|
||||
# Temporary generated media and screenshots
|
||||
*.pbm
|
||||
*.png
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# CircuitPython migration prototype for Waveshare ESP32-S3-RLCD-4.2
|
||||
|
||||
This folder is an initial CircuitPython port attempt for the existing MicroPython ESP32-S3-RLCD project.
|
||||
|
||||
## Goal
|
||||
|
||||
Move the board firmware toward CircuitPython so we can use built-in CircuitPython media modules:
|
||||
|
||||
- `audiomp3` for MP3 playback over I2S
|
||||
- `gifio` for frame-by-frame GIF decoding
|
||||
|
||||
The current MicroPython project already has working low-level knowledge for:
|
||||
|
||||
- ST7305 reflective LCD init and 2x4 packed monochrome framebuffer
|
||||
- ES8311 speaker DAC over I2C + I2S
|
||||
- GPIO46 speaker amp enable
|
||||
- ESP32-S3 board pins
|
||||
|
||||
## Current status
|
||||
|
||||
This is not a full replacement firmware yet. It is a bootable/iterable prototype layout:
|
||||
|
||||
- `code.py` — demo entrypoint for CircuitPython
|
||||
- `lib/rlcd_cp.py` — custom ST7305/RLCD driver using `busio`/`digitalio`
|
||||
- `lib/audio_cp.py` — ES8311 + I2S MP3 playback helper
|
||||
- `lib/gif_player.py` — GIF-to-RLCD playback helper using `gifio`
|
||||
|
||||
## Why a custom display driver is needed
|
||||
|
||||
Antigravity’s review confirmed the important constraint: ST7305/RLCD is not a normal `displayio` panel here. The existing `rlcd.py` packs pixels into 15,000 bytes where each byte represents a 2×4 pixel block, with vertical inversion. CircuitPython does not appear to have a native `displayio` ST7305 driver for this exact layout, so this port keeps a Python-side canvas and manually packs it before SPI transfer.
|
||||
|
||||
## Hardware pins copied from the working MicroPython firmware
|
||||
|
||||
Display SPI:
|
||||
|
||||
- SCK: IO11
|
||||
- MOSI: IO12
|
||||
- CS: IO40
|
||||
- DC: IO5
|
||||
- RST: IO41
|
||||
|
||||
Audio/I2C:
|
||||
|
||||
- I2C SDA: IO13
|
||||
- I2C SCL: IO14
|
||||
- I2S BCLK: IO9
|
||||
- I2S LRCK/WS: IO45
|
||||
- I2S DOUT: IO8
|
||||
- MCLK PWM: IO16
|
||||
- Speaker amp enable: IO46
|
||||
|
||||
## Copy to CIRCUITPY
|
||||
|
||||
After flashing a compatible CircuitPython build for the ESP32-S3 N16R8-style board:
|
||||
|
||||
```bash
|
||||
cp circuitpython/code.py /media/$USER/CIRCUITPY/code.py
|
||||
mkdir -p /media/$USER/CIRCUITPY/lib
|
||||
cp circuitpython/lib/*.py /media/$USER/CIRCUITPY/lib/
|
||||
```
|
||||
|
||||
Optional test assets:
|
||||
|
||||
```bash
|
||||
cp demo.mp3 /media/$USER/CIRCUITPY/demo.mp3
|
||||
cp demo.gif /media/$USER/CIRCUITPY/demo.gif
|
||||
```
|
||||
|
||||
## Expected next hardware tests
|
||||
|
||||
1. Boot with only `code.py` and libraries copied.
|
||||
2. Confirm display init + dashboard text appears.
|
||||
3. Copy a tiny 320px-wide-or-smaller monochrome/low-color GIF and test GIF playback. `gifio` cannot load 400px-wide GIFs on current CircuitPython builds; the prototype centers 320px GIFs on the 400px screen.
|
||||
4. Copy a short MP3 and test I2S/ES8311 playback.
|
||||
5. If display is scrambled, compare `pack()` output against MicroPython `rlcd.py` and host `notetaker.py` mapping.
|
||||
|
||||
## Known risks
|
||||
|
||||
- CircuitPython may not expose every `board.IO##` name on the selected build. If so, update `PINS` in `code.py` with the names present in `dir(board)`.
|
||||
- `pwmio.PWMOut` at 12.288 MHz on IO16 may not work on all CircuitPython ESP32-S3 builds. If MCLK fails, MP3 playback may be silent even if I2S is writing.
|
||||
- Pure Python display packing loops may be slow. This prototype favors correctness first; optimize with lookup tables after the first visible frame works.
|
||||
- GIF playback on a 1-bit reflective LCD will be low-framerate and monochrome-thresholded.
|
||||
- CircuitPython SPI configuration is lock-scoped, so `rlcd_cp.py` configures 20 MHz SPI after every lock.
|
||||
@@ -0,0 +1,115 @@
|
||||
"""CircuitPython entrypoint for Waveshare ESP32-S3-RLCD-4.2 migration.
|
||||
|
||||
Copy this file to CIRCUITPY/code.py after flashing CircuitPython.
|
||||
"""
|
||||
|
||||
import gc
|
||||
import os
|
||||
import time
|
||||
|
||||
import board
|
||||
import busio
|
||||
import digitalio
|
||||
|
||||
from audio_cp import BoardAudio
|
||||
from gif_player import GIFPlayer
|
||||
from rlcd_cp import RLCD
|
||||
|
||||
|
||||
# Keep pin choices in one place because CircuitPython board builds differ in
|
||||
# how they expose ESP32-S3 pins. If board.IO## is missing, run `dir(board)` on
|
||||
# the serial console and update these names.
|
||||
PINS = {
|
||||
"display_sck": board.IO11,
|
||||
"display_mosi": board.IO12,
|
||||
"display_cs": board.IO40,
|
||||
"display_dc": board.IO5,
|
||||
"display_rst": board.IO41,
|
||||
"i2c_sda": board.IO13,
|
||||
"i2c_scl": board.IO14,
|
||||
"i2s_bclk": board.IO9,
|
||||
"i2s_lrck": board.IO45,
|
||||
"i2s_dout": board.IO8,
|
||||
"audio_mclk": board.IO16,
|
||||
"audio_amp": board.IO46,
|
||||
}
|
||||
|
||||
|
||||
def exists(path):
|
||||
try:
|
||||
os.stat(path)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def init_display():
|
||||
spi = busio.SPI(clock=PINS["display_sck"], MOSI=PINS["display_mosi"])
|
||||
# Match the working MicroPython code's 20 MHz SPI if the build supports it.
|
||||
while not spi.try_lock():
|
||||
pass
|
||||
try:
|
||||
spi.configure(baudrate=20000000, phase=0, polarity=0)
|
||||
finally:
|
||||
spi.unlock()
|
||||
display = RLCD(
|
||||
spi,
|
||||
cs=digitalio.DigitalInOut(PINS["display_cs"]),
|
||||
dc=digitalio.DigitalInOut(PINS["display_dc"]),
|
||||
rst=digitalio.DigitalInOut(PINS["display_rst"]),
|
||||
)
|
||||
return display
|
||||
|
||||
|
||||
def init_audio():
|
||||
i2c = busio.I2C(scl=PINS["i2c_scl"], sda=PINS["i2c_sda"])
|
||||
return BoardAudio(
|
||||
i2c,
|
||||
bit_clock=PINS["i2s_bclk"],
|
||||
word_select=PINS["i2s_lrck"],
|
||||
data=PINS["i2s_dout"],
|
||||
mclk=PINS["audio_mclk"],
|
||||
amp=PINS["audio_amp"],
|
||||
)
|
||||
|
||||
|
||||
def draw_boot_screen(display, message="CircuitPython RLCD"):
|
||||
display.clear(0)
|
||||
display.text("ESP32-S3-RLCD", 10, 10, 1)
|
||||
display.line(10, 22, 390, 22, 1)
|
||||
display.text_large("CIRCUITPY", 18, 45, scale=3, color=1)
|
||||
display.text(message, 18, 86, 1)
|
||||
display.text("MP3: /demo.mp3", 18, 116, 1)
|
||||
display.text("GIF: /demo.gif", 18, 132, 1)
|
||||
display.text("If visible, ST7305 init works", 18, 170, 1)
|
||||
display.show()
|
||||
|
||||
|
||||
def main():
|
||||
display = init_display()
|
||||
draw_boot_screen(display, "Display initialized")
|
||||
time.sleep(1)
|
||||
|
||||
# Try GIF first because it verifies display animation support.
|
||||
if exists("/demo.gif"):
|
||||
draw_boot_screen(display, "Playing GIF...")
|
||||
GIFPlayer(display).play("/demo.gif", loops=1)
|
||||
draw_boot_screen(display, "GIF done")
|
||||
gc.collect()
|
||||
|
||||
# Try MP3 if present. If this fails silently, verify MCLK on IO16 and codec
|
||||
# register sequence against a known-good WAV/tone first.
|
||||
if exists("/demo.mp3"):
|
||||
draw_boot_screen(display, "Playing MP3...")
|
||||
try:
|
||||
audio = init_audio()
|
||||
ok = audio.play_mp3("/demo.mp3", volume=65)
|
||||
draw_boot_screen(display, "MP3 ok" if ok else "MP3 failed")
|
||||
except Exception as exc:
|
||||
draw_boot_screen(display, "MP3 error: " + str(exc)[:28])
|
||||
|
||||
while True:
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
main()
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CIRCUITPY_MOUNT="${CIRCUITPY_MOUNT:-}"
|
||||
|
||||
if [[ -z "$CIRCUITPY_MOUNT" ]]; then
|
||||
for candidate in \
|
||||
"/media/$USER/CIRCUITPY" \
|
||||
"/run/media/$USER/CIRCUITPY" \
|
||||
"/media/$(id -un)/CIRCUITPY" \
|
||||
"/Volumes/CIRCUITPY"; do
|
||||
if [[ -d "$candidate" ]]; then
|
||||
CIRCUITPY_MOUNT="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z "$CIRCUITPY_MOUNT" || ! -d "$CIRCUITPY_MOUNT" ]]; then
|
||||
echo "CIRCUITPY mount not found. Set CIRCUITPY_MOUNT=/path/to/CIRCUITPY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$CIRCUITPY_MOUNT/lib"
|
||||
cp "$ROOT/circuitpython/code.py" "$CIRCUITPY_MOUNT/code.py"
|
||||
cp "$ROOT/circuitpython/lib/"*.py "$CIRCUITPY_MOUNT/lib/"
|
||||
cp "$ROOT/circuitpython/lib/"*.mpy "$CIRCUITPY_MOUNT/lib/"
|
||||
sync
|
||||
find "$CIRCUITPY_MOUNT" -maxdepth 2 -type f | sort
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${PORT:-/dev/ttyACM0}"
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FIRMWARE="$ROOT/firmware/adafruit-circuitpython-espressif_esp32s3_devkitc_1_n8r8-en_US-10.2.1.bin"
|
||||
CIRCUITPY_MOUNT="${CIRCUITPY_MOUNT:-}"
|
||||
|
||||
if [[ ! -f "$FIRMWARE" ]]; then
|
||||
echo "Firmware not found: $FIRMWARE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -r "$PORT" || ! -w "$PORT" ]]; then
|
||||
cat >&2 <<EOF
|
||||
Cannot access $PORT as $(id -un).
|
||||
|
||||
Temporary fix from a local terminal:
|
||||
sudo chmod 666 $PORT
|
||||
|
||||
Permanent fix:
|
||||
sudo usermod -aG dialout $(id -un)
|
||||
# then log out/in, or start a new shell with: newgrp dialout
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "==> Probing ESP32-S3 on $PORT"
|
||||
uvx esptool --chip esp32s3 --port "$PORT" flash-id
|
||||
|
||||
echo "==> Erasing flash"
|
||||
uvx esptool --chip esp32s3 --port "$PORT" erase-flash
|
||||
|
||||
echo "==> Flashing CircuitPython N8R8 firmware"
|
||||
uvx esptool --chip esp32s3 --port "$PORT" --baud 460800 write-flash -z 0x0 "$FIRMWARE"
|
||||
|
||||
echo "==> Waiting for CIRCUITPY mount"
|
||||
for _ in $(seq 1 60); do
|
||||
if [[ -n "$CIRCUITPY_MOUNT" && -d "$CIRCUITPY_MOUNT" ]]; then
|
||||
break
|
||||
fi
|
||||
for candidate in \
|
||||
"/media/$USER/CIRCUITPY" \
|
||||
"/run/media/$USER/CIRCUITPY" \
|
||||
"/media/$(id -un)/CIRCUITPY" \
|
||||
"/Volumes/CIRCUITPY"; do
|
||||
if [[ -d "$candidate" ]]; then
|
||||
CIRCUITPY_MOUNT="$candidate"
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ -z "$CIRCUITPY_MOUNT" || ! -d "$CIRCUITPY_MOUNT" ]]; then
|
||||
cat >&2 <<EOF
|
||||
Flashing finished, but CIRCUITPY did not auto-mount.
|
||||
Reset the board once, then rerun just deployment with:
|
||||
CIRCUITPY_MOUNT=/path/to/CIRCUITPY bash circuitpython/deploy_only.sh
|
||||
EOF
|
||||
exit 3
|
||||
fi
|
||||
|
||||
echo "==> Deploying prototype to $CIRCUITPY_MOUNT"
|
||||
mkdir -p "$CIRCUITPY_MOUNT/lib"
|
||||
cp "$ROOT/circuitpython/code.py" "$CIRCUITPY_MOUNT/code.py"
|
||||
cp "$ROOT/circuitpython/lib/"*.py "$CIRCUITPY_MOUNT/lib/"
|
||||
cp "$ROOT/circuitpython/lib/"*.mpy "$CIRCUITPY_MOUNT/lib/"
|
||||
sync
|
||||
|
||||
echo "==> Deployed. Files on CIRCUITPY:"
|
||||
find "$CIRCUITPY_MOUNT" -maxdepth 2 -type f | sort
|
||||
|
||||
echo "==> Done. Reset the board if it does not reload automatically."
|
||||
Binary file not shown.
@@ -0,0 +1,130 @@
|
||||
"""CircuitPython audio helpers for Waveshare ESP32-S3-RLCD-4.2.
|
||||
|
||||
Ports the ES8311 setup sequence from MicroPython audio_util.py and adds MP3
|
||||
playback using CircuitPython's audiomp3 + audiobusio stack.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import audiobusio
|
||||
import audiomp3
|
||||
import digitalio
|
||||
import pwmio
|
||||
|
||||
|
||||
class ES8311:
|
||||
ADDR = 0x18
|
||||
|
||||
def __init__(self, i2c):
|
||||
self.i2c = i2c
|
||||
|
||||
def _write(self, register, value):
|
||||
# CircuitPython I2CDevice is not used to keep this dependency-free.
|
||||
while not self.i2c.try_lock():
|
||||
pass
|
||||
try:
|
||||
self.i2c.writeto(self.ADDR, bytes([register & 0xFF, value & 0xFF]))
|
||||
finally:
|
||||
self.i2c.unlock()
|
||||
|
||||
def init(self, sample_rate=44100):
|
||||
"""Initialize ES8311 for I2S DAC playback.
|
||||
|
||||
The register sequence is the existing MicroPython project's known-good
|
||||
sequence. The original comments targeted 16 kHz WAV playback, but the
|
||||
I2S peripheral is configured with the MP3 decoder sample rate at play
|
||||
time. If MP3 output is silent, hardware-test this sequence first with a
|
||||
16 kHz WAV/tone and then tune codec dividers for the MP3 sample rate.
|
||||
"""
|
||||
del sample_rate # Kept for API clarity; sequence currently fixed.
|
||||
self._write(0x00, 0x1F)
|
||||
time.sleep(0.01)
|
||||
self._write(0x00, 0x00)
|
||||
time.sleep(0.01)
|
||||
self._write(0x01, 0x3F)
|
||||
self._write(0x02, 0x48)
|
||||
self._write(0x03, 0x10)
|
||||
self._write(0x04, 0x20)
|
||||
self._write(0x05, 0x00)
|
||||
self._write(0x06, 0x03)
|
||||
self._write(0x07, 0x00)
|
||||
self._write(0x08, 0xFF)
|
||||
self._write(0x09, 0x0C)
|
||||
self._write(0x0A, 0x0C)
|
||||
self._write(0x0D, 0x01)
|
||||
self._write(0x0E, 0x02)
|
||||
self._write(0x12, 0x00)
|
||||
self._write(0x13, 0x10)
|
||||
self._write(0x1C, 0x6A)
|
||||
self._write(0x37, 0x08)
|
||||
self._write(0x32, 0xBF)
|
||||
self._write(0x31, 0x00)
|
||||
self._write(0x00, 0x80)
|
||||
|
||||
def set_volume(self, volume):
|
||||
volume = max(0, min(100, int(volume)))
|
||||
self._write(0x32, int(volume * 255 / 100))
|
||||
|
||||
|
||||
class BoardAudio:
|
||||
def __init__(self, i2c, *, bit_clock, word_select, data, mclk, amp):
|
||||
self.i2c = i2c
|
||||
self.bit_clock_pin = bit_clock
|
||||
self.word_select_pin = word_select
|
||||
self.data_pin = data
|
||||
self.mclk_pin = mclk
|
||||
self.amp_pin = amp
|
||||
self._mclk = None
|
||||
self._amp = digitalio.DigitalInOut(amp)
|
||||
self._amp.switch_to_output(value=False)
|
||||
self.codec = ES8311(i2c)
|
||||
|
||||
def start_mclk(self):
|
||||
if self._mclk is None:
|
||||
# External MCLK for ES8311. CircuitPython I2SOut on ESP32-S3 does
|
||||
# not accept a main_clock parameter, so keep this independent.
|
||||
self._mclk = pwmio.PWMOut(
|
||||
self.mclk_pin,
|
||||
frequency=12288000,
|
||||
duty_cycle=32768,
|
||||
variable_frequency=False,
|
||||
)
|
||||
|
||||
def stop_mclk(self):
|
||||
if self._mclk is not None:
|
||||
self._mclk.deinit()
|
||||
self._mclk = None
|
||||
|
||||
def play_mp3(self, filename, volume=60):
|
||||
"""Play an MP3 file from CIRCUITPY storage over the onboard speaker."""
|
||||
self.start_mclk()
|
||||
self.codec.init()
|
||||
self.codec.set_volume(volume)
|
||||
decoder = None
|
||||
audio = None
|
||||
f = None
|
||||
try:
|
||||
f = open(filename, "rb")
|
||||
decoder = audiomp3.MP3Decoder(f)
|
||||
# Use decoder-derived rate if available, else fall back.
|
||||
sample_rate = getattr(decoder, "sample_rate", 44100)
|
||||
audio = audiobusio.I2SOut(
|
||||
bit_clock=self.bit_clock_pin,
|
||||
word_select=self.word_select_pin,
|
||||
data=self.data_pin,
|
||||
)
|
||||
self._amp.value = True
|
||||
audio.play(decoder)
|
||||
while audio.playing:
|
||||
time.sleep(0.05)
|
||||
return True
|
||||
finally:
|
||||
self._amp.value = False
|
||||
if audio is not None:
|
||||
audio.deinit()
|
||||
if decoder is not None:
|
||||
decoder.deinit()
|
||||
if f is not None:
|
||||
f.close()
|
||||
# Leave MCLK running for repeated playback; call stop_mclk() before
|
||||
# deep sleep if power matters.
|
||||
@@ -0,0 +1,48 @@
|
||||
"""GIF playback helper for the custom RLCD CircuitPython driver."""
|
||||
|
||||
import gc
|
||||
import time
|
||||
|
||||
import gifio
|
||||
|
||||
|
||||
class GIFPlayer:
|
||||
def __init__(self, display):
|
||||
self.display = display
|
||||
|
||||
def play(self, filename, *, loops=1, x=40, y=0, threshold=1, clear_between_frames=False):
|
||||
"""Decode a GIF from disk and push frames to the reflective LCD.
|
||||
|
||||
CircuitPython gifio currently supports GIFs up to 320 pixels wide, so
|
||||
x defaults to 40 to center a 320-wide GIF on this 400-wide display.
|
||||
The ST7305/RLCD display is monochrome in this project, so frames are
|
||||
thresholded from palette indexes: palette index 0 is white; any index
|
||||
>= threshold is black.
|
||||
"""
|
||||
gif = gifio.OnDiskGif(filename)
|
||||
try:
|
||||
count = 0
|
||||
while loops < 0 or count < loops:
|
||||
while True:
|
||||
delay = gif.next_frame()
|
||||
if delay is None:
|
||||
break
|
||||
if clear_between_frames:
|
||||
self.display.clear(0)
|
||||
self.display.draw_bitmap_threshold(gif.bitmap, x, y, threshold=threshold)
|
||||
self.display.show()
|
||||
# gifio delay is seconds in modern CircuitPython builds. If
|
||||
# an older build plays 100x too slowly, divide by 100 here.
|
||||
time.sleep(max(0.0, delay))
|
||||
count += 1
|
||||
if loops < 0 or count < loops:
|
||||
# OnDiskGif has no reliable seek/rewind API; reopen to loop.
|
||||
gif.deinit()
|
||||
gc.collect()
|
||||
gif = gifio.OnDiskGif(filename)
|
||||
finally:
|
||||
try:
|
||||
gif.deinit()
|
||||
except AttributeError:
|
||||
pass
|
||||
gc.collect()
|
||||
@@ -0,0 +1,199 @@
|
||||
"""CircuitPython ST7305/RLCD driver for Waveshare ESP32-S3-RLCD-4.2.
|
||||
|
||||
This is a direct migration attempt from the project's MicroPython rlcd.py.
|
||||
It intentionally does not use displayio's display bus because the panel uses a
|
||||
non-standard 2x4 packed 1-bit memory layout.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
try:
|
||||
import adafruit_framebuf
|
||||
except ImportError: # CircuitPython bundle dependency.
|
||||
adafruit_framebuf = None
|
||||
|
||||
|
||||
class RLCD:
|
||||
WIDTH = 400
|
||||
HEIGHT = 300
|
||||
BUFFER_SIZE = (WIDTH * HEIGHT) // 8
|
||||
|
||||
def __init__(self, spi, cs, dc, rst, width=WIDTH, height=HEIGHT):
|
||||
if adafruit_framebuf is None:
|
||||
raise RuntimeError("adafruit_framebuf is required in CIRCUITPY/lib")
|
||||
self.spi = spi
|
||||
self.cs = cs
|
||||
self.dc = dc
|
||||
self.rst = rst
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.hw_buffer = bytearray((width * height) // 8)
|
||||
self.canvas_buffer = bytearray((width * height) // 8)
|
||||
self.canvas = adafruit_framebuf.FrameBuffer(
|
||||
self.canvas_buffer,
|
||||
width,
|
||||
height,
|
||||
adafruit_framebuf.MHMSB,
|
||||
)
|
||||
|
||||
self.cs.switch_to_output(value=True)
|
||||
self.dc.switch_to_output(value=False)
|
||||
self.rst.switch_to_output(value=True)
|
||||
|
||||
self.reset()
|
||||
self.init_display()
|
||||
|
||||
def reset(self):
|
||||
self.rst.value = True
|
||||
time.sleep(0.05)
|
||||
self.rst.value = False
|
||||
time.sleep(0.02)
|
||||
self.rst.value = True
|
||||
time.sleep(0.05)
|
||||
|
||||
def _lock_spi(self):
|
||||
while not self.spi.try_lock():
|
||||
pass
|
||||
# CircuitPython SPI settings are only guaranteed while the bus is
|
||||
# locked, so configure after every lock instead of only at startup.
|
||||
self.spi.configure(baudrate=20000000, phase=0, polarity=0)
|
||||
|
||||
def _unlock_spi(self):
|
||||
self.spi.unlock()
|
||||
|
||||
def write_cmd(self, cmd):
|
||||
self._lock_spi()
|
||||
try:
|
||||
self.cs.value = False
|
||||
self.dc.value = False
|
||||
self.spi.write(bytes([cmd & 0xFF]))
|
||||
self.cs.value = True
|
||||
finally:
|
||||
self._unlock_spi()
|
||||
|
||||
def write_data(self, data):
|
||||
if isinstance(data, int):
|
||||
payload = bytes([data & 0xFF])
|
||||
elif isinstance(data, (bytes, bytearray, memoryview)):
|
||||
payload = data
|
||||
else:
|
||||
payload = bytes(data)
|
||||
self._lock_spi()
|
||||
try:
|
||||
self.cs.value = False
|
||||
self.dc.value = True
|
||||
self.spi.write(payload)
|
||||
self.cs.value = True
|
||||
finally:
|
||||
self._unlock_spi()
|
||||
|
||||
def init_display(self):
|
||||
# Ported from MicroPython rlcd.py. The old file had one decimal 19 in
|
||||
# the 0xC5 payload; this port uses 0x19 consistently.
|
||||
self.write_cmd(0xD6); self.write_data([0x17, 0x02])
|
||||
self.write_cmd(0xD1); self.write_data(0x01)
|
||||
self.write_cmd(0xC0); self.write_data([0x11, 0x04])
|
||||
self.write_cmd(0xC1); self.write_data([0x41, 0x41, 0x41, 0x41])
|
||||
self.write_cmd(0xC2); self.write_data([0x19, 0x19, 0x19, 0x19])
|
||||
self.write_cmd(0xC4); self.write_data([0x41, 0x41, 0x41, 0x41])
|
||||
self.write_cmd(0xC5); self.write_data([0x19, 0x19, 0x19, 0x19])
|
||||
self.write_cmd(0xD8); self.write_data([0xA6, 0xE9])
|
||||
self.write_cmd(0xB2); self.write_data(0x05)
|
||||
self.write_cmd(0xB3); self.write_data([0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45])
|
||||
self.write_cmd(0xB4); self.write_data([0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45])
|
||||
self.write_cmd(0x62); self.write_data([0x32, 0x03, 0x1F])
|
||||
self.write_cmd(0xB7); self.write_data(0x13)
|
||||
self.write_cmd(0xB0); self.write_data(0x64)
|
||||
self.write_cmd(0x11)
|
||||
time.sleep(0.2)
|
||||
self.write_cmd(0xC9); self.write_data(0x00)
|
||||
self.write_cmd(0x36); self.write_data(0x48)
|
||||
self.write_cmd(0x3A); self.write_data(0x11)
|
||||
self.write_cmd(0xB9); self.write_data(0x20)
|
||||
self.write_cmd(0xB8); self.write_data(0x29)
|
||||
self.write_cmd(0x21)
|
||||
self.write_cmd(0x2A); self.write_data([0x12, 0x2A])
|
||||
self.write_cmd(0x2B); self.write_data([0x00, 0xC7])
|
||||
self.write_cmd(0x35); self.write_data(0x00)
|
||||
self.write_cmd(0xD0); self.write_data(0xFF)
|
||||
self.write_cmd(0x38)
|
||||
self.write_cmd(0x29)
|
||||
|
||||
def clear(self, color=0):
|
||||
self.canvas.fill(1 if color else 0)
|
||||
|
||||
def pixel(self, x, y, color):
|
||||
self.canvas.pixel(x, y, 1 if color else 0)
|
||||
|
||||
def line(self, x0, y0, x1, y1, color):
|
||||
self.canvas.line(x0, y0, x1, y1, 1 if color else 0)
|
||||
|
||||
def rect(self, x, y, width, height, color):
|
||||
self.canvas.rect(x, y, width, height, 1 if color else 0)
|
||||
|
||||
def fill_rect(self, x, y, width, height, color):
|
||||
self.canvas.fill_rect(x, y, width, height, 1 if color else 0)
|
||||
|
||||
def text(self, text, x, y, color=1):
|
||||
self.canvas.text(str(text), x, y, 1 if color else 0)
|
||||
|
||||
def text_large(self, text, x, y, scale=2, color=1):
|
||||
# Simple nearest-neighbor scaled 8x8 font rendering using a temp buffer.
|
||||
tmp = bytearray(8)
|
||||
fb = adafruit_framebuf.FrameBuffer(tmp, 8, 8, adafruit_framebuf.MHMSB)
|
||||
color = 1 if color else 0
|
||||
for ch in str(text):
|
||||
fb.fill(0)
|
||||
fb.text(ch, 0, 0, 1)
|
||||
for py in range(8):
|
||||
for px in range(8):
|
||||
if fb.pixel(px, py):
|
||||
self.canvas.fill_rect(x + px * scale, y + py * scale, scale, scale, color)
|
||||
x += 8 * scale
|
||||
|
||||
def draw_bitmap_threshold(self, bitmap, x=0, y=0, threshold=1):
|
||||
"""Copy any indexable bitmap-like object to the 1-bit canvas.
|
||||
|
||||
gifio/displayio bitmaps return palette indexes. Treat index 0 as white
|
||||
and anything >= threshold as black by default.
|
||||
"""
|
||||
width = min(getattr(bitmap, "width", self.width), self.width - x)
|
||||
height = min(getattr(bitmap, "height", self.height), self.height - y)
|
||||
for yy in range(height):
|
||||
for xx in range(width):
|
||||
self.canvas.pixel(x + xx, y + yy, 1 if bitmap[xx, yy] >= threshold else 0)
|
||||
|
||||
def pack(self):
|
||||
"""Convert standard horizontal 1-bit canvas into the RLCD 2x4 layout."""
|
||||
buf = self.hw_buffer
|
||||
for i in range(len(buf)):
|
||||
buf[i] = 0
|
||||
|
||||
# Correctness-first port of MicroPython native loop.
|
||||
height = self.height
|
||||
for y in range(height):
|
||||
inv_y = height - 1 - y
|
||||
block_y = inv_y // 4
|
||||
local_y = inv_y & 0x03
|
||||
local_y_shift = local_y * 2
|
||||
row_offset = block_y
|
||||
for x in range(self.width):
|
||||
if self.canvas.pixel(x, y):
|
||||
byte_x = x >> 1
|
||||
index = byte_x * 75 + row_offset
|
||||
local_x = x & 0x01
|
||||
bit = 7 - (local_y_shift + local_x)
|
||||
buf[index] |= 1 << bit
|
||||
return buf
|
||||
|
||||
def show(self):
|
||||
self.pack()
|
||||
self.write_cmd(0x2A)
|
||||
self.write_data([0x12, 0x2A])
|
||||
self.write_cmd(0x2B)
|
||||
self.write_data([0x00, 0xC7])
|
||||
self.write_cmd(0x2C)
|
||||
self.write_data(self.hw_buffer)
|
||||
|
||||
def invert(self, enable=True):
|
||||
self.write_cmd(0x21 if enable else 0x20)
|
||||
Reference in New Issue
Block a user