From ed713ee081eb991fda138620e53533113adfb830 Mon Sep 17 00:00:00 2001 From: Peter Hinch Date: Tue, 17 May 2022 16:38:19 +0100 Subject: [PATCH] Initial commit of bitmap.py. --- gui/widgets/__init__.py | 1 + gui/widgets/bitmap.py | 100 ++++++++++++++++++++++++++++++++++++++++ hardware_setup.py | 78 ++++++++++++++++++------------- 3 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 gui/widgets/bitmap.py diff --git a/gui/widgets/__init__.py b/gui/widgets/__init__.py index a62a4f4..b8fb27c 100644 --- a/gui/widgets/__init__.py +++ b/gui/widgets/__init__.py @@ -23,6 +23,7 @@ _attrs = { "Slider": "sliders", "HorizSlider": "sliders", "Textbox": "textbox", + "BMG": "bitmap", } # Lazy loader, effectively does: diff --git a/gui/widgets/bitmap.py b/gui/widgets/bitmap.py new file mode 100644 index 0000000..52c4ba6 --- /dev/null +++ b/gui/widgets/bitmap.py @@ -0,0 +1,100 @@ +# bitmap.py Provides the BMG (bitmapped graphics) class + +# Released under the MIT License (MIT). See LICENSE. +# Copyright (c) 2022 Peter Hinch +import gc +from framebuf import FrameBuffer, MONO_HLSB +from gui.core.ugui import Widget +from gui.core.colors import * +from gui.core.ugui import ssd + +def rbit8(v): + v = (v & 0x0f) << 4 | (v & 0xf0) >> 4 + v = (v & 0x33) << 2 | (v & 0xcc) >> 2 + return (v & 0x55) << 1 | (v & 0xaa) >> 1 + +class BMG(Widget): + + @staticmethod + def make_buffer(height, width): + w = (width >> 3) + int(width & 7 > 0) + return bytearray(height * w) + + def __init__(self, writer, row, col, height, width, scale=1, *, fgcolor=None, bgcolor=None, bdcolor=RED, buf=None): + super().__init__(writer, row, col, height, width, fgcolor, bgcolor, bdcolor, False) + if buf is None: + buf = BMG.make_buffer(height, width) + self._fb = FrameBuffer(buf, width, height, MONO_HLSB) + self._scale = scale + self._buf = buf + + def show(self): + if super().show(True): # Draw or erase border + palette = ssd.palette + palette.bg(self.bgcolor) + palette.fg(self.fgcolor) + ssd.blit(self._fb, self.col, self.row, -1, palette) + + def color(self, fgcolor=None, bgcolor=None): + if fgcolor is not None: + self.fgcolor = fgcolor + if bgcolor is not None: + self.bgcolor = bgcolor + self.draw = True + + def value(self, obj): + if isinstance(obj, list): # 2d list of booleans + self._fb.fill(1) + s = self._scale + wd = len(obj[0]) + ht = len(obj) + if wd * s > self.width or ht * s > self.height: + print('Object too large for buffer', wd * s, self.width, ht * s, self.height) + else: + print(f"Object is {wd} x {ht}") + for row in range(ht): + for col in range(wd): + v = obj[row][col] + for nc in range(s): + for nr in range(s): + self._fb.pixel(col * s + nc, row * s + nr, v) + elif isinstance(obj, str): # Assume filename + try: + with open(obj, "r") as f: + g = self.handle_stream(f) + n = 0 + for x in g: + self._buf[n] = rbit8(x) + n += 1 + except OSError: + print(f"Failed to input from {obj}") + self.draw = True + gc.collect() +# TODO graphic must be exactly the right size. Get dims from file in app, pass stream? + def handle_stream(self, f): + m = self._scale + s = f.readline() + elements = s.split(" ") + if elements[1].endswith("width"): + wd = int(elements[2]) + else: + raise OSError + s = f.readline() + elements = s.split(" ") + if elements[1].endswith("height"): + ht = int(elements[2]) + else: + raise OSError + if wd * m > self.width or ht * m > self.height: + print("Object too large for buffer", wd * m, self.width, ht * m, self.height) + raise OSError + s = f.readline() + if not s.startswith("static"): + raise OSError + while s := f.readline(): + if (lb := s.find("}")) != -1: + s = s[:lb] # Strip trailing }; + p = s.strip().split(',') + for x in p: + if x: + yield int(x, 16) diff --git a/hardware_setup.py b/hardware_setup.py index 21ba6ed..02cf963 100644 --- a/hardware_setup.py +++ b/hardware_setup.py @@ -1,40 +1,56 @@ -# hardware_setup.py Customise for your hardware config +# ili9341_pico.py Customise for your hardware config # Released under the MIT License (MIT). See LICENSE. -# Copyright (c) 2021 Peter Hinch, Ihor Nehrutsa +# Copyright (c) 2021 Peter Hinch -# Supports: -# Waveshare Pico LCD 1.14" 135*240(Pixel) based on ST7789V -# https://www.waveshare.com/wiki/Pico-LCD-1.14 -# https://www.waveshare.com/pico-lcd-1.14.htm +# As written, supports: +# ili9341 240x320 displays on Pi Pico +# Edit the driver import for other displays. -from machine import Pin, SPI +# Demo of initialisation procedure designed to minimise risk of memory fail +# when instantiating the frame buffer. The aim is to do this as early as +# possible before importing other modules. + +# WIRING +# Pico Display +# GPIO Pin +# 3v3 36 Vin +# IO6 9 CLK Hardware SPI0 +# IO7 10 DATA (AKA SI MOSI) +# IO8 11 DC +# IO9 12 Rst +# Gnd 13 Gnd +# IO10 14 CS + +# Pushbuttons are wired between the pin and Gnd +# Pico pin Meaning +# 16 Operate current control +# 17 Decrease value of current control +# 18 Select previous control +# 19 Select next control +# 20 Increase value of current control + +from machine import Pin, SPI, freq import gc -from drivers.st7789.st7789_4bit import * -SSD = ST7789 - -mode = LANDSCAPE # Options PORTRAIT, USD, REFLECT combined with | +from drivers.ili93xx.ili9341 import ILI9341 as SSD +freq(250_000_000) # RP2 overclock +# Create and export an SSD instance +pdc = Pin(8, Pin.OUT, value=0) # Arbitrary pins +prst = Pin(9, Pin.OUT, value=1) +pcs = Pin(10, Pin.OUT, value=1) +spi = SPI(0, baudrate=30_000_000) gc.collect() # Precaution before instantiating framebuf -# Conservative low baudrate. Can go to 62.5MHz. -spi = SPI(1, 30_000_000, sck=Pin(10), mosi=Pin(11), miso=None) -pcs = Pin(9, Pin.OUT, value=1) -prst = Pin(12, Pin.OUT, value=1) -pbl = Pin(13, Pin.OUT, value=1) -pdc = Pin(8, Pin.OUT, value=0) - -portrait = mode & PORTRAIT -ht, wd = (240, 135) if portrait else (135, 240) -ssd = SSD(spi, height=ht, width=wd, dc=pdc, cs=pcs, rst=prst, disp_mode=mode, display=TDISPLAY) +ssd = SSD(spi, pcs, pdc, prst, usd=True) +from gui.core.ugui import Display, quiet +# quiet() # Create and export a Display instance -from gui.core.ugui import Display -# Define control buttons: adjust joystick orientation to match display -# Orientation is only correct for basic LANDSCAPE and PORTRAIT modes -pnxt, pprev, pin, pdec = (2, 18, 16, 20) if portrait else (20, 16, 2, 18) -nxt = Pin(pnxt, Pin.IN, Pin.PULL_UP) # Move to next control -sel = Pin(3, Pin.IN, Pin.PULL_UP) # Operate current control -prev = Pin(pprev, Pin.IN, Pin.PULL_UP) # Move to previous control -increase = Pin(pin, Pin.IN, Pin.PULL_UP) # Increase control's value -decrease = Pin(pdec, Pin.IN, Pin.PULL_UP) # Decrease control's value -display = Display(ssd, nxt, sel, prev, increase, decrease) +# Define control buttons +nxt = Pin(19, Pin.IN, Pin.PULL_UP) # Move to next control +sel = Pin(16, Pin.IN, Pin.PULL_UP) # Operate current control +prev = Pin(18, Pin.IN, Pin.PULL_UP) # Move to previous control +increase = Pin(20, Pin.IN, Pin.PULL_UP) # Increase control's value +decrease = Pin(17, Pin.IN, Pin.PULL_UP) # Decrease control's value +# display = Display(ssd, nxt, sel, prev) # 3-button mode +display = Display(ssd, nxt, sel, prev, increase, decrease, 4) # Encoder mode