diff --git a/micropython/drivers/display/gc9a01/gc9a01.py b/micropython/drivers/display/gc9a01/gc9a01.py new file mode 100644 index 000000000..6c7cadf94 --- /dev/null +++ b/micropython/drivers/display/gc9a01/gc9a01.py @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2021 Tyler Crumpton +# +# SPDX-License-Identifier: MIT +""" +`gc9a01` +================================================================================ + +displayio driver for GC9A01 TFT LCD displays + + +* Author(s): Tyler Crumpton + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +""" + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/tylercrumpton/CircuitPython_GC9A01.git" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +_INIT_SEQUENCE = bytearray( + b"\xfe\x00" # Inter Register Enable1 (FEh) + b"\xef\x00" # Inter Register Enable2 (EFh) + b"\xb6\x02\x00\x00" # Display Function Control (B6h) [S1→S360 source, G1→G32 gate] + b"\x36\x01\x48" # Memory Access Control(36h) [Invert Row order, invert vertical scan order] + b"\x3a\x01\x05" # COLMOD: Pixel Format Set (3Ah) [16 bits / pixel] + b"\xc3\x01\x13" # Power Control 2 (C3h) [VREG1A = 5.06, VREG1B = 0.68] + b"\xc4\x01\x13" # Power Control 3 (C4h) [VREG2A = -3.7, VREG2B = 0.68] + b"\xc9\x01\x22" # Power Control 4 (C9h) + b"\xf0\x06\x45\x09\x08\x08\x26\x2a" # SET_GAMMA1 (F0h) + b"\xf1\x06\x43\x70\x72\x36\x37\x6f" # SET_GAMMA2 (F1h) + b"\xf2\x06\x45\x09\x08\x08\x26\x2a" # SET_GAMMA3 (F2h) + b"\xf3\x06\x43\x70\x72\x36\x37\x6f" # SET_GAMMA4 (F3h) + b"\x66\x0a\x3c\x00\xcd\x67\x45\x45\x10\x00\x00\x00" + b"\x67\x0a\x00\x3c\x00\x00\x00\x01\x54\x10\x32\x98" + b"\x74\x07\x10\x85\x80\x00\x00\x4e\x00" + b"\x98\x02\x3e\x07" + b"\x35\x00" # Tearing Effect Line ON (35h) [both V-blanking and H-blanking] + b"\x21\x00" # Display Inversion ON (21h) + b"\x11\x80\x78" # Sleep Out Mode (11h) and delay(120) + b"\x29\x80\x14" # Display ON (29h) and delay(20) + b"\x2a\x04\x00\x00\x00\xef" # Column Address Set (2Ah) [Start col = 0, end col = 239] + b"\x2b\x04\x00\x00\x00\xef" # Row Address Set (2Bh) [Start row = 0, end row = 239] +) + + +# pylint: disable=too-few-public-methods +class GC9A01(BusDisplay): + """GC9A01 display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/gc9a01/manifest.py b/micropython/drivers/display/gc9a01/manifest.py new file mode 100644 index 000000000..b8ca01c94 --- /dev/null +++ b/micropython/drivers/display/gc9a01/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay gc9a01 display driver", + version="0.0.1", +) +module("gc9a01.py", opt=3) diff --git a/micropython/drivers/display/gc9d01/gc9d01.py b/micropython/drivers/display/gc9d01/gc9d01.py new file mode 100644 index 000000000..dfeeb7dc2 --- /dev/null +++ b/micropython/drivers/display/gc9d01/gc9d01.py @@ -0,0 +1,106 @@ +# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: Copyright (c) 2023 Tyler Crumpton +# +# SPDX-License-Identifier: MIT +""" +`gc9d01` +================================================================================ + +displayio driver for GC9D01 TFT LCD displays + + +* Author(s): Tyler Crumpton + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads +""" + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/tylercrumpton/CircuitPython_GC9D01.git" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +_INIT_SEQUENCE = bytearray( + b"\xfe\x00" # Inter Register Enable1 (FEh) + b"\xef\x00" # Inter Register Enable2 (EFh) + b"\x80\x01\xff" + b"\x81\x01\xff" + b"\x82\x01\xff" + b"\x83\x01\xff" + b"\x84\x01\xff" + b"\x85\x01\xff" + b"\x86\x01\xff" + b"\x87\x01\xff" + b"\x88\x01\xff" + b"\x89\x01\xff" + b"\x8a\x01\xff" + b"\x8b\x01\xff" + b"\x8c\x01\xff" + b"\x8d\x01\xff" + b"\x8e\x01\xff" + b"\x8f\x01\xff" + b"\x3a\x01\x05" # COLMOD: Pixel Format Set (3Ah) MCU interface, 16 bits / pixel + b"\xec\x01\x10" # Inversion (ECh) DINV=1+2 column for Single Gate (BFh=0) + b"\x7e\x01\x7a" + b"\x74\x07\x02\x0e\x00\x00\x28\x00\x00" + b"\x98\x01\x3e" + b"\x99\x01\x3e" + b"\xb5\x03\x0e\x0e\x00" # Blanking Porch Control (B5h) VFP=14 VBP=14 HBP=Off + b"\x60\x04\x38\x09\x6d\x67" + b"\x63\x05\x38\xad\x6d\x67\x05" + b"\x64\x06\x38\x0b\x70\xab\x6d\x67" + b"\x66\x06\x38\x0f\x70\xaf\x6d\x67" + b"\x6a\x02\x00\x00" + b"\x68\x07\x3b\x08\x04\x00\x04\x64\x67" + b"\x6c\x07\x22\x02\x22\x02\x22\x22\x50" + b"\x6e\x1e\x00\x00\x00\x00\x07\x01\x13\x11\x0b\x09\x16\x15\x1d\x1e\x00\x00\x00\x00\x1e\x1d\x15\x16\x0a\x0c\x12\x14\x02\x08\x00\x00\x00\x00" # pylint: disable=line-too-long + b"\xa9\x01\x1b" + b"\xa8\x01\x6b" + b"\xa8\x01\x6d" + b"\xa7\x01\x40" + b"\xad\x01\x47" + b"\xaf\x01\x73" + b"\xaf\x01\x73" + b"\xac\x01\x44" + b"\xa3\x01\x6c" + b"\xcb\x01\x00" + b"\xcd\x01\x22" + b"\xc2\x01\x10" + b"\xc5\x01\x00" + b"\xc6\x01\x0e" + b"\xc7\x01\x1f" + b"\xc8\x01\x0e" + b"\xbf\x01\x00" # Dual-Single Gate Select (BFh) 0=>Single gate + b"\xf9\x01\x20" + b"\x9b\x01\x3b" + b"\x93\x03\x33\x7f\x00" + b"\x70\x06\x0e\x0f\x03\x0e\x0f\x03" + b"\x71\x03\x0e\x16\x03" + b"\x91\x02\x0e\x09" + b"\xc3\x01\x2c" # Vreg1a Voltage Control 2 (C3h) vreg1_vbp_d=0x2C + b"\xc4\x01\x1a" # Vreg1b Voltage Control 2 (C4h) vreg1_vbn_d=0x1A + b"\xf0\x06\x51\x13\x0c\x06\x00\x2f" # SET_GAMMA1 (F0h) + b"\xf2\x06\x51\x13\x0c\x06\x00\x33" # SET_GAMMA3 (F2h) + b"\xf1\x06\x3c\x94\x4f\x33\x34\xcf" # SET_GAMMA2 (F1h) + b"\xf3\x06\x4d\x94\x4f\x33\x34\xcf" # SET_GAMMA4 (F3h) + b"\x36\x01\x00" # Memory Access Control (36h) MY=0, MX=0, MV=0, ML=0, BGR=0, MH=0 + b"\x11\x80\xc8" # Sleep Out Mode (11h) and delay(200) + b"\x29\x80\x14" # Display ON (29h) and delay(20) + b"\x2c\x00" # Memory Write (2Ch) D=0 +) + + +# pylint: disable=too-few-public-methods +class GC9D01(BusDisplay): + """GC9D01 display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/gc9d01/manifest.py b/micropython/drivers/display/gc9d01/manifest.py new file mode 100644 index 000000000..817612645 --- /dev/null +++ b/micropython/drivers/display/gc9d01/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay gc9d01 display driver", + version="0.0.1", +) +module("gc9d01.py", opt=3) diff --git a/micropython/drivers/display/hx8357/hx8357.py b/micropython/drivers/display/hx8357/hx8357.py new file mode 100644 index 000000000..7489b2376 --- /dev/null +++ b/micropython/drivers/display/hx8357/hx8357.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: 2019 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_hx8357` +================================================================================ + +displayio driver for HX8357 Displays such as the 3.5-inch TFT FeatherWing and Breakout + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* 3.5" PiTFT Plus 480x320 3.5" TFT+Touchscreen for Raspberry Pi: + +* 3.5" TFT 320x480 + Touchscreen Breakout Board w/MicroSD Socket: + +* Adafruit TFT FeatherWing - 3.5" 480x320 Touchscreen for Feathers: + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HX8357.git" + +_INIT_SEQUENCE = ( + b"\x01\x80\x64" # _SWRESET and Delay 100ms + b"\xb9\x83\xff\x83\x57\xff" # _SETC and delay 500ms + b"\xb3\x04\x80\x00\x06\x06" # _SETRGB 0x80 enables SDO pin (0x00 disables) + b"\xb6\x01\x25" # _SETCOM -1.52V + b"\xb0\x01\x68" # _SETOSC Normal mode 70Hz, Idle mode 55 Hz + b"\xcc\x01\x05" # _SETPANEL BGR, Gate direction swapped + b"\xb1\x06\x00\x15\x1c\x1c\x83\xaa" # _SETPWR1 Not deep standby BT VSPR VSNR AP + b"\xc0\x06\x50\x50\x01\x3c\x1e\x08" # _SETSTBA OPON normal OPON idle STBA GEN + b"\xb4\x07\x02\x40\x00\x2a\x2a\x0d\x78" # _SETCYC NW 0x02 RTN DIV DUM DUM GDON GDOFF + b"\xe0\x22\x02\x0a\x11\x1d\x23\x35\x41\x4b\x4b\x42\x3a\x27\x1b\x08\x09\x03\x02\x0a" + b"\x11\x1d\x23\x35\x41\x4b\x4b\x42\x3a\x27\x1b\x08\x09\x03\x00\x01" # _SETGAMMA + b"\x3a\x01\x55" # _COLMOD 16 bit + b"\x36\x01\xc0" # _MADCTL + b"\x35\x01\x00" # _TEON TW off + b"\x44\x02\x00\x02" # _TEARLINE + b"\x11\x80\x96" # _SLPOUT and delay 150 ms + b"\x36\x01\xa0" + b"\x29\x80\x32" # _DISPON and delay 50 ms +) + + +# pylint: disable=too-few-public-methods +class HX8357(BusDisplay): + """HX8357D driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/hx8357/manifest.py b/micropython/drivers/display/hx8357/manifest.py new file mode 100644 index 000000000..25693c67e --- /dev/null +++ b/micropython/drivers/display/hx8357/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay hx8357 display driver", + version="0.0.1", +) +module("hx8357.py", opt=3) diff --git a/micropython/drivers/display/ili9163/ili9163.py b/micropython/drivers/display/ili9163/ili9163.py new file mode 100644 index 000000000..5952805c3 --- /dev/null +++ b/micropython/drivers/display/ili9163/ili9163.py @@ -0,0 +1,82 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Tavish Naruka for Electronut Labs (electronut.in) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`electronutlabs_ili9163` +================================================================================ + +displayio driver for ILI9163 TFT-LCD displays. + + +* Author(s): Tavish Naruka + +Implementation Notes +-------------------- + +**Hardware:** + + * `Electronut Labs Blip `_ + * `TFTM018 `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/electronut/Electronutlabs_CircuitPython_ILI9163.git" + +_INIT_SEQUENCE = ( + b"\x01\x80\x80" + b"\x11\x80\x05" + b"\x3a\x01\x05" + b"\x26\x01\x04" + b"\xf2\x01\x01" + b"\xe0\x0f\x3f\x25\x1c\x1e\x20\x12\x2a\x90\x24\x11\x00\x00\x00\x00\x00" + b"\xe1\x0f\x20\x20\x20\x20\x05\x00\x15\xa7\x3d\x18\x25\x2a\x2b\x2b\x3a" + b"\xb1\x02\x08\x08" + b"\xb4\x01\x07" + b"\xc0\x02\x0a\x02" + b"\xc1\x01\x02" + b"\xc5\x02\x50\x5b" + b"\xc7\x01\x40" + b"\x2a\x04\x00\x00\x00\x7f" + b"\x2b\x04\x00\x00\x00\x7f" + b"\x36\x01\x68" # rotation + b"\x29\x80\x78" + b"\x2c\x80\x78" +) + +# pylint: disable=too-few-public-methods + + +class ILI9163(BusDisplay): + """ILI9163 display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/ili9163/manifest.py b/micropython/drivers/display/ili9163/manifest.py new file mode 100644 index 000000000..951c75a48 --- /dev/null +++ b/micropython/drivers/display/ili9163/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay ili9163 display driver", + version="0.0.1", +) +module("ili9163.py", opt=3) diff --git a/micropython/drivers/display/ili9341/ili9341.py b/micropython/drivers/display/ili9341/ili9341.py new file mode 100644 index 000000000..ca9d9563b --- /dev/null +++ b/micropython/drivers/display/ili9341/ili9341.py @@ -0,0 +1,80 @@ +# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`ili9341` +==================================================== + +Display driver for ILI9341 + +* Author(s): Scott Shawcroft + +Implementation Notes +-------------------- + +**Hardware:** + +* Adafruit PiTFT 2.2" HAT Mini Kit - 320x240 2.2" TFT - No Touch + +* Adafruit PiTFT 2.4" HAT Mini Kit - 320x240 TFT Touchscreen + +* Adafruit PiTFT - 320x240 2.8" TFT+Touchscreen for Raspberry Pi + +* PiTFT 2.8" TFT 320x240 + Capacitive Touchscreen for Raspberry Pi + +* Adafruit PiTFT Plus 320x240 2.8" TFT + Capacitive Touchscreen + +* PiTFT Plus Assembled 320x240 2.8" TFT + Resistive Touchscreen + +* PiTFT Plus 320x240 3.2" TFT + Resistive Touchscreen + +* 2.2" 18-bit color TFT LCD display with microSD card breakout + +* 2.4" TFT LCD with Touchscreen Breakout Board w/MicroSD Socket + +* 2.8" TFT LCD with Touchscreen Breakout Board w/MicroSD Socket + +* 3.2" TFT LCD with Touchscreen Breakout Board w/MicroSD Socket + +* TFT FeatherWing - 2.4" 320x240 Touchscreen For All Feathers + +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +_INIT_SEQUENCE = ( + b"\x01\x80\x80" # Software reset then delay 0x80 (128ms) + b"\xef\x03\x03\x80\x02" + b"\xcf\x03\x00\xc1\x30" + b"\xed\x04\x64\x03\x12\x81" + b"\xe8\x03\x85\x00\x78" + b"\xcb\x05\x39\x2c\x00\x34\x02" + b"\xf7\x01\x20" + b"\xea\x02\x00\x00" + b"\xc0\x01\x23" # Power control VRH[5:0] + b"\xc1\x01\x10" # Power control SAP[2:0];BT[3:0] + b"\xc5\x02\x3e\x28" # VCM control + b"\xc7\x01\x86" # VCM control2 + b"\x36\x01\x38" # Memory Access Control + b"\x37\x01\x00" # Vertical scroll zero + b"\x3a\x01\x55" # COLMOD: Pixel Format Set + b"\xb1\x02\x00\x18" # Frame Rate Control (In Normal Mode/Full Colors) + b"\xb6\x03\x08\x82\x27" # Display Function Control + b"\xf2\x01\x00" # 3Gamma Function Disable + b"\x26\x01\x01" # Gamma curve selected + b"\xe0\x0f\x0f\x31\x2b\x0c\x0e\x08\x4e\xf1\x37\x07\x10\x03\x0e\x09\x00" # Set Gamma + b"\xe1\x0f\x00\x0e\x14\x03\x11\x07\x31\xc1\x48\x08\x0f\x0c\x31\x36\x0f" # Set Gamma + b"\x11\x80\x78" # Exit Sleep then delay 0x78 (120ms) + b"\x29\x80\x78" # Display on then delay 0x78 (120ms) +) + + +class ILI9341(BusDisplay): + """ILI9341 display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/ili9341/manifest.py b/micropython/drivers/display/ili9341/manifest.py new file mode 100644 index 000000000..ad9ab1d57 --- /dev/null +++ b/micropython/drivers/display/ili9341/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay ili9341 display driver", + version="0.0.1", +) +module("ili9341.py", opt=3) diff --git a/micropython/drivers/display/ili9488/ili9488.py b/micropython/drivers/display/ili9488/ili9488.py new file mode 100644 index 000000000..ae2259231 --- /dev/null +++ b/micropython/drivers/display/ili9488/ili9488.py @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`bagaloozy_ili9488` +==================================================== + +Display driver for ILI9488 + +* Author(s): Mark Winney + +Implementation Notes +-------------------- + +**Hardware:** + +* Buy Display LCD 3.5" 320x480 TFT Display Module,OPTL Touch Screen w/Breakout Board + + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ILI9488.git" + +_INIT_SEQUENCE = ( + b"\xe0\x0f\x00\x03\x09\x08\x16\x0a\x3f\x78\x4c\x09\x0a\x08\x16\x1a\x0f" + b"\xe1\x0f\x00\x16\x19\x03\x0f\x05\x32\x45\x46\x04\x0e\x0d\x35\x37\x0f" + b"\xc0\x02\x17\x15" # Power Control 1 Vreg1out Verg2out + b"\xc1\x01\x41" # Power Control 2 VGH,VGL + b"\xc5\x03\x00\x12\x80" # Power Control 3 Vcom + b"\x36\x01\x48" # Memory Access + b"\x3a\x01\x55" # Interface Pixel Format 16 bit + b"\xb0\x01\x00" # Interface Mode Control + b"\xb1\x01\xa0" # Frame rate 60Hz + b"\xb4\x01\x02" # Display Inversion Control 2-dot + b"\xb6\x00" # Display Function Control RGB/MCU Interface Control + b"\x02\x01\x02" # MCU Source,Gate scan direction + b"\xe9\x01\x00" # Set Image Function Disable 24 bit data + b"\xf7\x04\xa9\x51\x2c\x82" # Adjust Control D7 stream, loose + b"\x11\x80\x78" # Sleep out delay 120ms + b"\x29\x00" +) + + +# pylint: disable=too-few-public-methods +class ILI9488(BusDisplay): + """ILI9488 display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/ili9488/manifest.py b/micropython/drivers/display/ili9488/manifest.py new file mode 100644 index 000000000..5cc0e9063 --- /dev/null +++ b/micropython/drivers/display/ili9488/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay ili9488 display driver", + version="0.0.1", +) +module("ili9488.py", opt=3) diff --git a/micropython/drivers/display/st7701/manifest.py b/micropython/drivers/display/st7701/manifest.py new file mode 100644 index 000000000..badd9c589 --- /dev/null +++ b/micropython/drivers/display/st7701/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7701 display driver", + version="0.0.1", +) +module("st7701.py", opt=3) diff --git a/micropython/drivers/display/st7701/st7701.py b/micropython/drivers/display/st7701/st7701.py new file mode 100644 index 000000000..8a1d69e22 --- /dev/null +++ b/micropython/drivers/display/st7701/st7701.py @@ -0,0 +1,127 @@ +""" +GPL-3.0 License +see https://github.com/Xinyuan-LilyGO/lilygo-micropython/tree/master/target/esp32s3/boards/LILYGO_T-RGB/modules +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay +from time import sleep_ms + + +_INIT_SEQUENCE = [ + (0xFF, b"\x77\x01\x00\x00\x10", 0), + (0xC0, b"\x3b\x00", 0), + (0xC1, b"\x0b\x02", 0), + (0xC2, b"\x07\x02", 0), + (0xCC, b"\x10", 0), + (0xCD, b"\x08", 0), # 用565时屏蔽 666打开 + (0xB0, b"\x00\x11\x16\x0e\x11\x06\x05\x09\x08\x21\x06\x13\x10\x29\x31\x18", 0), + (0xB1, b"\x00\x11\x16\x0e\x11\x07\x05\x09\x09\x21\x05\x13\x11\x2a\x31\x18", 0), + (0xFF, b"\x77\x01\x00\x00\x11", 0), + (0xB0, b"\x6d", 0), + (0xB1, b"\x37", 0), + (0xB2, b"\x81", 0), + (0xB3, b"\x80", 0), + (0xB5, b"\x43", 0), + (0xB7, b"\x85", 0), + (0xB8, b"\x20", 0), + (0xC1, b"\x78", 0), + (0xC2, b"\x78", 0), + (0xC3, b"\x8c", 0), + (0xD0, b"\x88", 0), + (0xE0, b"\x00\x00\x02", 0), + (0xE1, b"\x03\xa0\x00\x00\x04\xa0\x00\x00\x00\x20\x20", 0), + (0xE2, b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0), + (0xE3, b"\x00\x00\x11\x00", 0), + (0xE4, b"\x22\x00", 0), + (0xE5, b"\x05\xec\xa0\xa0\x07\xee\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00", 0), + (0xE6, b"\x00\x00\x11\x00", 0), + (0xE7, b"\x22\x00", 0), + (0xE8, b"\x06\xed\xa0\xa0\x08\xef\xa0\xa0\x00\x00\x00\x00\x00\x00\x00\x00", 0), + (0xEB, b"\x00\x00\x40\x40\x00\x00\x00", 0), + (0xED, b"\xff\xff\xff\xba\x0a\xbf\x45\xff\xff\x54\xfb\xa0\xab\xff\xff\xff", 0), + (0xEF, b"\x10\x0d\x04\x08\x3f\x1f", 0), + (0xFF, b"\x77\x01\x00\x00\x13", 0), + (0xEF, b"\x08", 0), + (0xFF, b"\x77\x01\x00\x00\x00", 0), + (0x36, b"\x08", 0), + (0x3A, b"\x66", 0), + (0x11, b"\x00", 100), + # (0xFF, b'\x77\x01\x00\x00\x12', 0), + # (0xd1, b'\x81', 0), + # (0xd2, b'\x06', 0), + (0x29, b"\x00", 120), +] + + +class LCDPins: + def __init__(self, *, pwr_en, cs, sda, clk, rst): + self.pwr_en = pwr_en + self.cs = cs + self.sda = sda + self.clk = clk + self.rst = rst + + +class ST7701(BusDisplay): + """ + ST7701 display driver + + :param lcd_pins: the io pins to configure the display + """ + + def __init__(self, lcd_pins, bus, **kwargs): + self.lcd_pins = lcd_pins + + self.lcd_pins.pwr_en(1) + self.lcd_pins.cs(1) + self.lcd_pins.sda(1) + self.lcd_pins.clk(1) + + # Reset the display + self.lcd_pins.rst(1) + sleep_ms(200) + self.lcd_pins.rst(0) + sleep_ms(200) + self.lcd_pins.rst(1) + sleep_ms(200) + + super()._init_(bus, _INIT_SEQUENCE, **kwargs) + + def init(self): + # self.rotation_table = _ROTATION_TABLE + super().init(render_mode_full=True) + + def send(self, cmd, params=None): + self._tx_cmd(cmd) + if params: + self._tx_data(params) + + def _tx_cmd(self, cmd): + self.lcd_pins.cs(0) + self.lcd_pins.sda(0) + self.lcd_pins.clk(0) + self.lcd_pins.clk(1) + self._tx_byte(cmd) + self.lcd_pins.cs(1) + + def _tx_data(self, data): + for i in range(len(data)): + self.lcd_pins.cs(0) + self.lcd_pins.sda(1) + self.lcd_pins.clk(0) + self.lcd_pins.clk(1) + self._tx_byte(data[i]) + self.lcd_pins.cs(1) + + def _tx_byte(self, bits): + for _ in range(8): + if bits & 0x80: + self.lcd_pins.sda(1) + else: + self.lcd_pins.sda(0) + bits <<= 1 + self.lcd_pins.clk(0) + self.lcd_pins.clk(1) diff --git a/micropython/drivers/display/st7735/manifest.py b/micropython/drivers/display/st7735/manifest.py new file mode 100644 index 000000000..2515b52cb --- /dev/null +++ b/micropython/drivers/display/st7735/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7735 display driver", + version="0.0.1", +) +module("st7735.py", opt=3) diff --git a/micropython/drivers/display/st7735/st7735.py b/micropython/drivers/display/st7735/st7735.py new file mode 100644 index 000000000..ce6f603f7 --- /dev/null +++ b/micropython/drivers/display/st7735/st7735.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries +# SPDX-FileCopyrightText: 2019 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_st7735` +==================================================== + +Displayio driver for ST7735 based displays. + +* Author(s): Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ST7735.git" + +_INIT_SEQUENCE = ( + b"\x01\x80\x32" # _SWRESET and Delay 50ms + b"\x11\x80\xff" # _SLPOUT + b"\x3a\x81\x05\x0a" # _COLMOD + b"\xb1\x83\x00\x06\x03\x0a" # _FRMCTR1 + b"\x36\x01\x08" # _MADCTL + b"\xb6\x02\x15\x02" # _DISSET5 + # 1 clk cycle nonoverlap, 2 cycle gate, rise, 3 cycle osc equalize, Fix on VTL + b"\xb4\x01\x00" # _INVCTR line inversion + b"\xc0\x82\x02\x70\x0a" # _PWCTR1 GVDD = 4.7V, 1.0uA, 10 ms delay + b"\xc1\x01\x05" # _PWCTR2 VGH = 14.7V, VGL = -7.35V + b"\xc2\x02\x01\x02" # _PWCTR3 Opamp current small, Boost frequency + b"\xc5\x82\x3c\x38\x0a" # _VMCTR1 + b"\xfc\x02\x11\x15" # _PWCTR6 + b"\xe0\x10\x09\x16\x09\x20\x21\x1b\x13\x19\x17\x15\x1e\x2b\x04\x05\x02\x0e" # _GMCTRP1 Gamma + b"\xe1\x90\x0b\x14\x08\x1e\x22\x1d\x18\x1e\x1b\x1a\x24\x2b\x06\x06\x02\x0f\x0a" # _GMCTRN1 + b"\x13\x80\x0a" # _NORON + b"\x29\x80\xff" # _DISPON +) + + +# pylint: disable=too-few-public-methods +class ST7735(BusDisplay): + """ST7735 driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/st7735r/manifest.py b/micropython/drivers/display/st7735r/manifest.py new file mode 100644 index 000000000..46e0bbc5e --- /dev/null +++ b/micropython/drivers/display/st7735r/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7735r display driver", + version="0.0.1", +) +module("st7735r.py", opt=3) diff --git a/micropython/drivers/display/st7735r/st7735r.py b/micropython/drivers/display/st7735r/st7735r.py new file mode 100644 index 000000000..0702cd2cd --- /dev/null +++ b/micropython/drivers/display/st7735r/st7735r.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: 2019 Scott Shawcroft for Adafruit Industries +# SPDX-FileCopyrightText: 2019 Melissa LeBlanc-Williams for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`adafruit_st7735r` +==================================================== + +Displayio driver for ST7735R based displays. + +* Author(s): Scott Shawcroft and Melissa LeBlanc-Williams + +Implementation Notes +-------------------- + +**Hardware:** + +* `1.8" SPI TFT display, 160x128 18-bit color + `_ (Product ID: 618) +* `Adafruit 0.96" 160x80 Color TFT Display w/ MicroSD Card Breakout + `_ (Product ID: 3533) +* `1.8" Color TFT LCD display with MicroSD Card Breakout: + `_ (Product ID: 358) +* `Adafruit 1.44" Color TFT LCD Display with MicroSD Card breakout: + `_ (Product ID: 2088) +* `Adafruit Mini Color TFT with Joystick FeatherWing: + `_ (Product ID: 3321) + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ST7735R.git" + +_INIT_SEQUENCE = bytearray( + b"\x01\x80\x96" # SWRESET and Delay 150ms + b"\x11\x80\xff" # SLPOUT and Delay + b"\xb1\x03\x01\x2c\x2d" # _FRMCTR1 + b"\xb2\x03\x01\x2c\x2d" # _FRMCTR2 + b"\xb3\x06\x01\x2c\x2d\x01\x2c\x2d" # _FRMCTR3 + b"\xb4\x01\x07" # _INVCTR line inversion + b"\xc0\x03\xa2\x02\x84" # _PWCTR1 GVDD = 4.7V, 1.0uA + b"\xc1\x01\xc5" # _PWCTR2 VGH=14.7V, VGL=-7.35V + b"\xc2\x02\x0a\x00" # _PWCTR3 Opamp current small, Boost frequency + b"\xc3\x02\x8a\x2a" + b"\xc4\x02\x8a\xee" + b"\xc5\x01\x0e" # _VMCTR1 VCOMH = 4V, VOML = -1.1V + b"\x20\x00" # _INVOFF + b"\x36\x01\x18" # _MADCTL bottom to top refresh + # 1 clk cycle nonoverlap, 2 cycle gate rise, 3 sycle osc equalie, + # fix on VTL + b"\x3a\x01\x05" # COLMOD - 16bit color + b"\xe0\x10\x02\x1c\x07\x12\x37\x32\x29\x2d\x29\x25\x2b\x39\x00\x01\x03\x10" # _GMCTRP1 Gamma + b"\xe1\x10\x03\x1d\x07\x06\x2e\x2c\x29\x2d\x2e\x2e\x37\x3f\x00\x00\x02\x10" # _GMCTRN1 + b"\x13\x80\x0a" # _NORON + b"\x29\x80\x64" # _DISPON +) + + +# pylint: disable=too-few-public-methods +class ST7735R(BusDisplay): + """ST7735R display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/st7735r_1/manifest.py b/micropython/drivers/display/st7735r_1/manifest.py new file mode 100644 index 000000000..f03144a0e --- /dev/null +++ b/micropython/drivers/display/st7735r_1/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7735r_1 display driver", + version="0.0.1", +) +module("st7735r_1.py", opt=3) diff --git a/micropython/drivers/display/st7735r_1/st7735r_1.py b/micropython/drivers/display/st7735r_1/st7735r_1.py new file mode 100644 index 000000000..8e119fe3a --- /dev/null +++ b/micropython/drivers/display/st7735r_1/st7735r_1.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +from busdisplay import BusDisplay + + +_INIT_SEQUENCE = [ + (0x36, b"\x70", 0), + (0x3A, b"\x05", 0), + (0xB1, b"\x01\x2c\x2d", 0), + (0xB2, b"\x01\x2c\x2d", 0), + (0xB3, b"\x01\x2c\x2d\x01\x2c\x2d", 0), + (0xB4, b"\x07", 0), + (0xC0, b"\xa2\x02\x84", 0), + (0xC1, b"\xc5", 0), + (0xC2, b"\x0a\x00", 0), + (0xC3, b"\x8a\x2a", 0), + (0xC4, b"\x8a\xee", 0), + (0xC5, b"\x0e", 0), + (0xE0, b"\x0f\x1a\x0f\x18\x2f\x28\x20\x22\x1f\x1b\x23\x37\x00\x07\x02\x10", 0), + (0xE1, b"\x0f\x1b\x0f\x17\x33\x2c\x29\x2e\x30\x30\x39\x3f\x00\x07\x03\x10", 0), + (0xF0, b"\x01", 0), + (0xF6, b"\x00", 0), + (0x11, None, 255), + (0x29, None, 255), +] + + +class ST7735R(BusDisplay): + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/st7789/manifest.py b/micropython/drivers/display/st7789/manifest.py new file mode 100644 index 000000000..bd0649c0f --- /dev/null +++ b/micropython/drivers/display/st7789/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7789 display driver", + version="0.0.1", +) +module("st7789.py", opt=3) diff --git a/micropython/drivers/display/st7789/st7789.py b/micropython/drivers/display/st7789/st7789.py new file mode 100644 index 000000000..a8a56c647 --- /dev/null +++ b/micropython/drivers/display/st7789/st7789.py @@ -0,0 +1,40 @@ +""" +see https://github.com/Xinyuan-LilyGO/lilygo-micropython/tree/master/target/esp32s3/boards/LILYGO_T-RGB/modules +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + + +_INIT_SEQUENCE = [ + (0x11, b"\x00", 120), # Exit sleep mode + (0x13, b"\x00", 0), # Turn on the display + (0xB6, b"\x0a\x82", 0), # Set display function control + (0x31, b"\x55", 10), # Set pixel format to 16 bits per pixel (RGB565) + (0xB2, b"\x0c\x0c\x00\x33\x33", 0), # Set porch control + (0xB7, b"\x35", 0), # Set gate control + (0xBB, b"\x28", 0), # Set VCOMS setting + (0xC0, b"\x0c", 0), # Set power control 1 + (0xC2, b"\x01\xff", 0), # Set power control 2 + (0xC3, b"\x10", 0), # Set power control 3 + (0xC4, b"\x20", 0), # Set power control 4 + (0xC6, b"\x0f", 0), # Set VCOM control 1 + (0xD0, b"\xa4\xa1", 0), # Set power control A + # Set gamma curve positive polarity + (0xE0, b"\xd0\x00\x02\x07\x0a\x28\x32\x44\x42\x06\x0e\x12\x14\x17", 0), + # Set gamma curve negative polarity + (0xE1, b"\xd0\x00\x02\x07\x0a\x28\x31\x54\x47\x0e\x1c\x17\x1b\x1e", 0), + (0x21, b"\x00", 0), # Enable display inversion + (0x29, b"\x00", 120), # Turn on the display +] + + +class ST7789(BusDisplay): + """ + ST7789 display driver + """ + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/st7789vw/manifest.py b/micropython/drivers/display/st7789vw/manifest.py new file mode 100644 index 000000000..e7a30b68d --- /dev/null +++ b/micropython/drivers/display/st7789vw/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7789vw display driver", + version="0.0.1", +) +module("st7789vw.py", opt=3) diff --git a/micropython/drivers/display/st7789vw/st7789vw.py b/micropython/drivers/display/st7789vw/st7789vw.py new file mode 100644 index 000000000..836d50c86 --- /dev/null +++ b/micropython/drivers/display/st7789vw/st7789vw.py @@ -0,0 +1,40 @@ +""" +ST7789VW Driver +Adapted from LCD_Module_RPI_code.zip/LCD_Module_RPI_code/RaspberryPi/python/lib +at https://files.waveshare.com/upload/8/8d/LCD_Module_RPI_code.zip +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay + + +_INIT_SEQUENCE = [ + (0x36, b"\x00", 0), + (0x3A, b"\x05", 0), + (0x21, b"\x00", 0), + (0x2A, b"\x00\x00\x01\x3f", 0), + (0x2B, b"\x00\x00\x00\xef", 0), + (0xB2, b"\x0c\x0c\x00\x33\x33", 0), + (0xB7, b"\x35", 0), + (0xBB, b"\x1f", 0), + (0xC0, b"\x2c", 0), + (0xC2, b"\x01", 0), + (0xC3, b"\x12", 0), + (0xC4, b"\x20", 0), + (0xC6, b"\x0f", 0), + (0xD0, b"\xa4\xa1", 0), + (0xE0, b"\xd0\x08\x11\x08\x0c\x15\x39\x33\x50\x36\x13\x14\x29\x2d", 0), + (0xE1, b"\xd0\x08\x10\x08\x06\x06\x39\x44\x51\x0b\x16\x14\x2f\x31", 0), + (0x21, b"\x00", 0), + (0x11, b"\x00", 0), + (0x29, b"\x00", 120), +] + + +class ST7789VW(BusDisplay): + """ST789VW display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/display/st7796/manifest.py b/micropython/drivers/display/st7796/manifest.py new file mode 100644 index 000000000..ee1ada811 --- /dev/null +++ b/micropython/drivers/display/st7796/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7796 display driver", + version="0.0.1", +) +module("st7796.py", opt=3) diff --git a/micropython/drivers/display/st7796/st7796.py b/micropython/drivers/display/st7796/st7796.py new file mode 100644 index 000000000..fbb8c3888 --- /dev/null +++ b/micropython/drivers/display/st7796/st7796.py @@ -0,0 +1,142 @@ +""" +The init sequence is written out line by line in .init() +""" + +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay +from time import sleep_ms +from micropython import const + + +_SWRESET = const(0x01) +_SLPOUT = const(0x11) +_CSCON = const(0xF0) +_MADCTL = const(0x36) +_COLMOD = const(0x3A) +_DIC = const(0xB4) +_DFC = const(0xB6) +_DOCA = const(0xE8) +_PWR2 = const(0xC1) +_PWR3 = const(0xC2) +_VCMPCTL = const(0xC5) +_PGC = const(0xE0) +_NGC = const(0xE1) +_DISPON = const(0x29) + + +class ST7796(BusDisplay): + """ST7796 display driver""" + + def __init__(self, bus, **kwargs): + super().__init__(bus, **kwargs) + + def init(self): + # self.rotation_table = _ROTATION_TABLE + param_buf = bytearray(14) + param_mv = memoryview(param_buf) + + self.send(_SWRESET) + + sleep_ms(120) + + self.send(_SLPOUT) + + sleep_ms(120) + + param_buf[0] = 0xC3 + self.send(_CSCON, param_mv[:1]) + + param_buf[0] = 0x96 + self.send(_CSCON, param_mv[:1]) + + if self.color_depth // 8 == 2: + pixel_format = 0x55 + elif self.color_depth // 8 == 3: + pixel_format = 0x77 + else: + raise RuntimeError( + "ST7796 IC only supports " "lv.COLOR_FORMAT.RGB565 or lv.COLOR_FORMAT.RGB888" + ) + + param_buf[0] = pixel_format + self.send(_COLMOD, param_mv[:1]) + + param_buf[0] = 0x01 + self.send(_DIC, param_mv[:1]) + + param_buf[0] = 0x80 + param_buf[1] = 0x02 + param_buf[2] = 0x3B + self.send(_DFC, param_mv[:3]) + + param_buf[:8] = bytearray([0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33]) + self.send(_DOCA, param_mv[:8]) + + param_buf[0] = 0x06 + self.send(_PWR2, param_mv[:1]) + + param_buf[0] = 0xA7 + self.send(_PWR3, param_mv[:1]) + + param_buf[0] = 0x18 + self.send(_VCMPCTL, param_mv[:1]) + + sleep_ms(120) + + param_buf[:14] = bytearray( + [ + 0xF0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x15, + 0x2F, + 0x54, + 0x42, + 0x3C, + 0x17, + 0x14, + 0x18, + 0x1B, + ] + ) + self.send(_PGC, param_mv[:14]) + + param_buf[:14] = bytearray( + [ + 0xE0, + 0x09, + 0x0B, + 0x06, + 0x04, + 0x03, + 0x2B, + 0x43, + 0x42, + 0x3B, + 0x16, + 0x14, + 0x17, + 0x1B, + ] + ) + self.send(_NGC, param_mv[:14]) + + sleep_ms(120) + + param_buf[0] = 0x3C + self.send(_CSCON, param_mv[:1]) + + param_buf[0] = 0x69 + self.send(_CSCON, param_mv[:1]) + + sleep_ms(120) + + self.send(_DISPON) + + sleep_ms(120) + + super().init() diff --git a/micropython/drivers/display/st7796_test/manifest.py b/micropython/drivers/display/st7796_test/manifest.py new file mode 100644 index 000000000..aad5d8b7e --- /dev/null +++ b/micropython/drivers/display/st7796_test/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay st7796_test display driver", + version="0.0.1", +) +module("st7796_test.py", opt=3) diff --git a/micropython/drivers/display/st7796_test/st7796_test.py b/micropython/drivers/display/st7796_test/st7796_test.py new file mode 100644 index 000000000..5a52903c6 --- /dev/null +++ b/micropython/drivers/display/st7796_test/st7796_test.py @@ -0,0 +1,50 @@ +try: + from displaysys.busdisplay import BusDisplay +except ImportError: + from busdisplay import BusDisplay +from micropython import const + +_SWRESET = const(0x01) +_SLPOUT = const(0x11) +_CSCON = const(0xF0) +_MADCTL = const(0x36) +_COLMOD = const(0x3A) +_DIC = const(0xB4) +_DFC = const(0xB6) +_DOCA = const(0xE8) +_PWR2 = const(0xC1) +_PWR3 = const(0xC2) +_VCMPCTL = const(0xC5) +_PGC = const(0xE0) +_NGC = const(0xE1) +_DISPON = const(0x29) + +_INIT_SEQUENCE = [ + (_SWRESET, None, 120), # Software reset + (_SLPOUT, None, 120), # Sleep out + (_CSCON, b"\xc3", 0), # Enable extension command 2 partI + (_CSCON, b"\x96", 0), # Enable extension command 2 partII + (_MADCTL, b"\x48", 0), # Memory data access control + (_COLMOD, b"\x55", 0), # Interface pixel format set to 16 + (_DIC, b"\x01", 0), # Column inversion + (_DFC, b"\x80\x02\x3b", 0), # Display function control + (_DOCA, b"\x40\x8a\x00\x00\x29\x19\xa5\x33", 0), # Display output control adjust + (_PWR2, b"\x06", 0), # Power control2 + (_PWR3, b"\xa7", 0), # Power control3 + (_VCMPCTL, b"\x18", 120), # VCOM control + (_PGC, b"\xf0\x09\x0b\x06\x04\x15\x2f\x54\x42\x3c\x17\x14\x18\x1b", 0), # Gamma positive + # Should first byte be 0xF0 or 0xE0? + (_NGC, b"\xe0\x09\x0b\x06\x04\x03\x2b\x43\x42\x3b\x16\x14\x17\x1b", 120), # Gamma negative + (_CSCON, b"\x3c", 0), # Command Set control + (_CSCON, b"\x69", 120), # Command Set control + (_DISPON, None, 120), # Display on +] + + +class ST7796(BusDisplay): + """ + ST7796 display driver + """ + + def __init__(self, bus, **kwargs): + super().__init__(bus, _INIT_SEQUENCE, **kwargs) diff --git a/micropython/drivers/touch/chsc6x/chsc6x.py b/micropython/drivers/touch/chsc6x/chsc6x.py new file mode 100644 index 000000000..7c1c58806 --- /dev/null +++ b/micropython/drivers/touch/chsc6x/chsc6x.py @@ -0,0 +1,63 @@ +# SPDX-FileCopyrightText: 2023 Brad Barnett +# +# SPDX-License-Identifier: MIT +# +# Suggest setting I2C freq = 400000 and using IRQ pin for best performance +# Set IRQ speed to 100000 if not using an IRQ pin + + +from machine import Pin, I2C +from time import sleep_ms +from micropython import const + + +CHSC6X_I2C_ID = const(0x2E) +CHSC6X_READ_POINT_LEN = const(5) + + +class CHSC6X: + def __init__(self, i2c, addr=CHSC6X_I2C_ID, irq_pin=None): + self._i2c = i2c + self._addr = addr + self._irq = Pin(irq_pin, Pin.IN, Pin.PULL_UP) if irq_pin else None + self._buffer = bytearray(CHSC6X_READ_POINT_LEN) + sleep_ms(100) + + def is_touched(self): + if self._irq is not None: + if self._irq.value() is False: + return True + return False + return self.touch_read() is not None + + def touch_read(self): + if self._irq is not None: + if self.is_touched() is True: + self._i2c.readfrom_into(self._addr, self._buffer) + else: + return None + else: + try: + self._i2c.readfrom_into(self._addr, self._buffer) + except OSError: # Thrown when reading too fast + return None + + results = list(self._buffer) + # first byte is non-zero when touched, 3rd byte is x, 5th byte is y + if results[0]: + return results[2], results[4] + return None + + +def main(): + print("Started...") + i2c = I2C(0, sda=Pin(7), scl=Pin(6), freq=400000) + touch = CHSC6X(i2c, irq_pin=16) + + while True: + if touch.is_touched(): + print("Touched: ", touch.touch_read()) + + +if __name__ == "__main__": + main() diff --git a/micropython/drivers/touch/chsc6x/manifest.py b/micropython/drivers/touch/chsc6x/manifest.py new file mode 100644 index 000000000..f5f84355a --- /dev/null +++ b/micropython/drivers/touch/chsc6x/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay chsc6x touch driver", + version="0.0.1", +) +module("chsc6x.py", opt=3) diff --git a/micropython/drivers/touch/cst226/cst226.py b/micropython/drivers/touch/cst226/cst226.py new file mode 100644 index 000000000..d79851bf5 --- /dev/null +++ b/micropython/drivers/touch/cst226/cst226.py @@ -0,0 +1,149 @@ +""" +Adapted from: + https://github.com/lewisxhe/SensorLib/blob/master/src/touch/TouchClassCST226.cpp +""" + +from machine import Pin +from time import sleep_ms +from micropython import const + +# CST226-specific constants +_CST226_ID = const(0xA8) # CST226 specific chip ID +_CST226_REG_STATUS = const(0x00) +_CST226_BUFFER_NUM = const(28) + +# Register Definitions +_REG_SLEEP_MODE = const(0xE5) +_REG_LONG_PRESS_TICK = const(0xEB) +_REG_MOTION_MASK = const(0xEC) +_REG_IRQ_CTL = const(0xFA) +_REG_DIS_AUTOSLEEP = const(0xFE) + +# Motion and IRQ masks +MOTION_MASK_CONTINUOUS_LEFT_RIGHT = const(0b100) +MOTION_MASK_CONTINUOUS_UP_DOWN = const(0b010) +MOTION_MASK_DOUBLE_CLICK = const(0b001) + +IRQ_EN_TOUCH = const(0x40) +IRQ_EN_CHANGE = const(0x20) +IRQ_EN_MOTION = const(0x10) +IRQ_EN_LONGPRESS = const(0x01) + + +class CST226: + def __init__( + self, + bus, + address=0x5A, + rst_pin=None, + irq_pin=None, + irq_handler=lambda pin: None, + irq_en=0x00, + motion_mask=0b000, + ): + self._bus = bus + self._address = address + + # Detect if the chip is CST226 + buffer = bytearray(8) + write_buffer = bytearray(2) + write_buffer[0] = 0xD2 + write_buffer[1] = 0x04 + self._write_then_read(write_buffer, buffer, 4) + chipType = (buffer[3] << 8) | buffer[2] + if chipType != _CST226_ID: + raise ValueError("Error: CST226 not detected.") + + # Setup pins and reset the device + self.rst = Pin(rst_pin, Pin.OUT) if isinstance(rst_pin, int) else rst_pin + self.reset() + self.disable_autosleep() + + # Setup interrupt pin if available + self.irq = Pin(irq_pin, Pin.IN, Pin.PULL_UP) if isinstance(irq_pin, int) else irq_pin + if self.irq: + self.irq.irq(trigger=Pin.IRQ_FALLING, handler=irq_handler) + self.set_irq_ctl(irq_en, motion_mask) + + def touched(self): + # Read finger count for the CST226 + return self._read(0x02)[0] + + def get_point(self): + # CST226-specific point handling + buffer = self._read(_CST226_REG_STATUS, _CST226_BUFFER_NUM) + if buffer[0] == 0xAB or buffer[5] == 0x80: + return 0 # No touch detected or button press + + point_count = buffer[5] & 0x7F + if point_count > 5 or point_count == 0: + self._write(0x00, 0xAB) + return 0 + + points = [] + index = 0 + for i in range(point_count): + x = (buffer[index + 1] << 4) | ((buffer[index + 3] >> 4) & 0x0F) + y = (buffer[index + 2] << 4) | (buffer[index + 3] & 0x0F) + points.append((x, y)) + index += 7 if i == 0 else 5 + + return points + + def get_gestures(self): + # CST226-specific gesture handling (not available in this example) + return None + + def get_points(self): + raise NotImplementedError("get_points() not implemented (yet)") + + def reset(self): + if self.rst: + self.rst(0) + sleep_ms(1) + self.rst(1) + sleep_ms(50) + else: + # For CST226 specific reset + self._write(0xD1, 0x0E) + sleep_ms(20) + + def disable_autosleep(self, val=0x01): + self._write(_REG_DIS_AUTOSLEEP, val) + + def set_irq_ctl(self, irq_en, motion_mask=0b000): + self._write(_REG_IRQ_CTL, irq_en) + self._write(_REG_MOTION_MASK, motion_mask) + + def set_long_press_tick(self, val): + self._write(_REG_LONG_PRESS_TICK, val) + + def set_sleep_mode(self, val): + self._write(_REG_SLEEP_MODE, val) + + def sleep(self): + # CST226-specific sleep command + self._write(0xD1, 0x05) + + def wakeup(self): + # CST226-specific wakeup using reset + self.reset() + + def get_resolution(self): + # CST226-specific resolution handling + buffer = self._read(0xD1, 8) + x_res = (buffer[1] << 8) | buffer[0] + y_res = (buffer[3] << 8) | buffer[2] + return x_res, y_res + + def _read(self, reg, length=1): + return self._bus.readfrom_mem(self._address, int(reg), length) + + def _write(self, reg, val): + self._bus.writeto_mem(self._address, int(reg), bytes([int(val)])) + + def _write_then_read(self, write_buffer, read_buffer, read_length): + self._bus.writeto(self._address, write_buffer) + read_data = self._bus.readfrom(self._address, read_length) + for i in range(read_length): + read_buffer[i] = read_data[i] diff --git a/micropython/drivers/touch/cst226/manifest.py b/micropython/drivers/touch/cst226/manifest.py new file mode 100644 index 000000000..b6d9b86fb --- /dev/null +++ b/micropython/drivers/touch/cst226/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay cst226 touch driver", + version="0.0.1", +) +module("cst226.py", opt=3) diff --git a/micropython/drivers/touch/cst8xx/cst8xx.py b/micropython/drivers/touch/cst8xx/cst8xx.py new file mode 100644 index 000000000..8aba5e715 --- /dev/null +++ b/micropython/drivers/touch/cst8xx/cst8xx.py @@ -0,0 +1,115 @@ +""" +Adapted from https://github.com/waveshareteam/RP2040-Touch-LCD-1.28/blob/main/python/RP2040-LCD-1.28.py +and https://github.com/koendv/cst816t/blob/master/src/cst816t.cpp +by Brad Barnett, 2024 +Reference: https://files.waveshare.com/upload/c/c2/CST816S_register_declaration.pdf +""" + +from machine import Pin +from time import sleep_ms +from micropython import const + + +_CST816S_ID = const(0xB4) +_CST816T_ID = const(0xB5) +_CST816D_ID = const(0xB6) +_CST820_ID = const(0xB7) +_CST826_ID = const(0x11) + +_REG_GESTURE_ID = const(0x01) +_REG_FINGER_NUM = const(0x02) # Number of fingers currently touching the screen +_REG_TOUCHDATA = const(0x03) # 4 bytes: X[11:8], X[7:0], Y[11:8], Y[7:0] +_REG_CHIP_ID = const(0xA7) +# _REG_PROJ_ID = const(0xA8) +# _REG_FW_VERSION = const(0xA9) +# _REG_FACTORY_ID = const(0xAA) +_REG_SLEEP_MODE = const(0xE5) +_REG_LONG_PRESS_TICK = const(0xEB) +_REG_MOTION_MASK = const(0xEC) +_REG_IRQ_CTL = const(0xFA) +_REG_DIS_AUTOSLEEP = const(0xFE) + +MOTION_MASK_CONTINUOUS_LEFT_RIGHT = const(0b100) +MOTION_MASK_CONTINUOUS_UP_DOWN = const(0b010) +MOTION_MASK_DOUBLE_CLICK = const(0b001) + +IRQ_EN_TOUCH = const(0x40) +IRQ_EN_CHANGE = const(0x20) +IRQ_EN_MOTION = const(0x10) +IRQ_EN_LONGPRESS = const(0x01) + + +class CST8XX: + def __init__( + self, + bus, + address=0x15, + rst_pin=None, + irq_pin=None, + irq_handler=lambda pin: None, + irq_en=0x00, + motion_mask=0b000, + ): + self._bus = bus + self._address = address + self.rst = Pin(rst_pin, Pin.OUT) if isinstance(rst_pin, int) else rst_pin + self.reset() + if self._read(_REG_CHIP_ID)[0] not in ( + _CST816S_ID, + _CST816T_ID, + _CST816D_ID, + _CST820_ID, + _CST826_ID, + ): + raise ValueError("Error: CST8xx not detected.") + self.disable_autosleep() + + self.irq = Pin(irq_pin, Pin.IN, Pin.PULL_UP) if isinstance(irq_pin, int) else irq_pin + if self.irq: + self.irq.irq(trigger=Pin.IRQ_FALLING, handler=irq_handler) + self.set_irq_ctl(irq_en, motion_mask) + + def touched(self): + return self._read(_REG_FINGER_NUM)[0] + + def get_point(self): + if self.touched() != 1: + return None + xy_data = self._read(_REG_TOUCHDATA, 4) + x = ((xy_data[0] & 0x0F) << 8) + xy_data[1] + y = ((xy_data[2] & 0x0F) << 8) + xy_data[3] + return (x, y) + + def get_gestures(self): + if not self.touched(): + return None + return self._read(_REG_GESTURE_ID)[0] + + def get_points(self): + raise NotImplementedError("get_points() not implemented (yet)") + + def reset(self): + if self.rst: + self.rst(0) + sleep_ms(1) + self.rst(1) + sleep_ms(50) + + def disable_autosleep(self, val=0x01): + self._write(_REG_DIS_AUTOSLEEP, val) + + def set_irq_ctl(self, irq_en, motion_mask=0b000): + self._write(_REG_IRQ_CTL, irq_en) + self._write(_REG_MOTION_MASK, motion_mask) + + def set_long_press_tick(self, val): + self._write(_REG_LONG_PRESS_TICK, val) + + def set_sleep_mode(self, val): + self._write(_REG_SLEEP_MODE, val) + + def _read(self, reg, length=1): + return self._bus.readfrom_mem(self._address, int(reg), length) + + def _write(self, reg, val): + self._bus.writeto_mem(self._address, int(reg), bytes([int(val)])) diff --git a/micropython/drivers/touch/cst8xx/manifest.py b/micropython/drivers/touch/cst8xx/manifest.py new file mode 100644 index 000000000..24d81ccdb --- /dev/null +++ b/micropython/drivers/touch/cst8xx/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay cst8xx touch driver", + version="0.0.1", +) +module("cst8xx.py", opt=3) diff --git a/micropython/drivers/touch/ft6x36/ft6x36.py b/micropython/drivers/touch/ft6x36/ft6x36.py new file mode 100644 index 000000000..277e01135 --- /dev/null +++ b/micropython/drivers/touch/ft6x36/ft6x36.py @@ -0,0 +1,235 @@ +import time as _time +from micropython import const + + +_FT6x36_ADDR = const(0x38) + +_DEV_MODE_REG = const(0x00) +_GEST_ID_REG = const(0x01) +_TD_STATUS_REG = const(0x02) +_P1_XH_REG = const(0x03) +_P1_XL_REG = const(0x04) +_P1_YH_REG = const(0x05) +_P1_YL_REG = const(0x06) +_P1_WEIGHT_REG = const(0x07) +_P1_MISC_REG = const(0x08) +_P2_XH_REG = const(0x09) +_P2_XL_REG = const(0x0A) +_P2_YH_REG = const(0x0B) +_P2_YL_REG = const(0x0C) +_P2_WEIGHT_REG = const(0x0D) +_P2_MISC_REG = const(0x0E) +_TH_GROUP_REG = const(0x80) +_TH_DIFF_REG = const(0x85) +_CTRL_REG = const(0x86) +_TIMEENTERMONITOR_REG = const(0x87) +_PERIODACTIVE_REG = const(0x88) +_PERIODMONITOR_REG = const(0x89) +_RADIAN_VALUE_REG = const(0x91) +_OFFSET_LEFT_RIGHT_REG = const(0x92) +_OFFSET_UP_DOWN_REG = const(0x93) +_DISTANCE_LEFT_RIGHT_REG = const(0x94) +_DISTANCE_UP_DOWN_REG = const(0x95) +_DISTANCE_ZOOM_REG = const(0x96) +_LIB_VER_H_REG = const(0xA1) +_LIB_VER_L_REG = const(0xA2) +_CIPHER_REG = const(0xA3) +_G_MODE_REG = const(0xA4) +_PWR_MODE_REG = const(0xA5) +_FIRMID_REG = const(0xA6) +_FOCALTECH_ID_REG = const(0xA8) +_RELEASE_CODE_ID_REG = const(0xAF) +_STATE_REG = const(0xBC) + +GESTURE_NO_GESTRUE = const(0) +GESTURE_MOVE_UP = const(1) +GESTURE_MOVE_LEFT = const(2) +GESTURE_MOVE_DOWN = const(3) +GESTURE_MOVE_RIGHT = const(4) +GESTURE_ZOOM_IN = const(5) +GESTURE_ZOOM_OUT = const(6) + +POLLING_MODE = const(0x00) +TRIGGER_MODE = const(0x01) + + +class FT6x36: + """ + FocalTech Self-Capacitive Touch Panel Controller module + + :param I2C i2c: The board I2C object + :param int address: The I2C address + :param Pin rst: The reset Pin object + """ + + def __init__(self, i2c, address: int = _FT6x36_ADDR, rst=None) -> None: + self._i2c = i2c + self._address = address + self._rst = rst + self._read_buffer = bytearray(4) + self._write_buffer = bytearray(1) + + def get_gesture(self) -> int: + """ + Get Gesture events. Should be a value of: + + * ``GESTURE_NO_GESTRUE``: No Gesture + * ``GESTURE_MOVE_UP``: Move Up + * ``GESTURE_MOVE_RIGHT``: Move Right + * ``GESTURE_MOVE_DOWN``: Move Down + * ``GESTURE_MOVE_LEFT``: Move Left + * ``GESTURE_ZOOM_IN``: Zoom In + * ``GESTURE_ZOOM_OUT``: Zoom Out + """ + gesture = self._i2c.readfrom_mem(self._address, _GEST_ID_REG, 1)[0] + if 0x10 == gesture: + return GESTURE_MOVE_UP + elif 0x14 == gesture: + return GESTURE_MOVE_RIGHT + elif 0x18 == gesture: + return GESTURE_MOVE_DOWN + elif 0x1C == gesture: + return GESTURE_MOVE_LEFT + elif 0x48 == gesture: + return GESTURE_ZOOM_IN + elif 0x49 == gesture: + return GESTURE_ZOOM_OUT + else: + return GESTURE_NO_GESTRUE + + def get_positions(self) -> list: + positions = [] + num_points = self._i2c.readfrom_mem(self._address, _TD_STATUS_REG, 1)[0] & 0x0F + if num_points > 0: + positions.append(self._get_p1()) + if num_points > 1: + positions.append(self._get_p2()) + return positions + + @property + def theshold(self) -> int: + """ + Threshold for touch detection. + """ + return self._i2c.readfrom_mem(self._address, _TH_GROUP_REG, 1)[0] + + @theshold.setter + def theshold(self, val: int) -> None: + self._write_buffer[0] = val + self._i2c.writeto_mem(self._address, _TH_GROUP_REG, self._write_buffer) + + @property + def monitor_time(self) -> int: + """ + The time period of switching from Active mode to Monitor mode when there is no touching. + """ + return self._i2c.readfrom_mem(self._address, _TIMEENTERMONITOR_REG, 1)[0] + + @monitor_time.setter + def monitor_time(self, val: int) -> None: + self._write_buffer[0] = val + self._i2c.writeto_mem(self._address, _TIMEENTERMONITOR_REG, self._write_buffer) + + @property + def active_period(self) -> int: + """ + Report rate in Active mode. + """ + return self._i2c.readfrom_mem(self._address, _PERIODACTIVE_REG, 1)[0] + + @active_period.setter + def active_period(self, val: int) -> None: + self._write_buffer[0] = val + self._i2c.writeto_mem(self._address, _PERIODACTIVE_REG, self._write_buffer) + + @property + def monitor_period(self) -> int: + """ + Report rate in Monitor mode. + """ + return self._i2c.readfrom_mem(self._address, _PERIODMONITOR_REG, 1)[0] + + @monitor_period.setter + def monitor_period(self, val: int) -> None: + self._write_buffer[0] = val + self._i2c.writeto_mem(self._address, _PERIODMONITOR_REG, self._write_buffer) + + @property + def library_version(self): + """ + Library Version info. + """ + buffer = self._i2c.readfrom_mem(self._address, _LIB_VER_H_REG, 2) + return buffer[0] << 8 | buffer[1] + + @property + def firmware_version(self) -> int: + """ + Firmware Version. + """ + return self._i2c.readfrom_mem(self._address, _FIRMID_REG, 1) + + @property + def interrupt_mode(self) -> int: + """ + Interrupt mode for valid data. Should be a value of: + + * ``POLLING_MODE``: Interrupt Polling mode + * ``TRIGGER_MODE``: Interrupt Trigger mode + """ + return self._i2c.readfrom_mem(self._address, _G_MODE_REG, 1)[0] + + @interrupt_mode.setter + def interrupt_mode(self, val: int) -> None: + self._write_buffer[0] = val + self._i2c.writeto_mem(self._address, _G_MODE_REG, self._write_buffer) + + @property + def power_mode(self) -> int: + """ + Current power mode which system is in. + """ + return self._i2c.readfrom_mem(self._address, _PWR_MODE_REG, 1)[0] + + @power_mode.setter + def power_mode(self, val: int) -> None: + self._write_buffer[0] = val + self._i2c.writeto_mem(self._address, _PWR_MODE_REG, self._write_buffer) + + @property + def vendor_id(self) -> None: + """ + Chip Selecting. + """ + return self._i2c.readfrom_mem(self._address, _CIPHER_REG, 1)[0] + + @property + def panel_id(self) -> None: + """ + FocalTech's Panel ID. + """ + return self._i2c.readfrom_mem(self._address, _FOCALTECH_ID_REG, 1)[0] + + def reset(self) -> None: + """ + Hardware reset touch screen. + """ + if self._rst is None: + return + self._rst.off() + _time.sleep_ms(1) + self._rst.on() + + def _get_p1(self) -> tuple: + self._i2c.readfrom_mem_into(self._address, _P1_XH_REG, self._read_buffer) + return ( + (self._read_buffer[0] << 8 | self._read_buffer[1]) & 0x0FFF, + (self._read_buffer[2] << 8 | self._read_buffer[3]) & 0x0FFF, + ) + + def _get_p2(self) -> tuple: + self._i2c.readfrom_mem_into(self._address, _P2_XH_REG, self._read_buffer) + return ( + (self._read_buffer[0] << 8 | self._read_buffer[1]) & 0x0FFF, + (self._read_buffer[2] << 8 | self._read_buffer[3]) & 0x0FFF, + ) diff --git a/micropython/drivers/touch/ft6x36/manifest.py b/micropython/drivers/touch/ft6x36/manifest.py new file mode 100644 index 000000000..5e1ade085 --- /dev/null +++ b/micropython/drivers/touch/ft6x36/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay ft6x36 touch driver", + version="0.0.1", +) +module("ft6x36.py", opt=3) diff --git a/micropython/drivers/touch/gt911/gt911.py b/micropython/drivers/touch/gt911/gt911.py new file mode 100644 index 000000000..664ee8161 --- /dev/null +++ b/micropython/drivers/touch/gt911/gt911.py @@ -0,0 +1,137 @@ +""" +This file is part of the OpenMV project. + +Copyright (c) 2023 Ibrahim Abdelkader +Copyright (c) 2023 Kwabena W. Agyeman + +This work is licensed under the MIT license, see the file LICENSE for details. + +GT911 5-Point Capacitive Touch Controller driver for MicroPython. + +Basic polling mode example usage: + +import time +from gt911 import GT911 +from machine import I2C + +# Note use pin numbers or names not Pin objects because the +# driver needs to change pin directions to reset the controller. +touch = GT911(I2C(1, freq=400_000), reset_pin="P1", irq_pin="P2", touch_points=5) + +while True: + n, points = touch.read_points() + for i in range(0, n): + print(f"id {points[i][3]} x {points[i][0]} y {points[i][1]} size {points[i][2]}") + time.sleep_ms(100) +""" + +from time import sleep_ms +from array import array +from machine import Pin +from micropython import const + +_DEFAULT_ADDR = const(0x5D) + +_COMMAND = const(0x8040) +_REFRESH_RATE = const(0x8056) +_RESOLUTION_X = const(0x8048) +_RESOLUTION_Y = const(0x804A) +_TOUCH_POINTS = const(0x804C) +_MODULE_SWITCH1 = const(0x804D) +_CONFIG_CHKSUM = const(0x80FF) +_CONFIG_FRESH = const(0x8100) +_POINT_DATA_START = const(0x8150) +_DATA_BUFFER = const(0x814E) + + +class GT911: + def __init__( + self, + bus, + reset_pin, + irq_pin, + address=_DEFAULT_ADDR, + width=800, + height=480, + touch_points=1, + reverse_x=False, + reverse_y=False, + reverse_axis=True, + sito=True, + refresh_rate=240, + touch_callback=None, + ): + self.bus = bus + self.address = address + self.touch_callback = touch_callback + self.rst_pin = Pin(reset_pin, Pin.OUT_PP, value=0) + self.irq_pin = None + self.irq_pin_label = irq_pin + + # Reset the touch panel controller. + self.reset() + + # Write and update the config. + self._write_reg(_RESOLUTION_X, width, 2) + self._write_reg(_RESOLUTION_Y, height, 2) + self._write_reg(_TOUCH_POINTS, touch_points) + self._write_reg( + _MODULE_SWITCH1, + (int(reverse_y) << 7) + | (int(reverse_x) << 6) + | (int(reverse_axis) << 3) + | (int(sito) << 2) + | 0x01, + ) + self._write_reg(_REFRESH_RATE, (1000 * 1000) // (refresh_rate * 250)) + self._write_reg(_COMMAND, 0x00) + self._update_config() + + # Allocate scratch buffer. + self.points_data = [array("H", [0, 0, 0, 0]) for x in range(5)] + + def _read_reg(self, reg, size=1, buf=None): + if buf is not None: + self.bus.readfrom_mem_into(self.address, reg, buf, addrsize=16) + else: + return self.bus.readfrom_mem(self.address, reg, size, addrsize=16) + + def _write_reg(self, reg, val, size=1): + buf = bytes([val & 0xFF]) if size == 1 else bytes([val & 0xFF, val >> 8]) + self.bus.writeto_mem(self.address, reg, buf, addrsize=16) + + def _update_config(self): + # Read current config + chksum = ~sum(self._read_reg(0x8047, 184)) + 1 + # Calculate checksum + self._write_reg(_CONFIG_CHKSUM, chksum) + # Update the config + self._write_reg(_CONFIG_FRESH, 0x01) + + def read_id(self): + return self._read_reg(0x8140, 4) + + def read_points(self): + status = self._read_reg(_DATA_BUFFER)[0] + n_points = status & 0x0F + if status & 0x80: + for i in range(n_points): + self._read_reg(_POINT_DATA_START + i * 8, buf=self.points_data[i]) + # We read an extra reserved byte, shift track ID to fix it. + self.points_data[i][-1] = self.points_data[i][-1] >> 8 + self._write_reg(_DATA_BUFFER, 0) + return n_points, self.points_data + + def reset(self): + if self.irq_pin is not None: + self.irq_pin.irq(handler=None) + self.rst_pin(0) + sleep_ms(10) + self.irq_pin = Pin(self.irq_pin_label, Pin.OUT_PP, value=0) + sleep_ms(50) + self.rst_pin(1) + # Note must wait for at least 50ms before switching the IRQ pin to input. + sleep_ms(100) + self.irq_pin = Pin(self.irq_pin_label, Pin.IN, Pin.PULL_UP) + if self.touch_callback is not None: + self.irq_pin.irq(handler=self.touch_callback, trigger=Pin.IRQ_FALLING, hard=False) diff --git a/micropython/drivers/touch/gt911/manifest.py b/micropython/drivers/touch/gt911/manifest.py new file mode 100644 index 000000000..3e26e3d41 --- /dev/null +++ b/micropython/drivers/touch/gt911/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay gt911 touch driver", + version="0.0.1", +) +module("gt911.py", opt=3) diff --git a/micropython/drivers/touch/xpt2046/manifest.py b/micropython/drivers/touch/xpt2046/manifest.py new file mode 100644 index 000000000..a5c419665 --- /dev/null +++ b/micropython/drivers/touch/xpt2046/manifest.py @@ -0,0 +1,5 @@ +metadata( + description="PyDisplay xpt2046 touch driver", + version="0.0.1", +) +module("xpt2046.py", opt=3) diff --git a/micropython/drivers/touch/xpt2046/xpt2046.py b/micropython/drivers/touch/xpt2046/xpt2046.py new file mode 100644 index 000000000..046c3a15a --- /dev/null +++ b/micropython/drivers/touch/xpt2046/xpt2046.py @@ -0,0 +1,99 @@ +"""XPT2046 Touch module Micropython driver. + +Made by Lesept (May 2023) +Inspired by https://github.com/rdagger/micropython-ili9341 + +""" + +from time import sleep +from micropython import const + + +class Touch(object): + """Serial interface for XPT2046 Touch Screen Controller.""" + + GET_X = const(0b11010000) # X position + GET_Y = const(0b10010000) # Y position + + def __init__(self, spi, cs, int_pin=None, int_handler=None): + self.spi = spi + self.cs = cs + self.cs.init(self.cs.OUT, value=1) + self.cal = False + self.rx_buf = bytearray(3) # Receive buffer + self.tx_buf = bytearray(3) # Transmit buffer + if int_pin is not None: + self.int_pin = int_pin + self.int_pin.init(int_pin.IN) + if int_handler is not None: + self.int_handler = int_handler + self.int_locked = False + int_pin.irq(trigger=int_pin.IRQ_FALLING | int_pin.IRQ_RISING, handler=self.int_press) + + def set_orientation(self, orientation): + self.orientation = orientation + + def int_press(self, pin): + """Send X,Y values to passed interrupt handler.""" + if not pin.value() and not self.int_locked: + self.int_locked = True # Lock Interrupt + x, y = self.get_touch() + self.int_handler(x, y) + sleep(0.1) # Debounce falling edge + elif pin.value() and self.int_locked: + sleep(0.1) # Debounce rising edge + self.int_locked = False # Unlock interrupt + + def calibrate(self, xmin, xmax, ymin, ymax, width, height, orientation): + self.xmin = xmin + self.xmax = xmax + self.ymin = ymin + self.ymax = ymax + self.orientation = orientation + if self.orientation % 2 == 0: + self.width = height + self.height = width + else: + self.width = width + self.height = height + self.cal = True + + def raw_touch(self): + x = self.send_command(self.GET_X) + y = self.send_command(self.GET_Y) + return x, y + + def map_value(self, v, vmin, vmax, maxv): + # Map x or y value to display + return int((v - vmin) / (vmax - vmin) * maxv) + + def get_touch(self, clip=False): + if not self.cal: + print("Touch is not calibrated: use raw_touch or calibrate") + return 0, 0 + xraw, yraw = self.raw_touch() + x = self.map_value(xraw, self.xmin, self.xmax, self.width) + y = self.map_value(yraw, self.ymin, self.ymax, self.height) + + # Clip values + if clip: + x = max(x, 0) + y = max(y, 0) + x = min(x, self.width) + y = min(y, self.height) + + if self.orientation % 2 == 1: + return y, self.width - x + else: + return x, y + + def is_touched(self): + return self.int_pin.value() == 0 + + def send_command(self, command): + # Write command to XPT2046 + self.tx_buf[0] = command + self.cs(0) + self.spi.write_readinto(self.tx_buf, self.rx_buf) + self.cs(1) + return (self.rx_buf[1] << 4) | (self.rx_buf[2] >> 4) diff --git a/micropython/pydisplay/displaybuf/README.md b/micropython/pydisplay/displaybuf/README.md new file mode 100644 index 000000000..1a01afa0c --- /dev/null +++ b/micropython/pydisplay/displaybuf/README.md @@ -0,0 +1,151 @@ +logo + +

pydisplay

+ +

Cross-platform User Interface and Event Drivers for *Python

+ +

+ About • + Key Features • + Getting Started • + Running Your First App • + API • + Roadmap • + Contributing • + Thanks • + Screenshots +

+ +| ![peterhinch's active.py](screenshots/active.gif) | ![russhughes's tiny_toasters.py](screenshots/tiny_toasters.gif) | +|-------------------------|--------------------------------| +| @peterhinch's active.py | @russhughes's tiny_toasters.py | + +## About + +WARNINGS: pydisplay is currently alpha quality. Every effort has been made to test on as many platforms as possible, but I need your help and feedback to get it to its inital release. A lot has changed and I am working on catching up the documentation. + +pydisplay is a universal display, event and device driver framework for multiple flavors of Python, including MicroPython, CircuitPython and CPython (big Python). It may be used as-is to create graphic frontends to your apps, or may be used as a foundation with GUI libraries such as [LVGL](https://github.com/lvgl/lv_micropython), [MicroPython-touch](https://github.com/peterhinch/micropython-touch) or maybe even a GUI framework you've been thinking of developing. Its primary purpose is to provide display and touch drivers for MicroPython, but it is equally useful for developers who may never touch MicroPython. + +It is important to note that pydisplay is meant to be a foundation for GUI libraries and is not itself a GUI library. It doesn't provide widgets, such as buttons, checkboxes or sliders, and it doesn't provide a timing mechanism. You will need a GUI library to provide those if necessary, although many apps won't need them. (There is a cross-platform repository [multimer](https://github.com/PyDevices/pydisplay/tree/main/src/lib/multimer) you can use if you want to used scheduled interrupts. It works with CPython and MicroPython, but doesn't work with CircuitPython. You can also use asyncio for timing.) + +## Key Features + +- May be used without additional libraries to add graphics capabilities to MicroPython, CircuitPython and CPython, with a consistent API across them all. +- Enables moving from one platform to another, for example MicroPython on ESP32-S3 to CPython on Windows without changing your code. Do your graphics development on your desktop, laptop or ChromeBook and then move to a microcontroller when you are ready to interface with your sensors and devices. CPython has much better error messages than MicroPython making it easier to troubleshoot when things go wrong! +- Built around devices available on microcontrollers but not necessarily available on desktop operating systems. For instance, rotary encoders and mouse scroll wheels show up as the same device type and yield the same events. Touchscreens on microcontrollers yield the same events as mice on desktops. Likewise with keypads / keyboards. +- Easily extensible. Use the primitives provided by pydisplay and add your own libraries, classes and functions to have even greater functionality. +- Provides several built-in color palettes and a mechanism to generate your own palettes. +- Lots of examples included, whether developed specifically for pydisplay or ported from [Russ Hughes's st7789py_mpy](https://github.com/russhughes/st7789py_mpy). Also works with all of the examples from Peter Hinch's MicroPython GUI libraries [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) and [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) on MicroPython. +- Support MicroPython on microcontrollers and on Unix(-like) operating systems. +- On MicroPython, can be configured to work with [kdschlosser's lvgl_micropython bus drivers](https://github.com/kdschlosser/lvgl_micropython), which are very fast bus drivers written in C. +- Works with CircuitPython's FourWire and ParallelBus bus drivers, as well as FrameBufferDisplay based interfaces such as dotclockframebuffer, usb_video and rgbmatrix + +## Getting Started + +This section is under construction. For now, see [Getting Started](GETTING-STARTED.md) for more information. + + +## Running your first app + +You will need to import the `path.py` file before running any of the examples. + +On desktop operating systems, `cd` into the `mp` directory (or wherever you have the files staged) and type: +``` +python3 -i path.py +``` +or +``` +micropython -i path.py +``` + +On microcontrollers, either add the following to your `boot.py` (MicroPython) or `code.py` (CircuitPython), or simply import it at the REPL before importing your desired app: +``` +import lib.path +``` + +The [examples](examples) directory will be on the system path, so to run an app from it, you just need to type: +``` +import calculator # substitute `calculator` with the file OR directory you want to run, omitting the .py extension +``` + +To run any of the examples from MicroPython-Touch (remember, its for MicroPython only) type: +``` +import gui.demos.various # substitute `various` with the file you want to run, omitting the .py extension +``` + +## API + +Where possible, existing, proven APIs were used. + +- There are currently 5 display classes, and hopefully another `EPaperDisplay` display class will be added soon, although I will need help from the community for this. + - BusDisplay is for microcontrollers, both on MicroPython and CircuitPython. CircuitPython provides the required bus drivers, as mentioned elsewhere in this README, but MicroPython doesn't have display bus drivers. The [buses](src/lib/buses) packages are included with the installer. It is my hope that community members will create other C bus drivers similar to @kdschlosser's bus drivers in [lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython). + - SDL2Display - the preferred class for desktop operating systems as it is faster than PGDisplay. It uses an SDL `texture` in place of an LCD's Graphics RAM (GRAM). + - PGDisplay - an optional class for desktop operating systems. It uses a pygame `surface` in place of an LCD's GRAM. It can be benificial in a couple of instances: + - SDL2Display "glitches" on my ChromeBook, but PGDisplay doesn't + - On Windows, it is easier to install PyGame than SDL2 + - FBDisplay works with CircuitPython framebufferio.FramebufferDisplay objects, such as dotclockframebuffer (RGB displays), usb_video and rgbmatrix. (usb_video may be the coolest thing you can do with displaysys, although I'm not sure how practical or useful it is. It allows your board to function as a webcam, even without a camera, and to render the display through USB to any application on your host PC that can open a webcam! My Windows machine sees it as an unsupported device, so it will not work, but it does work on my ChromeBook. Currently it is limited to RP2040 only and is hardcoded to a 128 x 96 resolution, but that likely will change. See the [screen capture](examples/circuitpython_usb_video_chromebook.gif) and the [board_config.py](board_configs/circuitpython/usb_video/board_config.py) for more details.) + - JNDisplay for Jupyter Notebooks. No input devices are currently supported. + - PSDisplay for PyScript. Only touchscreens are currently supported. +- Names of events and Devices in [eventsys](src/lib/eventsys/) are taken from PyGame and/or SDL2 to keep the API consistent. +- All drawing targets, sometimes referred to as `canvas` in the code, may be written to using the API from MicroPython's framebuf.FrameBuffer API + - CPython and CircuitPython don't have a `framebuf` module that is API compliant with MicroPython's `framebuf`, so [framebuf.py](add_ons/framebuf.py) is provided for those platforms. It is not used in MicroPython unless framebuf wasn't compiled in. + - A `graphics` module is provided that subclasses `FrameBuffer` (either built-in or from framebuf.py) and provides additional drawing tools, such as `round_rect`. All methods in graphics return an Area object with x, y, w and h attributes describing a bounding box of what was changed. This can be used by applications to only update the part of the display that needs it. That functionality is implemented in DisplayBuffer and will likely be required by EPaperDisplay when it is implemented. + - Canvases include, but are not limited to, the display itself, framebuf bytearrays, bmp565 (16-bit Windows Bitmap files) and displaybuf.DisplayBuffer objects. + - displaybuf.DisplayBuffer implements @peterhinch's API that represents the full display as a framebuffer and allows for 4-, 8- and 16-bit bytearrays while still drawing to the screen as 16-bit. It is required for `MicroPython-Touch` and is very useful outside of that library as well, especially when memory is constrained. +- Display drivers for MicroPython BusDisplay use the constructor API of CircuitPython's DisplayIO drivers. This includes rotation = 0, 90, 180, 270 instead of 0, 1, 2, 3. +- BusDisplay can communicate with the underlying bus driver using either CircuitPython's DisplayIO method calls or @kdschlosser's [lvgl_micropython] method calls. +- There are 3 primary mechanism's for fonts: the graphics.Font class, tft_text.text() and tft_write.write() methods. All 3 of these return an Area object as mentioned earlier. A fourth font mechanism called EZFont is included in the utils folder, but it doesn't return an Area object, which is why it isn't in the lib folder. + - Font is derived from Tony DiCola's 5x7 font class and reads 8x8, 8x14 and 8x16 .bin files from [@spaceraces romfont repo](https://github.com/spacerace/romfont) + - .text() is written by @russhughes and uses fonts generated by his [text_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/text_font_converter.py) It reads 8 and 16bit wide fonts in heights that are multiples of 8. + - .write() is written by @russhughes and uses fonts generated by his [write_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/write_font_converter.py) + - EZFont is a subclass of [@easytarget's microPyEZfonts](https://github.com/easytarget/microPyEZfonts) which uses fonts generated from [@peterhinch's font-to-py](https://github.com/peterhinch/micropython-font-to-py). + - NOTE: @peterhinch's Writer class is inlcuded in MicroPyton-Touch and may be used on MicroPython platforms, but, like EZFont, it doesn't return an Area object. +- Graphics files may be used by 3 mechanisms: + - bmp565.BM565 is a class that can read and write Windows Bitmap files saved with RGB565 color encoding. GIMP supports exporting RGB565 .BMPs. The BMP565 class can open a file and read it's entire contents into memory, or with the `streamed = True` flag, it will only read the slice requested, allowing progressive rendering of files much too large to fit into memory. The slice can be 2 dimensional (BMP565[1:5, 6:10] gets pixels 1 through 5 on rows 6 through 10) or 1 dimensional (BMP565[6:10] gets all pixels in rows 6 through 10). This slicing mechanism is very useful when rendering sprites. It can reverse the order of pixels in a row with `mirrored = False`, which is needed when rendering a background image when rotation is 90 or 270 and (horizontal) scrolling is desired. Finally, it can use an existing bytearray as its buffer instead of reading from a file, which allows saving screenshots from existing canvases such as a FrameBuffer or DisplayBuffer. + - .bitmap() is written by @russhughes and reads .py graphics files encoded with his [image_converter.py utiliity](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or his [sprite_converter.py utility](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py). It renders the entire image to a buffer, and then copies that buffer to the display. + - .pbitmap() is also written by @russhughes and renders the same fonts as .bitmap(), but it does it progressively, one line at a time using a one line buffer. +- Config files - All files that are intended for you to edit to customize your configuration are in the [configs](src/configs/) directory. They are: + - `board_config.py` - required in all circumstances. Feel free to add your own setup code here, such as for real-time clocks, wifi, sensors, etc. + - `path.py` - required in all circumstances + - `color_setup.py` - required for [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) + - `hardware_setup.py` - required for [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) + - `lv_config.py` - required for LVGL + - `tft_config.py` - required for @russhughes's examples. I had to do some search and replace to get those examples to work. + + +## Roadmap + +- [ ] Much more documentation on Github +- [ ] Document the files to produce output for ReadTheDocs +- [ ] Implement EPaperDisplay +- [ ] Optimize with more Numpy and Viper code +- [ ] Decrease the memory footprint where possible +- [ ] Test with frozen modules +- [ ] On MicroPython on Unix, the screen gets cleared when the display is rotated. Microcontroller displays don't do this. It's not an issue unless you want to draw to the display, rotate it, then draw more on top. This functionality allow drawing text in all four 90 degree orientations. +- [ ] Scrolling vertically on desktop operating sytems works correctly, but not when rotated. When rotated, it show scroll horizontally, but continues to scroll vertically. +- [ ] Scrolling on microcontrollers has issues when trying to write spanning the cutoff line. For instance, if drawing a 16 pixel high image at the 8th line from the cutoff line, the bottom 8 lines don't end up where you expect. See the [bmp565_sprite](examples/bmp565_sprite.py) example. +- [ ] Ensure multiple displays work at the same time +- [ ] Implement color depths other than 16 bit +- [ ] Add a Joystick class to eventsys +- [ ] Test with CircuitPython Blinka on SBC's such as Raspberry Pi 4 +- [ ] Need C bus drivers from the community, especially for STM32H7 and MIMXRT + +## Contributing + +This is a community project and I need your help! If you have a suggestion that would make this better, please fork the repo and create a pull request. +Don't forget to give the project a star! Thanks again! + +1. Fork the project +2. Clone it open the repository in command line +3. Create your feature branch (`git checkout -b feature/amazing-feature`) +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a pull request from your feature branch from your repository into this repository main branch, and provide a description of your changes + +## Thanks + +I very much appreciate @peterhinch, @russhughes and the team at Adafruit for their contributions to the Python on microcontrollers community. + +## Why + +I started out just wanting to create drivers that worked with MicroPython the way DisplayIO drivers work for CircuitPython, except without DisplayIO and instead usable by any GUI framework like, but not limited to, LVGL. That snowballed into adding more platforms and then adding drawing primitives, font classes, palettes, an event system, a barebones SDL2 library, a Bitmap 565 reader/writer and supporting as many platforms as possible. I stopped short of creating a full fledged GUI and plan to leave it as a very capable graphics library. I think this is a great foundation for building a GUI framework with widgets and a task scheduler, although it is very usable and useful without one. @peterhinch has a great GUI for MicroPython that works on top of pydisplay, and I'm hoping someone will make a GUI that works across platforms. diff --git a/micropython/pydisplay/displaybuf/displaybuf/__init__.py b/micropython/pydisplay/displaybuf/displaybuf/__init__.py new file mode 100644 index 000000000..901a33543 --- /dev/null +++ b/micropython/pydisplay/displaybuf/displaybuf/__init__.py @@ -0,0 +1,265 @@ +# SPDX-FileCopyrightText: 2020 Peter Hinch, 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +`displaybuf` +==================================================== + +FrameBuffer wrapper for using framebuf based GUIs with displaysys. +Works with MicroPython Nano-GUI, Micro-GUI and MicroPython-Touch from Peter Hinch, +but may also be used without them. + +Usage: + 'color_setup.py' + from displaybuf import DisplayBuffer as SSD + from board_config import display_drv + + format = SSD.RGB565 # or .GS8 or .GS4_HMSB + + ssd = SSD(display_drv, format) + + 'main.py' + from color_setup import ssd + +""" + +import gc +import sys +from displaysys import alloc_buffer, color565, color565_swapped, color332 + +try: + import graphics as framebuf +except ImportError: + import framebuf + +_has_viper_tools = False +if sys.implementation.name == "micropython": + try: + from viper_tools import _bounce8, _bounce4 + + _has_viper_tools = True + except Exception: + pass + +if not _has_viper_tools: + + def _bounce8(*args, **kwargs): + raise NotImplementedError( + ".GS8 and .GS4_HMSB DisplayBuffer formats are only implemented in viper_tools.py for MicroPython." + ) + + _bounce4 = _bounce8 + + +gc.collect() + + +_display_drv_get_attrs = {"set_vscroll", "tfa", "bfa", "vsa", "vscroll", "translate_point"} +_display_drv_set_attrs = {"vscroll"} + + +class DisplayBuffer(framebuf.FrameBuffer): + """ + DisplayBuffer: A class to wrap an displaysys driver and provide a framebuf + compatible interface to it. It provides a show() method to copy the framebuf + to the display. The show() method is optimized for the format. + The format must be one of the following: + DisplayBuffer.RGB565 + DisplayBuffer.GS8 + DisplayBuffer.GS4_HMSB + """ + + rgb = None # Function to convert r, g, b to a color value; used by Nano-GUI and Micro-GUI. + colors_registered = 0 # For .color(). Not used in Nano-GUI or Micro-GUI. + + RGB565 = framebuf.RGB565 + GS8 = framebuf.GS8 + GS4_HMSB = framebuf.GS4_HMSB + + def __init__(self, display_drv, format=framebuf.RGB565, stride=8): + gc.collect() + self.display_drv = display_drv + self.vscrdef = display_drv.vscrdef + self.vscsad = display_drv.vscsad + height = display_drv.height + width = display_drv.width + BPP = display_drv.color_depth // 8 + self.palette = BoolPalette( + format + ) # a 2-value color palette for rendering monochrome glyphs + # with ssd.blit(glyph_buf, x, y, key=-1, palette=ssd.palette) + + # If byte swapping is required and the display bus is capable of having byte swapping disabled, + # disable it and set a flag so we can swap the color bytes as they are created. + if self.display_drv.requires_byteswap: + # self.display_drv.disable_auto_byteswap(True) returns True if it was successful, False if not. + self.needs_swap = self.display_drv.disable_auto_byteswap(True) + else: + self.needs_swap = False + + # Set the DisplayBuffer.rgb function to the appropriate one for the format and byte swapping + if format == DisplayBuffer.GS8: + DisplayBuffer.rgb = color332 + else: + if self.needs_swap: + DisplayBuffer.rgb = color565_swapped + else: + DisplayBuffer.rgb = color565 + + # Set the show function to the appropriate one for the format and + # allocate the buffer. Also create the line buffer and lut if needed. + gc.collect() + if format == DisplayBuffer.RGB565: + buffer = bytearray(width * height * BPP) + self.show = self._show16 + elif format == DisplayBuffer.GS8: + self._stride = stride + self._bounce_buf = alloc_buffer(width * self._stride * BPP) + buffer = bytearray(width * height) + self.show = self._show8 + elif format == DisplayBuffer.GS4_HMSB: + DisplayBuffer.lut = bytearray(0x00 for _ in range(32)) + self._stride = stride + self._bounce_buf = alloc_buffer(width * self._stride * BPP) + buffer = bytearray(width * height // 2) + self.show = self._show4 + else: + raise ValueError(f"Unsupported format: {format}") + + super().__init__(buffer, width, height, format) + self._mvb = memoryview(self.buffer) + self.show() # Clear the display + gc.collect() + + def __getattr__(self, name): + if name in _display_drv_get_attrs: + return getattr(self.display_drv, name) + raise AttributeError(f"{self.__class__.__name__} object has no attribute '{name}'") + + def __setattr__(self, name, value): + if name in _display_drv_set_attrs: + return setattr(self.display_drv, name, value) + super().__setattr__(name, value) + + @property + def color_palette(self): + return self._color_palette + + @color_palette.setter + def color_palette(self, palette): + if len(palette) > 16: + raise ValueError("Palette must be 16 colors or less") + self._color_palette = palette + for index, color in enumerate(palette): + r, g, b = color >> 16 & 0xFF, color >> 8 & 0xFF, color & 0xFF + self.color(r, g, b, index) + + @staticmethod + def color(r, g, b, idx=None): + """ + Get an RGB565 or RGB332 value for a color and optionally register it in the display's LUT. + This is a convenience function for using this framework WITHOUT Nano-GUI or Micro-GUI. + Those packages have their own methods of registering colors. + + Args: + r (int): Red component (0-255) + g (int): Green component (0-255) + b (int): Blue component (0-255) + idx (int): Optional index to register the color in the display's LUT (0-15); + ignored if the display doesn't use a LUT in its current format + + Raises: + ValueError: If 16 colors have already been registered or if the index is out of range + + Returns: + (int): RGB565 color value in RG565 format; + RGB332 color value in GS8 format; + the index of the registered color in the LUT in GS4_HMSB format + """ + c = DisplayBuffer.rgb(r, g, b) # Convert the color to RGB565 or RGB332 + if not hasattr(DisplayBuffer, "lut"): # If the ssd doesn't use a LUT in its current format + return c # Return the color as-is + if idx is None: # If no index was provided + if ( + DisplayBuffer.colors_registered < 16 + ): # If there are fewer than 16 colors registered + idx = DisplayBuffer.colors_registered # Set the index to the next index + DisplayBuffer.colors_registered += 1 # Increment the number of colors registered + else: # If there are already 16 colors registered + raise ValueError("16 colors have already been registered") + if not 0 <= idx <= 15: # If the index is out of range + raise ValueError("Color numbers must be 0..15") + offset = idx << 1 # Multiply by 2 (2 bytes per 16-bit color) + DisplayBuffer.lut[offset] = c & 0xFF # Set the lower 8 bits of the color + DisplayBuffer.lut[offset + 1] = c >> 8 # Set the upper 8 bits of the color + return idx # Return the index of the registered color + + def _show16(self, area=None): + if area is not None: + x, y, w, h = area + for row in range(y, y + h): + buffer_begin = (row * self.width + x) * 2 + buffer_end = buffer_begin + w * 2 + self.display_drv.blit_rect(self.buffer[buffer_begin:buffer_end], x, row, w, 1) + else: + self.display_drv.blit_rect(self.buffer, 0, 0, self.width, self.height) + + def _show8(self, area=None): + # Note: area is ignored for now in _show8 + # Convert the 8 bit RGB332 values to 16 bit RGB565 values and then copy the line + # to the display, line by line. + swap = self.needs_swap + buf = self._mvb + bb = self._bounce_buf + wd = self.width + ht = self.height + lines = self._stride + stride = lines * wd + chunks, remainder = divmod(ht, lines) + for chunk in range(chunks): + start = chunk * stride + _bounce8(bb, buf[start:], stride, swap) + self.display_drv.blit_rect(bb, 0, chunk * lines, wd, lines) + if remainder: + start = chunks * stride + _bounce8(bb, buf[start:], remainder * wd, swap) + self.display_drv.blit_rect(bb, 0, chunks * lines, wd, remainder) + + def _show4(self, area=None): + # Note: area is ignored for now in _show4 + # Convert the 4 bit index values to 16 bit RGB565 values using a lookup table + # and then copy the line to the display, line by line. + clut = DisplayBuffer.lut + buf = self._mvb + bb = self._bounce_buf + wd = self.width + ht = self.height + lines = self._stride + stride = lines * wd // 2 # 2 pixels per byte + chunks, remainder = divmod(ht, lines) + for chunk in range(chunks): + start = chunk * stride + _bounce4(bb, buf[start:], stride, clut) + self.display_drv.blit_rect(bb, 0, chunk * lines, wd, lines) + if remainder: + start = chunks * stride + _bounce4(bb, buf[start:], remainder * wd // 2, clut) + self.display_drv.blit_rect(bb, 0, chunks * lines, wd, remainder) + + +class BoolPalette(framebuf.FrameBuffer): + # This is a 2-value color palette for rendering monochrome glyphs to color + # FrameBuffer instances. Supports destinations with up to 16 bit color. + + # Copyright (c) Peter Hinch 2021 + # Released under the MIT license see LICENSE + def __init__(self, format): + buf = bytearray(4) # OK for <= 16 bit color + super().__init__(buf, 2, 1, format) + + def fg(self, color): # Set foreground color + self.pixel(1, 0, color) + + def bg(self, color): + self.pixel(0, 0, color) diff --git a/micropython/pydisplay/displaybuf/examples/displaybuf_blit.py b/micropython/pydisplay/displaybuf/examples/displaybuf_blit.py new file mode 100644 index 000000000..0aebcd282 --- /dev/null +++ b/micropython/pydisplay/displaybuf/examples/displaybuf_blit.py @@ -0,0 +1,14 @@ +from color_setup import ssd +from framebuf import FrameBuffer, RGB565 + + +ssd.fill(0xF800) +ssd.show() + +ba = bytearray(100 * 100 * 2) +mv = memoryview(ba) +fb = FrameBuffer(mv, 100, 100, RGB565) +fb.fill(0x000F) + +ssd.blit(fb, 100, 100) +ssd.show() diff --git a/micropython/pydisplay/displaybuf/examples/displaybuf_simpletest.py b/micropython/pydisplay/displaybuf/examples/displaybuf_simpletest.py new file mode 100644 index 000000000..16b712214 --- /dev/null +++ b/micropython/pydisplay/displaybuf/examples/displaybuf_simpletest.py @@ -0,0 +1,60 @@ +""" +displaybuf_simpletest.py - Simple test program for displaybuf.py +""" + +from color_setup import ssd +from array import array # for defining a polygon + + +FONT_WIDTH = 8 + +# Define colors (max 16 colors if using lookup tables / GS4_HMSB mode) +# Note: ssd.color and ssd.colors_registered should not be used with Nano-GUI or +# Micro-GUI because those packages have their own mechanisms for managing colors. +WHITE = ssd.color(255, 255, 255) +RED = ssd.color(255, 0, 0) +GREEN = ssd.color(0, 255, 0) +BLUE = ssd.color(0, 0, 255) +CYAN = ssd.color(0, 255, 255) +MAGENTA = ssd.color(255, 0, 255) +YELLOW = ssd.color(255, 255, 0) +BLACK = ssd.color(0, 0, 0) +LIGHT_GREY = ssd.color(192, 192, 192) +GREY = ssd.color(96, 96, 96) +DARK_GREY = ssd.color(64, 64, 64) +GREY = ssd.color(128, 128, 128, GREY) # Example of how to redefine a color in the lookup table +if ssd.colors_registered: # Will be 0 if not using lookup tables / GS4_HMSB mode. + print(f"{ssd.colors_registered} colors registered.") + + +# Main loop +def main(scroll=False, animate=False, text1="displaybuf", text2="simpletest"): + WIDTH = ssd.width + HEIGHT = ssd.height + poly = array("h", [0, 0, WIDTH // 2, -HEIGHT // 4, WIDTH - 1, 0]) + y_range = range(HEIGHT - 1, -1, -1) if animate else [HEIGHT - 1] + for y in y_range: + ssd.fill(BLACK) + ssd.poly(0, y, poly, YELLOW, True) + ssd.fill_rect(WIDTH // 6, HEIGHT // 3, WIDTH * 2 // 3, HEIGHT // 3, GREY) + ssd.line(0, 0, WIDTH - 1, HEIGHT - 1, GREEN) + ssd.rect(0, 0, 15, 15, RED, True) + ssd.rect(WIDTH - 15, HEIGHT - 15, 15, 15, BLUE, True) + ssd.hline(WIDTH // 8, HEIGHT // 2, WIDTH * 3 // 4, MAGENTA) + ssd.vline(WIDTH // 2, HEIGHT // 4, HEIGHT // 2, CYAN) + ssd.pixel(WIDTH // 2, HEIGHT * 1 // 8, WHITE) + ssd.ellipse(WIDTH // 2, HEIGHT // 2, WIDTH // 4, HEIGHT // 8, BLACK, True, 0b1111) + ssd.text(text1, (WIDTH - FONT_WIDTH * len(text1)) // 2, HEIGHT // 2 - 8, WHITE) + ssd.text(text2, (WIDTH - FONT_WIDTH * len(text2)) // 2, HEIGHT // 2, WHITE) + ssd.show() + + ssd.hline(0, 0, WIDTH, BLACK) + ssd.vline(0, 0, HEIGHT, BLACK) + + scroll_range = range(min(WIDTH, HEIGHT)) if scroll else [] + for _ in scroll_range: + ssd.scroll(1, 1) + ssd.show() + + +main() diff --git a/micropython/pydisplay/displaybuf/manifest.py b/micropython/pydisplay/displaybuf/manifest.py new file mode 100644 index 000000000..5a51c3275 --- /dev/null +++ b/micropython/pydisplay/displaybuf/manifest.py @@ -0,0 +1,8 @@ +metadata( + description="PyDisplay displaybuf", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaybuf", +) +package("displaybuf") diff --git a/micropython/pydisplay/displaysys/displaysys-busdisplay/displaysys/busdisplay.py b/micropython/pydisplay/displaysys/displaysys-busdisplay/displaysys/busdisplay.py new file mode 100644 index 000000000..1b7998e15 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-busdisplay/displaysys/busdisplay.py @@ -0,0 +1,607 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett and Kevin Schlosser +# +# SPDX-License-Identifier: MIT + +""" +displaysys.busdisplay +""" + +from displaysys import DisplayDriver +from micropython import const +import struct +import sys +import gc + +try: + from typing import Optional +except ImportError: + pass + +if sys.implementation.name == "micropython": + from machine import Pin + from time import sleep_ms + from micropython import alloc_emergency_exception_buf + + alloc_emergency_exception_buf(256) +elif sys.implementation.name == "circuitpython": + import digitalio + from time import sleep + + def sleep_ms(ms): + return sleep(ms / 1000) +else: + raise ImportError("BusDisplay is not supported on this platform.") + + +gc.collect() + +# MIPI DCS (Display Command Set) Command Constants +_INVOFF = const(0x20) +_INVON = const(0x21) +_CASET = const(0x2A) +_RASET = const(0x2B) +_RAMWR = const(0x2C) +_COLMOD = const(0x3A) +_MADCTL = const(0x36) +_RAMCONT = const(0x3C) +_SWRESET = const(0x01) +_SLPIN = const(0x10) +_SLPOUT = const(0x11) +_VSCRDEF = const(0x33) +_VSCSAD = const(0x37) + +# fmt: off + +# MIPI DCS MADCTL bits +# Bits 0 (Flip Vertical) and 1 (Flip Horizontal) affect how the display is refreshed, not how frame memory is written. +# Instead of using them, we only change Bits 6 (column/horizontal) and 7 (page/vertical). +_RGB = const(0x00) # (Bit 3: 0=RGB order, 1=BGR order) +_BGR = const(0x08) # (Bit 3: 0=RGB order, 1=BGR order) +_MADCTL_MH = const(0x04) # Refresh 0=Left to Right, 1=Right to Left (Bit 2: Display Data Latch Order) +_MADCTL_ML = const(0x10) # Refresh 0=Top to Bottom, 1=Bottom to Top (Bit 4: Line Refresh Order) +_MADCTL_MV = const(0x20) # 0=Normal, 1=Row/column exchange (Bit 5: Page/Column Addressing Order) +_MADCTL_MX = const(0x40) # 0=Left to Right, 1=Right to Left (Bit 6: Column Address Order) +_MADCTL_MY = const(0x80) # 0=Top to Bottom, 1=Bottom to Top (Bit 7: Page Address Order) + +# MADCTL values for each of the rotation constants. +_DEFAULT_ROTATION_TABLE = ( + _MADCTL_MX, # mirrored = False, rotation = 0 + _MADCTL_MV, # mirrored = False, rotation = 90 + _MADCTL_MY, # mirrored = False, rotation = 180 + _MADCTL_MY | _MADCTL_MX | _MADCTL_MV, # mirrored = False, rotation = 270 +) + +_MIRRORED_ROTATION_TABLE = ( + 0, # mirrored = True, rotation = 0 + _MADCTL_MV | _MADCTL_MX, # mirrored = True, rotation = 90 + _MADCTL_MX | _MADCTL_MY, # mirrored = True, rotation = 180 + _MADCTL_MV | _MADCTL_MY, # mirrored = True, rotation = 270 +) +# fmt: on + + +class BusDisplay(DisplayDriver): + """ + Base class for displays connected via a bus. + + Args: + display_bus (SPIBus, I80Bus): The bus the display is connected to. + init_sequence (bytes, list): The initialization sequence for the display. + width (int): The width of the display in pixels. + height (int): The height of the display in pixels. + colstart (int): The column start address for the display. + rowstart (int): The row start address for the display. + rotation (int): The rotation of the display in degrees. + mirrored (bool): If True, the display is mirrored. + color_depth (int): The color depth of the display in bits. + bgr (bool): If True, the display uses BGR color order. + invert (bool): If True, the display colors are inverted. + reverse_bytes_in_word (bool): If True, the bytes in 16-bit colors are reversed. + brightness (float): The brightness of the display as a float between 0.0 and 1.0. + backlight_pin (int, Pin): The pin the display backlight is connected to. + backlight_on_high (bool): If True, the backlight is on when the pin is high. + reset_pin (int, Pin): The pin the display reset is connected to. + reset_high (bool): If True, the reset pin is high. + power_pin (int, Pin): The pin the display power is connected to. + power_on_high (bool): If True, the power pin is high. + set_column_command (int): The command to set the column address. + set_row_command (int): The command to set the row address. + write_ram_command (int): The command to write to the display RAM. + brightness_command (int): The command to set the display brightness. + data_as_commands (bool): If True, data is sent as commands. + single_byte_bounds (bool): If True, single byte bounds are used. + + Attributes: + display_bus (SPIBus, I80Bus): The bus the display is connected to. + color_depth (int): The color depth of the display in bits. + bgr (bool): If True, the display uses BGR color order. + rotation_table (tuple): The rotation table for the display. + """ + + def __init__( # noqa: PLR0913 + self, + display_bus, + init_sequence=None, + *, + width=0, + height=0, + colstart=0, + rowstart=0, + rotation=0, + mirrored=False, + color_depth=16, + bgr=False, + invert=False, + reverse_bytes_in_word=False, + brightness=1.0, + backlight_pin=None, + backlight_on_high=True, + reset_pin=None, + reset_high=True, + power_pin=None, + power_on_high=True, + set_column_command=_CASET, + set_row_command=_RASET, + write_ram_command=_RAMWR, + brightness_command=None, # For color OLEDs + data_as_commands=False, # For color OLEDs + single_byte_bounds=False, # For color OLEDs + ): + print("Started BusDisplay") + gc.collect() + self.display_bus = display_bus + self._width = width + self._height = height + self._colstart = colstart + self._rowstart = rowstart + self._rotation = rotation + self.color_depth = color_depth + self.bgr = bgr + self._invert = invert + self._requires_byteswap = reverse_bytes_in_word + self._set_column_command = set_column_command + self._set_row_command = set_row_command + self._write_ram_command = write_ram_command + self._brightness_command = brightness_command + self._data_as_commands = data_as_commands # not implemented + self._single_byte_bounds = single_byte_bounds # not implemented + + self.send = display_bus.send + self.send_color = ( + display_bus.send if not hasattr(display_bus, "send_color") else display_bus.send_color + ) + + self.rotation_table = _DEFAULT_ROTATION_TABLE if not mirrored else _MIRRORED_ROTATION_TABLE + + self._param_buf = bytearray(4) + self._param_mv = memoryview(self._param_buf) + + self._reset_pin = self._config_output_pin(reset_pin, value=not reset_high) + self._reset_high = reset_high + + self._power_pin = self._config_output_pin(power_pin, value=power_on_high) + self._power_on_high = power_on_high + + self._backlight_pin = self._config_output_pin(backlight_pin, value=backlight_on_high) + self._backlight_on_high = backlight_on_high + + if self._backlight_pin is not None: + try: + from machine import PWM + + self._backlight_pin = PWM(self._backlight_pin, freq=1000, duty_u16=0) + self._backlight_is_pwm = True + except ImportError: + # PWM not implemented on this platform or Pin + self._backlight_is_pwm = False + + # Run the display driver init_sequence. + if isinstance(init_sequence, bytes): + self._init_bytes(init_sequence) + elif isinstance(init_sequence, list) or isinstance(init_sequence, tuple): + self._init_list(init_sequence) + + # Run the display driver init() method, which also gets called by rotation.setter + # This should run immediately after _init_bytes() or _init_list() but before + # sending other commands such as _INVON, _INVOFF, _COLMOD, brightness, etc. + self._initialized = False + super().__init__() + if not self._initialized: + raise RuntimeError("Display driver init() must call super().init()") + + # Set COLMOD (color mode) based on color_depth + pixel_formats = {3: 0x11, 8: 0x22, 12: 0x33, 16: 0x55, 18: 0x66, 24: 0x77} + self._param_buf[0] = pixel_formats[self.color_depth] + self.send(_COLMOD, self._param_mv[:1]) + + self.brightness = brightness + + gc.collect() + print("Finished BusDisplay") + + ############### Required API Methods ################ + + def init(self) -> None: + """ + Post initialization tasks. + + This method may be overridden by subclasses to perform any post initialization. + If it is overridden, it must call super().init() or set self._initialized = True. + """ + self._initialized = True + + # Convert from degrees to one quarter rotations. Wrap at the number of entries in the rotations table. + # For example, rotation = 90 -> index = 1. With 4 entries in the rotation table, rotation = 540 -> index = 2 + index = (self._rotation // 90) % len(self.rotation_table) + + # Set the display MADCTL bits for the given rotation. + self._param_buf[0] = self.rotation_table[index] | _BGR if self.bgr else _RGB + self.send(_MADCTL, self._param_mv[:1]) + + # Set the display inversion mode + self.invert_colors(self._invert) + + def blit_rect(self, buf: memoryview, x: int, y: int, w: int, h: int): + """ + Blit a buffer to the display. + + This method takes a buffer of pixel data and writes it to a specified + rectangular area of the display. The top-left corner of the rectangle is + specified by the x and y parameters, and the size of the rectangle is + specified by the width and height parameters. + + Args: + buf (memoryview): The buffer containing the pixel data. + x (int): The x-coordinate of the top-left corner of the rectangle. + y (int): The y-coordinate of the top-left corner of the rectangle. + w (int): The width of the rectangle in pixels. + h (int): The height of the rectangle in pixels. + + Returns: + (tuple): A tuple containing the x, y, width, and height of the rectangle. + """ + if self._auto_byteswap: + self.byteswap(buf) + + x1 = x + self.colstart + x2 = x1 + w - 1 + y1 = y + self.rowstart + y2 = y1 + h - 1 + + self._set_window(x1, y1, x2, y2) + self.send_color(self._write_ram_command, buf) + return (x, y, w, h) + + def fill_rect(self, x: int, y: int, w: int, h: int, c: int): + """ + Draw a rectangle at the given location, size and filled with color. + + This method draws a filled rectangle on the display. The top-left corner of + the rectangle is specified by the x and y parameters, and the size of the + rectangle is specified by the width and height parameters. The rectangle is + filled with the specified color. + + Args: + x (int): The x-coordinate of the top-left corner of the rectangle. + y (int): The y-coordinate of the top-left corner of the rectangle. + w (int): The width of the rectangle in pixels. + h (int): The height of the rectangle in pixels. + c (int): The color of the rectangle. + + Returns: + (tuple): A tuple containing the x, y, width, and height of the rectangle. + """ + color_bytes = ( + (c & 0xFFFF).to_bytes(2, "big") + if self._auto_byteswap + else (c & 0xFFFF).to_bytes(2, "little") + ) + x1 = x + self.colstart + x2 = x1 + w - 1 + y1 = y + self.rowstart + y2 = y1 + h - 1 + + if h > w: + buf = memoryview(bytearray(color_bytes * h)) + passes = w + else: + buf = memoryview(bytearray(color_bytes * w)) + passes = h + + self._set_window(x1, y1, x2, y2) + self.send(_RAMWR) + for _ in range(passes): + self.send_color(_RAMCONT, buf) + return (x, y, w, h) + + def pixel(self, x: int, y: int, c: int): + """ + Set a pixel on the display. + + Args: + x (int): The x-coordinate of the pixel. + y (int): The y-coordinate of the pixel. + c (int): The color of the pixel. + + Returns: + (tuple): A tuple containing the x, y, width, and height of the pixel. + """ + color_bytes = ( + (c & 0xFFFF).to_bytes(2, "big") + if self._auto_byteswap + else (c & 0xFFFF).to_bytes(2, "little") + ) + if self._auto_byteswap: + c = c >> 8 | c << 8 + xpos = x + self.colstart + ypos = y + self.rowstart + self._set_window(xpos, ypos, xpos, ypos) + self.send(_RAMWR, color_bytes) + return (x, y, 1, 1) + + ############### API Method Overrides ################ + + def vscrdef(self, tfa: int, vsa: int, bfa: int) -> None: + """ + Set Vertical Scrolling Definition. + + To scroll a 135x240 display these values should be 40, 240, 40. + There are 40 lines above the display that are not shown followed by + 240 lines that are shown followed by 40 more lines that are not shown. + You could write to these areas off display and scroll them into view by + changing the TFA, VSA and BFA values. + + Args: + tfa (int): Top Fixed Area. + vsa (int): Vertical Scrolling Area. + bfa (int): Bottom Fixed Area. + """ + super().vscrdef(tfa, vsa, bfa) + self.send(_VSCRDEF, struct.pack(">HHH", tfa, vsa, bfa)) + + def vscsad(self, vssa: Optional[int] = None) -> int: + """ + Set the vertical scroll start address. + + Args: + vssa (int, None): The vertical scroll start address. + + Returns: + int: The vertical scroll start address. + """ + if vssa is not None: + super().vscsad(vssa) + self.send(_VSCSAD, struct.pack(">H", self._vssa)) + return self._vssa + + ############### Optional API Methods ################ + + @property + def colstart(self): + """ + The offset in pixels to the first column of the visible display. + """ + rot = self.rotation % 360 + if rot == 0 or rot == 180: + return self._colstart + return self._rowstart + + @property + def rowstart(self): + """ + The offset in pixels to the first row of the visible display. + """ + rot = self.rotation % 360 + if rot == 0 or rot == 180: + return self._rowstart + return self._colstart + + @property + def power(self) -> bool: + """ + The power state of the display. + + Returns: + bool: The power state of the display. + """ + if self._power_pin is None: + return -1 + + state = self._power_pin.value() + if self._power_on_high: + return state + + return not state + + @power.setter + def power(self, value: bool) -> None: + """ + Set the power state of the display. + + Args: + value (bool): The power state to set, True for on, False for off. + """ + if self._power_pin is None: + return + + if self._power_on_high: + self._power_pin.value(value) + else: + self._power_pin.value(not value) + + @property + def brightness(self) -> float: + """ + The brightness of the display. + """ + if self._backlight_pin is None and self._brightness_command is None: + return -1 + + return self._brightness + + @brightness.setter + def brightness(self, value: float) -> None: + """ + Set the brightness of the display. + + Args: + value (float): The brightness of the display as a float between 0.0 and 1.0. + """ + if 0 <= float(value) <= 1: + self._brightness = value + if self._backlight_pin: + if not self._backlight_on_high: + value = 1 - value + if self._backlight_is_pwm: + if sys.implementation.name == "micropython": + self._backlight_pin.duty_u16(int(value * 0xFFFF)) + elif sys.implementation.name == "circuitpython": + self._backlight_pin.duty_cycle = int(value * 0xFFFF) + else: + if sys.implementation.name == "micropython": + self._backlight_pin.value(value > 0.5) # noqa: PLR2004 + elif sys.implementation.name == "circuitpython": + self._backlight_pin.value = value > 0.5 # noqa: PLR2004 + elif self._brightness_command is not None: + self._param_buf[0] = int(value * 255) + self.send(self._brightness_command, self._param_mv[:1]) + + def invert_colors(self, value: bool) -> None: + """ + Invert the colors of the display. + + Args: + value (bool): If True, invert the colors of the display. + """ + if value: + self.send(_INVON) + else: + self.send(_INVOFF) + + def reset(self) -> None: + """ + Reset display. + + This method resets the display. If the display has a reset pin, it is + reset using the reset pin. Otherwise, the display is reset using the + software reset command. + """ + if self._reset_pin is not None: + self.hard_reset() + else: + self.soft_reset() + + def hard_reset(self) -> None: + """ + Hard reset display. + """ + self._reset_pin.value(self._reset_high) + sleep_ms(120) + self._reset_pin.value(not self._reset_high) + + def soft_reset(self) -> None: + """ + Soft reset display. + """ + self.send(_SWRESET) + sleep_ms(150) + + def sleep_mode(self, value: bool) -> None: + """ + Enable or disable display sleep mode. + + Args: + value (bool): If True, enable sleep mode. If False, disable sleep mode. + """ + self.send(_SLPIN if value else _SLPOUT) + + ############### Class Specific Methods ############## + + def _set_window(self, x1, y1, x2, y2): + # See https://github.com/adafruit/Adafruit_Blinka_Displayio/blob/main/displayio/_displaysys.py#L271-L363 + # TODO: Add `if self._single_byte_bounds is True:` for Column and Row _param_buf packing + + # Column addresses + self._param_buf[0] = (x1 >> 8) & 0xFF + self._param_buf[1] = x1 & 0xFF + self._param_buf[2] = (x2 >> 8) & 0xFF + self._param_buf[3] = x2 & 0xFF + self.send(self._set_column_command, self._param_mv[:4]) + + # Row addresses + self._param_buf[0] = (y1 >> 8) & 0xFF + self._param_buf[1] = y1 & 0xFF + self._param_buf[2] = (y2 >> 8) & 0xFF + self._param_buf[3] = y2 & 0xFF + self.send(self._set_row_command, self._param_mv[:4]) + + def _init_bytes(self, init_sequence): + """ + Send an initialization sequence to the display. + + Used by display driver subclass if init_sequence is a CircuitPython displayIO compatible bytes object. + The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins + with a command byte followed by a byte to determine the parameter count and if a + delay is need after. When the top bit of the second byte is 1, the next byte will be + the delay time in milliseconds. The remaining 7 bits are the parameter count + excluding any delay byte. The third through final bytes are the remaining command + parameters. The next byte will begin a new command definition. + + Args: + init_sequence (bytes): The initialization sequence to send to the display. + """ + DELAY = 0x80 + + i = 0 + while i < len(init_sequence): + command = init_sequence[i] + data_size = init_sequence[i + 1] + delay = (data_size & DELAY) != 0 + data_size &= ~DELAY + + self.send(command, init_sequence[i + 2 : i + 2 + data_size]) + + delay_time_ms = 10 + if delay: + data_size += 1 + delay_time_ms = init_sequence[i + 1 + data_size] + if delay_time_ms == 255: + delay_time_ms = 500 + + sleep_ms(delay_time_ms) + i += 2 + data_size + + def _init_list(self, init_sequence): + """ + Send an initialization sequence to the display. + + Used by display driver subclass if init_sequence is a list of tuples. + As a list, it can be modified in .init(), for example: + self._INIT_SEQUENCE[-1] = (0x29, b"\x00", 100) + Each tuple contains the following: + - The first element is the register address (command) + - The second element is the register value (data) + - The third element is the delay in milliseconds after the register is set + + Args: + init_sequence (list): The initialization sequence to send to the display + """ + for line in init_sequence: + self.send(line[0], line[1]) + if line[2] != 0: + sleep_ms(line[2]) + + def _config_output_pin(self, pin, value=None): + if pin is None: + return None + + if sys.implementation.name == "micropython": + p = Pin(pin, Pin.OUT) + if value is not None: + p.value(value) + elif sys.implementation.name == "circuitpython": + p = digitalio.DigitalInOut(pin) + p.direction = digitalio.Direction.OUTPUT + if value is not None: + p.value = value + return p diff --git a/micropython/pydisplay/displaysys/displaysys-busdisplay/examples/board_config.py b/micropython/pydisplay/displaysys/displaysys-busdisplay/examples/board_config.py new file mode 100644 index 000000000..fa43bd07f --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-busdisplay/examples/board_config.py @@ -0,0 +1,59 @@ +"""WT32-SC01 Plus 320x480 ST7796 display""" + +from i80bus import I80Bus +from st7796 import ST7796 +from machine import I2C, Pin # See the note about reset below +from ft6x36 import FT6x36 +from machine import freq +from eventsys import devices + + +freq(240_000_000) +# The WT32-SC01 Plus has the reset pins of the display IC and the touch IC both +# tied to pin 4. Controlling this pin with the display driver can lead to an +# unresponsive touchscreen. This case is uncommon. If they aren't tied +# together on your board, define reset in ST7796 instead, like: +# ST7796(reset=4) +reset = Pin(4, Pin.OUT, value=1) + +display_bus = I80Bus( + dc=0, + cs=6, + wr=47, + data=[9, 46, 3, 8, 18, 17, 16, 15], +) + +display_drv = ST7796( + display_bus, + width=320, + height=480, + colstart=0, + rowstart=0, + rotation=0, + mirrored=False, + color_depth=16, + bgr=True, + reverse_bytes_in_word=True, + invert=True, + brightness=1.0, + backlight_pin=45, + backlight_on_high=True, + reset_pin=None, + reset_high=True, + power_pin=None, + power_on_high=True, +) + +i2c = I2C(0, sda=Pin(6), scl=Pin(5), freq=100000) +touch_drv = FT6x36(i2c) +touch_read_func = touch_drv.get_positions +touch_rotation_table = None + +broker = devices.Broker() + +touch_dev = broker.create_device( + type=devices.types.TOUCH, + read=touch_read_func, + data=display_drv, + data2=touch_rotation_table, +) diff --git a/micropython/pydisplay/displaysys/displaysys-busdisplay/manifest.py b/micropython/pydisplay/displaysys/displaysys-busdisplay/manifest.py new file mode 100644 index 000000000..d09f92cae --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-busdisplay/manifest.py @@ -0,0 +1,9 @@ +metadata( + description="PyDisplay displaysys-busdisplay", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaysys-busdisplay", +) +require("displaysys") +package("displaysys") diff --git a/micropython/pydisplay/displaysys/displaysys-fbdisplay/displaysys/fbdisplay.py b/micropython/pydisplay/displaysys/displaysys-fbdisplay/displaysys/fbdisplay.py new file mode 100644 index 000000000..056d2db0b --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-fbdisplay/displaysys/fbdisplay.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +displaysys.fbdisplay +""" + +from displaysys import DisplayDriver + + +class FBDisplay(DisplayDriver): + """ + A class to interface with CircuitPython FrameBuffer objects. + + Args: + buffer (FrameBuffer): The CircuitPython FrameBuffer object. + width (int, optional): The width of the display. Defaults to None. + height (int, optional): The height of the display. Defaults to None. + reverse_bytes_in_word (bool, optional): Whether to reverse the bytes in a word. Defaults to False. + + Attributes: + color_depth (int): The color depth of the display + """ + + def __init__(self, buffer, width=None, height=None, reverse_bytes_in_word=False): + self._raw_buffer = buffer + self._buffer = memoryview(buffer) + self._width = width if width else buffer.width + self._height = height if height else buffer.height + self._requires_byteswap = reverse_bytes_in_word + self._rotation = 0 + self.color_depth = 16 + + super().__init__() + + ############### Required API Methods ################ + + def init(self) -> None: + """ + Initializes the display instance. Called by __init__ and rotation setter. + """ + + def fill_rect(self, x, y, w, h, c): + """ + Fills a rectangle with the given color. + + Args: + x (int): The x-coordinate of the top-left corner of the rectangle. + y (int): The y-coordinate of the top-left corner of the rectangle. + w (int): The width of the rectangle. + h (int): The height of the rectangle. + c (int): The color to fill the rectangle with. + + Returns: + (tuple): A tuple containing the x, y, w, h values + """ + BPP = self.color_depth // 8 + if self._auto_byteswap: + color_bytes = (c & 0xFFFF).to_bytes(2, "big") + else: + color_bytes = (c & 0xFFFF).to_bytes(2, "little") + + for _y in range(y, y + h): + begin = (_y * self.width + x) * BPP + end = begin + w * BPP + self._buffer[begin:end] = color_bytes * w + return (x, y, w, h) + + def blit_rect(self, buf, x, y, w, h): + """ + Blits a buffer to the display at the given coordinates. + + Args: + buf (memoryview): The buffer to blit. + x (int): The x-coordinate of the buffer. + y (int): The y-coordinate of the buffer. + w (int): The width of the buffer. + h (int): The height of the buffer. + + Returns: + (tuple): A tuple containing the x, y, w, h values. + """ + if self._auto_byteswap: + self.byteswap(buf) + + BPP = self.color_depth // 8 + if x < 0 or y < 0 or x + w > self.width or y + h > self.height: + raise ValueError("The provided x, y, w, h values are out of range") + if len(buf) != w * h * BPP: + raise ValueError("The source buffer is not the correct size") + for row in range(h): + source_begin = row * w * BPP + source_end = source_begin + w * BPP + dest_begin = ((y + row) * self.width + x) * BPP + dest_end = dest_begin + w * BPP + self._buffer[dest_begin:dest_end] = buf[source_begin:source_end] + return (x, y, w, h) + + def pixel(self, x, y, c): + """ + Sets the color of the pixel at the given coordinates. + + Args: + x (int): The x-coordinate of the pixel. + y (int): The y-coordinate of the pixel. + c (int): The color of the pixel. + + Returns: + (tuple): A tuple containing the x, y values. + """ + return self.fill_rect(x, y, 1, 1, c) + + ############### Optional API Methods ################ + + def show(self) -> None: + """ + Refreshes the display. + """ + self._raw_buffer.refresh() diff --git a/micropython/pydisplay/displaysys/displaysys-fbdisplay/examples/board_config.py b/micropython/pydisplay/displaysys/displaysys-fbdisplay/examples/board_config.py new file mode 100644 index 000000000..fa3e915f6 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-fbdisplay/examples/board_config.py @@ -0,0 +1,97 @@ +"""Qualia S3 RGB-666 with TL040HDS20 4.0" 720x720 Square Display""" +# Similar configs may be available for RGBMatrix, is31fl3741 and picodvi + +from rgbframebuffer import RGBFrameBuffer +from machine import I2C, Pin +from pca9554 import PCA9554 +from ft6x36 import FT6x36 +from displaysys.fbdisplay import FBDisplay +from eventsys import devices + + +def send_init_sequence(init_sequence, mosi, sck, cs): + cs(0) + for byte in init_sequence: + for _ in range(8): + mosi(byte & 0x80) + sck(1) + byte <<= 1 + sck(0) + cs(1) + + +tft_pins = { + "de": 17, + "vsync": 3, + "hsync": 46, + "dclk": 9, + "red": (1, 2, 42, 41, 40), + "green": (21, 47, 48, 45, 38, 39), + "blue": (10, 11, 12, 13, 14), +} + +tft_timings = { + "frequency": 16_000_000, + "width": 720, + "height": 720, + "hsync_pulse_width": 2, + "hsync_front_porch": 46, + "hsync_back_porch": 44, + "vsync_pulse_width": 2, + "vsync_front_porch": 16, + "vsync_back_porch": 18, + "hsync_idle_low": False, + "vsync_idle_low": False, + "de_idle_high": False, + "pclk_active_high": False, + "pclk_idle_high": False, +} + +init_sequence = bytes() + +i2c = I2C(0, sda=Pin(8), scl=Pin(18), freq=100000) +print(f"i2c.scan() = {i2c.scan()}") +iox = PCA9554(i2c, address=0x38) +btn_down = iox.Pin(6, Pin.IN) +btn_up = iox.Pin(5, Pin.IN) +reset = iox.Pin(2, Pin.OUT, value=1) +backlight = iox.Pin(4, Pin.OUT, value=1) + +send_init_sequence( + init_sequence, + mosi=iox.Pin(7, Pin.OUT), + sck=iox.Pin(0, Pin.OUT, value=0), + cs=iox.Pin(1, Pin.OUT, value=1), +) + + +fb = RGBFrameBuffer(**tft_pins, **tft_timings) +mv = memoryview(fb) +# mv is typecode "H" (unsigned short) and we need to fill it with 0x1234 +mv[::] = b"\x34\x12" * (fb.width * fb.height) +fb.refresh() + +touch_drv = FT6x36(i2c, address=0x48) # , irq = iox.Pin(3, Pin.OUT)) + + +def touch_read_func(): + touches = touch_drv.touches + if len(touches): + return touches[0]["x"], touches[0]["y"] + return None + + +# Typical board_config.py setup from here on out + +display_drv = FBDisplay(fb) + +touch_rotation_table = (0, 0, 0, 0) + +broker = devices.Broker() + +touch_dev = broker.create_device( + type=devices.types.TOUCH, + read=touch_read_func, + data=display_drv, + data2=touch_rotation_table, +) diff --git a/micropython/pydisplay/displaysys/displaysys-fbdisplay/manifest.py b/micropython/pydisplay/displaysys/displaysys-fbdisplay/manifest.py new file mode 100644 index 000000000..a7505bcca --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-fbdisplay/manifest.py @@ -0,0 +1,9 @@ +metadata( + description="PyDisplay displaysys-fbdisplay", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaysys-fbdisplay", +) +require("displaysys") +package("displaysys") diff --git a/micropython/pydisplay/displaysys/displaysys-jndisplay/displaysys/jndisplay.py b/micropython/pydisplay/displaysys/displaysys-jndisplay/displaysys/jndisplay.py new file mode 100644 index 000000000..44c25ec85 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-jndisplay/displaysys/jndisplay.py @@ -0,0 +1,123 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +displaysys.jndisplay +""" + +from displaysys import DisplayDriver, color_rgb +from IPython.display import display, update_display +from PIL import Image, ImageDraw + + +class JNDisplay(DisplayDriver): + """ + A class to emulate a display on Jupyter Notebook. + + Args: + width (int): The width of the display. + height (int): The height of the display. + + Attributes: + color_depth (int): The color depth of the display + """ + + _next_display_id = 0 + + def __init__(self, width, height): + self._display_id = f"JNDisplay_{JNDisplay._next_display_id}" + JNDisplay._next_display_id += 1 + self._width = width + self._height = height + self._requires_byteswap = False + self._rotation = 0 + self.color_depth = 16 + self._buffer = Image.new("RGB", (self.width, self.height)) + self._draw = ImageDraw.Draw(self._buffer) + + super().__init__(auto_refresh=True) + + ############### Required API Methods ################ + + def init(self) -> None: + """ + Initializes the display instance. Called by __init__ and rotation setter. + """ + display(self._buffer, display_id=self._display_id) + + def fill_rect(self, x, y, w, h, c): + """ + Fills a rectangle with the given color. + + Args: + x (int): The x-coordinate of the top-left corner of the rectangle. + y (int): The y-coordinate of the top-left corner of the rectangle. + w (int): The width of the rectangle. + h (int): The height of the rectangle. + c (int): The color to fill the rectangle with. + + Returns: + (tuple): A tuple containing the x, y, w, h values + """ + color = c & 0xFFFF + r, g, b = color_rgb(color) + x2 = x + w + y2 = y + h + top = min(y, y2) + left = min(x, x2) + bottom = max(y, y2) + right = max(x, x2) + self._draw.rectangle([(left, top), (right, bottom)], fill=(r, g, b)) + return (x, y, w, h) + + def blit_rect(self, buf, x, y, w, h): + """ + Blits a buffer to the display at the given coordinates. + + Args: + buf (bytearray): The buffer to blit to the display. + x (int): The x-coordinate of the top-left corner of the buffer. + y (int): The y-coordinate of the top-left corner of the buffer. + w (int): The width of the buffer. + h (int): The height of the buffer. + + Returns: + (tuple): A tuple containing the x, y, w, h values. + """ + + BPP = self.color_depth // 8 + if x < 0 or y < 0 or x + w > self.width or y + h > self.height: + raise ValueError("The provided x, y, w, h values are out of range") + if len(buf) != w * h * BPP: + raise ValueError("The source buffer is not the correct size") + + for j in range(h): + for i in range(w): + color = buf[(j * w + i) * BPP : (j * w + i) * BPP + BPP] + self.pixel(x + i, y + j, color) + return (x, y, w, h) + + def pixel(self, x, y, c): + """ + Sets a pixel to the given color. + + Args: + x (int): The x-coordinate of the pixel. + y (int): The y-coordinate of the pixel. + c (int): The color to set the pixel to. + + Returns: + (tuple): A tuple containing the x, y, w and h values. + """ + r, g, b = color_rgb(c) + self._draw.point((x, y), fill=(r, g, b)) + return (x, y, 1, 1) + + ############### Optional API Methods ################ + + def show(self) -> None: + """ + Updates the display with the current buffer. + """ + update_display(self._buffer, display_id=self._display_id) diff --git a/micropython/pydisplay/displaysys/displaysys-jndisplay/examples/board_config.py b/micropython/pydisplay/displaysys/displaysys-jndisplay/examples/board_config.py new file mode 100644 index 000000000..e91b2adef --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-jndisplay/examples/board_config.py @@ -0,0 +1,16 @@ +""" +Board configuration for Jupyter Notebook. +""" + +from displaysys.jndisplay import JNDisplay +from eventsys import devices + + +width = 320 +height = 480 + +broker = devices.Broker() + +display_drv = JNDisplay(width, height) + +display_drv.fill(0) diff --git a/micropython/pydisplay/displaysys/displaysys-jndisplay/manifest.py b/micropython/pydisplay/displaysys/displaysys-jndisplay/manifest.py new file mode 100644 index 000000000..c9970edb4 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-jndisplay/manifest.py @@ -0,0 +1,9 @@ +metadata( + description="PyDisplay displaysys-jndisplay", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaysys-jndisplay", +) +require("displaysys") +package("displaysys") diff --git a/micropython/pydisplay/displaysys/displaysys-pgdisplay/displaysys/pgdisplay.py b/micropython/pydisplay/displaysys/displaysys-pgdisplay/displaysys/pgdisplay.py new file mode 100644 index 000000000..2a4fb524c --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-pgdisplay/displaysys/pgdisplay.py @@ -0,0 +1,260 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +displaysys.pgdisplay +""" + +from displaysys import DisplayDriver, color_rgb +import pygame as pg + +try: + from typing import Optional, Union, Sequence +except ImportError: + pass + + +def poll() -> Optional[pg.event.Event]: + """ + Polls for an event and returns the event type and data. + + Returns: + Optional[pg.event.Event | False]: The event type and data. + """ + return pg.event.poll() + + +class PGDisplay(DisplayDriver): + """ + A class to emulate an LCD using pygame. + Provides scrolling and rotation functions similar to an LCD. The .texture + object functions as the LCD's internal memory. + + Args: + width (int, optional): The width of the display. Defaults to 320. + height (int, optional): The height of the display. Defaults to 240. + rotation (int, optional): The rotation of the display. Defaults to 0. + color_depth (int, optional): The color depth of the display. Defaults to 16. + title (str, optional): The title of the display window. Defaults to "displaysys". + scale (float, optional): The scale of the display. Defaults to 1.0. + window_flags (int, optional): The flags for creating the display window. Defaults to pg.SHOWN + + Attributes: + color_depth (int): The color depth of the display. + touch_scale (float): The touch scale of the display. + """ + + def __init__( + self, + width=320, + height=240, + rotation=0, + color_depth=16, + title="displaysys", + scale=1.0, + window_flags=pg.SHOWN, + ): + self._width = width + self._height = height + self._rotation = rotation + self.color_depth = color_depth + self._title = title + self._window_flags = window_flags + self._scale = scale + self.touch_scale = scale + self._buffer = None + self._requires_byteswap = False + + self._bytes_per_pixel = color_depth // 8 + + if self._scale != 1 and not hasattr(pg.transform, "scale_by"): + print( + f"PGDisplay: Scaling is set to {self._scale}, but pygame {pg.ver} does not support it." + ) + self._scale = 1 + + pg.init() + + self._buffer = pg.Surface(size=(self._width, self._height), depth=self.color_depth) + self._buffer.fill((0, 0, 0)) + + super().__init__(auto_refresh=True) + + ############### Required API Methods ################ + + def init(self) -> None: + """ + Initializes the display instance. Called by __init__ and rotation setter. + """ + self._window = pg.display.set_mode( + size=(int(self.width * self._scale), int(self.height * self._scale)), + flags=self._window_flags, + depth=self.color_depth, + display=0, + vsync=0, + ) + pg.display.set_caption(self._title) + + super().vscrdef( + 0, self.height, 0 + ) # Set the vertical scroll definition without calling show + self.vscsad(False) # Scroll offset; set to False to disable scrolling + + def blit_rect(self, buffer: memoryview, x: int, y: int, w: int, h: int): + """ + Blits a buffer to the display. + + Args: + buffer (memoryview): The buffer to blit. + x (int): The x-coordinate of the buffer. + y (int): The y-coordinate of the buffer. + w (int): The width to blit. + h (int): The height to blit. + + Returns: + (tuple): A tuple containing the x, y, w, h values. + """ + + blitRect = pg.Rect(x, y, w, h) + for i in range(h): + for j in range(w): + pixel_index = (i * w + j) * self._bytes_per_pixel + color = color_rgb(buffer[pixel_index : pixel_index + self._bytes_per_pixel]) + self._buffer.set_at((x + j, y + i), color) + self.render(blitRect) + return (x, y, w, h) + + def fill_rect(self, x: int, y: int, w: int, h: int, c: int): + """ + Fill a rectangle with a color. + + Renders to the texture instead of directly to the window + to facilitate scrolling and scaling. + + Args: + x (int): The x-coordinate of the rectangle. + y (int): The y-coordinate of the rectangle. + w (int): The width of the rectangle. + h (int): The height of the rectangle. + c (int): The color of the rectangle. + + Returns: + (tuple): A tuple containing the x, y, w, h values. + """ + fillRect = pg.Rect(x, y, w, h) + self._buffer.fill(color_rgb(c), fillRect) + self.render(fillRect) + return (x, y, w, h) + + def pixel(self, x: int, y: int, c: int): + """ + Set a pixel on the display. + + Args: + x (int): The x-coordinate of the pixel. + y (int): The y-coordinate of the pixel. + c (int): The color of the pixel. + + Returns: + (tuple): A tuple containing the x, y, w & h values. + """ + return self.blit_rect(bytearray(c.to_bytes(2, "little")), x, y, 1, 1) + + ############### API Method Overrides ################ + + def vscrdef(self, tfa: int, vsa: int, bfa: int) -> None: + """ + Set the vertical scroll definition. + + Args: + tfa (int): The top fixed area. + vsa (int): The vertical scroll area. + bfa (int): The bottom fixed area. + """ + super().vscrdef(tfa, vsa, bfa) + self.render() + + def vscsad(self, vssa: Optional[int] = None) -> int: + """ + Set the vertical scroll start address. + + Args: + vssa (Optional[int], optional): The vertical scroll start address. Defaults to None. + + Returns: + int: The vertical scroll start address. + """ + if vssa is not None: + super().vscsad(vssa) + self.render() + return self._vssa + + def _rotation_helper(self, value): + """ + Helper function for the rotation setter. + """ + if (angle := (value % 360) - (self._rotation % 360)) != 0: + tempBuffer = pg.transform.rotate(self._buffer, -angle) + self._buffer = tempBuffer + + ############### Class Specific Methods ############## + + def render(self, renderRect: Optional[pg.Rect] = None) -> None: + """ + Render the display. Automatically called after blitting or filling the display. + + Args: + renderRect (Optional[pg.Rect], optional): The rectangle to render. Defaults to None. + """ + s = self._scale + if s != 1: + buffer = pg.transform.scale_by(self._buffer, s) + else: + buffer = self._buffer + if not (y_start := self.vscsad()): + if renderRect is not None: + x, y, w, h = renderRect + renderRect = pg.Rect(x * s, y * s, w * s, h * s) + dest = renderRect + else: + dest = (0, 0) + self._window.blit(buffer, dest, renderRect) + else: + # Ignore renderRect and render the entire buffer to the window in four steps + y_start *= s + tfa = self._tfa * s + vsa = self._vsa * s + bfa = self._bfa * s + width = self.width * s + + if tfa > 0: + tfaRect = pg.Rect(0, 0, width, tfa) + self._window.blit(buffer, tfaRect, tfaRect) + + vsaTopHeight = vsa + tfa - y_start + vsaTopSrcRect = pg.Rect(0, y_start, width, vsaTopHeight) + vsaTopDestRect = pg.Rect(0, tfa, width, vsaTopHeight) + self._window.blit(buffer, vsaTopDestRect, vsaTopSrcRect) + + vsaBtmHeight = vsa - vsaTopHeight + vsaBtmSrcRect = pg.Rect(0, tfa, width, vsaBtmHeight) + vsaBtmDestRect = pg.Rect(0, tfa + vsaTopHeight, width, vsaBtmHeight) + self._window.blit(buffer, vsaBtmDestRect, vsaBtmSrcRect) + + if bfa > 0: + bfaRect = pg.Rect(0, tfa + vsa, width, bfa) + self._window.blit(buffer, bfaRect, bfaRect) + + def show(self, param=None) -> None: + """ + Show the display. + """ + pg.display.flip() + + def deinit(self) -> None: + """ + Deinitializes the pygame instance. + """ + pg.display.quit() + pg.quit() diff --git a/micropython/pydisplay/displaysys/displaysys-pgdisplay/examples/board_config.py b/micropython/pydisplay/displaysys/displaysys-pgdisplay/examples/board_config.py new file mode 100644 index 000000000..c913c137f --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-pgdisplay/examples/board_config.py @@ -0,0 +1,32 @@ +""" +Board configuration for PyGame. +""" + +from displaysys.pgdisplay import PGDisplay as DTDisplay, poll +from eventsys import devices +import sys + + +width = 320 +height = 480 +rotation = 0 +scale = 2.0 + +display_drv = DTDisplay( + width=width, + height=height, + rotation=rotation, + title=f"{sys.implementation.name} on {sys.platform}", + scale=scale, +) + +broker = devices.Broker() + +events_dev = broker.create_device( + type=devices.types.QUEUE, + read=poll, + data=display_drv, + # data2=events.filter, +) + +display_drv.fill(0) diff --git a/micropython/pydisplay/displaysys/displaysys-pgdisplay/manifest.py b/micropython/pydisplay/displaysys/displaysys-pgdisplay/manifest.py new file mode 100644 index 000000000..f171682c5 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-pgdisplay/manifest.py @@ -0,0 +1,9 @@ +metadata( + description="PyDisplay displaysys-pgdisplay", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaysys-pgdisplay", +) +require("displaysys") +package("displaysys") diff --git a/micropython/pydisplay/displaysys/displaysys-psdisplay/displaysys/psdisplay.py b/micropython/pydisplay/displaysys/displaysys-psdisplay/displaysys/psdisplay.py new file mode 100644 index 000000000..038cebe3c --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-psdisplay/displaysys/psdisplay.py @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +displaysys.psdisplay +""" + +from displaysys import DisplayDriver, color_rgb +from pyscript.ffi import create_proxy +from js import document, console + + +def log(*args): + console.log(*args) + + +class PSDevices: + """ + A class to emulate a display on PyScript. + + Args: + id (str): The id of the canvas element. + """ + + def __init__(self, id): + self.canvas = document.getElementById(id) + self._mouse_pos = None + + # self.canvas.oncontextmenu = self._no_context + + # Proxy functions are required for javascript + self.on_down = create_proxy(self._on_down) + self.on_up = create_proxy(self._on_up) + self.on_move = create_proxy(self._on_move) + self.on_enter = create_proxy(self._on_enter) + self.on_leave = create_proxy(self._on_leave) + + self.canvas.addEventListener("mousedown", self.on_down) + self.canvas.addEventListener("mouseup", self.on_up) + self.canvas.addEventListener("mousemove", self.on_move) + self.canvas.addEventListener("mouseenter", self.on_enter) + self.canvas.addEventListener("mouseleave", self.on_leave) + + def get_mouse_pos(self) -> tuple | None: + """ + Returns the current mouse position. + + Returns: + tuple or None: The x, y coordinates of the mouse position. + """ + return self._mouse_pos + + def _on_down(self, e): + if e.button == 0: # left mouse button + log(f"Mouse down {e.offsetX}, {e.offsetY}") + self._mouse_pos = (e.offsetX, e.offsetY) + else: + return False + + def _on_up(self, e): + if e.button == 0: # left mouse button + log(f"Mouse up {e.offsetX}, {e.offsetY}") + self._mouse_pos = None + else: + return False + + def _on_move(self, e): + if e.buttons & 1: + log(f"Mouse move {e.offsetX}, {e.offsetY}") + self._mouse_pos = (e.offsetX, e.offsetY) + + def _on_enter(self, e): + log("Mouse enter") + + def _on_leave(self, e): + log("Mouse leave") + self._mouse_pos = None + + def _no_context(self, e): + e.preventDefault() + e.stopPropagation() + return False + + +class PSDisplay(DisplayDriver): + """ + A class to emulate a display on PyScript. + + Args: + id (str): The id of the canvas element. + width (int, optional): The width of the display. Defaults to None. + height (int, optional): The height of the display. Defaults to None. + """ + + def __init__(self, id, width=None, height=None): + self._canvas = document.getElementById(id) + self._ctx = self._canvas.getContext("2d") + self._width = width or self._canvas.width + self._height = height or self._canvas.height + self._requires_byteswap = False + self._rotation = 0 + self.color_depth = 16 + self._draw = self._ctx + + super().__init__() + + ############### Required API Methods ################ + + def init(self) -> None: + """ + Initializes the display instance. Called by __init__ and rotation setter. + """ + self._canvas.width = self.width + self._canvas.height = self.height + + def fill_rect(self, x, y, w, h, c): + """ + Fills a rectangle with the given color. + + Args: + x (int): The x-coordinate of the top-left corner of the rectangle. + y (int): The y-coordinate of the top-left corner of the rectangle. + w (int): The width of the rectangle. + h (int): The height of the rectangle. + c (int): The color to fill the rectangle with. + + Returns: + (tuple): A tuple containing the x, y, w, h values + """ + r, g, b = color_rgb(c) + self._ctx.fillStyle = f"rgb({r},{g},{b})" + self._ctx.fillRect(x, y, w, h) + return (x, y, w, h) + + def blit_rect(self, buf, x, y, w, h): + """ + Blits a buffer to the display. + + Args: + buf (bytearray): The buffer to blit. + x (int): The x-coordinate of the top-left corner of the buffer. + y (int): The y-coordinate of the top-left corner of the buffer. + w (int): The width of the buffer. + h (int): The height of the buffer. + + Returns: + (tuple): A tuple containing the x, y, w, h values + """ + BPP = self.color_depth // 8 + if x < 0 or y < 0 or x + w > self.width or y + h > self.height: + raise ValueError("The provided x, y, w, h values are out of range") + if len(buf) != w * h * BPP: + raise ValueError("The source buffer is not the correct size") + img_data = self._ctx.createImageData(w, h) + for i in range(0, len(buf), BPP): + r, g, b = color_rgb(buf[i : i + BPP]) + j = i * 2 + img_data.data[j] = r + img_data.data[j + 1] = g + img_data.data[j + 2] = b + img_data.data[j + 3] = 255 + self._ctx.putImageData(img_data, x, y) + return (x, y, w, h) + + def pixel(self, x, y, c): + """ + Sets a pixel to the given color. + + Args: + x (int): The x-coordinate of the pixel. + y (int): The y-coordinate of the pixel. + c (int): The color to set the pixel to. + + Returns: + (tuple): A tuple containing the x, y, w & h values. + """ + return self.fill_rect(x, y, 1, 1, c) diff --git a/micropython/pydisplay/displaysys/displaysys-psdisplay/examples/board_config.py b/micropython/pydisplay/displaysys/displaysys-psdisplay/examples/board_config.py new file mode 100644 index 000000000..04fc367b9 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-psdisplay/examples/board_config.py @@ -0,0 +1,23 @@ +""" +Board configuration for PyScript. +""" + +from displaysys.psdisplay import PSDisplay, PSDevices +from eventsys import devices + +width = 320 +height = 480 + +display_drv = PSDisplay("display_canvas", width, height) + +broker = devices.Broker() + +touch_drv = PSDevices("display_canvas") + +touch_dev = broker.create_device( + type=devices.types.TOUCH, + read=touch_drv.get_mouse_pos, + data=display_drv, +) + +display_drv.fill(0) diff --git a/micropython/pydisplay/displaysys/displaysys-psdisplay/manifest.py b/micropython/pydisplay/displaysys/displaysys-psdisplay/manifest.py new file mode 100644 index 000000000..03bd8e795 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-psdisplay/manifest.py @@ -0,0 +1,9 @@ +metadata( + description="PyDisplay displaysys-psdisplay", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaysys-psdisplay", +) +require("displaysys") +package("displaysys") diff --git a/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/__init__.py b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/__init__.py new file mode 100644 index 000000000..1cec1d50a --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/__init__.py @@ -0,0 +1,470 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +displaysys.sdldisplay +""" + +from displaysys import DisplayDriver, color_rgb +from eventsys import events +from sys import implementation +from micropython import schedule +from ._sdl2_lib import ( + SDL_Init, + SDL_Quit, + SDL_GetError, + SDL_CreateWindow, + SDL_CreateRenderer, + SDL_DestroyWindow, + SDL_DestroyRenderer, + SDL_DestroyTexture, + SDL_SetRenderDrawColor, + SDL_RenderPresent, + SDL_RenderSetLogicalSize, + SDL_SetWindowSize, + SDL_RenderCopyEx, + SDL_SetRenderTarget, + SDL_SetTextureBlendMode, + SDL_RenderFillRect, + SDL_RenderCopy, + SDL_UpdateTexture, + SDL_CreateTexture, + SDL_PIXELFORMAT_ARGB8888, + SDL_PIXELFORMAT_RGB888, + SDL_PIXELFORMAT_RGB565, + SDL_TEXTUREACCESS_TARGET, + SDL_BLENDMODE_NONE, + SDL_RENDERER_ACCELERATED, + SDL_RENDERER_PRESENTVSYNC, + SDL_WINDOWPOS_CENTERED, + SDL_WINDOW_SHOWN, + SDL_INIT_EVERYTHING, + SDL_Rect, + SDL_PollEvent, + SDL_GetKeyName, + SDL_Event, + SDL_QUIT, + SDL_BUTTON_LMASK, + SDL_BUTTON_MMASK, + SDL_BUTTON_RMASK, + SDL_MOUSEBUTTONDOWN, + SDL_MOUSEBUTTONUP, + SDL_MOUSEMOTION, + SDL_MOUSEWHEEL, + SDL_KEYDOWN, + SDL_KEYUP, +) + +try: + from typing import Optional, Union, Sequence +except ImportError: + pass + +if implementation.name == "cpython": + import ctypes + + is_cpython = True +else: + is_cpython = False + +try: + from time import ticks_ms, ticks_add +except ImportError: + from adafruit_ticks import ticks_ms, ticks_add + + +def scheduler(param): + func, next_run, interval = param + if (current_time := ticks_ms()) >= next_run: + interval = func(interval) + next_run = ticks_add(current_time, interval) + if interval > 0: + schedule(scheduler, (func, next_run, interval)) + + +_event = SDL_Event() + + +def poll() -> Optional[events]: + """ + Polls for an event and returns the event type and data. + + Returns: + Optional[events]: The event type and data. + """ + global _event + if SDL_PollEvent(_event): + if is_cpython: + if _event.type in events.filter: + return _convert(SDL_Event(_event)) + else: + if int.from_bytes(_event[:4], "little") in events.filter: + return _convert(SDL_Event(_event)) + return None + +def _convert(e): + # Convert an SDL event to a Pygame event + if e.type == SDL_MOUSEMOTION: + l = 1 if e.motion.state & SDL_BUTTON_LMASK else 0 # noqa: E741 + m = 1 if e.motion.state & SDL_BUTTON_MMASK else 0 + r = 1 if e.motion.state & SDL_BUTTON_RMASK else 0 + evt = events.Motion( + e.type, + (e.motion.x, e.motion.y), + (e.motion.xrel, e.motion.yrel), + (l, m, r), + e.motion.which != 0, + e.motion.windowID, + ) + elif e.type in (SDL_MOUSEBUTTONDOWN, SDL_MOUSEBUTTONUP): + evt = events.Button( + e.type, + (e.button.x, e.button.y), + e.button.button, + e.button.which != 0, + e.button.windowID, + ) + elif e.type == SDL_MOUSEWHEEL: + evt = events.Wheel( + e.type, + e.wheel.direction != 0, + e.wheel.x, + e.wheel.y, + e.wheel.preciseX, + e.wheel.preciseY, + e.wheel.which != 0, + e.wheel.windowID, + ) + elif e.type in (SDL_KEYDOWN, SDL_KEYUP): + name = SDL_GetKeyName(e.key.keysym.sym) + evt = events.Key( + e.type, + name, + e.key.keysym.sym, + e.key.keysym.mod, + e.key.keysym.scancode, + e.key.windowID, + ) + elif e.type == SDL_QUIT: + evt = events.Quit(e.type) + else: + evt = events.Unknown(e.type) + return evt + +def retcheck(retvalue): + # Check the return value of an SDL function and raise an exception if it's not 0 + if retvalue: + raise RuntimeError(SDL_GetError()) + + +class SDLDisplay(DisplayDriver): + """ + A class to emulate an LCD using SDL2. + Provides scrolling and rotation functions similar to an LCD. The .texture + object functions as the LCD's internal memory. + + Args: + width (int, optional): The width of the display. Defaults to 320. + height (int, optional): The height of the display. Defaults to 240. + rotation (int, optional): The rotation of the display. Defaults to 0. + color_depth (int, optional): The color depth of the display. Defaults to 16. + title (str, optional): The title of the display window. Defaults to "SDL2 Display". + scale (float, optional): The scale of the display. Defaults to 1.0. + window_flags (int, optional): The flags for creating the display window. Defaults to SDL_WINDOW_SHOWN. + render_flags (int, optional): The flags for creating the renderer. Defaults to SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC. + x (int, optional): The x-coordinate of the display window's position. Defaults to SDL_WINDOWPOS_CENTERED. + y (int, optional): The y-coordinate of the display window's position. Defaults to SDL_WINDOWPOS_CENTERED. + """ + + def __init__( + self, + width=320, + height=240, + rotation=0, + color_depth=16, + title="SDL2 Display", + scale=1.0, + window_flags=SDL_WINDOW_SHOWN, + render_flags=SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC, + x=SDL_WINDOWPOS_CENTERED, + y=SDL_WINDOWPOS_CENTERED, + ): + self._width = width + self._height = height + self._rotation = rotation + self.color_depth = color_depth + self._title = title + self._window_flags = window_flags + self._scale = scale + self._buffer = None + self._requires_byteswap = False + + # Determine the pixel format + if color_depth == 32: + self._px_format = SDL_PIXELFORMAT_ARGB8888 + elif color_depth == 24: + self._px_format = SDL_PIXELFORMAT_RGB888 + elif color_depth == 16: + self._px_format = SDL_PIXELFORMAT_RGB565 + else: + raise ValueError("Unsupported color_depth") + + retcheck(SDL_Init(SDL_INIT_EVERYTHING)) + self._window = SDL_CreateWindow( + self._title.encode(), + x, + y, + int(self.width * self._scale), + int(self.height * self._scale), + self._window_flags, + ) + if not self._window: + raise RuntimeError(f"{SDL_GetError()}") + self._renderer = SDL_CreateRenderer(self._window, -1, render_flags) + if not self._renderer: + raise RuntimeError(f"{SDL_GetError()}") + + self._buffer = SDL_CreateTexture( + self._renderer, + self._px_format, + SDL_TEXTUREACCESS_TARGET, + self.width, + self.height, + ) + if not self._buffer: + raise RuntimeError(f"{SDL_GetError()}") + retcheck(SDL_SetTextureBlendMode(self._buffer, SDL_BLENDMODE_NONE)) + + super().__init__(auto_refresh=True) + + ############### Required API Methods ################ + + def init(self) -> None: + """ + Initializes the display instance. Called by __init__ and rotation setter. + """ + retcheck( + SDL_SetWindowSize( + self._window, + int(self.width * self._scale), + int(self.height * self._scale), + ) + ) + retcheck(SDL_RenderSetLogicalSize(self._renderer, self.width, self.height)) + + super().vscrdef( + 0, self.height, 0 + ) # Set the vertical scroll definition without calling .render() + self.vscsad(False) # Scroll offset; set to False to disable scrolling + + def blit_rect(self, buffer: memoryview, x: int, y: int, w: int, h: int): + """ + Blits a buffer to the display. + + Args: + buffer (memoryview): The buffer to blit. + x (int): The x-coordinate of the buffer. + y (int): The y-coordinate of the buffer. + w (int): The width to blit. + h (int): The height to blit. + + Returns: + (tuple): A tuple containing the x, y, w, h values. + """ + pitch = int(w * self.color_depth // 8) + if len(buffer) != pitch * h: + raise ValueError("Buffer size does not match dimensions") + blitRect = SDL_Rect(x, y, w, h) + if is_cpython: + if isinstance(buffer, memoryview): + buffer_array = (ctypes.c_ubyte * len(buffer.obj)).from_buffer(buffer.obj) + elif type(buffer) is bytearray: + buffer_array = (ctypes.c_ubyte * len(buffer)).from_buffer(buffer) + else: + raise ValueError( + f"Buffer is of type {type(buffer)} instead of memoryview or bytearray" + ) + buffer_ptr = ctypes.c_void_p(ctypes.addressof(buffer_array)) + retcheck(SDL_UpdateTexture(self._buffer, blitRect, buffer_ptr, pitch)) + else: + retcheck(SDL_UpdateTexture(self._buffer, blitRect, buffer, pitch)) + self.render(blitRect) + return (x, y, w, h) + + def fill_rect(self, x: int, y: int, w: int, h: int, c: int): + """ + Fill a rectangle with a color. + + Renders to the texture instead of directly to the window + to facilitate scrolling and scaling. + + Args: + x (int): The x-coordinate of the rectangle. + y (int): The y-coordinate of the rectangle. + w (int): The width of the rectangle. + h (int): The height of the rectangle. + c (int): The color of the rectangle. + + Returns: + (tuple): A tuple containing the x, y, w, h values + """ + fillRect = SDL_Rect(x, y, w, h) + r, g, b = color_rgb(c) + + retcheck( + SDL_SetRenderTarget(self._renderer, self._buffer) + ) # Set the render target to the texture + retcheck( + SDL_SetRenderDrawColor(self._renderer, r, g, b, 255) + ) # Set the color to fill the rectangle + retcheck(SDL_RenderFillRect(self._renderer, fillRect)) # Fill the rectangle on the texture + retcheck( + SDL_SetRenderTarget(self._renderer, None) + ) # Reset the render target back to the window + self.render(fillRect) + return (x, y, w, h) + + def pixel(self, x: int, y: int, c: int): + """ + Set a pixel on the display. + + Args: + x (int): The x-coordinate of the pixel. + y (int): The y-coordinate of the pixel. + c (int): The color of the pixel. + + Returns: + (tuple): A tuple containing the x, y values. + """ + return self.blit_rect(bytearray(c.to_bytes(2, "little")), x, y, 1, 1) + + ############### API Method Overrides ################ + + def vscrdef(self, tfa: int, vsa: int, bfa: int) -> None: + """ + Set the vertical scroll definition. + + Args: + tfa (int): The top fixed area. + vsa (int): The vertical scroll area. + bfa (int): The bottom fixed area. + """ + super().vscrdef(tfa, vsa, bfa) + self.render() + + def vscsad(self, vssa: Optional[int] = None) -> int: + """ + Set or get the vertical scroll start address. + + Args: + vssa (int): The vertical scroll start address. Defaults to None. + + Returns: + int: The vertical scroll start address. + """ + if vssa is not None: + super().vscsad(vssa) + self.render() + return self._vssa + + def _rotation_helper(self, value): + """ + Creates a new texture to use as the buffer and copies the old one, + applying rotation with SDL_RenderCopyEx. Destroys the old buffer. + + Args: + value (int): The new rotation value. + """ + + print("here") + if (angle := (value % 360) - (self._rotation % 360)) != 0: + if implementation.name == "cpython": + tempBuffer = SDL_CreateTexture( + self._renderer, + self._px_format, + SDL_TEXTUREACCESS_TARGET, + self.height, + self.width, + ) + if not tempBuffer: + raise RuntimeError(f"{SDL_GetError()}") + + retcheck(SDL_SetTextureBlendMode(tempBuffer, SDL_BLENDMODE_NONE)) + retcheck(SDL_SetRenderTarget(self._renderer, tempBuffer)) + if abs(angle) != 180: + dstrect = SDL_Rect( + (self.height - self.width) // 2, + (self.width - self.height) // 2, + self.width, + self.height, + ) + else: + dstrect = None + retcheck( + SDL_RenderCopyEx(self._renderer, self._buffer, None, dstrect, angle, None, 0) + ) + retcheck(SDL_SetRenderTarget(self._renderer, None)) + retcheck(SDL_DestroyTexture(self._buffer)) + self._buffer = tempBuffer + else: + retcheck(SDL_DestroyTexture(self._buffer)) + self._buffer = SDL_CreateTexture( + self._renderer, + self._px_format, + SDL_TEXTUREACCESS_TARGET, + self.height, + self.width, + ) + if not self._buffer: + raise RuntimeError(f"{SDL_GetError()}") + retcheck(SDL_SetTextureBlendMode(self._buffer, SDL_BLENDMODE_NONE)) + + ############### Class Specific Methods ############## + + def render(self, renderRect: Optional[SDL_Rect] = None): + """ + Render the display. Automatically called after blitting or filling the display. + + Args: + renderRect (Optional[SDL_Rect], optional): The rectangle to render. Defaults to None. + """ + # if (y_start := self.vscsad()) == False: + if False: + # The following line is not working on Chromebooks, Ubuntu and Raspberry Pi OS + retcheck(SDL_RenderCopy(self._renderer, self._buffer, renderRect, renderRect)) + else: + # Ignore renderRect and render the entire texture to the window in four steps + y_start = self.vscsad() + if self._tfa > 0: + tfaRect = SDL_Rect(0, 0, self.width, self._tfa) + retcheck(SDL_RenderCopy(self._renderer, self._buffer, tfaRect, tfaRect)) + + vsaTopHeight = self._vsa + self._tfa - y_start + vsaTopSrcRect = SDL_Rect(0, y_start, self.width, vsaTopHeight) + vsaTopDestRect = SDL_Rect(0, self._tfa, self.width, vsaTopHeight) + retcheck(SDL_RenderCopy(self._renderer, self._buffer, vsaTopSrcRect, vsaTopDestRect)) + + vsaBtmHeight = self._vsa - vsaTopHeight + vsaBtmSrcRect = SDL_Rect(0, self._tfa, self.width, vsaBtmHeight) + vsaBtmDestRect = SDL_Rect(0, self._tfa + vsaTopHeight, self.width, vsaBtmHeight) + retcheck(SDL_RenderCopy(self._renderer, self._buffer, vsaBtmSrcRect, vsaBtmDestRect)) + + if self._bfa > 0: + bfaRect = SDL_Rect(0, self._tfa + self._vsa, self.width, self._bfa) + retcheck(SDL_RenderCopy(self._renderer, self._buffer, bfaRect, bfaRect)) + + def show(self) -> None: + """ + Show the display. + """ + SDL_RenderPresent(self._renderer) + + def deinit(self) -> None: + """ + Deinitializes the sdl2lcd instance. + """ + retcheck(SDL_DestroyTexture(self._buffer)) + retcheck(SDL_DestroyRenderer(self._renderer)) + retcheck(SDL_DestroyWindow(self._window)) + retcheck(SDL_Quit()) diff --git a/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/__init__.py b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/__init__.py new file mode 100644 index 000000000..65a9e29d3 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/__init__.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +This module provides the SDL2 library implementation for MicroPython and CPython. + +The module checks the implementation name and imports the appropriate module +based on whether the current Python implementation is MicroPython or CPython. +""" + +from sys import implementation + +if implementation.name == "micropython": + from ._micropython import * # noqa: F403 +else: + from ._cpython import * # noqa: F403 diff --git a/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_constants.py b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_constants.py new file mode 100644 index 000000000..a7200073d --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_constants.py @@ -0,0 +1,246 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +from micropython import const + + +############################################################################### +# SDL2 Constants # +############################################################################### + +# SDL_WindowPos values +SDL_WINDOWPOS_UNDEFINED = const(0x1FFF0000) +SDL_WINDOWPOS_CENTERED = const(0x2FFF0000) + +# SDL_Window flags +SDL_WINDOW_FULLSCREEN = const(0x00000001) +SDL_WINDOW_OPENGL = const(0x00000002) +SDL_WINDOW_SHOWN = const(0x00000004) +SDL_WINDOW_HIDDEN = const(0x00000008) +SDL_WINDOW_BORDERLESS = const(0x00000010) +SDL_WINDOW_RESIZABLE = const(0x00000020) +SDL_WINDOW_MINIMIZED = const(0x00000040) +SDL_WINDOW_MAXIMIZED = const(0x00000080) +SDL_WINDOW_INPUT_GRABBED = const(0x00000100) +SDL_WINDOW_INPUT_FOCUS = const(0x00000200) +SDL_WINDOW_MOUSE_FOCUS = const(0x00000400) +SDL_WINDOW_FULLSCREEN_DESKTOP = const(0x00001001) +SDL_WINDOW_ALLOW_HIGHDPI = const(0x00002000) +SDL_WINDOW_MOUSE_CAPTURE = const(0x00004000) +SDL_WINDOW_ALWAYS_ON_TOP = const(0x00008000) +SDL_WINDOW_SKIP_TASKBAR = const(0x00010000) +SDL_WINDOW_UTILITY = const(0x00020000) +SDL_WINDOW_TOOLTIP = const(0x00040000) +SDL_WINDOW_POPUP_MENU = const(0x00080000) +SDL_WINDOW_VULKAN = const(0x10000000) + +# SDL_Renderer flags +SDL_RENDERER_SOFTWARE = const(0x00000001) +SDL_RENDERER_ACCELERATED = const(0x00000002) +SDL_RENDERER_PRESENTVSYNC = const(0x00000004) +SDL_RENDERER_TARGETTEXTURE = const(0x00000008) + +# SDL_Init flags +SDL_INIT_TIMER = const(0x00000001) +SDL_INIT_AUDIO = const(0x00000010) +SDL_INIT_VIDEO = const(0x00000020) +SDL_INIT_JOYSTICK = const(0x00000200) +SDL_INIT_HAPTIC = const(0x00001000) +SDL_INIT_GAMECONTROLLER = const(0x00002000) +SDL_INIT_EVENTS = const(0x00004000) +SDL_INIT_EVERYTHING = const(0x0000000F) +SDL_INIT_NOPARACHUTE = const(0x00100000) + +# SDL_Texture values +SDL_TEXTUREACCESS_STATIC = const(0) +SDL_TEXTUREACCESS_STREAMING = const(1) +SDL_TEXTUREACCESS_TARGET = const(2) + +# SDL_BlendMode values +SDL_BLENDMODE_NONE = const(1) +SDL_BLENDMODE_BLEND = const(2) +SDL_BLENDMODE_ADD = const(3) +SDL_BLENDMODE_MOD = const(4) +SDL_BLENDMODE_MUL = const(5) + +# SDL_Event types (not complete) +SDL_QUIT = const(0x100) # User clicked the window close button +SDL_KEYDOWN = const(0x300) # Key pressed +SDL_KEYUP = const(0x301) # Key released +SDL_MOUSEMOTION = const(0x400) # Mouse moved +SDL_MOUSEBUTTONDOWN = const(0x401) # Mouse button pressed +SDL_MOUSEBUTTONUP = const(0x402) # Mouse button released +SDL_MOUSEWHEEL = const(0x403) # Mouse wheel motion +SDL_POLLSENTINEL = const(0x7F00) # Signals the end of an event poll cycle + +# SDL_MouseMotionEvent button masks +SDL_BUTTON_LMASK = const(1 << 0) # Left mouse button +SDL_BUTTON_MMASK = const(1 << 1) # Middle mouse button +SDL_BUTTON_RMASK = const(1 << 2) # Right mouse button + + +############################################################################### +# SDL2 Pixel Formats # +############################################################################### + + +def SDL_DEFINE_PIXELFORMAT(type, order, layout, bits, bytes): + """ + Define a pixel format. + """ + return ( + (1 << 28) + | ((type) << 24) + | ((order) << 20) + | ((layout) << 16) + | ((bits) << 8) + | ((bytes) << 0) + ) + + +# SDL_PIXELTYPE values +SDL_PIXELTYPE_UNKNOWN = const(0) +SDL_PIXELTYPE_INDEX1 = const(1) +SDL_PIXELTYPE_INDEX4 = const(2) +SDL_PIXELTYPE_INDEX8 = const(3) +SDL_PIXELTYPE_PACKED8 = const(4) +SDL_PIXELTYPE_PACKED16 = const(5) +SDL_PIXELTYPE_PACKED32 = const(6) +SDL_PIXELTYPE_ARRAYU8 = const(7) +SDL_PIXELTYPE_ARRAYU16 = const(8) +SDL_PIXELTYPE_ARRAYU32 = const(9) +SDL_PIXELTYPE_ARRAYF16 = const(10) +SDL_PIXELTYPE_ARRAYF32 = const(11) + +# SDL_PACKEDORDER values +SDL_PACKEDORDER_NONE = const(0) +SDL_PACKEDORDER_XRGB = const(1) +SDL_PACKEDORDER_RGBX = const(2) +SDL_PACKEDORDER_ARGB = const(3) +SDL_PACKEDORDER_RGBA = const(4) +SDL_PACKEDORDER_XBGR = const(5) +SDL_PACKEDORDER_BGRX = const(6) +SDL_PACKEDORDER_ABGR = const(7) +SDL_PACKEDORDER_BGRA = const(8) + +# SDL_ARRAYORDER values +SDL_ARRAYORDER_NONE = const(0) +SDL_ARRAYORDER_RGB = const(1) +SDL_ARRAYORDER_RGBA = const(2) +SDL_ARRAYORDER_ARGB = const(3) +SDL_ARRAYORDER_BGR = const(4) +SDL_ARRAYORDER_BGRA = const(5) +SDL_ARRAYORDER_ABGR = const(6) + +# SDL_PACKEDLAYOUT values +SDL_PACKEDLAYOUT_NONE = const(0) +SDL_PACKEDLAYOUT_332 = const(1) +SDL_PACKEDLAYOUT_4444 = const(2) +SDL_PACKEDLAYOUT_1555 = const(3) +SDL_PACKEDLAYOUT_5551 = const(4) +SDL_PACKEDLAYOUT_565 = const(5) +SDL_PACKEDLAYOUT_8888 = const(6) +SDL_PACKEDLAYOUT_2101010 = const(7) +SDL_PACKEDLAYOUT_1010102 = const(8) + +# SDL_BITMAPORDER values +SDL_BITMAPORDER_NONE = const(0) +SDL_BITMAPORDER_4321 = const(1) +SDL_BITMAPORDER_1234 = const(2) + +# SDL_PIXELFORMAT values +SDL_PIXELFORMAT_UNKNOWN = const(0) +SDL_PIXELFORMAT_INDEX1LSB = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_INDEX1, SDL_BITMAPORDER_4321, 0, 1, 0 +) +SDL_PIXELFORMAT_INDEX1MSB = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_INDEX1, SDL_BITMAPORDER_1234, 0, 1, 0 +) +SDL_PIXELFORMAT_INDEX4LSB = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_INDEX4, SDL_BITMAPORDER_4321, 0, 4, 0 +) +SDL_PIXELFORMAT_INDEX4MSB = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_INDEX4, SDL_BITMAPORDER_1234, 0, 4, 0 +) +SDL_PIXELFORMAT_INDEX8 = SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_INDEX8, 0, 0, 8, 1) +SDL_PIXELFORMAT_RGB332 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED8, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_332, 8, 1 +) +SDL_PIXELFORMAT_XRGB4444 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_4444, 12, 2 +) +SDL_PIXELFORMAT_RGB444 = SDL_PIXELFORMAT_XRGB4444 +SDL_PIXELFORMAT_XBGR4444 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_4444, 12, 2 +) +SDL_PIXELFORMAT_BGR444 = SDL_PIXELFORMAT_XBGR4444 +SDL_PIXELFORMAT_XRGB1555 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_1555, 15, 2 +) +SDL_PIXELFORMAT_RGB555 = SDL_PIXELFORMAT_XRGB1555 +SDL_PIXELFORMAT_XBGR1555 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_1555, 15, 2 +) +SDL_PIXELFORMAT_BGR555 = SDL_PIXELFORMAT_XBGR1555 +SDL_PIXELFORMAT_ARGB4444 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_4444, 16, 2 +) +SDL_PIXELFORMAT_RGBA4444 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_RGBA, SDL_PACKEDLAYOUT_4444, 16, 2 +) +SDL_PIXELFORMAT_ABGR4444 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ABGR, SDL_PACKEDLAYOUT_4444, 16, 2 +) +SDL_PIXELFORMAT_BGRA4444 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_BGRA, SDL_PACKEDLAYOUT_4444, 16, 2 +) +SDL_PIXELFORMAT_ARGB1555 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_1555, 16, 2 +) +SDL_PIXELFORMAT_RGBA5551 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_RGBA, SDL_PACKEDLAYOUT_5551, 16, 2 +) +SDL_PIXELFORMAT_ABGR1555 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_ABGR, SDL_PACKEDLAYOUT_1555, 16, 2 +) +SDL_PIXELFORMAT_BGRA5551 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_BGRA, SDL_PACKEDLAYOUT_5551, 16, 2 +) +SDL_PIXELFORMAT_RGB565 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_565, 16, 2 +) +SDL_PIXELFORMAT_BGR565 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED16, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_565, 16, 2 +) +SDL_PIXELFORMAT_RGB24 = SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_RGB, 0, 24, 3) +SDL_PIXELFORMAT_BGR24 = SDL_DEFINE_PIXELFORMAT(SDL_PIXELTYPE_ARRAYU8, SDL_ARRAYORDER_BGR, 0, 24, 3) +SDL_PIXELFORMAT_XRGB8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XRGB, SDL_PACKEDLAYOUT_8888, 24, 4 +) +SDL_PIXELFORMAT_RGB888 = SDL_PIXELFORMAT_XRGB8888 +SDL_PIXELFORMAT_RGBX8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBX, SDL_PACKEDLAYOUT_8888, 24, 4 +) +SDL_PIXELFORMAT_XBGR8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_XBGR, SDL_PACKEDLAYOUT_8888, 24, 4 +) +SDL_PIXELFORMAT_BGR888 = SDL_PIXELFORMAT_XBGR8888 +SDL_PIXELFORMAT_BGRX8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRX, SDL_PACKEDLAYOUT_8888, 24, 4 +) +SDL_PIXELFORMAT_ARGB8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_8888, 32, 4 +) +SDL_PIXELFORMAT_RGBA8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_RGBA, SDL_PACKEDLAYOUT_8888, 32, 4 +) +SDL_PIXELFORMAT_ABGR8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ABGR, SDL_PACKEDLAYOUT_8888, 32, 4 +) +SDL_PIXELFORMAT_BGRA8888 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_BGRA, SDL_PACKEDLAYOUT_8888, 32, 4 +) +SDL_PIXELFORMAT_ARGB2101010 = SDL_DEFINE_PIXELFORMAT( + SDL_PIXELTYPE_PACKED32, SDL_PACKEDORDER_ARGB, SDL_PACKEDLAYOUT_2101010, 32, 4 +) diff --git a/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_cpython.py b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_cpython.py new file mode 100644 index 000000000..0b7d39506 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_cpython.py @@ -0,0 +1,304 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +A bare implementation of SDL2 written in CPython using ctypes. +""" + +import ctypes +from ._constants import * # noqa: F403 +from sys import platform + +# Load the SDL2 shared library using ctypes +if platform == "win32": + _libSDL2 = ctypes.CDLL("SDL2.dll") +else: + _libSDL2 = ctypes.CDLL("libSDL2-2.0.so.0") + + +############################################################################### +# SDL2 structs # +############################################################################### + + +class SDL_Rect(ctypes.Structure): + _fields_ = [ + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ("w", ctypes.c_int), + ("h", ctypes.c_int), + ] + + +class SDL_Point(ctypes.Structure): + _fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)] + + +class SDL_CommonEvent(ctypes.Structure): + _fields_ = [ + ("type", ctypes.c_uint), + ("timestamp", ctypes.c_uint), + ("unused", ctypes.c_uint * 12), + ] + + +class SDL_KeyboardEvent(ctypes.Structure): + class Key(ctypes.Structure): + class SDL_Keysym(ctypes.Structure): + _fields_ = [ + ("scancode", ctypes.c_int), + ("sym", ctypes.c_int), + ("mod", ctypes.c_uint16), + ("unused", ctypes.c_uint), + ] + + _fields_ = [ + ("windowID", ctypes.c_uint), + ("state", ctypes.c_uint8), + ("repeat", ctypes.c_uint8), + ("padding2", ctypes.c_uint8), + ("padding3", ctypes.c_uint8), + ("keysym", SDL_Keysym), + ] + + _fields_ = [("type", ctypes.c_uint), ("timestamp", ctypes.c_uint), ("key", Key)] + + +class SDL_MouseMotionEvent(ctypes.Structure): + class Motion(ctypes.Structure): + _fields_ = [ + ("windowID", ctypes.c_uint), + ("which", ctypes.c_uint), + ("state", ctypes.c_uint), + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ("xrel", ctypes.c_int), + ("yrel", ctypes.c_int), + ] + + _fields_ = [ + ("type", ctypes.c_uint), + ("timestamp", ctypes.c_uint), + ("motion", Motion), + ] + + +class SDL_MouseButtonEvent(ctypes.Structure): + class Button(ctypes.Structure): + _fields_ = [ + ("windowID", ctypes.c_uint), + ("which", ctypes.c_uint), + ("button", ctypes.c_uint8), + ("state", ctypes.c_uint8), + ("clicks", ctypes.c_uint8), + ("padding", ctypes.c_uint8), + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ] + + _fields_ = [ + ("type", ctypes.c_uint), + ("timestamp", ctypes.c_uint), + ("button", Button), + ] + + +class SDL_MouseWheelEvent(ctypes.Structure): + class Wheel(ctypes.Structure): + _fields_ = [ + ("windowID", ctypes.c_uint), + ("which", ctypes.c_uint), + ("x", ctypes.c_int), + ("y", ctypes.c_int), + ("direction", ctypes.c_uint), + ("preciseX", ctypes.c_float), + ("preciseY", ctypes.c_float), + ] + + _fields_ = [("type", ctypes.c_uint), ("timestamp", ctypes.c_uint), ("wheel", Wheel)] + + +############################################################################### +# SDL2 functions # +############################################################################### + +# SDL misc functions +_libSDL2.SDL_Init.argtypes = [ctypes.c_uint] +_libSDL2.SDL_Init.restype = ctypes.c_int +SDL_Init = _libSDL2.SDL_Init + +_libSDL2.SDL_Quit.argtypes = [] +_libSDL2.SDL_Quit.restype = None +SDL_Quit = _libSDL2.SDL_Quit + +_libSDL2.SDL_GetError.argtypes = [] +_libSDL2.SDL_GetError.restype = ctypes.c_char_p +SDL_GetError = _libSDL2.SDL_GetError + +_libSDL2.SDL_PollEvent.argtypes = [ctypes.POINTER(SDL_CommonEvent)] +_libSDL2.SDL_PollEvent.restype = ctypes.c_int +SDL_PollEvent = _libSDL2.SDL_PollEvent + +# SDL key functions +_libSDL2.SDL_GetKeyName.argtypes = [ctypes.c_int] +_libSDL2.SDL_GetKeyName.restype = ctypes.c_char_p +SDL_GetKeyName = _libSDL2.SDL_GetKeyName + +_libSDL2.SDL_GetKeyFromName.argtypes = [ctypes.c_char_p] +_libSDL2.SDL_GetKeyFromName.restype = ctypes.c_int +SDL_GetKeyFromName = _libSDL2.SDL_GetKeyFromName + +# SDL window functions +_libSDL2.SDL_CreateWindow.argtypes = [ + ctypes.c_char_p, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ctypes.c_uint, +] +_libSDL2.SDL_CreateWindow.restype = ctypes.c_void_p +SDL_CreateWindow = _libSDL2.SDL_CreateWindow + +_libSDL2.SDL_DestroyWindow.argtypes = [ctypes.c_void_p] +_libSDL2.SDL_DestroyWindow.restype = None +SDL_DestroyWindow = _libSDL2.SDL_DestroyWindow + +_libSDL2.SDL_SetWindowSize.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int] +_libSDL2.SDL_SetWindowSize.restype = None +SDL_SetWindowSize = _libSDL2.SDL_SetWindowSize + +# SDL renderer functions +_libSDL2.SDL_CreateRenderer.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_uint] +_libSDL2.SDL_CreateRenderer.restype = ctypes.c_void_p +SDL_CreateRenderer = _libSDL2.SDL_CreateRenderer + +_libSDL2.SDL_DestroyRenderer.argtypes = [ctypes.c_void_p] +_libSDL2.SDL_DestroyRenderer.restype = None +SDL_DestroyRenderer = _libSDL2.SDL_DestroyRenderer + +_libSDL2.SDL_SetRenderDrawColor.argtypes = [ + ctypes.c_void_p, + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, + ctypes.c_uint, +] +_libSDL2.SDL_SetRenderDrawColor.restype = ctypes.c_int +SDL_SetRenderDrawColor = _libSDL2.SDL_SetRenderDrawColor + +_libSDL2.SDL_SetRenderTarget.argtypes = [ctypes.c_void_p, ctypes.c_void_p] +_libSDL2.SDL_SetRenderTarget.restype = ctypes.c_int +SDL_SetRenderTarget = _libSDL2.SDL_SetRenderTarget + +_libSDL2.SDL_RenderClear.argtypes = [ctypes.c_void_p] +_libSDL2.SDL_RenderClear.restype = ctypes.c_int +SDL_RenderClear = _libSDL2.SDL_RenderClear + +_libSDL2.SDL_RenderCopy.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(SDL_Rect), + ctypes.POINTER(SDL_Rect), +] +_libSDL2.SDL_RenderCopy.restype = ctypes.c_int +SDL_RenderCopy = _libSDL2.SDL_RenderCopy + +_libSDL2.SDL_RenderCopyEx.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.POINTER(SDL_Rect), + ctypes.POINTER(SDL_Rect), + ctypes.c_double, + ctypes.POINTER(SDL_Point), + ctypes.c_int, +] +_libSDL2.SDL_RenderCopyEx.restype = ctypes.c_int +SDL_RenderCopyEx = _libSDL2.SDL_RenderCopyEx + +_libSDL2.SDL_RenderPresent.argtypes = [ctypes.c_void_p] +_libSDL2.SDL_RenderPresent.restype = None +SDL_RenderPresent = _libSDL2.SDL_RenderPresent + +_libSDL2.SDL_RenderFillRect.argtypes = [ctypes.c_void_p, ctypes.POINTER(SDL_Rect)] +_libSDL2.SDL_RenderFillRect.restype = ctypes.c_int +SDL_RenderFillRect = _libSDL2.SDL_RenderFillRect + +_libSDL2.SDL_RenderSetLogicalSize.argtypes = [ + ctypes.c_void_p, + ctypes.c_int, + ctypes.c_int, +] +_libSDL2.SDL_RenderSetLogicalSize.restype = ctypes.c_int +SDL_RenderSetLogicalSize = _libSDL2.SDL_RenderSetLogicalSize + +# SDL texture functions +_libSDL2.SDL_CreateTexture.argtypes = [ + ctypes.c_void_p, + ctypes.c_uint, + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, +] +_libSDL2.SDL_CreateTexture.restype = ctypes.c_void_p +SDL_CreateTexture = _libSDL2.SDL_CreateTexture + +_libSDL2.SDL_DestroyTexture.argtypes = [ctypes.c_void_p] +_libSDL2.SDL_DestroyTexture.restype = None +SDL_DestroyTexture = _libSDL2.SDL_DestroyTexture + +_libSDL2.SDL_SetTextureBlendMode.argtypes = [ctypes.c_void_p, ctypes.c_int] +_libSDL2.SDL_SetTextureBlendMode.restype = ctypes.c_int +SDL_SetTextureBlendMode = _libSDL2.SDL_SetTextureBlendMode + +_libSDL2.SDL_UpdateTexture.argtypes = [ + ctypes.c_void_p, + ctypes.POINTER(SDL_Rect), + ctypes.c_void_p, + ctypes.c_int, +] +_libSDL2.SDL_UpdateTexture.restype = ctypes.c_int +SDL_UpdateTexture = _libSDL2.SDL_UpdateTexture + +# SDL timer functions +_libSDL2.SDL_AddTimer.argtypes = [ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p] +_libSDL2.SDL_AddTimer.restype = ctypes.c_void_p +SDL_AddTimer = _libSDL2.SDL_AddTimer + +_libSDL2.SDL_RemoveTimer.argtypes = [ctypes.c_void_p] +_libSDL2.SDL_RemoveTimer.restype = ctypes.c_int +SDL_RemoveTimer = _libSDL2.SDL_RemoveTimer + +SDL_TimerCallback = ctypes.CFUNCTYPE(ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p) + + +############################################################################### +# SDL event union # +############################################################################### + +_event_struct_map = { + # Constants from _constants.py + SDL_KEYDOWN: SDL_KeyboardEvent, # noqa: F405 + SDL_KEYUP: SDL_KeyboardEvent, # noqa: F405 + SDL_MOUSEMOTION: SDL_MouseMotionEvent, # noqa: F405 + SDL_MOUSEBUTTONDOWN: SDL_MouseButtonEvent, # noqa: F405 + SDL_MOUSEBUTTONUP: SDL_MouseButtonEvent, # noqa: F405 + SDL_MOUSEWHEEL: SDL_MouseWheelEvent, # noqa: F405 + SDL_POLLSENTINEL: SDL_CommonEvent, # noqa: F405 +} + + +def SDL_Event(event=None): + """ + Convert event to an SDL_Event object using ctypes. + The size of the largest SDL_Event struct is 56 bytes. + """ + if event is None: + return SDL_CommonEvent.from_buffer(ctypes.create_string_buffer(56)) + + event_type = event.type + + if event_type in _event_struct_map: + return _event_struct_map[event_type].from_buffer(event) + return SDL_CommonEvent.from_buffer(event) diff --git a/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_micropython.py b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_micropython.py new file mode 100644 index 000000000..fda726fa3 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-sdldisplay/displaysys/sdldisplay/_sdl2_lib/_micropython.py @@ -0,0 +1,188 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +A bare implementation of SDL2 written in MicroPython using ffi +""" + +import uctypes +import ffi +import struct +from ._constants import * # noqa: F403 + + +# Load the SDL2 shared library using ffi +_libSDL2 = ffi.open("libSDL2-2.0.so.0") + + +############################################################################### +# SDL2 structs # +############################################################################### + + +def SDL_Rect(x=0, y=0, w=0, h=0): + return struct.pack("iiii", x, y, w, h) + + +def SDL_Point(x=0, y=0): + return struct.pack("ii", x, y) + + +SDL_CommonEvent = { + "type": uctypes.UINT32 | 0, + "timestamp": uctypes.UINT32 | 4, +} + +SDL_KeyboardEvent = { + "type": uctypes.UINT32 | 0, + "timestamp": uctypes.UINT32 | 4, + "key": ( + 8, + { + "windowID": uctypes.UINT32 | 0, + "state": uctypes.UINT8 | 4, + "repeat": uctypes.UINT8 | 5, + "padding2": uctypes.UINT8 | 6, + "padding3": uctypes.UINT8 | 7, + "keysym": ( + 8, + { + "scancode": 0 | uctypes.UINT32, + "sym": 4 | uctypes.UINT32, + "mod": 8 | uctypes.UINT16, + "unused": 10 | uctypes.UINT32, + }, + ), + }, + ), +} + +SDL_MouseMotionEvent = { + "type": uctypes.UINT32 | 0, + "timestamp": uctypes.UINT32 | 4, + "motion": ( + 8, + { + "windowID": uctypes.UINT32 | 0, + "which": uctypes.UINT32 | 4, + "state": uctypes.UINT32 | 8, + "x": uctypes.INT32 | 12, + "y": uctypes.INT32 | 16, + "xrel": uctypes.INT32 | 20, + "yrel": uctypes.INT32 | 8, + }, + ), +} + +SDL_MouseButtonEvent = { + "type": uctypes.UINT32 | 0, + "timestamp": uctypes.UINT32 | 4, + "button": ( + 8, + { + "windowID": uctypes.UINT32 | 0, + "which": uctypes.UINT32 | 4, + "button": uctypes.UINT8 | 8, + "state": uctypes.UINT8 | 9, + "clicks": uctypes.UINT8 | 10, + "padding1": uctypes.UINT8 | 11, + "x": uctypes.INT32 | 12, + "y": uctypes.INT32 | 16, + }, + ), +} + +SDL_MouseWheelEvent = { + "type": uctypes.UINT32 | 0, + "timestamp": uctypes.UINT32 | 4, + "wheel": ( + 8, + { + "windowID": uctypes.UINT32 | 0, + "which": uctypes.UINT32 | 4, + "x": uctypes.INT32 | 8, + "y": uctypes.INT32 | 12, + "direction": uctypes.UINT32 | 16, + "preciseX": uctypes.FLOAT32 | 20, + "preciseY": uctypes.FLOAT32 | 24, + }, + ), +} + + +############################################################################### +# SDL2 functions # +############################################################################### + +# SDL misc functions +SDL_Init = _libSDL2.func("i", "SDL_Init", "I") +SDL_Quit = _libSDL2.func("v", "SDL_Quit", "") +SDL_GetError = _libSDL2.func("s", "SDL_GetError", "") +SDL_PollEvent = _libSDL2.func("i", "SDL_PollEvent", "P") + +# SDL key functions +SDL_GetKeyName = _libSDL2.func("s", "SDL_GetKeyName", "i") +SDL_GetKeyFromName = _libSDL2.func("i", "SDL_GetKeyFromName", "s") + +# SDL window functions +SDL_CreateWindow = _libSDL2.func("P", "SDL_CreateWindow", "siiiii") +SDL_DestroyWindow = _libSDL2.func("v", "SDL_DestroyWindow", "P") +SDL_SetWindowSize = _libSDL2.func("v", "SDL_SetWindowSize", "Pii") + +# SDL renderer functions +SDL_CreateRenderer = _libSDL2.func("P", "SDL_CreateRenderer", "PiI") +SDL_DestroyRenderer = _libSDL2.func("v", "SDL_DestroyRenderer", "P") +SDL_SetRenderDrawColor = _libSDL2.func("i", "SDL_SetRenderDrawColor", "PPPP") +SDL_SetRenderTarget = _libSDL2.func("i", "SDL_SetRenderTarget", "pP") +SDL_RenderClear = _libSDL2.func("v", "SDL_RenderClear", "P") +SDL_RenderCopy = _libSDL2.func("v", "SDL_RenderCopy", "PPPP") +SDL_RenderCopyEx = _libSDL2.func("v", "SDL_RenderCopyEx", "PPPPdPPi") +SDL_RenderPresent = _libSDL2.func("v", "SDL_RenderPresent", "P") +SDL_RenderFillRect = _libSDL2.func("i", "SDL_RenderFillRect", "PP") +SDL_RenderSetLogicalSize = _libSDL2.func("i", "SDL_RenderSetLogicalSize", "Pii") + +# SDL texture functions +SDL_CreateTexture = _libSDL2.func("P", "SDL_CreateTexture", "PIiiii") +SDL_DestroyTexture = _libSDL2.func("v", "SDL_DestroyTexture", "P") +SDL_SetTextureBlendMode = _libSDL2.func("i", "SDL_SetTextureBlendMode", "PI") +SDL_UpdateTexture = _libSDL2.func("i", "SDL_UpdateTexture", "PPPi") + +# SDL timer functions NOT WORKING +SDL_AddTimer = _libSDL2.func("P", "SDL_AddTimer", "IPP") +SDL_RemoveTimer = _libSDL2.func("i", "SDL_RemoveTimer", "P") + + +def SDL_TimerCallback(tcb): + return ffi.callback("I", tcb, "IP") + + +############################################################################### +# SDL event union # +############################################################################### + +_event_struct_map = { + # Constants from _constants.py + SDL_KEYDOWN: SDL_KeyboardEvent, # noqa: F405 + SDL_KEYUP: SDL_KeyboardEvent, # noqa: F405 + SDL_MOUSEMOTION: SDL_MouseMotionEvent, # noqa: F405 + SDL_MOUSEBUTTONDOWN: SDL_MouseButtonEvent, # noqa: F405 + SDL_MOUSEBUTTONUP: SDL_MouseButtonEvent, # noqa: F405 + SDL_MOUSEWHEEL: SDL_MouseWheelEvent, # noqa: F405 + SDL_POLLSENTINEL: SDL_CommonEvent, # noqa: F405 +} + + +def SDL_Event(event=None): + """ + Convert event to an SDL_Event object using ctypes. + The size of the largest SDL_Event struct is 56 bytes. + """ + if event is None: + return bytearray(56) # Size of the largest SDL_Event struct + + event_type = int.from_bytes(event[:4], "little") + + if event_type in _event_struct_map: + return uctypes.struct(uctypes.addressof(event), _event_struct_map[event_type]) + return uctypes.struct(uctypes.addressof(event), SDL_CommonEvent) diff --git a/micropython/pydisplay/displaysys/displaysys-sdldisplay/examples/board_config.py b/micropython/pydisplay/displaysys/displaysys-sdldisplay/examples/board_config.py new file mode 100644 index 000000000..49b3a2191 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-sdldisplay/examples/board_config.py @@ -0,0 +1,31 @@ +""" +Combination board configuration for desktop, pyscript and jupyter notebook platforms. +""" + +from displaysys.sdldisplay import SDLDisplay as DTDisplay, poll +from eventsys import devices +import sys + +width = 320 +height = 480 +rotation = 0 +scale = 2.0 + +display_drv = DTDisplay( + width=width, + height=height, + rotation=rotation, + title=f"{sys.implementation.name} on {sys.platform}", + scale=scale, +) + +broker = devices.Broker() + +events_dev = broker.create_device( + type=devices.types.QUEUE, + read=poll, + data=display_drv, + # data2=events.filter, +) + +display_drv.fill(0) diff --git a/micropython/pydisplay/displaysys/displaysys-sdldisplay/manifest.py b/micropython/pydisplay/displaysys/displaysys-sdldisplay/manifest.py new file mode 100644 index 000000000..2bfafbe70 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys-sdldisplay/manifest.py @@ -0,0 +1,9 @@ +metadata( + description="PyDisplay displaysys-sdldisplay", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaysys-sdldisplay", +) +require("displaysys") +package("displaysys") diff --git a/micropython/pydisplay/displaysys/displaysys/README.md b/micropython/pydisplay/displaysys/displaysys/README.md new file mode 100644 index 000000000..1a01afa0c --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys/README.md @@ -0,0 +1,151 @@ +logo + +

pydisplay

+ +

Cross-platform User Interface and Event Drivers for *Python

+ +

+ About • + Key Features • + Getting Started • + Running Your First App • + API • + Roadmap • + Contributing • + Thanks • + Screenshots +

+ +| ![peterhinch's active.py](screenshots/active.gif) | ![russhughes's tiny_toasters.py](screenshots/tiny_toasters.gif) | +|-------------------------|--------------------------------| +| @peterhinch's active.py | @russhughes's tiny_toasters.py | + +## About + +WARNINGS: pydisplay is currently alpha quality. Every effort has been made to test on as many platforms as possible, but I need your help and feedback to get it to its inital release. A lot has changed and I am working on catching up the documentation. + +pydisplay is a universal display, event and device driver framework for multiple flavors of Python, including MicroPython, CircuitPython and CPython (big Python). It may be used as-is to create graphic frontends to your apps, or may be used as a foundation with GUI libraries such as [LVGL](https://github.com/lvgl/lv_micropython), [MicroPython-touch](https://github.com/peterhinch/micropython-touch) or maybe even a GUI framework you've been thinking of developing. Its primary purpose is to provide display and touch drivers for MicroPython, but it is equally useful for developers who may never touch MicroPython. + +It is important to note that pydisplay is meant to be a foundation for GUI libraries and is not itself a GUI library. It doesn't provide widgets, such as buttons, checkboxes or sliders, and it doesn't provide a timing mechanism. You will need a GUI library to provide those if necessary, although many apps won't need them. (There is a cross-platform repository [multimer](https://github.com/PyDevices/pydisplay/tree/main/src/lib/multimer) you can use if you want to used scheduled interrupts. It works with CPython and MicroPython, but doesn't work with CircuitPython. You can also use asyncio for timing.) + +## Key Features + +- May be used without additional libraries to add graphics capabilities to MicroPython, CircuitPython and CPython, with a consistent API across them all. +- Enables moving from one platform to another, for example MicroPython on ESP32-S3 to CPython on Windows without changing your code. Do your graphics development on your desktop, laptop or ChromeBook and then move to a microcontroller when you are ready to interface with your sensors and devices. CPython has much better error messages than MicroPython making it easier to troubleshoot when things go wrong! +- Built around devices available on microcontrollers but not necessarily available on desktop operating systems. For instance, rotary encoders and mouse scroll wheels show up as the same device type and yield the same events. Touchscreens on microcontrollers yield the same events as mice on desktops. Likewise with keypads / keyboards. +- Easily extensible. Use the primitives provided by pydisplay and add your own libraries, classes and functions to have even greater functionality. +- Provides several built-in color palettes and a mechanism to generate your own palettes. +- Lots of examples included, whether developed specifically for pydisplay or ported from [Russ Hughes's st7789py_mpy](https://github.com/russhughes/st7789py_mpy). Also works with all of the examples from Peter Hinch's MicroPython GUI libraries [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) and [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) on MicroPython. +- Support MicroPython on microcontrollers and on Unix(-like) operating systems. +- On MicroPython, can be configured to work with [kdschlosser's lvgl_micropython bus drivers](https://github.com/kdschlosser/lvgl_micropython), which are very fast bus drivers written in C. +- Works with CircuitPython's FourWire and ParallelBus bus drivers, as well as FrameBufferDisplay based interfaces such as dotclockframebuffer, usb_video and rgbmatrix + +## Getting Started + +This section is under construction. For now, see [Getting Started](GETTING-STARTED.md) for more information. + + +## Running your first app + +You will need to import the `path.py` file before running any of the examples. + +On desktop operating systems, `cd` into the `mp` directory (or wherever you have the files staged) and type: +``` +python3 -i path.py +``` +or +``` +micropython -i path.py +``` + +On microcontrollers, either add the following to your `boot.py` (MicroPython) or `code.py` (CircuitPython), or simply import it at the REPL before importing your desired app: +``` +import lib.path +``` + +The [examples](examples) directory will be on the system path, so to run an app from it, you just need to type: +``` +import calculator # substitute `calculator` with the file OR directory you want to run, omitting the .py extension +``` + +To run any of the examples from MicroPython-Touch (remember, its for MicroPython only) type: +``` +import gui.demos.various # substitute `various` with the file you want to run, omitting the .py extension +``` + +## API + +Where possible, existing, proven APIs were used. + +- There are currently 5 display classes, and hopefully another `EPaperDisplay` display class will be added soon, although I will need help from the community for this. + - BusDisplay is for microcontrollers, both on MicroPython and CircuitPython. CircuitPython provides the required bus drivers, as mentioned elsewhere in this README, but MicroPython doesn't have display bus drivers. The [buses](src/lib/buses) packages are included with the installer. It is my hope that community members will create other C bus drivers similar to @kdschlosser's bus drivers in [lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython). + - SDL2Display - the preferred class for desktop operating systems as it is faster than PGDisplay. It uses an SDL `texture` in place of an LCD's Graphics RAM (GRAM). + - PGDisplay - an optional class for desktop operating systems. It uses a pygame `surface` in place of an LCD's GRAM. It can be benificial in a couple of instances: + - SDL2Display "glitches" on my ChromeBook, but PGDisplay doesn't + - On Windows, it is easier to install PyGame than SDL2 + - FBDisplay works with CircuitPython framebufferio.FramebufferDisplay objects, such as dotclockframebuffer (RGB displays), usb_video and rgbmatrix. (usb_video may be the coolest thing you can do with displaysys, although I'm not sure how practical or useful it is. It allows your board to function as a webcam, even without a camera, and to render the display through USB to any application on your host PC that can open a webcam! My Windows machine sees it as an unsupported device, so it will not work, but it does work on my ChromeBook. Currently it is limited to RP2040 only and is hardcoded to a 128 x 96 resolution, but that likely will change. See the [screen capture](examples/circuitpython_usb_video_chromebook.gif) and the [board_config.py](board_configs/circuitpython/usb_video/board_config.py) for more details.) + - JNDisplay for Jupyter Notebooks. No input devices are currently supported. + - PSDisplay for PyScript. Only touchscreens are currently supported. +- Names of events and Devices in [eventsys](src/lib/eventsys/) are taken from PyGame and/or SDL2 to keep the API consistent. +- All drawing targets, sometimes referred to as `canvas` in the code, may be written to using the API from MicroPython's framebuf.FrameBuffer API + - CPython and CircuitPython don't have a `framebuf` module that is API compliant with MicroPython's `framebuf`, so [framebuf.py](add_ons/framebuf.py) is provided for those platforms. It is not used in MicroPython unless framebuf wasn't compiled in. + - A `graphics` module is provided that subclasses `FrameBuffer` (either built-in or from framebuf.py) and provides additional drawing tools, such as `round_rect`. All methods in graphics return an Area object with x, y, w and h attributes describing a bounding box of what was changed. This can be used by applications to only update the part of the display that needs it. That functionality is implemented in DisplayBuffer and will likely be required by EPaperDisplay when it is implemented. + - Canvases include, but are not limited to, the display itself, framebuf bytearrays, bmp565 (16-bit Windows Bitmap files) and displaybuf.DisplayBuffer objects. + - displaybuf.DisplayBuffer implements @peterhinch's API that represents the full display as a framebuffer and allows for 4-, 8- and 16-bit bytearrays while still drawing to the screen as 16-bit. It is required for `MicroPython-Touch` and is very useful outside of that library as well, especially when memory is constrained. +- Display drivers for MicroPython BusDisplay use the constructor API of CircuitPython's DisplayIO drivers. This includes rotation = 0, 90, 180, 270 instead of 0, 1, 2, 3. +- BusDisplay can communicate with the underlying bus driver using either CircuitPython's DisplayIO method calls or @kdschlosser's [lvgl_micropython] method calls. +- There are 3 primary mechanism's for fonts: the graphics.Font class, tft_text.text() and tft_write.write() methods. All 3 of these return an Area object as mentioned earlier. A fourth font mechanism called EZFont is included in the utils folder, but it doesn't return an Area object, which is why it isn't in the lib folder. + - Font is derived from Tony DiCola's 5x7 font class and reads 8x8, 8x14 and 8x16 .bin files from [@spaceraces romfont repo](https://github.com/spacerace/romfont) + - .text() is written by @russhughes and uses fonts generated by his [text_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/text_font_converter.py) It reads 8 and 16bit wide fonts in heights that are multiples of 8. + - .write() is written by @russhughes and uses fonts generated by his [write_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/write_font_converter.py) + - EZFont is a subclass of [@easytarget's microPyEZfonts](https://github.com/easytarget/microPyEZfonts) which uses fonts generated from [@peterhinch's font-to-py](https://github.com/peterhinch/micropython-font-to-py). + - NOTE: @peterhinch's Writer class is inlcuded in MicroPyton-Touch and may be used on MicroPython platforms, but, like EZFont, it doesn't return an Area object. +- Graphics files may be used by 3 mechanisms: + - bmp565.BM565 is a class that can read and write Windows Bitmap files saved with RGB565 color encoding. GIMP supports exporting RGB565 .BMPs. The BMP565 class can open a file and read it's entire contents into memory, or with the `streamed = True` flag, it will only read the slice requested, allowing progressive rendering of files much too large to fit into memory. The slice can be 2 dimensional (BMP565[1:5, 6:10] gets pixels 1 through 5 on rows 6 through 10) or 1 dimensional (BMP565[6:10] gets all pixels in rows 6 through 10). This slicing mechanism is very useful when rendering sprites. It can reverse the order of pixels in a row with `mirrored = False`, which is needed when rendering a background image when rotation is 90 or 270 and (horizontal) scrolling is desired. Finally, it can use an existing bytearray as its buffer instead of reading from a file, which allows saving screenshots from existing canvases such as a FrameBuffer or DisplayBuffer. + - .bitmap() is written by @russhughes and reads .py graphics files encoded with his [image_converter.py utiliity](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or his [sprite_converter.py utility](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py). It renders the entire image to a buffer, and then copies that buffer to the display. + - .pbitmap() is also written by @russhughes and renders the same fonts as .bitmap(), but it does it progressively, one line at a time using a one line buffer. +- Config files - All files that are intended for you to edit to customize your configuration are in the [configs](src/configs/) directory. They are: + - `board_config.py` - required in all circumstances. Feel free to add your own setup code here, such as for real-time clocks, wifi, sensors, etc. + - `path.py` - required in all circumstances + - `color_setup.py` - required for [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) + - `hardware_setup.py` - required for [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) + - `lv_config.py` - required for LVGL + - `tft_config.py` - required for @russhughes's examples. I had to do some search and replace to get those examples to work. + + +## Roadmap + +- [ ] Much more documentation on Github +- [ ] Document the files to produce output for ReadTheDocs +- [ ] Implement EPaperDisplay +- [ ] Optimize with more Numpy and Viper code +- [ ] Decrease the memory footprint where possible +- [ ] Test with frozen modules +- [ ] On MicroPython on Unix, the screen gets cleared when the display is rotated. Microcontroller displays don't do this. It's not an issue unless you want to draw to the display, rotate it, then draw more on top. This functionality allow drawing text in all four 90 degree orientations. +- [ ] Scrolling vertically on desktop operating sytems works correctly, but not when rotated. When rotated, it show scroll horizontally, but continues to scroll vertically. +- [ ] Scrolling on microcontrollers has issues when trying to write spanning the cutoff line. For instance, if drawing a 16 pixel high image at the 8th line from the cutoff line, the bottom 8 lines don't end up where you expect. See the [bmp565_sprite](examples/bmp565_sprite.py) example. +- [ ] Ensure multiple displays work at the same time +- [ ] Implement color depths other than 16 bit +- [ ] Add a Joystick class to eventsys +- [ ] Test with CircuitPython Blinka on SBC's such as Raspberry Pi 4 +- [ ] Need C bus drivers from the community, especially for STM32H7 and MIMXRT + +## Contributing + +This is a community project and I need your help! If you have a suggestion that would make this better, please fork the repo and create a pull request. +Don't forget to give the project a star! Thanks again! + +1. Fork the project +2. Clone it open the repository in command line +3. Create your feature branch (`git checkout -b feature/amazing-feature`) +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a pull request from your feature branch from your repository into this repository main branch, and provide a description of your changes + +## Thanks + +I very much appreciate @peterhinch, @russhughes and the team at Adafruit for their contributions to the Python on microcontrollers community. + +## Why + +I started out just wanting to create drivers that worked with MicroPython the way DisplayIO drivers work for CircuitPython, except without DisplayIO and instead usable by any GUI framework like, but not limited to, LVGL. That snowballed into adding more platforms and then adding drawing primitives, font classes, palettes, an event system, a barebones SDL2 library, a Bitmap 565 reader/writer and supporting as many platforms as possible. I stopped short of creating a full fledged GUI and plan to leave it as a very capable graphics library. I think this is a great foundation for building a GUI framework with widgets and a task scheduler, although it is very usable and useful without one. @peterhinch has a great GUI for MicroPython that works on top of pydisplay, and I'm hoping someone will make a GUI that works across platforms. diff --git a/micropython/pydisplay/displaysys/displaysys/displaysys/__init__.py b/micropython/pydisplay/displaysys/displaysys/displaysys/__init__.py new file mode 100644 index 000000000..4382cd0d4 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys/displaysys/__init__.py @@ -0,0 +1,554 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +`displaysys` +==================================================== + +A collection of classes and functions for working with displays and input devices +in *Python. The goal is to provide a common API for working with displays and +input devices across different platforms including MicroPython, CircuitPython and +CPython. It works on microcontrollers, desktops, web browsers and Jupyter notebooks. +""" + +import gc + +try: + from byteswap import byteswap +except ImportError: + + def byteswap(buf): + """ + Swap the bytes of a 16-bit buffer in place with no dependencies. + """ + buf[::2], buf[1::2] = buf[1::2], buf[::2] + + +gc.collect() + + +def alloc_buffer(size): + """ + Create a new buffer of the specified size. In the future, this function may be + modified to use port-specific memory allocation such as ESP32's heap_caps_malloc. + + Args: + size (int): The size of the buffer to create. + + Returns: + (memoryview): The new buffer. + """ + return memoryview(bytearray(size)) + + +def color888(r, g, b): + """ + Convert RGB values to a 24-bit color value. + + Args: + r (int): The red value. + g (int): The green value. + b (int): The blue value. + + Returns: + (int): The 24-bit color value. + """ + return (r << 16) | (g << 8) | b + + +def color565(r, g=None, b=None): + """ + Convert RGB values to a 16-bit color value. + + Args: + r (int, tuple or list): The red value or a tuple or list of RGB values. + g (int): The green value. + b (int): The blue value. + + Returns: + (int): The 16-bit color value + """ + if isinstance(r, (tuple, list)): + r, g, b = r[:3] + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) + + +def color565_swapped(r, g=0, b=0): + # Convert r, g, b in range 0-255 to a 16 bit color value RGB565 + # ggbbbbbb rrrrrggg + if isinstance(r, (tuple, list)): + r, g, b = r[:3] + color = color565(r, g, b) + return (color & 0xFF) << 8 | (color & 0xFF00) >> 8 + + +def color332(r, g, b): + # Convert r, g, b in range 0-255 to an 8 bit color value RGB332 + # rrrgggbb + return (r & 0xE0) | ((g >> 3) & 0x1C) | (b >> 6) + + +def color_rgb(color): + """ + color can be an 16-bit integer or a tuple, list or bytearray of length 2 or 3. + """ + if isinstance(color, int): + # convert 16-bit int color to 2 bytes + color = (color & 0xFF, color >> 8) + if len(color) == 2: + r = color[1] & 0xF8 | (color[1] >> 5) & 0x7 # 5 bit to 8 bit red + g = color[1] << 5 & 0xE0 | (color[0] >> 3) & 0x1F # 6 bit to 8 bit green + b = color[0] << 3 & 0xF8 | (color[0] >> 2) & 0x7 # 5 bit to 8 bit blue + else: + r, g, b = color + return (r, g, b) + + +class DisplayDriver: + def __init__(self, auto_refresh=False): + print(f"Initializing {self.__class__.__name__}...") + gc.collect() + + self.byteswap = byteswap + self._vssa = False # False means no vertical scroll + self._auto_byteswap = self.requires_byteswap + self._touch_device = None + if auto_refresh: + period = 33 if isinstance(auto_refresh, bool) else auto_refresh + try: + from multimer import get_timer + + self._timer = get_timer(self.show, period=period) + except ImportError: + raise ImportError("multimer is required for auto_refresh") + else: + self._timer = None + self.init() + gc.collect() + print(f"{self.__class__.__name__}: initialized.") + print(f"{self.__class__.__name__}: requires_byteswap = {self.requires_byteswap}") + + def __del__(self): + self.deinit() + + ############### Universal API Methods, not usually overridden ################ + + @property + def width(self) -> int: + """The width of the display in pixels.""" + if ((self._rotation // 90) & 0x1) == 0x1: # if rotation index is odd + return self._height + return self._width + + @property + def height(self) -> int: + """The height of the display in pixels.""" + if ((self._rotation // 90) & 0x1) == 0x1: # if rotation index is odd + return self._width + return self._height + + @property + def rotation(self) -> int: + """ + The rotation of the display. + """ + return self._rotation + + @rotation.setter + def rotation(self, value) -> None: + """ + Sets the rotation of the display. + + Args: + value (int): The rotation of the display in degrees. + """ + + if value % 90 != 0: + value = value * 90 + + if value == self._rotation: + return + + print(f"{self.__class__.__name__}.rotation(): Setting rotation to {value}") + self._rotation_helper(value) + print("done setting rotation") + + self._rotation = value + + if self._touch_device is not None: + self._touch_device.rotation = value + + self.init() + + @property + def touch_device(self) -> object: + """ + The touch device. + """ + return self._touch_device + + @touch_device.setter + def touch_device(self, value) -> None: + """ + Sets the touch device. + + Args: + value (object): The touch device. + """ + if hasattr(value, "rotation") or value is None: + self._touch_device = value + else: + raise ValueError("touch_device must have a rotation attribute") + self._touch_device.rotation = self.rotation + + def fill(self, color): + """ + Fill the display with a color. + + Args: + color (int): The color to fill the display with. + """ + return self.fill_rect(0, 0, self.width, self.height, color) + + def scroll(self, dx, dy) -> None: + """ + Scroll the display. + + Args: + dx (int): The number of pixels to scroll horizontally. + dy (int): The number of pixels to scroll vertically. + """ + if dy != 0: + if self._vssa is not None: + self.vscsad(self._vssa + dy) + else: + self.vscsad(dy) + if dx != 0: + raise NotImplementedError("Horizontal scrolling not supported") + + def disable_auto_byteswap(self, value: bool) -> bool: + """ + Disable byte swapping in the display driver. + + If self.requires_byteswap and the guest application is capable of byte swapping color data + check to see if byte swapping can be disabled. If so, disable it. + + Usage: + ``` + # If byte swapping is required and the display driver is capable of having byte swapping disabled, + # disable it and set a flag so we can swap the color bytes as they are created. + if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) + else: + needs_swap = False + ``` + + Args: + value (bool): Whether to disable byte swapping. + + Returns: + (bool): Whether byte swapping was disabled successfully. + + """ + if self._requires_byteswap: + self._auto_byteswap = not value + else: + self._auto_byteswap = False + print(f"{self.__class__.__name__}: auto byte swapping = {self._auto_byteswap}") + return not self._auto_byteswap + + @property + def requires_byteswap(self) -> bool: + """ + Whether the display requires byte swapping. + """ + return self._requires_byteswap + + def blit_transparent(self, buf: memoryview, x: int, y: int, w: int, h: int, key: int): + """ + Blit a buffer with transparency. + + Args: + buf (memoryview): The buffer to blit. + x (int): The x coordinate to blit to. + y (int): The y coordinate to blit to. + w (int): The width to blit. + h (int): The height to blit. + key (int): The color key to use for transparency. + + Returns: + (tuple): The x, y, w, h coordinates of the blitted area. + """ + BPP = self.color_depth // 8 + key_bytes = key.to_bytes(BPP, "little") + stride = w * BPP + for j in range(h): + rowstart = j * stride + colstart = 0 + # iterate over each pixel looking for the first non-key pixel + while colstart < stride: + startoffset = rowstart + colstart + if buf[startoffset : startoffset + BPP] != key_bytes: + # found a non-key pixel + # then iterate over each pixel looking for the next key pixel + colend = colstart + while colend < stride: + endoffset = rowstart + colend + if buf[endoffset : endoffset + BPP] == key_bytes: + break + colend += BPP + # blit the non-key pixels + self.blit_rect( + buf[rowstart + colstart : rowstart + colend], + x + colstart // BPP, + y + j, + (colend - colstart) // BPP, + 1, + ) + colstart = colend + else: + colstart += BPP + return (x, y, w, h) + + @property + def vscroll(self) -> int: + """ + The vertical scroll position relative to the top fixed area. + + Returns: + (int): The vertical scroll position. + """ + return self.vscsad() - self._tfa + + @vscroll.setter + def vscroll(self, y) -> None: + """ + Set the vertical scroll position relative to the top fixed area. + + Args: + y (int): The vertical scroll position. + """ + self.vscsad((y % self._vsa) + self._tfa) + + def set_vscroll(self, tfa=0, bfa=0) -> None: + """ + Set the vertical scroll definition and move the vertical scroll to the top. + + Args: + tfa (int): The top fixed area. + bfa (int): The bottom fixed area. + """ + self.vscrdef(tfa, self.height - tfa - bfa, bfa) + self.vscroll = 0 + + @property + def tfa(self) -> int: + """ + The top fixed area set by set_vscroll or vscrdef. + + Returns: + (int): The top fixed area. + """ + return self._tfa + + @property + def vsa(self) -> int: + """ + The vertical scroll area set by set_vscroll or vscrdef. + + Returns: + (int): The vertical scroll area. + """ + return self._vsa + + @property + def bfa(self) -> int: + """ + The bottom fixed area set by set_vscroll or vscrdef. + + Returns: + (int): The bottom fixed area. + """ + return self._bfa + + def translate_point(self, point) -> tuple: + """ + Translate a point from real coordinates to scrolled coordinates. + + Useful for touch events. + + Args: + point (tuple): The x and y coordinates to translate. + + Returns: + (tuple): The translated x and y coordinates. + """ + x, y = point + if self.vscsad() and self.tfa < y < self.height - self.bfa: + y = y + self.vscsad() - self.tfa + if y >= (self.vsa + self.tfa): + y %= self.vsa + return x, y + + def scroll_by(self, value): + self.vscroll += value + + def scroll_to(self, value): + self.vscroll = value + + @property + def tfa_area(self): + """ + The top fixed area as an Area object. + + Returns: + (tuple): The top fixed area. + """ + return (0, 0, self.width, self.tfa) + + @property + def vsa_area(self): + """ + The vertical scroll area as an Area object. + + Returns: + (tuple): The vertical scroll area. + """ + return (0, self.tfa, self.width, self.vsa) + + @property + def bfa_area(self): + """ + The bottom fixed area as an Area object. + + Returns: + (tuple): The bottom fixed area. + """ + return (0, self.tfa + self.vsa, self.width, self.bfa) + + ############### Common API Methods, sometimes overridden ################ + + def vscrdef(self, tfa: int, vsa: int, bfa: int) -> None: + """ + Set the vertical scroll definition. Should be overridden by the + subclass and called as super().vscrdef(tfa, vsa, bfa). + + Args: + tfa (int): The top fixed area. + vsa (int): The vertical scroll area. + bfa (int): The bottom fixed area. + """ + if tfa + vsa + bfa != self.height: + raise ValueError("Sum of top, scroll and bottom areas must equal screen height") + self._tfa = tfa + self._vsa = vsa + self._bfa = bfa + + def vscsad(self, vssa: int | None = None) -> int: + """ + Set or get the vertical scroll start address. Should be overridden by the + subclass and called as super().vscsad(y). + + Args: + vssa (int): The vertical scroll start address. + + Returns: + (int): The vertical scroll start address. + """ + if vssa is not None: + while vssa < 0: + vssa += self._height + if vssa >= self._height: + vssa %= self._height + self._vssa = vssa + return vssa + + def _rotation_helper(self, value): + """ + Helper function to set the rotation of the display. + + Args: + value (int): The rotation of the display in degrees. + """ + # override this method in subclasses to handle rotation + + ############### Empty API Methods, must be overridden if applicable ################ + + @property + def power(self) -> bool: + """The power state of the display.""" + return -1 + + @power.setter + def power(self, value: bool) -> None: + """ + Set the power state of the display. Should be overridden by the subclass. + + Args: + value (bool): True to power on, False to power off. + """ + return + + @property + def brightness(self) -> float: + """The brightness of the display.""" + return -1 + + @brightness.setter + def brightness(self, value: float) -> None: + """ + Set the brightness of the display. Should be overridden by the subclass. + + Args: + value (int, float): The brightness value from 0 to 1. + """ + return + + def invert_colors(self, value: bool) -> None: + """ + Invert the colors of the display. Should be overridden by the subclass. + + Args: + value (bool): True to invert the colors, False to restore the colors. + """ + return + + def reset(self) -> None: + """ + Perform a reset of the display. Should be overridden by the subclass. + """ + return + + def hard_reset(self) -> None: + """ + Perform a hardware reset of the display. Should be overridden by the subclass. + """ + return + + def soft_reset(self) -> None: + """ + Perform a software reset of the display. Should be overridden by the subclass. + """ + return + + def sleep_mode(self, value: bool) -> None: + """ + Set the sleep mode of the display. Should be overridden by the subclass. + + Args: + value (bool): True to enter sleep mode, False to exit sleep mode. + """ + return + + def deinit(self) -> None: + """ + Deinitialize the display. + """ + self.__del__() + + def show(self, *args, **kwargs) -> None: + """ + Show the display. Base class method does nothing. May be overridden by subclasses. + """ + return diff --git a/micropython/pydisplay/displaysys/displaysys/examples/displaysys_block_test.py b/micropython/pydisplay/displaysys/displaysys/examples/displaysys_block_test.py new file mode 100644 index 000000000..b6afb8f49 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys/examples/displaysys_block_test.py @@ -0,0 +1,52 @@ +"""displaysys_block_test.py""" + +from board_config import display_drv +import random +import time +import gc + + +gc.collect() +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + + +def test(): + raise Exception("Test exception") + + +def main(): + # display_bus.register_callback(test) + block_size = 32 + blocks = [] + + max_x = display_drv.width - block_size - 1 + max_y = display_drv.height - block_size - 1 + + for pixel_color in [0x0000, 0xFFFF, 0xF800, 0x07E0, 0x001F, 0xFFE0, 0x07FF, 0xF81F]: + pixel_bytes = ( + pixel_color.to_bytes(2, "big") if needs_swap else pixel_color.to_bytes(2, "little") + ) + blocks.append(memoryview(bytearray(pixel_bytes * (block_size * block_size)))) + + print("Drawing blocks on display") + count = 0 + start_time = time.time() + while True: + display_drv.blit_rect( + random.choice(blocks), + random.randint(0, max_x), + random.randint(0, max_y), + block_size, + block_size, + ) + count += 1 + if count % 2000 == 0: + print(f"\rblocks/sec: {(count / (time.time() - start_time)):5.2f}", end="") + + +main() diff --git a/micropython/pydisplay/displaysys/displaysys/examples/displaysys_fill_rect_test.py b/micropython/pydisplay/displaysys/displaysys/examples/displaysys_fill_rect_test.py new file mode 100644 index 000000000..dc16a2a09 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys/examples/displaysys_fill_rect_test.py @@ -0,0 +1,40 @@ +"""displaysys_fill_rect_test.py""" + +from board_config import display_drv +from random import randint, getrandbits +import time +import gc + + +gc.collect() +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + + +def main(): + block_size = 32 + + max_x = display_drv.width - block_size - 1 + max_y = display_drv.height - block_size - 1 + + print("Drawing blocks on display") + count = 0 + start_time = time.time() + while True: + display_drv.fill_rect( + randint(0, max_x), + randint(0, max_y), + block_size, + block_size, + getrandbits(16), + ) + count += 1 + if count % 1000 == 0: + print(f"\rblocks/sec: {(count / (time.time() - start_time)):5.2f}", end="") + + +main() diff --git a/micropython/pydisplay/displaysys/displaysys/examples/displaysys_simpletest.py b/micropython/pydisplay/displaysys/displaysys/examples/displaysys_simpletest.py new file mode 100644 index 000000000..388bc4701 --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys/examples/displaysys_simpletest.py @@ -0,0 +1,11 @@ +from board_config import display_drv, broker +from random import getrandbits +from graphics import Area + +button_area = Area(display_drv.fill_rect(10, 10, 100, 100, 0xF800)) +while True: + if evt := broker.poll(): + if evt.type == broker.events.MOUSEBUTTONDOWN: + if button_area.contains(evt.pos): + display_drv.fill_rect(*button_area, getrandbits(16)) + print(f"Button pressed at {evt.pos}") diff --git a/micropython/pydisplay/displaysys/displaysys/manifest.py b/micropython/pydisplay/displaysys/displaysys/manifest.py new file mode 100644 index 000000000..fc8f4cf9f --- /dev/null +++ b/micropython/pydisplay/displaysys/displaysys/manifest.py @@ -0,0 +1,8 @@ +metadata( + description="PyDisplay displaysys", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="displaysys", +) +package("displaysys") diff --git a/micropython/pydisplay/eventsys/README.md b/micropython/pydisplay/eventsys/README.md new file mode 100644 index 000000000..1a01afa0c --- /dev/null +++ b/micropython/pydisplay/eventsys/README.md @@ -0,0 +1,151 @@ +logo + +

pydisplay

+ +

Cross-platform User Interface and Event Drivers for *Python

+ +

+ About • + Key Features • + Getting Started • + Running Your First App • + API • + Roadmap • + Contributing • + Thanks • + Screenshots +

+ +| ![peterhinch's active.py](screenshots/active.gif) | ![russhughes's tiny_toasters.py](screenshots/tiny_toasters.gif) | +|-------------------------|--------------------------------| +| @peterhinch's active.py | @russhughes's tiny_toasters.py | + +## About + +WARNINGS: pydisplay is currently alpha quality. Every effort has been made to test on as many platforms as possible, but I need your help and feedback to get it to its inital release. A lot has changed and I am working on catching up the documentation. + +pydisplay is a universal display, event and device driver framework for multiple flavors of Python, including MicroPython, CircuitPython and CPython (big Python). It may be used as-is to create graphic frontends to your apps, or may be used as a foundation with GUI libraries such as [LVGL](https://github.com/lvgl/lv_micropython), [MicroPython-touch](https://github.com/peterhinch/micropython-touch) or maybe even a GUI framework you've been thinking of developing. Its primary purpose is to provide display and touch drivers for MicroPython, but it is equally useful for developers who may never touch MicroPython. + +It is important to note that pydisplay is meant to be a foundation for GUI libraries and is not itself a GUI library. It doesn't provide widgets, such as buttons, checkboxes or sliders, and it doesn't provide a timing mechanism. You will need a GUI library to provide those if necessary, although many apps won't need them. (There is a cross-platform repository [multimer](https://github.com/PyDevices/pydisplay/tree/main/src/lib/multimer) you can use if you want to used scheduled interrupts. It works with CPython and MicroPython, but doesn't work with CircuitPython. You can also use asyncio for timing.) + +## Key Features + +- May be used without additional libraries to add graphics capabilities to MicroPython, CircuitPython and CPython, with a consistent API across them all. +- Enables moving from one platform to another, for example MicroPython on ESP32-S3 to CPython on Windows without changing your code. Do your graphics development on your desktop, laptop or ChromeBook and then move to a microcontroller when you are ready to interface with your sensors and devices. CPython has much better error messages than MicroPython making it easier to troubleshoot when things go wrong! +- Built around devices available on microcontrollers but not necessarily available on desktop operating systems. For instance, rotary encoders and mouse scroll wheels show up as the same device type and yield the same events. Touchscreens on microcontrollers yield the same events as mice on desktops. Likewise with keypads / keyboards. +- Easily extensible. Use the primitives provided by pydisplay and add your own libraries, classes and functions to have even greater functionality. +- Provides several built-in color palettes and a mechanism to generate your own palettes. +- Lots of examples included, whether developed specifically for pydisplay or ported from [Russ Hughes's st7789py_mpy](https://github.com/russhughes/st7789py_mpy). Also works with all of the examples from Peter Hinch's MicroPython GUI libraries [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) and [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) on MicroPython. +- Support MicroPython on microcontrollers and on Unix(-like) operating systems. +- On MicroPython, can be configured to work with [kdschlosser's lvgl_micropython bus drivers](https://github.com/kdschlosser/lvgl_micropython), which are very fast bus drivers written in C. +- Works with CircuitPython's FourWire and ParallelBus bus drivers, as well as FrameBufferDisplay based interfaces such as dotclockframebuffer, usb_video and rgbmatrix + +## Getting Started + +This section is under construction. For now, see [Getting Started](GETTING-STARTED.md) for more information. + + +## Running your first app + +You will need to import the `path.py` file before running any of the examples. + +On desktop operating systems, `cd` into the `mp` directory (or wherever you have the files staged) and type: +``` +python3 -i path.py +``` +or +``` +micropython -i path.py +``` + +On microcontrollers, either add the following to your `boot.py` (MicroPython) or `code.py` (CircuitPython), or simply import it at the REPL before importing your desired app: +``` +import lib.path +``` + +The [examples](examples) directory will be on the system path, so to run an app from it, you just need to type: +``` +import calculator # substitute `calculator` with the file OR directory you want to run, omitting the .py extension +``` + +To run any of the examples from MicroPython-Touch (remember, its for MicroPython only) type: +``` +import gui.demos.various # substitute `various` with the file you want to run, omitting the .py extension +``` + +## API + +Where possible, existing, proven APIs were used. + +- There are currently 5 display classes, and hopefully another `EPaperDisplay` display class will be added soon, although I will need help from the community for this. + - BusDisplay is for microcontrollers, both on MicroPython and CircuitPython. CircuitPython provides the required bus drivers, as mentioned elsewhere in this README, but MicroPython doesn't have display bus drivers. The [buses](src/lib/buses) packages are included with the installer. It is my hope that community members will create other C bus drivers similar to @kdschlosser's bus drivers in [lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython). + - SDL2Display - the preferred class for desktop operating systems as it is faster than PGDisplay. It uses an SDL `texture` in place of an LCD's Graphics RAM (GRAM). + - PGDisplay - an optional class for desktop operating systems. It uses a pygame `surface` in place of an LCD's GRAM. It can be benificial in a couple of instances: + - SDL2Display "glitches" on my ChromeBook, but PGDisplay doesn't + - On Windows, it is easier to install PyGame than SDL2 + - FBDisplay works with CircuitPython framebufferio.FramebufferDisplay objects, such as dotclockframebuffer (RGB displays), usb_video and rgbmatrix. (usb_video may be the coolest thing you can do with displaysys, although I'm not sure how practical or useful it is. It allows your board to function as a webcam, even without a camera, and to render the display through USB to any application on your host PC that can open a webcam! My Windows machine sees it as an unsupported device, so it will not work, but it does work on my ChromeBook. Currently it is limited to RP2040 only and is hardcoded to a 128 x 96 resolution, but that likely will change. See the [screen capture](examples/circuitpython_usb_video_chromebook.gif) and the [board_config.py](board_configs/circuitpython/usb_video/board_config.py) for more details.) + - JNDisplay for Jupyter Notebooks. No input devices are currently supported. + - PSDisplay for PyScript. Only touchscreens are currently supported. +- Names of events and Devices in [eventsys](src/lib/eventsys/) are taken from PyGame and/or SDL2 to keep the API consistent. +- All drawing targets, sometimes referred to as `canvas` in the code, may be written to using the API from MicroPython's framebuf.FrameBuffer API + - CPython and CircuitPython don't have a `framebuf` module that is API compliant with MicroPython's `framebuf`, so [framebuf.py](add_ons/framebuf.py) is provided for those platforms. It is not used in MicroPython unless framebuf wasn't compiled in. + - A `graphics` module is provided that subclasses `FrameBuffer` (either built-in or from framebuf.py) and provides additional drawing tools, such as `round_rect`. All methods in graphics return an Area object with x, y, w and h attributes describing a bounding box of what was changed. This can be used by applications to only update the part of the display that needs it. That functionality is implemented in DisplayBuffer and will likely be required by EPaperDisplay when it is implemented. + - Canvases include, but are not limited to, the display itself, framebuf bytearrays, bmp565 (16-bit Windows Bitmap files) and displaybuf.DisplayBuffer objects. + - displaybuf.DisplayBuffer implements @peterhinch's API that represents the full display as a framebuffer and allows for 4-, 8- and 16-bit bytearrays while still drawing to the screen as 16-bit. It is required for `MicroPython-Touch` and is very useful outside of that library as well, especially when memory is constrained. +- Display drivers for MicroPython BusDisplay use the constructor API of CircuitPython's DisplayIO drivers. This includes rotation = 0, 90, 180, 270 instead of 0, 1, 2, 3. +- BusDisplay can communicate with the underlying bus driver using either CircuitPython's DisplayIO method calls or @kdschlosser's [lvgl_micropython] method calls. +- There are 3 primary mechanism's for fonts: the graphics.Font class, tft_text.text() and tft_write.write() methods. All 3 of these return an Area object as mentioned earlier. A fourth font mechanism called EZFont is included in the utils folder, but it doesn't return an Area object, which is why it isn't in the lib folder. + - Font is derived from Tony DiCola's 5x7 font class and reads 8x8, 8x14 and 8x16 .bin files from [@spaceraces romfont repo](https://github.com/spacerace/romfont) + - .text() is written by @russhughes and uses fonts generated by his [text_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/text_font_converter.py) It reads 8 and 16bit wide fonts in heights that are multiples of 8. + - .write() is written by @russhughes and uses fonts generated by his [write_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/write_font_converter.py) + - EZFont is a subclass of [@easytarget's microPyEZfonts](https://github.com/easytarget/microPyEZfonts) which uses fonts generated from [@peterhinch's font-to-py](https://github.com/peterhinch/micropython-font-to-py). + - NOTE: @peterhinch's Writer class is inlcuded in MicroPyton-Touch and may be used on MicroPython platforms, but, like EZFont, it doesn't return an Area object. +- Graphics files may be used by 3 mechanisms: + - bmp565.BM565 is a class that can read and write Windows Bitmap files saved with RGB565 color encoding. GIMP supports exporting RGB565 .BMPs. The BMP565 class can open a file and read it's entire contents into memory, or with the `streamed = True` flag, it will only read the slice requested, allowing progressive rendering of files much too large to fit into memory. The slice can be 2 dimensional (BMP565[1:5, 6:10] gets pixels 1 through 5 on rows 6 through 10) or 1 dimensional (BMP565[6:10] gets all pixels in rows 6 through 10). This slicing mechanism is very useful when rendering sprites. It can reverse the order of pixels in a row with `mirrored = False`, which is needed when rendering a background image when rotation is 90 or 270 and (horizontal) scrolling is desired. Finally, it can use an existing bytearray as its buffer instead of reading from a file, which allows saving screenshots from existing canvases such as a FrameBuffer or DisplayBuffer. + - .bitmap() is written by @russhughes and reads .py graphics files encoded with his [image_converter.py utiliity](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or his [sprite_converter.py utility](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py). It renders the entire image to a buffer, and then copies that buffer to the display. + - .pbitmap() is also written by @russhughes and renders the same fonts as .bitmap(), but it does it progressively, one line at a time using a one line buffer. +- Config files - All files that are intended for you to edit to customize your configuration are in the [configs](src/configs/) directory. They are: + - `board_config.py` - required in all circumstances. Feel free to add your own setup code here, such as for real-time clocks, wifi, sensors, etc. + - `path.py` - required in all circumstances + - `color_setup.py` - required for [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) + - `hardware_setup.py` - required for [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) + - `lv_config.py` - required for LVGL + - `tft_config.py` - required for @russhughes's examples. I had to do some search and replace to get those examples to work. + + +## Roadmap + +- [ ] Much more documentation on Github +- [ ] Document the files to produce output for ReadTheDocs +- [ ] Implement EPaperDisplay +- [ ] Optimize with more Numpy and Viper code +- [ ] Decrease the memory footprint where possible +- [ ] Test with frozen modules +- [ ] On MicroPython on Unix, the screen gets cleared when the display is rotated. Microcontroller displays don't do this. It's not an issue unless you want to draw to the display, rotate it, then draw more on top. This functionality allow drawing text in all four 90 degree orientations. +- [ ] Scrolling vertically on desktop operating sytems works correctly, but not when rotated. When rotated, it show scroll horizontally, but continues to scroll vertically. +- [ ] Scrolling on microcontrollers has issues when trying to write spanning the cutoff line. For instance, if drawing a 16 pixel high image at the 8th line from the cutoff line, the bottom 8 lines don't end up where you expect. See the [bmp565_sprite](examples/bmp565_sprite.py) example. +- [ ] Ensure multiple displays work at the same time +- [ ] Implement color depths other than 16 bit +- [ ] Add a Joystick class to eventsys +- [ ] Test with CircuitPython Blinka on SBC's such as Raspberry Pi 4 +- [ ] Need C bus drivers from the community, especially for STM32H7 and MIMXRT + +## Contributing + +This is a community project and I need your help! If you have a suggestion that would make this better, please fork the repo and create a pull request. +Don't forget to give the project a star! Thanks again! + +1. Fork the project +2. Clone it open the repository in command line +3. Create your feature branch (`git checkout -b feature/amazing-feature`) +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a pull request from your feature branch from your repository into this repository main branch, and provide a description of your changes + +## Thanks + +I very much appreciate @peterhinch, @russhughes and the team at Adafruit for their contributions to the Python on microcontrollers community. + +## Why + +I started out just wanting to create drivers that worked with MicroPython the way DisplayIO drivers work for CircuitPython, except without DisplayIO and instead usable by any GUI framework like, but not limited to, LVGL. That snowballed into adding more platforms and then adding drawing primitives, font classes, palettes, an event system, a barebones SDL2 library, a Bitmap 565 reader/writer and supporting as many platforms as possible. I stopped short of creating a full fledged GUI and plan to leave it as a very capable graphics library. I think this is a great foundation for building a GUI framework with widgets and a task scheduler, although it is very usable and useful without one. @peterhinch has a great GUI for MicroPython that works on top of pydisplay, and I'm hoping someone will make a GUI that works across platforms. diff --git a/micropython/pydisplay/eventsys/eventsys/__init__.py b/micropython/pydisplay/eventsys/eventsys/__init__.py new file mode 100644 index 000000000..ec5eff21c --- /dev/null +++ b/micropython/pydisplay/eventsys/eventsys/__init__.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`eventsys` +==================================================== +An Event System including event types and device types for *Python. +""" + +from micropython import const +from collections import namedtuple + + +def custom_type(types: dict[str, int]={}, classes: dict[str, str]={}): + """ + Create new event types and classes for the events class. + + For example, to recreate the events for the keypad device: + ``` + import eventsys + + types = [("KEYDOWN", 0x300), ("KEYUP", 0x301)] + classes = {"Key": "type name key mod scancode window"} + eventsys.custom_type(types, classes) + + # Optionally update the filter + events.filter += [events.KEYDOWN, events.KEYUP] + ``` + + Args: + types (dict[str, int]): Dictionary of event types and values. + classes (dict[str, str]): Dictionary of event classes and fields. + """ + for type_name, value in types.items(): + type_name = type_name.upper() + if hasattr(events, type_name): + raise ValueError(f"Event type {type_name} already exists in events class.") + else: + setattr(events, type_name, value or events._USER_TYPE_BASE) + if not value: + events._USER_TYPE_BASE += 1 + + for event_class_name, event_class_fields in classes.items(): + event_class_name = event_class_name[0].upper() + event_class_name[1:].lower() + if hasattr(events, event_class_name): + raise ValueError(f"Event class {event_class_name} already exists in events class.") + else: + event_class_fields = event_class_fields.lower() + setattr( + events, + event_class_name, + namedtuple(event_class_name, event_class_fields), # noqa: PYI024 + ) + +class events: + """ + A container for event types and classes. Similar to a C enum and struct. + """ + + # Event types (from SDL2 / PyGame, not complete) + QUIT = const(0x100) # User clicked the window close button + KEYDOWN = const(0x300) # Key pressed + KEYUP = const(0x301) # Key released + MOUSEMOTION = const(0x400) # Mouse moved + MOUSEBUTTONDOWN = const(0x401) # Mouse button pressed + MOUSEBUTTONUP = const(0x402) # Mouse button released + MOUSEWHEEL = const(0x403) # Mouse wheel motion + JOYAXISMOTION = const(0x600) # Joystick axis motion + JOYBALLMOTION = const(0x601) # Joystick trackball motion + JOYHATMOTION = const(0x602) # Joystick hat position change + JOYBUTTONDOWN = const(0x603) # Joystick button pressed + JOYBUTTONUP = const(0x604) # Joystick button released + _USER_TYPE_BASE = 0x8000 + + filter = [ + QUIT, + KEYDOWN, + KEYUP, + MOUSEMOTION, + MOUSEBUTTONDOWN, + MOUSEBUTTONUP, + MOUSEWHEEL, + ] + + # Event classes from PyGame + Unknown = namedtuple("Common", "type") # noqa: PYI024 + Motion = namedtuple("Motion", "type pos rel buttons touch window") # noqa: PYI024 + Button = namedtuple("Button", "type pos button touch window") # noqa: PYI024 + Wheel = namedtuple("Wheel", "type flipped x y precise_x precise_y touch window") # noqa: PYI024 + Key = namedtuple("Key", "type name key mod scancode window") # noqa: PYI024 + Quit = namedtuple("Quit", "type") # noqa: PYI024 + Any = namedtuple("Any", "type") # noqa: PYI024 diff --git a/micropython/pydisplay/eventsys/eventsys/devices.py b/micropython/pydisplay/eventsys/eventsys/devices.py new file mode 100644 index 000000000..d1e8954c4 --- /dev/null +++ b/micropython/pydisplay/eventsys/eventsys/devices.py @@ -0,0 +1,738 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +`eventsys.devices` +==================================================== + +Device classes for eventsys's Event System. May also be used +with other applications. Devices are objects that poll for events +and return them. They can be subscribed to and unsubscribed from +to receive events. + +Devices can be created with Broker.create_device() or by calling the +constructor of the device class directly. Devices can be +subscribed to with .subscribe() and unsubscribed from with +.unsubscribe(). Devices can be polled for events with .poll(). +Devices can be registered with a broker device with .register_device() +and unregistered with .unregister_device(). Devices can be chained +together by setting the .broker property of a device to another device. + +Devices can be created with the following types: +- types.BROKER: A device that polls multiple devices. +- types.QUEUE: A device that returns multiple types of events. +- types.TOUCH: A device that returns MOUSEBUTTONDOWN when touched, + MOUSEMOTION when moved and MOUSEBUTTONUP when released. +- types.ENCODER: A device that returns MOUSEWHEEL events when turned, + MOUSEBUTTONDOWN when pressed. +- types.KEYPAD: A device that returns KEYDOWN and KEYUP events when + keys are pressed or released. +- types.JOYSTICK: A device that returns joystick events (not implemented). +""" + +from micropython import const +from . import events +from sys import exit + + +_DEFAULT_TOUCH_ROTATION_TABLE = (0b000, 0b101, 0b110, 0b011) + +SWAP_XY = const(0b001) +REVERSE_X = const(0b010) +REVERSE_Y = const(0b100) + + +def custom_type(type_name, responses): + """ + Create a new device type with a list of responses. + + Args: + type_name (str): The name of the device type. + responses (list[int]): A list of event types that the device can return. + + Returns: + (Device): The newly created device type. + + Raises: + ValueError: If `type_name` is not a string, `responses` is not a list, or any response is not an integer. + ValueError: If a device type with the same name already exists in the `types` class. + ValueError: If a device class with the same name already exists. + + Example: + To create the KEYPAD device type and `KeypadDevice` class: + + ```python + import eventsys.device as device + from eventsys import events + + KeypadDevice = device.new_type("KEYPAD", [events.KEYDOWN, events.KEYUP]) + ``` + """ + if not isinstance(type_name, str): + raise ValueError("type_name must be a string") + type_name = type_name.strip().upper() + if not isinstance(responses, list): + raise ValueError("responses must be a list") + if not all(isinstance(event, int) for event in responses): + raise ValueError("all responses must be integers") + + if hasattr(types, type_name): + raise ValueError(f"Device type {type_name} already exists in types class.") + class_name = type_name[0].upper() + type_name[1:].lower() + "Device" + if class_name in [cls.__name__ for cls in _mapping.values()]: + raise ValueError(f"Device class {class_name} already exists.") + + value = len(_mapping) + setattr(types, type_name, value) + NewClass = type(class_name, (Device,), {"type": value, "responses": responses}) + _mapping[value] = NewClass + return NewClass + + + +class types: + """ + Device types for the Event System. + """ + UNDEFINED = const(-1) + BROKER = const(0x00) + QUEUE = const(0x01) + TOUCH = const(0x02) + ENCODER = const(0x03) + KEYPAD = const(0x04) + JOYSTICK = const(0x05) + + +class Device: + """ + Base class for devices. Must be subclassed. Should not be instantiated directly. + + Attributes: + type (Devices): The type of the device. + responses (list): The list of event types that the device can respond to. + """ + + type = types.UNDEFINED + responses = events.filter + + def __init__(self, read=None, data=None, read2=None, data2=None): + """ + Create a new device object. + + Args: + read (callable, optional): A function that returns an event or None. Defaults to None. + data (Any, optional): Data to pass to the read function. Defaults to None. + read2 (callable, optional): A function that returns a value or None. Defaults to None. + data2 (Any, optional): Data to pass to the read2 function. Defaults to None. + """ + self._event_callbacks = {} + + self._read = read if read else lambda: None + self._data = data + self._read2 = read2 if read2 else lambda: None + self._data2 = data2 + + self._broker = None + self._state = None + self._user_data = None # Can be set and retrieved by apps such as lv_config + + def poll(self, *args) -> events: + """ + Poll the device for events. + + Args: + *args (Any): Additional arguments that can be passed to the read callback functions. + + Returns: + Event: The event that was polled or None if no event was polled. + """ + if (event := self._poll()) is not None: + if event.type in events.filter: + if event.type == events.QUIT: + if self._broker: + self._broker.quit() + if callback_list := self._event_callbacks.get(event.type): + for callback in callback_list: + callback(event, *args) + return event + return None + + def subscribe(self, callback, event_types=None): + """ + Subscribe to events from the device. + + Args: + callback (function): The function to call when an event is received. + event_types (list[int] | None): A list of event types to subscribe to. + + Raises: + ValueError: If `callback` is not callable. + ValueError: If any event type in `event_types` is not a response from this device. + + Example: + ```python + def callback(event): + print(event) + + device.subscribe(callback, [events.MOUSEBUTTONDOWN, events.MOUSEBUTTONUP]) + ``` + + This will call `callback` when the receives a MOUSEBUTTONDOWN or MOUSEBUTTONUP event. + """ + event_types = event_types or self.responses + if not callable(callback): + raise ValueError("callback is not callable.") + for event_type in event_types: + if event_type not in self.responses: + raise ValueError("the specified event_type is not a response from this device") + callback_set = self._event_callbacks.get(event_type, set()) + callback_set.add(callback) + self._event_callbacks[event_type] = callback_set + + def unsubscribe(self, callback, event_types=None): + """ + Unsubscribes a callback function from one or more event types. + + Args: + callback (function): The callback function to unsubscribe. + event_types (list): A list of event types to unsubscribe from. + """ + event_types = event_types or self.responses + for event_type in event_types: + if callback_set := self._event_callbacks.get(event_type): + callback_set.remove(callback) + + @property + def broker(self): + """ + The broker that manages this device. + """ + return self._broker + + @broker.setter + def broker(self, broker): + self._broker = broker + + @property + def user_data(self): + """ + User data that can be set and retrieved by applications. + """ + return self._user_data + + @user_data.setter + def user_data(self, value): + self._user_data = value + + +class Broker(Device): + """ + The Broker class is a device that polls multiple devices for events and forwards them to + subscribers. + + Attributes: + type (Devices): The type of the device (set to `types.BROKER`). + responses (list): The list of event types that the device can respond to. + events (events): The events class for convenience. + Applications can use Broker.events.KEYDOWN, etc. + """ + + type = types.BROKER + responses = events.filter + events = events # Create a reference to the events class for convenience. + + def __init__(self): + super().__init__() + self.devices = [] # List of devices to poll + self._device_callbacks = {} + # Function to call when the window close button is clicked. + # Set it like `display_drv.quit_func = cleanup_func` where `cleanup_func` is a + # function that cleans up resources and calls `sys.exit()`. + # .poll() must be called periodically to check for the quit event. + self._quit_func = exit + + def subscribe(self, callback, event_types=None, device_types=None): + """ + Subscribes a callback function to receive events. + + Args: + callback (function): The callback function to subscribe. + event_types (list, optional): The list of event types to subscribe to. Defaults to None. + device_types (list, optional): The list of device types to subscribe to. Defaults to None. + + Raises: + ValueError: If the callback is not callable. + ValueError: If both device_types and event_types are provided. + ValueError: If neither device_types nor event_types are provided. + """ + if not callable(callback): + raise ValueError("callback is not callable.") + if device_types is not None and event_types is not None: + raise ValueError("set one of device_type or event_type but not both.") + if device_types is None and event_types is None: + raise ValueError("set one of device_type or event_type but not both.") + if device_types is not None: + for device_type in device_types: + callback_set = self._device_callbacks.get(device_type, set()) + callback_set.add(callback) + self._device_callbacks[device_type] = callback_set + else: + super().subscribe(callback, event_types) + + def unsubscribe(self, callback, event_types=None, device_types=None): + """ + Unsubscribes a callback function from receiving events. + + Args: + callback (function): The callback function to unsubscribe. + event_types (list, optional): The list of event types to unsubscribe from. Defaults to None. + device_types (list, optional): The list of device types to unsubscribe from. Defaults to None. + + Raises: + ValueError: If both device_types and event_types are provided. + ValueError: If neither device_types nor event_types are provided. + """ + if device_types is not None and event_types is not None: + raise ValueError("set one of device_type or event_type but not both.") + if device_types is None and event_types is None: + raise ValueError("set one of device_type or event_type but not both.") + if device_types is not None: + for device_type in device_types: + if callback_set := self._device_callbacks.get(device_type): + callback_set.remove(callback) + else: + super().unsubscribe(callback, event_types) + + def create_device(self, type=types.QUEUE, **kwargs) -> Device: + """ + Create a device object. + + Args: + type (int, optional): The type of device to create. Defaults to types.QUEUE. + **kwargs (Any): Arbitrary keyword arguments for the class constructor. + + Returns: + Device: The created device object. + + Raises: + ValueError: If the device type is invalid. + """ + if cls := _mapping.get(type): + dev = cls(**kwargs) + self.register_device(dev) + return dev + raise ValueError("Invalid device type") + + def register_device(self, dev): + """ + Register a device to be polled. + + Args: + dev (Device): The device object to register. + """ + dev.broker = self + self.devices.append(dev) + + def unregister_device(self, dev): + """ + Unregister a device. + + Args: + dev (Device): The device object to unregister. + """ + if dev in self.devices: + self.devices.remove(dev) + dev.broker = None + + @property + def quit_func(self): + """ + The function to call when the window close button is clicked. + """ + return self._quit_func + + @quit_func.setter + def quit_func(self, value): + """ + Sets the function to call when the window close button is clicked. + + Args: + value (function): The function to call when the window close button is clicked. + """ + if not callable(value): + raise ValueError("quit_func must be callable") + self._quit_func = value + + def quit(self): + """ + Call the quit function. + """ + self._quit_func() + + def _poll(self): + """ + Polls the registered devices for events. + + Returns: + object: The event object if an event is received, otherwise None. + """ + for device in self.devices: + if (event := device.poll()) is not None: + if callback_list := self._device_callbacks.get(device.type): + for func in callback_list(): + func(event) + return event + return None + + +class QueueDevice(Device): + """ + Represents a queue device. + + Attributes: + type (str): The type of the device. + responses (list): The list of events that the device can respond to. + """ + + type = types.QUEUE + responses = events.filter + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self._data2 is None: + self._data2 = events.filter + if hasattr(self._data, "touch_scale"): + self.scale = self._data.touch_scale + else: + self.scale = 1 + + def _poll(self): + """ + Polls the device for events. + + Returns: + Event or None: The next event from the device, or None if no event is available. + """ + if (event := self._read()) is not None: + if event.type in self._data2: + if event.type in ( + events.MOUSEMOTION, + events.MOUSEBUTTONDOWN, + events.MOUSEBUTTONUP, + ): + if (scale := self.scale) != 1: + event.pos = ( + int(event.pos[0] // scale), + int(event.pos[1] // scale), + ) + if event.type == events.MOUSEMOTION: + event.rel = (event.rel[0] // scale, event.rel[1] // scale) + return event + return None + + def peek(self) -> bool: + """ + Peek at the next event in the queue without removing it. + + Returns: + bool: True if there is an event in the queue that matches the filter in self._data, otherwise False. + Note: self._data defaults to events.filter but may be set to a different list. + """ + raise NotImplementedError("QueueDevice.peek() not implemented") + + +class TouchDevice(Device): + """ + Represents a touch input device. + + This class handles touch input events and provides methods to read touch data + from the underlying touch driver. It supports reporting mouse button 1 events + such as mouse motion, mouse button down, and mouse button up. + + Attributes: + type (str): The type of the device (set to types.TOUCH). + responses (tuple): The supported event types for the device. + + Args: + *args (Any): Variable length argument list. + **kwargs (Any): Arbitrary keyword arguments. + """ + + type = types.TOUCH + responses = (events.MOUSEMOTION, events.MOUSEBUTTONDOWN, events.MOUSEBUTTONUP) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self._data is None: + raise ValueError("TouchDevice requires a display device as 'data'") + if self._data2 is None: # self._data is a rotation table + self._data2 = _DEFAULT_TOUCH_ROTATION_TABLE + self._data.touch_device = self + self.rotation = self._data.rotation + + @property + def rotation(self): + """ + Get the rotation value of the touch device. + + Returns: + rotation (int): The rotation value in degrees. + """ + return self._rotation + + @rotation.setter + def rotation(self, value): + """ + Set the rotation value of the touch device. + + Args: + value (int): The rotation value in degrees. + """ + self._rotation = value % 360 + + # _mask is an integer from 0 to 7 (or 0b001 to 0b111, 3 bits) + # Currently, bit 2 = invert_y, bit 1 is invert_x and bit 0 is swap_xy, but that may change. + self._mask = self._data2[self._rotation // 90] + + @property + def rotation_table(self): + """ + Get the rotation table of the touch device. + + Returns: + (list): The rotation table. + """ + return self._data2 + + @rotation_table.setter + def rotation_table(self, value): + """ + Set the rotation table of the touch device. + + Args: + value (list): The rotation table. + """ + self._data2 = value + + def _poll(self): + """ + Poll the touch device for touch events. + + Returns: + Event: The touch event generated by the touch device. + """ + try: # If called too quickly, the touch driver may raise OSError: [Errno 116] ETIMEDOUT + touched = self._read() + except OSError: + return None + if touched: + last_pos = self._state + # If it looks like a point, use it, otherwise get the first point out of the list / tuple + (x, y, *_) = touched if isinstance(touched[0], int) else touched[0] + + if self._mask & SWAP_XY: + x, y = y, x + if self._mask & REVERSE_X: + x = self._data.width - x - 1 + if self._mask & REVERSE_Y: + y = self._data.height - y - 1 + self._state = (x, y) + if last_pos is not None: + last_x, last_y = last_pos + return events.Motion( + events.MOUSEMOTION, + self._state, + (x - last_x, y - last_y), + (1, 0, 0), + False, + None, + ) + else: + return events.Button(events.MOUSEBUTTONDOWN, self._state, 1, False, None) + elif self._state is not None: + last_pos = self._state + self._state = None + return events.Button(events.MOUSEBUTTONUP, last_pos, 1, False, None) + return None + + +class EncoderDevice(Device): + """ + A class representing an encoder device. + + Attributes: + type (str): The type of the device (ENCODER). + responses (tuple): The events that the device can respond to (MOUSEWHEEL, MOUSEBUTTONDOWN, MOUSEBUTTONUP). + """ + + type = types.ENCODER + responses = (events.MOUSEWHEEL, events.MOUSEBUTTONDOWN, events.MOUSEBUTTONUP) + + def __init__(self, *args, **kwargs): + """ + Initializes a new instance of the EncoderDevice class. + + Args: + *args (Any): Variable length argument list. + **kwargs (Any): Arbitrary keyword arguments. + + Notes: + - self._data is the mouse button number to report for the switch. + Default is 2 (middle mouse button). If the mouse button number is even, + the wheel will report vertical (y) movement. If the mouse button number is odd, + the wheel will report horizontal (x) movement. This corresponds to a typical mouse + wheel being button 2 and the wheel moving vertically. It also corresponds to + scrolling horizontally on a touchpad with two-finger scrolling and using the right button. + """ + super().__init__(*args, **kwargs) + self._state = (0, False) # (position, pressed) + self._data = self._data if self._data else 2 # Default to middle mouse button + + def _poll(self): + """ + Polls the encoder device for changes and returns the corresponding event. + + Returns: + Event: The event generated by the encoder device, or None if no event occurred. + """ + last_pos, last_pressed = self._state + pressed = self._read2() + if pressed != last_pressed: + self._state = (last_pos, pressed) + return events.Button( + events.MOUSEBUTTONDOWN if pressed else events.MOUSEBUTTONUP, + (0, 0), + self._data, + False, + None, + ) + + pos = self._read() + if pos != last_pos: + steps = pos - last_pos + self._state = (pos, last_pressed) + if self._data % 2 == 0: + return events.Wheel(events.MOUSEWHEEL, False, 0, steps, 0, steps, False, None) + return events.Wheel(events.MOUSEWHEEL, False, steps, 0, steps, 0, False, None) + return None + + +class KeypadDevice(Device): + """ + Represents a keypad device. + + Attributes: + type (Devices): The type of the device (set to `types.KEYPAD`). + responses (tuple): The types of events that the device can respond to (set to `(events.KEYDOWN, events.KEYUP)`). + + Methods: + __init__: Initializes the KeypadDevice object. + _poll: Polls the keypad for key events. + """ + + type = types.KEYPAD + responses = (events.KEYDOWN, events.KEYUP) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._state = set() + + def _poll(self): + """ + Polls the keypad for key events. + + Returns: + events.Key or None: An instance of the `events.Key` class representing the key event, or `None` if no key event occurred. + """ + keys = set(self._read()) + released = self._state - keys + if released: + key = released.pop() + self._state.remove(key) + return events.Key(events.KEYUP, chr(key), key, 0, 0) + pressed = keys - self._state + if pressed: + key = pressed.pop() + self._state.add(key) + return events.Key(events.KEYDOWN, chr(key), key, 0, 0) + return None + + +class JoystickDevice(Device): + """ + Represents a joystick device. + + Attributes: + type (Devices): The type of the device, set to `types.JOYSTICK`. + responses (tuple): A tuple of event types that this device can respond to. + + Methods: + __init__(*args, **kwargs): Initializes the JoystickDevice instance. + _poll(): Polls the device for events. + + Raises: + NotImplementedError: If the `_poll` method is not implemented. + """ + + type = types.JOYSTICK + responses = ( + events.JOYAXISMOTION, + events.JOYBALLMOTION, + events.JOYHATMOTION, + events.JOYBUTTONDOWN, + events.JOYBUTTONUP, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _poll(self): + raise NotImplementedError("JoystickDevice.read() not implemented") + + + +class VirtualDevices: + class VirtualDevice: + def __init__(self, virtual_devices, device_type): + self._virtual_devices = virtual_devices + self.type = device_type + self.user_data = None + self._fifo = [] + + def subscribe(self, callback): + self._callback = callback + + def poll(self, *args): + self._virtual_devices.poll_queue_device() + event = self._fifo.pop(0) if self._fifo else None + self._callback(event, *args) + + def add_event(self, event): + self._fifo.append(event) + + def __init__(self, queue_device): + self._queue_device = queue_device + self._vd_touch = self.VirtualDevice(self, types.TOUCH) + self._vd_encoder = self.VirtualDevice(self, types.ENCODER) + self._vd_keypad = self.VirtualDevice(self, types.KEYPAD) + self.devices = [self._vd_touch, self._vd_encoder, self._vd_keypad] + + def poll_queue_device(self): + if e:= self._queue_device.poll(): + if e.type == events.MOUSEBUTTONDOWN or e.type == events.MOUSEBUTTONUP: + self._vd_touch.add_event(e) + elif e.type == events.MOUSEWHEEL: + self._vd_encoder.add_event(e) + elif e.type == events.KEYDOWN or e.type == events.KEYUP: + self._vd_keypad.add_event(e) + +_mapping = { + # Mapping of device types to device classes + types.BROKER: Broker, + types.QUEUE: QueueDevice, + types.TOUCH: TouchDevice, + types.ENCODER: EncoderDevice, + types.KEYPAD: KeypadDevice, + types.JOYSTICK: JoystickDevice, +} diff --git a/micropython/pydisplay/eventsys/eventsys/keys.py b/micropython/pydisplay/eventsys/eventsys/keys.py new file mode 100644 index 000000000..9531c0dc6 --- /dev/null +++ b/micropython/pydisplay/eventsys/eventsys/keys.py @@ -0,0 +1,571 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT + +""" +`eventsys.keys` +==================================================== +""" + +from micropython import const as _const + + +class Keys: + """ + A container for key codes and names. Similar to a C enum and struct. + """ + + def keyname(x): + return Keys._keytable.get(x, "Unknown") + + def key(x): + return list(Keys._keytable.keys())[list(Keys._keytable.values()).index(x)] + + def modname(x): + return Keys._modtable.get(x, "Unknown") + + def mod(x): + return list(Keys._modtable.keys())[list(Keys._modtable.values()).index(x)] + + K_UNKNOWN = _const(0) + K_BACKSPACE = _const(8) + K_TAB = _const(9) + K_RETURN = _const(13) + K_ESCAPE = _const(27) + K_SPACE = _const(32) + K_EXCLAIM = _const(33) + K_QUOTEDBL = _const(34) + K_HASH = _const(35) + K_DOLLAR = _const(36) + K_PERCENT = _const(37) + K_AMPERSAND = _const(38) + K_QUOTE = _const(39) + K_LEFTPAREN = _const(40) + K_RIGHTPAREN = _const(41) + K_ASTERISK = _const(42) + K_PLUS = _const(43) + K_COMMA = _const(44) + K_MINUS = _const(45) + K_PERIOD = _const(46) + K_SLASH = _const(47) + K_0 = _const(48) + K_1 = _const(49) + K_2 = _const(50) + K_3 = _const(51) + K_4 = _const(52) + K_5 = _const(53) + K_6 = _const(54) + K_7 = _const(55) + K_8 = _const(56) + K_9 = _const(57) + K_COLON = _const(58) + K_SEMICOLON = _const(59) + K_LESS = _const(60) + K_EQUALS = _const(61) + K_GREATER = _const(62) + K_QUESTION = _const(63) + K_AT = _const(64) + K_LEFTBRACKET = _const(91) + K_BACKSLASH = _const(92) + K_RIGHTBRACKET = _const(93) + K_CARET = _const(94) + K_UNDERSCORE = _const(95) + K_BACKQUOTE = _const(96) + K_a = _const(97) + K_b = _const(98) + K_c = _const(99) + K_d = _const(100) + K_e = _const(101) + K_f = _const(102) + K_g = _const(103) + K_h = _const(104) + K_i = _const(105) + K_j = _const(106) + K_k = _const(107) + K_l = _const(108) + K_m = _const(109) + K_n = _const(110) + K_o = _const(111) + K_p = _const(112) + K_q = _const(113) + K_r = _const(114) + K_s = _const(115) + K_t = _const(116) + K_u = _const(117) + K_v = _const(118) + K_w = _const(119) + K_x = _const(120) + K_y = _const(121) + K_z = _const(122) + K_DELETE = _const(127) + K_CAPSLOCK = _const(1073741881) + K_F1 = _const(1073741882) + K_F2 = _const(1073741883) + K_F3 = _const(1073741884) + K_F4 = _const(1073741885) + K_F5 = _const(1073741886) + K_F6 = _const(1073741887) + K_F7 = _const(1073741888) + K_F8 = _const(1073741889) + K_F9 = _const(1073741890) + K_F10 = _const(1073741891) + K_F11 = _const(1073741892) + K_F12 = _const(1073741893) + K_PRINTSCREEN = _const(1073741894) + K_SCROLLLOCK = _const(1073741895) + K_PAUSE = _const(1073741896) + K_INSERT = _const(1073741897) + K_HOME = _const(1073741898) + K_PAGEUP = _const(1073741899) + K_END = _const(1073741901) + K_PAGEDOWN = _const(1073741902) + K_RIGHT = _const(1073741903) + K_LEFT = _const(1073741904) + K_DOWN = _const(1073741905) + K_UP = _const(1073741906) + K_NUMLOCKCLEAR = _const(1073741907) + K_KP_DIVIDE = _const(1073741908) + K_KP_MULTIPLY = _const(1073741909) + K_KP_MINUS = _const(1073741910) + K_KP_PLUS = _const(1073741911) + K_KP_ENTER = _const(1073741912) + K_KP_1 = _const(1073741913) + K_KP_2 = _const(1073741914) + K_KP_3 = _const(1073741915) + K_KP_4 = _const(1073741916) + K_KP_5 = _const(1073741917) + K_KP_6 = _const(1073741918) + K_KP_7 = _const(1073741919) + K_KP_8 = _const(1073741920) + K_KP_9 = _const(1073741921) + K_KP_0 = _const(1073741922) + K_KP_PERIOD = _const(1073741923) + K_APPLICATION = _const(1073741925) + K_POWER = _const(1073741926) + K_KP_EQUALS = _const(1073741927) + K_F13 = _const(1073741928) + K_F14 = _const(1073741929) + K_F15 = _const(1073741930) + K_F16 = _const(1073741931) + K_F17 = _const(1073741932) + K_F18 = _const(1073741933) + K_F19 = _const(1073741934) + K_F20 = _const(1073741935) + K_F21 = _const(1073741936) + K_F22 = _const(1073741937) + K_F23 = _const(1073741938) + K_F24 = _const(1073741939) + K_EXECUTE = _const(1073741940) + K_HELP = _const(1073741941) + K_MENU = _const(1073741942) + K_SELECT = _const(1073741943) + K_STOP = _const(1073741944) + K_AGAIN = _const(1073741945) + K_UNDO = _const(1073741946) + K_CUT = _const(1073741947) + K_COPY = _const(1073741948) + K_PASTE = _const(1073741949) + K_FIND = _const(1073741950) + K_MUTE = _const(1073741951) + K_VOLUMEUP = _const(1073741952) + K_VOLUMEDOWN = _const(1073741953) + K_KP_COMMA = _const(1073741957) + K_KP_EQUALSAS400 = _const(1073741958) + K_ALTERASE = _const(1073741977) + K_SYSREQ = _const(1073741978) + K_CANCEL = _const(1073741979) + K_CLEAR = _const(1073741980) + K_PRIOR = _const(1073741981) + K_RETURN2 = _const(1073741982) + K_SEPARATOR = _const(1073741983) + K_OUT = _const(1073741984) + K_OPER = _const(1073741985) + K_CLEARAGAIN = _const(1073741986) + K_CRSEL = _const(1073741987) + K_EXSEL = _const(1073741988) + K_KP_00 = _const(1073742000) + K_KP_000 = _const(1073742001) + K_THOUSANDSSEPARATOR = _const(1073742002) + K_DECIMALSEPARATOR = _const(1073742003) + K_CURRENCYUNIT = _const(1073742004) + K_CURRENCYSUBUNIT = _const(1073742005) + K_KP_LEFTPAREN = _const(1073742006) + K_KP_RIGHTPAREN = _const(1073742007) + K_KP_LEFTBRACE = _const(1073742008) + K_KP_RIGHTBRACE = _const(1073742009) + K_KP_TAB = _const(1073742010) + K_KP_BACKSPACE = _const(1073742011) + K_KP_A = _const(1073742012) + K_KP_B = _const(1073742013) + K_KP_C = _const(1073742014) + K_KP_D = _const(1073742015) + K_KP_E = _const(1073742016) + K_KP_F = _const(1073742017) + K_KP_XOR = _const(1073742018) + K_KP_POWER = _const(1073742019) + K_KP_PERCENT = _const(1073742020) + K_KP_LESS = _const(1073742021) + K_KP_GREATER = _const(1073742022) + K_KP_AMPERSAND = _const(1073742023) + K_KP_DBLAMPERSAND = _const(1073742024) + K_KP_VERTICALBAR = _const(1073742025) + K_KP_DBLVERTICALBAR = _const(1073742026) + K_KP_COLON = _const(1073742027) + K_KP_HASH = _const(1073742028) + K_KP_SPACE = _const(1073742029) + K_KP_AT = _const(1073742030) + K_KP_EXCLAM = _const(1073742031) + K_KP_MEMSTORE = _const(1073742032) + K_KP_MEMRECALL = _const(1073742033) + K_KP_MEMCLEAR = _const(1073742034) + K_KP_MEMADD = _const(1073742035) + K_KP_MEMSUBTRACT = _const(1073742036) + K_KP_MEMMULTIPLY = _const(1073742037) + K_KP_MEMDIVIDE = _const(1073742038) + K_KP_PLUSMINUS = _const(1073742039) + K_KP_CLEAR = _const(1073742040) + K_KP_CLEARENTRY = _const(1073742041) + K_KP_BINARY = _const(1073742042) + K_KP_OCTAL = _const(1073742043) + K_KP_DECIMAL = _const(1073742044) + K_KP_HEXADECIMAL = _const(1073742045) + K_LCTRL = _const(1073742048) + K_LSHIFT = _const(1073742049) + K_LALT = _const(1073742050) + K_LGUI = _const(1073742051) + K_RCTRL = _const(1073742052) + K_RSHIFT = _const(1073742053) + K_RALT = _const(1073742054) + K_RGUI = _const(1073742055) + K_MODE = _const(1073742081) + K_AUDIONEXT = _const(1073742082) + K_AUDIOPREV = _const(1073742083) + K_AUDIOSTOP = _const(1073742084) + K_AUDIOPLAY = _const(1073742085) + K_AUDIOMUTE = _const(1073742086) + K_MEDIASELECT = _const(1073742087) + K_WWW = _const(1073742088) + K_MAIL = _const(1073742089) + K_CALCULATOR = _const(1073742090) + K_COMPUTER = _const(1073742091) + K_AC_SEARCH = _const(1073742092) + K_AC_HOME = _const(1073742093) + K_AC_BACK = _const(1073742094) + K_AC_FORWARD = _const(1073742095) + K_AC_STOP = _const(1073742096) + K_AC_REFRESH = _const(1073742097) + K_AC_BOOKMARKS = _const(1073742098) + K_BRIGHTNESSDOWN = _const(1073742099) + K_BRIGHTNESSUP = _const(1073742100) + K_DISPLAYSWITCH = _const(1073742101) + K_KBDILLUMTOGGLE = _const(1073742102) + K_KBDILLUMDOWN = _const(1073742103) + K_KBDILLUMUP = _const(1073742104) + K_EJECT = _const(1073742105) + K_SLEEP = _const(1073742106) + + _keytable = { + K_UNKNOWN: "Unknown", + K_BACKSPACE: "Backspace", + K_TAB: "Tab", + K_RETURN: "Return", + K_ESCAPE: "Escape", + K_SPACE: "Space", + K_EXCLAIM: "!", + K_QUOTEDBL: '"', + K_HASH: "#", + K_DOLLAR: "$", + K_PERCENT: "%", + K_AMPERSAND: "&", + K_QUOTE: "'", + K_LEFTPAREN: "(", + K_RIGHTPAREN: ")", + K_ASTERISK: "*", + K_PLUS: "+", + K_COMMA: ",", + K_MINUS: "-", + K_PERIOD: ".", + K_SLASH: "/", + K_0: "0", + K_1: "1", + K_2: "2", + K_3: "3", + K_4: "4", + K_5: "5", + K_6: "6", + K_7: "7", + K_8: "8", + K_9: "9", + K_COLON: ":", + K_SEMICOLON: ";", + K_LESS: "<", + K_EQUALS: "=", + K_GREATER: ">", + K_QUESTION: "?", + K_AT: "@", + # 65: " A", + # 66: " B", + # 67: " C", + # 68: " D", + # 69: " E", + # 70: " F", + # 71: " G", + # 72: " H", + # 73: " I", + # 74: " J", + # 75: " K", + # 76: " L", + # 77: " M", + # 78: " N", + # 79: " O", + # 80: " P", + # 81: " Q", + # 82: " R", + # 83: " S", + # 84: " T", + # 85: " U", + # 86: " V", + # 87: " W", + # 88: " X", + # 89: " Y", + # 90: " Z", + K_LEFTBRACKET: "[", + K_BACKSLASH: "\\", + K_RIGHTBRACKET: "]", + K_CARET: "^", + K_UNDERSCORE: "_", + K_BACKQUOTE: "`", + K_a: "A", + K_b: "B", + K_c: "C", + K_d: "D", + K_e: "E", + K_f: "F", + K_g: "G", + K_h: "H", + K_i: "I", + K_j: "J", + K_k: "K", + K_l: "L", + K_m: "M", + K_n: "N", + K_o: "O", + K_p: "P", + K_q: "Q", + K_r: "R", + K_s: "S", + K_t: "T", + K_u: "U", + K_v: "V", + K_w: "W", + K_x: "X", + K_y: "Y", + K_z: "Z", + # 123: "{", + # 124: "|", + # 125: "}", + # 126: "~", + K_DELETE: "Delete", + K_CAPSLOCK: "CapsLock", + K_F1: "F1", + K_F2: "F2", + K_F3: "F3", + K_F4: "F4", + K_F5: "F5", + K_F6: "F6", + K_F7: "F7", + K_F8: "F8", + K_F9: "F9", + K_F10: "F10", + K_F11: "F11", + K_F12: "F12", + K_PRINTSCREEN: "PrintScreen", + K_SCROLLLOCK: "ScrollLock", + K_PAUSE: "Pause", + K_INSERT: "Insert", + K_HOME: "Home", + K_PAGEUP: "PageUp", + K_END: "End", + K_PAGEDOWN: "PageDown", + K_RIGHT: "Right", + K_LEFT: "Left", + K_DOWN: "Down", + K_UP: "Up", + K_NUMLOCKCLEAR: "Numlock", + K_KP_DIVIDE: "Keypad /", + K_KP_MULTIPLY: "Keypad *", + K_KP_MINUS: "Keypad -", + K_KP_PLUS: "Keypad +", + K_KP_ENTER: "Keypad Enter", + K_KP_1: "Keypad 1", + K_KP_2: "Keypad 2", + K_KP_3: "Keypad 3", + K_KP_4: "Keypad 4", + K_KP_5: "Keypad 5", + K_KP_6: "Keypad 6", + K_KP_7: "Keypad 7", + K_KP_8: "Keypad 8", + K_KP_9: "Keypad 9", + K_KP_0: "Keypad 0", + K_KP_PERIOD: "Keypad .", + K_APPLICATION: "Application", + K_POWER: "Power", + K_KP_EQUALS: "Keypad =", + K_F13: "F13", + K_F14: "F14", + K_F15: "F15", + K_F16: "F16", + K_F17: "F17", + K_F18: "F18", + K_F19: "F19", + K_F20: "F20", + K_F21: "F21", + K_F22: "F22", + K_F23: "F23", + K_F24: "F24", + K_EXECUTE: "Execute", + K_HELP: "Help", + K_MENU: "Menu", + K_SELECT: "Select", + K_STOP: "Stop", + K_AGAIN: "Again", + K_UNDO: "Undo", + K_CUT: "Cut", + K_COPY: "Copy", + K_PASTE: "Paste", + K_FIND: "Find", + K_MUTE: "Mute", + K_VOLUMEUP: "VolumeUp", + K_VOLUMEDOWN: "VolumeDown", + K_KP_COMMA: "Keypad ", + K_KP_EQUALSAS400: "Keypad = (AS400)", + K_ALTERASE: "AltErase", + K_SYSREQ: "SysReq", + K_CANCEL: "Cancel", + K_CLEAR: "Clear", + K_PRIOR: "Prior", + K_RETURN2: "Return", + K_SEPARATOR: "Separator", + K_OUT: "Out", + K_OPER: "Oper", + K_CLEARAGAIN: "Clear / Again", + K_CRSEL: "CrSel", + K_EXSEL: "ExSel", + K_KP_00: "Keypad 00", + K_KP_000: "Keypad 000", + K_THOUSANDSSEPARATOR: "ThousandsSeparator", + K_DECIMALSEPARATOR: "DecimalSeparator", + K_CURRENCYUNIT: "CurrencyUnit", + K_CURRENCYSUBUNIT: "CurrencySubUnit", + K_KP_LEFTPAREN: "Keypad (", + K_KP_RIGHTPAREN: "Keypad )", + K_KP_LEFTBRACE: "Keypad {", + K_KP_RIGHTBRACE: "Keypad }", + K_KP_TAB: "Keypad Tab", + K_KP_BACKSPACE: "Keypad Backspace", + K_KP_A: "Keypad A", + K_KP_B: "Keypad B", + K_KP_C: "Keypad C", + K_KP_D: "Keypad D", + K_KP_E: "Keypad E", + K_KP_F: "Keypad F", + K_KP_XOR: "Keypad XOR", + K_KP_POWER: "Keypad ^", + K_KP_PERCENT: "Keypad %", + K_KP_LESS: "Keypad <", + K_KP_GREATER: "Keypad >", + K_KP_AMPERSAND: "Keypad &", + K_KP_DBLAMPERSAND: "Keypad &&", + K_KP_VERTICALBAR: "Keypad |", + K_KP_DBLVERTICALBAR: "Keypad ||", + K_KP_COLON: "Keypad :", + K_KP_HASH: "Keypad #", + K_KP_SPACE: "Keypad Space", + K_KP_AT: "Keypad @", + K_KP_EXCLAM: "Keypad !", + K_KP_MEMSTORE: "Keypad MemStore", + K_KP_MEMRECALL: "Keypad MemRecall", + K_KP_MEMCLEAR: "Keypad MemClear", + K_KP_MEMADD: "Keypad MemAdd", + K_KP_MEMSUBTRACT: "Keypad MemSubtract", + K_KP_MEMMULTIPLY: "Keypad MemMultiply", + K_KP_MEMDIVIDE: "Keypad MemDivide", + K_KP_PLUSMINUS: "Keypad +/-", + K_KP_CLEAR: "Keypad Clear", + K_KP_CLEARENTRY: "Keypad ClearEntry", + K_KP_BINARY: "Keypad Binary", + K_KP_OCTAL: "Keypad Octal", + K_KP_DECIMAL: "Keypad Decimal", + K_KP_HEXADECIMAL: "Keypad Hexadecimal", + K_LCTRL: "Left Ctrl", + K_LSHIFT: "Left Shift", + K_LALT: "Left Alt", + K_LGUI: "Left GUI", + K_RCTRL: "Right Ctrl", + K_RSHIFT: "Right Shift", + K_RALT: "Right Alt", + K_RGUI: "Right GUI", + K_MODE: "ModeSwitch", + K_AUDIONEXT: "AudioNext", + K_AUDIOPREV: "AudioPrev", + K_AUDIOSTOP: "AudioStop", + K_AUDIOPLAY: "AudioPlay", + K_AUDIOMUTE: "AudioMute", + K_MEDIASELECT: "MediaSelect", + K_WWW: "WWW", + K_MAIL: "Mail", + K_CALCULATOR: "Calculator", + K_COMPUTER: "Computer", + K_AC_SEARCH: "AC Search", + K_AC_HOME: "AC Home", + K_AC_BACK: "AC Back", + K_AC_FORWARD: "AC Forward", + K_AC_STOP: "AC Stop", + K_AC_REFRESH: "AC Refresh", + K_AC_BOOKMARKS: "AC Bookmarks", + K_BRIGHTNESSDOWN: "BrightnessDown", + K_BRIGHTNESSUP: "BrightnessUp", + K_DISPLAYSWITCH: "DisplaySwitch", + K_KBDILLUMTOGGLE: "KBDIllumToggle", + K_KBDILLUMDOWN: "KBDIllumDown", + K_KBDILLUMUP: "KBDIllumUp", + K_EJECT: "Eject", + K_SLEEP: "Sleep", + } + + # SDL_Keycode mod values (not complete) + KMOD_NONE = _const(0x0000) + KMOD_LSHIFT = _const(0x0001) + KMOD_RSHIFT = _const(0x0002) + KMOD_LCTRL = _const(0x0040) + KMOD_RCTRL = _const(0x0080) + KMOD_LALT = _const(0x0100) + KMOD_RALT = _const(0x0200) + KMOD_LGUI = _const(0x0400) + KMOD_RGUI = _const(0x0800) + KMOD_NUM = _const(0x1000) + KMOD_CAPS = _const(0x2000) + KMOD_MODE = _const(0x4000) + KMOD_CTRL = KMOD_LCTRL | KMOD_RCTRL + KMOD_SHIFT = KMOD_LSHIFT | KMOD_RSHIFT + KMOD_ALT = KMOD_LALT | KMOD_RALT + KMOD_GUI = KMOD_LGUI | KMOD_RGUI + + _modtable = { + KMOD_NONE: "None", + KMOD_LSHIFT: "Left Shift", + KMOD_RSHIFT: "Right Shift", + KMOD_LCTRL: "Left Ctrl", + KMOD_RCTRL: "Right Ctrl", + KMOD_LALT: "Left Alt", + KMOD_RALT: "Right Alt", + KMOD_LGUI: "Left GUI", + KMOD_RGUI: "Right GUI", + KMOD_NUM: "Num Lock", + KMOD_CAPS: "Caps Lock", + KMOD_MODE: "Mode", + KMOD_CTRL: "Ctrl", + KMOD_SHIFT: "Shift", + KMOD_ALT: "Alt", + KMOD_GUI: "GUI", + } diff --git a/micropython/pydisplay/eventsys/examples/eventsys_encoder_test.py b/micropython/pydisplay/eventsys/examples/eventsys_encoder_test.py new file mode 100644 index 000000000..5e4820224 --- /dev/null +++ b/micropython/pydisplay/eventsys/examples/eventsys_encoder_test.py @@ -0,0 +1,48 @@ +""" +A simple test of an encoder in eventsys. +""" + +from board_config import display_drv, broker + +color_byte = 1 +bg_color = 0xFF00 +w = display_drv.width +h = display_drv.height +thickness = 10 +y_pos = h // 2 +x_pos = w // 2 +factor = -1 # change the sign to invert the direction + + +def draw_line(): + color = color_byte << 8 | color_byte + display_drv.fill_rect(0, 0, x_pos, thickness, color) + display_drv.fill_rect(x_pos, 0, w - x_pos, thickness, bg_color) + + +display_drv.vscsad(y_pos) +draw_line() + +while True: + if not (e := broker.poll()): + continue + if e.type == broker.events.MOUSEWHEEL: + if e.y != 0: + direction = factor if e.y > 0 else -factor + delta = e.y * e.y * direction # Quadratic acceleration + y_pos = (y_pos + delta) % h + display_drv.vscsad(y_pos) + if e.x != 0: + direction = factor if e.x > 0 else -factor + delta = e.x * e.x * direction + x_pos = (x_pos + delta) % w + draw_line() + elif e.type == broker.events.MOUSEBUTTONDOWN: + if e.button == 2: + color_byte = color_byte << 1 & 0xFF + if color_byte == 0: + color_byte = 1 + draw_line() + elif e.button == 3: + bg_color = ~bg_color + draw_line() diff --git a/micropython/pydisplay/eventsys/examples/eventsys_simpletest.py b/micropython/pydisplay/eventsys/examples/eventsys_simpletest.py new file mode 100644 index 000000000..f523d9aeb --- /dev/null +++ b/micropython/pydisplay/eventsys/examples/eventsys_simpletest.py @@ -0,0 +1,18 @@ +from board_config import broker +import asyncio + + +async def main(): + while True: + e = broker.poll() + if e: + print(e) + if e == broker.events.QUIT: + break + await asyncio.sleep(0.001) + + +loop = asyncio.get_event_loop() +main_task = loop.create_task(main()) # noqa: RUF006 +if hasattr(loop, "run_forever"): + loop.run_forever() diff --git a/micropython/pydisplay/eventsys/examples/eventsys_touch_test.py b/micropython/pydisplay/eventsys/examples/eventsys_touch_test.py new file mode 100644 index 000000000..d51d7cac6 --- /dev/null +++ b/micropython/pydisplay/eventsys/examples/eventsys_touch_test.py @@ -0,0 +1,128 @@ +""" +eventsys_touch_test.py - Touch rotation test. +Tests the touch driver and finds the correct rotation masks for the touch screen. +Sets the rotation to each of 4 possible values and asks the user to touch the rectangle in each of the 4 corners. +Then it prints the touch_rotation_table that should be set in board_config.py. +""" + +from board_config import display_drv, broker +from eventsys.devices import types +from graphics import round_rect, text16 + + +demo = False + +FG_COLOR = -1 # white +BG_COLOR = 0 # black + +text = "Touch here" +text_width = len(text) * 8 + +SWAP_XY = 0b001 +REVERSE_X = 0b010 +REVERSE_Y = 0b100 + + +def set_rotation_table(table): + if display_drv.touch_device is not None: + if display_drv.touch_device.type == types.TOUCH: + display_drv.touch_device.rotation_table = table + + +def loop(): + display_drv.fill_rect(0, 0, display_drv.width - 1, display_drv.height - 1, BG_COLOR) + + print("Touch the rectangle in each corner for 4 rotations.\n") + + touch_rotation_table = [] + + for rotation in range(0, 360, 90): + touched_zones = [] + display_drv.rotation = rotation + + width = display_drv.width + height = display_drv.height + half_width = width // 2 + half_height = height // 2 + + for y in range(2): + for x in range(2): + round_rect( + display_drv, + x * half_width + 10, + y * half_height + 10, + half_width - 20, + half_height - 20, + 10, + FG_COLOR, + True, + ) + text16( + display_drv, + text, + x * half_width + ((half_width - text_width) // 2), + y * half_height + ((half_height - 8) // 2), + BG_COLOR, + ) + touched_point = None + while not touched_point: + event = broker.poll() + if event and event.type == broker.events.MOUSEBUTTONDOWN and event.button == 1: + touched_point = event.pos + zone = (touched_point[1] // half_height) * 2 + (touched_point[0] // half_width) + touched_zones.append(zone) + print(f"{touched_point=} in {zone=}") + display_drv.fill_rect( + x * half_width, + y * half_height, + half_width - 1, + half_height - 1, + BG_COLOR, + ) + + if touched_zones == [0, 1, 2, 3]: + mask = 0b0 + elif touched_zones == [1, 0, 3, 2]: + mask = REVERSE_X + elif touched_zones == [2, 3, 0, 1]: + mask = REVERSE_Y + elif touched_zones == [3, 2, 1, 0]: + mask = REVERSE_X | REVERSE_Y + elif touched_zones == [0, 2, 1, 3]: + mask = SWAP_XY + elif touched_zones == [2, 0, 3, 1]: + mask = SWAP_XY | REVERSE_X + elif touched_zones == [1, 3, 0, 2]: + mask = SWAP_XY | REVERSE_Y + elif touched_zones == [3, 1, 2, 0]: + mask = SWAP_XY | REVERSE_X | REVERSE_Y + else: + print("Invalid touch sequence. Starting over...\n") + return False + + touch_rotation_table.append(mask) + print(f"{rotation=} {mask=} ({mask:#05b})\n") + + if not demo: + set_rotation_table(touch_rotation_table) + print("Set the `touch_rotation_table` in board_config.py to the following:") + else: + print("Demo complete.") + out_text = f"touch_rotation_table = {tuple(touch_rotation_table)}" + print(" ", out_text, "\n") + text16( + display_drv, + out_text, + (display_drv.width - len(out_text) * 8) // 2, + (display_drv.height - 8) // 2, + FG_COLOR, + ) + return True + + +if not demo: + set_rotation_table((0, 0, 0, 0)) + +completed = False +while not completed: + completed = loop() diff --git a/micropython/pydisplay/eventsys/manifest.py b/micropython/pydisplay/eventsys/manifest.py new file mode 100644 index 000000000..0b16ea038 --- /dev/null +++ b/micropython/pydisplay/eventsys/manifest.py @@ -0,0 +1,8 @@ +metadata( + description="PyDisplay eventsys", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="eventsys", +) +package("eventsys") diff --git a/micropython/pydisplay/graphics/README.md b/micropython/pydisplay/graphics/README.md new file mode 100644 index 000000000..1a01afa0c --- /dev/null +++ b/micropython/pydisplay/graphics/README.md @@ -0,0 +1,151 @@ +logo + +

pydisplay

+ +

Cross-platform User Interface and Event Drivers for *Python

+ +

+ About • + Key Features • + Getting Started • + Running Your First App • + API • + Roadmap • + Contributing • + Thanks • + Screenshots +

+ +| ![peterhinch's active.py](screenshots/active.gif) | ![russhughes's tiny_toasters.py](screenshots/tiny_toasters.gif) | +|-------------------------|--------------------------------| +| @peterhinch's active.py | @russhughes's tiny_toasters.py | + +## About + +WARNINGS: pydisplay is currently alpha quality. Every effort has been made to test on as many platforms as possible, but I need your help and feedback to get it to its inital release. A lot has changed and I am working on catching up the documentation. + +pydisplay is a universal display, event and device driver framework for multiple flavors of Python, including MicroPython, CircuitPython and CPython (big Python). It may be used as-is to create graphic frontends to your apps, or may be used as a foundation with GUI libraries such as [LVGL](https://github.com/lvgl/lv_micropython), [MicroPython-touch](https://github.com/peterhinch/micropython-touch) or maybe even a GUI framework you've been thinking of developing. Its primary purpose is to provide display and touch drivers for MicroPython, but it is equally useful for developers who may never touch MicroPython. + +It is important to note that pydisplay is meant to be a foundation for GUI libraries and is not itself a GUI library. It doesn't provide widgets, such as buttons, checkboxes or sliders, and it doesn't provide a timing mechanism. You will need a GUI library to provide those if necessary, although many apps won't need them. (There is a cross-platform repository [multimer](https://github.com/PyDevices/pydisplay/tree/main/src/lib/multimer) you can use if you want to used scheduled interrupts. It works with CPython and MicroPython, but doesn't work with CircuitPython. You can also use asyncio for timing.) + +## Key Features + +- May be used without additional libraries to add graphics capabilities to MicroPython, CircuitPython and CPython, with a consistent API across them all. +- Enables moving from one platform to another, for example MicroPython on ESP32-S3 to CPython on Windows without changing your code. Do your graphics development on your desktop, laptop or ChromeBook and then move to a microcontroller when you are ready to interface with your sensors and devices. CPython has much better error messages than MicroPython making it easier to troubleshoot when things go wrong! +- Built around devices available on microcontrollers but not necessarily available on desktop operating systems. For instance, rotary encoders and mouse scroll wheels show up as the same device type and yield the same events. Touchscreens on microcontrollers yield the same events as mice on desktops. Likewise with keypads / keyboards. +- Easily extensible. Use the primitives provided by pydisplay and add your own libraries, classes and functions to have even greater functionality. +- Provides several built-in color palettes and a mechanism to generate your own palettes. +- Lots of examples included, whether developed specifically for pydisplay or ported from [Russ Hughes's st7789py_mpy](https://github.com/russhughes/st7789py_mpy). Also works with all of the examples from Peter Hinch's MicroPython GUI libraries [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) and [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) on MicroPython. +- Support MicroPython on microcontrollers and on Unix(-like) operating systems. +- On MicroPython, can be configured to work with [kdschlosser's lvgl_micropython bus drivers](https://github.com/kdschlosser/lvgl_micropython), which are very fast bus drivers written in C. +- Works with CircuitPython's FourWire and ParallelBus bus drivers, as well as FrameBufferDisplay based interfaces such as dotclockframebuffer, usb_video and rgbmatrix + +## Getting Started + +This section is under construction. For now, see [Getting Started](GETTING-STARTED.md) for more information. + + +## Running your first app + +You will need to import the `path.py` file before running any of the examples. + +On desktop operating systems, `cd` into the `mp` directory (or wherever you have the files staged) and type: +``` +python3 -i path.py +``` +or +``` +micropython -i path.py +``` + +On microcontrollers, either add the following to your `boot.py` (MicroPython) or `code.py` (CircuitPython), or simply import it at the REPL before importing your desired app: +``` +import lib.path +``` + +The [examples](examples) directory will be on the system path, so to run an app from it, you just need to type: +``` +import calculator # substitute `calculator` with the file OR directory you want to run, omitting the .py extension +``` + +To run any of the examples from MicroPython-Touch (remember, its for MicroPython only) type: +``` +import gui.demos.various # substitute `various` with the file you want to run, omitting the .py extension +``` + +## API + +Where possible, existing, proven APIs were used. + +- There are currently 5 display classes, and hopefully another `EPaperDisplay` display class will be added soon, although I will need help from the community for this. + - BusDisplay is for microcontrollers, both on MicroPython and CircuitPython. CircuitPython provides the required bus drivers, as mentioned elsewhere in this README, but MicroPython doesn't have display bus drivers. The [buses](src/lib/buses) packages are included with the installer. It is my hope that community members will create other C bus drivers similar to @kdschlosser's bus drivers in [lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython). + - SDL2Display - the preferred class for desktop operating systems as it is faster than PGDisplay. It uses an SDL `texture` in place of an LCD's Graphics RAM (GRAM). + - PGDisplay - an optional class for desktop operating systems. It uses a pygame `surface` in place of an LCD's GRAM. It can be benificial in a couple of instances: + - SDL2Display "glitches" on my ChromeBook, but PGDisplay doesn't + - On Windows, it is easier to install PyGame than SDL2 + - FBDisplay works with CircuitPython framebufferio.FramebufferDisplay objects, such as dotclockframebuffer (RGB displays), usb_video and rgbmatrix. (usb_video may be the coolest thing you can do with displaysys, although I'm not sure how practical or useful it is. It allows your board to function as a webcam, even without a camera, and to render the display through USB to any application on your host PC that can open a webcam! My Windows machine sees it as an unsupported device, so it will not work, but it does work on my ChromeBook. Currently it is limited to RP2040 only and is hardcoded to a 128 x 96 resolution, but that likely will change. See the [screen capture](examples/circuitpython_usb_video_chromebook.gif) and the [board_config.py](board_configs/circuitpython/usb_video/board_config.py) for more details.) + - JNDisplay for Jupyter Notebooks. No input devices are currently supported. + - PSDisplay for PyScript. Only touchscreens are currently supported. +- Names of events and Devices in [eventsys](src/lib/eventsys/) are taken from PyGame and/or SDL2 to keep the API consistent. +- All drawing targets, sometimes referred to as `canvas` in the code, may be written to using the API from MicroPython's framebuf.FrameBuffer API + - CPython and CircuitPython don't have a `framebuf` module that is API compliant with MicroPython's `framebuf`, so [framebuf.py](add_ons/framebuf.py) is provided for those platforms. It is not used in MicroPython unless framebuf wasn't compiled in. + - A `graphics` module is provided that subclasses `FrameBuffer` (either built-in or from framebuf.py) and provides additional drawing tools, such as `round_rect`. All methods in graphics return an Area object with x, y, w and h attributes describing a bounding box of what was changed. This can be used by applications to only update the part of the display that needs it. That functionality is implemented in DisplayBuffer and will likely be required by EPaperDisplay when it is implemented. + - Canvases include, but are not limited to, the display itself, framebuf bytearrays, bmp565 (16-bit Windows Bitmap files) and displaybuf.DisplayBuffer objects. + - displaybuf.DisplayBuffer implements @peterhinch's API that represents the full display as a framebuffer and allows for 4-, 8- and 16-bit bytearrays while still drawing to the screen as 16-bit. It is required for `MicroPython-Touch` and is very useful outside of that library as well, especially when memory is constrained. +- Display drivers for MicroPython BusDisplay use the constructor API of CircuitPython's DisplayIO drivers. This includes rotation = 0, 90, 180, 270 instead of 0, 1, 2, 3. +- BusDisplay can communicate with the underlying bus driver using either CircuitPython's DisplayIO method calls or @kdschlosser's [lvgl_micropython] method calls. +- There are 3 primary mechanism's for fonts: the graphics.Font class, tft_text.text() and tft_write.write() methods. All 3 of these return an Area object as mentioned earlier. A fourth font mechanism called EZFont is included in the utils folder, but it doesn't return an Area object, which is why it isn't in the lib folder. + - Font is derived from Tony DiCola's 5x7 font class and reads 8x8, 8x14 and 8x16 .bin files from [@spaceraces romfont repo](https://github.com/spacerace/romfont) + - .text() is written by @russhughes and uses fonts generated by his [text_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/text_font_converter.py) It reads 8 and 16bit wide fonts in heights that are multiples of 8. + - .write() is written by @russhughes and uses fonts generated by his [write_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/write_font_converter.py) + - EZFont is a subclass of [@easytarget's microPyEZfonts](https://github.com/easytarget/microPyEZfonts) which uses fonts generated from [@peterhinch's font-to-py](https://github.com/peterhinch/micropython-font-to-py). + - NOTE: @peterhinch's Writer class is inlcuded in MicroPyton-Touch and may be used on MicroPython platforms, but, like EZFont, it doesn't return an Area object. +- Graphics files may be used by 3 mechanisms: + - bmp565.BM565 is a class that can read and write Windows Bitmap files saved with RGB565 color encoding. GIMP supports exporting RGB565 .BMPs. The BMP565 class can open a file and read it's entire contents into memory, or with the `streamed = True` flag, it will only read the slice requested, allowing progressive rendering of files much too large to fit into memory. The slice can be 2 dimensional (BMP565[1:5, 6:10] gets pixels 1 through 5 on rows 6 through 10) or 1 dimensional (BMP565[6:10] gets all pixels in rows 6 through 10). This slicing mechanism is very useful when rendering sprites. It can reverse the order of pixels in a row with `mirrored = False`, which is needed when rendering a background image when rotation is 90 or 270 and (horizontal) scrolling is desired. Finally, it can use an existing bytearray as its buffer instead of reading from a file, which allows saving screenshots from existing canvases such as a FrameBuffer or DisplayBuffer. + - .bitmap() is written by @russhughes and reads .py graphics files encoded with his [image_converter.py utiliity](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or his [sprite_converter.py utility](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py). It renders the entire image to a buffer, and then copies that buffer to the display. + - .pbitmap() is also written by @russhughes and renders the same fonts as .bitmap(), but it does it progressively, one line at a time using a one line buffer. +- Config files - All files that are intended for you to edit to customize your configuration are in the [configs](src/configs/) directory. They are: + - `board_config.py` - required in all circumstances. Feel free to add your own setup code here, such as for real-time clocks, wifi, sensors, etc. + - `path.py` - required in all circumstances + - `color_setup.py` - required for [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) + - `hardware_setup.py` - required for [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) + - `lv_config.py` - required for LVGL + - `tft_config.py` - required for @russhughes's examples. I had to do some search and replace to get those examples to work. + + +## Roadmap + +- [ ] Much more documentation on Github +- [ ] Document the files to produce output for ReadTheDocs +- [ ] Implement EPaperDisplay +- [ ] Optimize with more Numpy and Viper code +- [ ] Decrease the memory footprint where possible +- [ ] Test with frozen modules +- [ ] On MicroPython on Unix, the screen gets cleared when the display is rotated. Microcontroller displays don't do this. It's not an issue unless you want to draw to the display, rotate it, then draw more on top. This functionality allow drawing text in all four 90 degree orientations. +- [ ] Scrolling vertically on desktop operating sytems works correctly, but not when rotated. When rotated, it show scroll horizontally, but continues to scroll vertically. +- [ ] Scrolling on microcontrollers has issues when trying to write spanning the cutoff line. For instance, if drawing a 16 pixel high image at the 8th line from the cutoff line, the bottom 8 lines don't end up where you expect. See the [bmp565_sprite](examples/bmp565_sprite.py) example. +- [ ] Ensure multiple displays work at the same time +- [ ] Implement color depths other than 16 bit +- [ ] Add a Joystick class to eventsys +- [ ] Test with CircuitPython Blinka on SBC's such as Raspberry Pi 4 +- [ ] Need C bus drivers from the community, especially for STM32H7 and MIMXRT + +## Contributing + +This is a community project and I need your help! If you have a suggestion that would make this better, please fork the repo and create a pull request. +Don't forget to give the project a star! Thanks again! + +1. Fork the project +2. Clone it open the repository in command line +3. Create your feature branch (`git checkout -b feature/amazing-feature`) +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a pull request from your feature branch from your repository into this repository main branch, and provide a description of your changes + +## Thanks + +I very much appreciate @peterhinch, @russhughes and the team at Adafruit for their contributions to the Python on microcontrollers community. + +## Why + +I started out just wanting to create drivers that worked with MicroPython the way DisplayIO drivers work for CircuitPython, except without DisplayIO and instead usable by any GUI framework like, but not limited to, LVGL. That snowballed into adding more platforms and then adding drawing primitives, font classes, palettes, an event system, a barebones SDL2 library, a Bitmap 565 reader/writer and supporting as many platforms as possible. I stopped short of creating a full fledged GUI and plan to leave it as a very capable graphics library. I think this is a great foundation for building a GUI framework with widgets and a task scheduler, although it is very usable and useful without one. @peterhinch has a great GUI for MicroPython that works on top of pydisplay, and I'm hoping someone will make a GUI that works across platforms. diff --git a/micropython/pydisplay/graphics/examples/graphics_area_test.py b/micropython/pydisplay/graphics/examples/graphics_area_test.py new file mode 100644 index 000000000..39e836c77 --- /dev/null +++ b/micropython/pydisplay/graphics/examples/graphics_area_test.py @@ -0,0 +1,23 @@ +""" +Test the Area return type of the shapes functions. + +Shape functions return an Area object that represents the bounding box of the shape drawn. +This object can be used to optimize the display by redrawing only the area that has changed. + +Area objects have the attributes x, y, w and h. They may be added together, such as: + area3 = area1 + area2 + +and may be unpacked, such as: + x, y, w, h = area3 +or as a function argument: + rect(display_drv, *area3, 0x00FF) + +""" + +from board_config import display_drv +from graphics import rect, circle, ellipse + + +dirty = circle(display_drv, 120, 120, 50, 0xFF00, True) +dirty += ellipse(display_drv, 100, 85, 50, 30, 0x0FF0, True, 0b1111) +rect(display_drv, *dirty, 0x00FF) diff --git a/micropython/pydisplay/graphics/examples/graphics_simpletest.py b/micropython/pydisplay/graphics/examples/graphics_simpletest.py new file mode 100644 index 000000000..73612eae8 --- /dev/null +++ b/micropython/pydisplay/graphics/examples/graphics_simpletest.py @@ -0,0 +1,72 @@ +""" +Simple test example to demonstrate the use of graphics. +""" + +from board_config import display_drv +from array import array # for defining a polygon +from palettes import get_palette +import graphics + + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +WIDTH = display_drv.width +HEIGHT = display_drv.height +FONT_WIDTH = 8 + +# Define color palette +pal = get_palette(swapped=needs_swap) + +# Define objects +triangle = array("h", [0, 0, WIDTH // 2, -HEIGHT // 4, WIDTH - 1, 0]) + + +# Main loop +def main(animate=False, text1="Shapes", text2="simpletest", poly=triangle): + y_range = range(HEIGHT - 1, -1, -1) if animate else [HEIGHT - 1] + for y in y_range: + graphics.fill(display_drv, pal.BLACK) + graphics.poly(display_drv, 0, y, poly, pal.YELLOW, True) + graphics.fill_rect( + display_drv, + WIDTH // 6, + HEIGHT // 3, + WIDTH * 2 // 3, + HEIGHT // 3, + pal.GREY, + ) + graphics.line(display_drv, 0, 0, WIDTH - 1, HEIGHT - 1, pal.GREEN) + graphics.rect(display_drv, 0, 0, 15, 15, pal.RED, True) + graphics.rect(display_drv, WIDTH - 15, HEIGHT - 15, 15, 15, pal.BLUE, True) + graphics.hline(display_drv, WIDTH // 8, HEIGHT // 2, WIDTH * 3 // 4, pal.MAGENTA) + graphics.vline(display_drv, WIDTH // 2, HEIGHT // 4, HEIGHT // 2, pal.CYAN) + graphics.pixel(display_drv, WIDTH // 2, HEIGHT * 1 // 8, pal.WHITE) + graphics.ellipse( + display_drv, + WIDTH // 2, + HEIGHT // 2, + WIDTH // 4, + HEIGHT // 8, + pal.BLACK, + True, + 0b1111, + ) + graphics.text( + display_drv, text1, (WIDTH - FONT_WIDTH * len(text1)) // 2, HEIGHT // 2 - 8, pal.WHITE + ) + graphics.text( + display_drv, text2, (WIDTH - FONT_WIDTH * len(text2)) // 2, HEIGHT // 2, pal.WHITE + ) + + graphics.hline(display_drv, 0, 0, WIDTH, pal.BLACK) + graphics.vline(display_drv, 0, 0, HEIGHT, pal.BLACK) + + +launch = lambda: main(animate=True) # noqa: E731 + +main() diff --git a/micropython/pydisplay/graphics/graphics/__init__.py b/micropython/pydisplay/graphics/graphics/__init__.py new file mode 100644 index 000000000..ecc4b0935 --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/__init__.py @@ -0,0 +1,79 @@ +""" +`graphics` +==================================================== +Graphics library extending MicroPython's framebuf module. +""" + +from ._area import Area +from ._draw import Draw +from ._font import Font, text, text8, text14, text16 +from ._files import pbm_to_framebuffer, pgm_to_framebuffer, bmp_to_framebuffer +from ._framebuf_plus import ( + FrameBuffer, + MONO_VLSB, + MONO_HLSB, + MONO_HMSB, + GS2_HMSB, + GS4_HMSB, + GS8, + RGB565, +) +from ._shapes import ( + arc, + blit, + blit_rect, + blit_transparent, + circle, + ellipse, + fill, + fill_rect, + gradient_rect, + hline, + line, + pixel, + poly, + polygon, + rect, + round_rect, + triangle, + vline, +) + +__all__ = [ + "Area", + "Draw", + "Font", + "text", + "text8", + "text14", + "text16", + "pbm_to_framebuffer", + "pgm_to_framebuffer", + "bmp_to_framebuffer", + "FrameBuffer", + "MONO_VLSB", + "MONO_HLSB", + "MONO_HMSB", + "GS2_HMSB", + "GS4_HMSB", + "GS8", + "RGB565", + "arc", + "blit", + "blit_rect", + "blit_transparent", + "circle", + "ellipse", + "fill", + "fill_rect", + "gradient_rect", + "hline", + "line", + "pixel", + "poly", + "polygon", + "rect", + "round_rect", + "triangle", + "vline", +] diff --git a/micropython/pydisplay/graphics/graphics/_area.py b/micropython/pydisplay/graphics/graphics/_area.py new file mode 100644 index 000000000..285ed33d2 --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_area.py @@ -0,0 +1,272 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`graphics._area` +==================================================== +Area class for defining rectangular areas. +""" + + +class Area: + """ + Represents a rectangular area defined by its position and dimensions. + + Attributes: + x (int | float): The x-coordinate of the top-left corner of the area. + y (int | float): The y-coordinate of the top-left corner of the area. + w (int | float): The width of the area. + h (int | float): The height of the area. + + Methods: + contains(x, y): Checks if the specified point is contained within the area. + contains_area(other): Checks if the specified area is contained within the area. + intersects(other): Checks if the current Area object intersects with another Area object. + touches_or_intersects(other): Checks if the current Area object touches or intersects with another Area object. + shift(dx=0, dy=0): Returns a new Area shifted by the specified amount in the x and y directions. + clip(other): Clips the current Area object to the specified Area object. + + Special Methods: + __eq__(other): Checks if the current Area object is equal to another Area object. + __ne__(other): Checks if the current Area object is not equal to another Area object. + __add__(other): Computes the union of the current Area object and another Area object. + __iter__(): Returns an iterator over the elements of the Area object. + __repr__(): Returns a string representation of the Area object. + __str__(): Returns a string representation of the Area object. + """ + + def __init__(self, x, y=None, w=None, h=None): + """ + Initializes a new instance of the Area class. + + Args: + x (int | float | tuple): The x-coordinate of the top-left corner of the area or + a tuple containing the x, y, w, and h coordinates of the area. + y (int | float): The y-coordinate of the top-left corner of the area. + w (int | float): The width of the area. + h (int | float): The height of the area. + """ + if isinstance(x, tuple): + x, y, w, h = x + if y is None or w is None or h is None: + raise ValueError("Invalid arguments") + self.x = x + self.y = y + self.w = w + self.h = h + + def contains(self, x, y=None): + """ + Checks if the specified point is contained within the area. + + Args: + x (int | tuple): The x-coordinate of the point to check + or a tuple containing the x and y coordinates of the point. + y (int): The y-coordinate of the point to check. + + Returns: + (bool): True if the point is contained within the area, False otherwise. + """ + if isinstance(x, tuple): + x, y = x + return self.x <= x < self.x + self.w and self.y <= y < self.y + self.h + + def contains_area(self, other): + """ + Checks if the specified area is contained within the area. + + Args: + other (Area): The other area to check. + + Returns: + (bool): True if the other area is contained within the area, False otherwise. + """ + return ( + self.x <= other.x + and self.y <= other.y + and self.x + self.w >= other.x + other.w + and self.y + self.h >= other.y + other.h + ) + + def intersects(self, other): + """ + Checks if the current Area object intersects with another Area object. + + Args: + other (Area): The other Area object to check for overlap. + + Returns: + (bool): True if the two Area objects intersect, False otherwise. + """ + if self.x + self.w <= other.x or other.x + other.w <= self.x: + return False + if self.y + self.h <= other.y or other.y + other.h <= self.y: + return False + return True + + def touches_or_intersects(self, other): + """ + Checks if the current Area object touches or intersects with another Area object. + + Args: + other (Area): The other Area object to check for overlap or touch. + + Returns: + (bool): True if the two Area objects touch or intersect, False otherwise. + """ + if self.x + self.w < other.x or other.x + other.w < self.x: + return False + if self.y + self.h < other.y or other.y + other.h < self.y: + return False + return True + + def shift(self, dx=0, dy=0): + """ + Returns a new Area shifted by the specified amount in the x and y directions. + + Args: + dx (int | float): The amount to shift the area in the x direction. + dy (int | float): The amount to shift the area in the y direction. + + Returns: + (Area): A new Area object shift by the specified amount in the x and y directions. + """ + return Area(self.x + dx, self.y + dy, self.w, self.h) + + def clip(self, other): + """ + Clips the current Area object to the specified Area object. + + Args: + other (Area): The other Area object to clip to. + + Returns: + (Area): A new Area object representing the clipped area. + """ + x = max(self.x, other.x) + y = max(self.y, other.y) + w = min(self.x + self.w, other.x + other.w) - x + h = min(self.y + self.h, other.y + other.h) - y + return Area(x, y, w, h) + + def offset(self, d1, d2=None, d3=None, d4=None): + """ + Returns a new Area offset by the specified amount(s). + + If only one argument is provided, it is used as the offset in all 4 directions. + If two arguments are provided, the first is used as the offset in the x direction and the second as the offset in the y direction. + If three arguments are provided, they are used as the offsets in the left, top/bottom, and right directions, respectively. + If four arguments are provided, they are used as the offsets in the left, top, right, and bottom directions, respectively. + + Args: + d1 (int | float): The offset in the x direction or the offset in all 4 directions. + d2 (int | float): The offset in the y direction or the offset in the top/bottom direction. + d3 (int | float): The offset in the right direction. + d4 (int | float): The offset in the bottom direction. + + Returns: + (Area): A new Area object offset by the specified amount(s). + """ + if d2 is None: + d2 = d3 = d4 = d1 + elif d3 is None: + d3 = d1 + d4 = d2 + elif d4 is None: + d4 = d2 + return Area(self.x - d1, self.y - d2, self.w + d1 + d3, self.h + d2 + d4) + + def inset(self, d1, d2=None, d3=None, d4=None): + """ + Returns a new Area inset by the specified amount(s). + + If only one argument is provided, it is used as the inset in all 4 directions. + If two arguments are provided, the first is used as the inset in the x direction and the second as the inset in the y direction. + If three arguments are provided, they are used as the insets in the left, top/bottom, and right directions, respectively. + If four arguments are provided, they are used as the insets in the left, top, right, and bottom directions, respectively. + + Args: + d1 (int | float): The inset in the x direction or the inset in all 4 directions. + d2 (int | float): The inset in the y direction or the inset in the top/bottom direction. + d3 (int | float): The inset in the right direction. + d4 (int | float): The inset in the bottom direction. + + Returns: + (Area): A new Area object inset by the specified amount(s). + """ + if d2 is None: + d2 = d3 = d4 = d1 + elif d3 is None: + d3 = d1 + d4 = d2 + elif d4 is None: + d4 = d2 + return Area(self.x + d1, self.y + d2, self.w - d1 - d3, self.h - d2 - d4) + + def __eq__(self, other): + """ + Checks if the current Area object is equal to another Area object. + + Args: + other (Area): The other Area object to compare with. + + Returns: + (bool): True if the two Area objects are equal, False otherwise. + """ + return self.x == other.x and self.y == other.y and self.w == other.w and self.h == other.h + + def __ne__(self, other): + """ + Checks if the current Area object is not equal to another Area object. + + Args: + other (Area): The other Area object to compare with. + + Returns: + (bool): True if the two Area objects are not equal, False otherwise. + """ + return not self.__eq__(other) + + def __add__(self, other): + """ + Computes the union of the current Area object and another Area object. + + Args: + other (Area): The other Area object to compute the union with. + + Returns: + (Area): A new Area object representing the union of the two areas. + """ + return Area( + min(self.x, other.x), + min(self.y, other.y), + max(self.x + self.w, other.x + other.w) - min(self.x, other.x), + max(self.y + self.h, other.y + other.h) - min(self.y, other.y), + ) + + def __iter__(self): + """ + Returns an iterator over the elements of the Area object. + + Returns: + (iterator): An iterator over the elements of the Area object. + """ + return iter((self.x, self.y, self.w, self.h)) + + def __repr__(self): + """ + Returns a string representation of the Area object. + + Returns: + (str): A string representation of the Area object. + """ + return f"Area({self.x}, {self.y}, {self.w}, {self.h})" + + def __str__(self): + """ + Returns a string representation of the Area object. + + Returns: + (str): A string representation of the Area object. + """ + return f"Area({self.x}, {self.y}, {self.w}, {self.h})" diff --git a/micropython/pydisplay/graphics/graphics/_draw.py b/micropython/pydisplay/graphics/graphics/_draw.py new file mode 100644 index 000000000..b676643d3 --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_draw.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`graphics._draw` +==================================================== +Graphics Draw class +""" + +from . import _shapes +from . import _font + + +class Draw: + """ + A Draw class to draw shapes onto a specified canvas. + + Args: + canvas (Canvas): The canvas to draw on. + + Usage: + ``` + # canvas is an instance of DisplayDriver, FrameBuffer, or other canvas-like object + draw = Draw(canvas) + draw.fill(0x0000) + draw.rect(10, 10, 100, 100, 0xFFFF) + ``` + """ + + def __init__(self, canvas): + self.canvas = canvas + + def arc(self, x, y, r, a0, a1, c): + return _shapes.arc(self.canvas, x, y, r, a0, a1, c) + + def blit(self, source, x, y, key=-1, palette=None): + return _shapes.blit(self.canvas, source, x, y, key, palette) + + def blit_rect(self, buf, x, y, w, h): + return _shapes.blit_rect(self.canvas, buf, x, y, w, h) + + def blit_tranparent(self, buf, x, y, w, h, key=None): + return _shapes.blit_transparent(self.canvas, buf, x, y, w, h, key) + + def circle(self, x, y, r, c, f=False): + return _shapes.circle(self.canvas, x, y, r, c, f) + + def ellipse(self, x, y, r1, r2, c, f=False, m=0b1111, w=None, h=None): + return _shapes.ellipse(self.canvas, x, y, r1, r2, c, f, m, w, h) + + def fill(self, c): + return _shapes.fill(self.canvas, c) + + def fill_rect(self, x, y, w, h, c): + return _shapes.fill_rect(self.canvas, x, y, w, h, c) + + def gradient_rect(self, x, y, w, h, c1, c2=None, vertical=True): + return _shapes.gradient_rect(self.canvas, x, y, w, h, c1, c2, vertical) + + def hline(self, x, y, w, c): + return _shapes.hline(self.canvas, x, y, w, c) + + def line(self, x1, y1, x2, y2, c): + return _shapes.line(self.canvas, x1, y1, x2, y2, c) + + def pixel(self, x, y, c): + return _shapes.pixel(self.canvas, x, y, c) + + def poly(self, x, y, coords, c, f=False): + return _shapes.poly(self.canvas, x, y, coords, c, f) + + def polygon(self, points, x, y, c, angle=0, center_x=0, center_y=0): + return _shapes.polygon(self.canvas, points, x, y, c, angle, center_x, center_y) + + def rect(self, x, y, w, h, c, f=False): + return _shapes.rect(self.canvas, x, y, w, h, c, f) + + def round_rect(self, x, y, w, h, r, c, f=False): + return _shapes.round_rect(self.canvas, x, y, w, h, r, c, f) + + def triangle(self, x1, y1, x2, y2, x3, y3, c, f=False): + return _shapes.triangle(self.canvas, x1, y1, x2, y2, x3, y3, c, f) + + def vline(self, x, y, h, c): + return _shapes.vline(self.canvas, x, y, h, c) + + def text(self, *args, **kwargs): + return _font.text(self.canvas, *args, **kwargs) + + def text8(self, *args, **kwargs): + return _font.text8(self.canvas, *args, **kwargs) + + def text14(self, *args, **kwargs): + return _font.text14(self.canvas, *args, **kwargs) + + def text16(self, *args, **kwargs): + return _font.text16(self.canvas, *args, **kwargs) diff --git a/micropython/pydisplay/graphics/graphics/_files.py b/micropython/pydisplay/graphics/graphics/_files.py new file mode 100644 index 000000000..3362a4379 --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_files.py @@ -0,0 +1,86 @@ +from ._framebuf_plus import FrameBuffer, MONO_HLSB, GS2_HMSB, GS4_HMSB, GS8, RGB565 +import struct + + +def pbm_to_framebuffer(filename): + """ + Convert a PBM file to a MONO_HLSB FrameBuffer + + Args: + filename (str): Filename of the PBM file + """ + with open(filename, "rb") as f: + if f.read(3) != b"P4\n": + raise ValueError(f"Invalid PBM file {filename}") + data = f.read() # Read the rest as binary, since MicroPython can't do readline here + while data[0] == 35: # Ignore comment lines starting with b'#' + data = data.split(b"\n", 1)[1] + dims, data = data.split(b"\n", 1) # Assumes no comments after dimensions + width, height = map(int, dims.split()) + buffer = memoryview(bytearray((width + 7) // 8 * height)) + buffer[:] = data + return FrameBuffer(buffer, width, height, MONO_HLSB) + + +def pgm_to_framebuffer(filename): + """ + Convert a PGM file to a GS2_HMSB, GS4_HMSB or GS8 FrameBuffer + + Args: + filename (str): Filename of the PGM file + """ + with open(filename, "rb") as f: + if f.read(3) != b"P5\n": + raise ValueError(f"Invalid PGM file {filename}") + data = f.read() # Read the rest as binary, since MicroPython can't do readline here + while data[0] == 35: # Ignore comment lines starting with b'#' + data = data.split(b"\n", 1)[1] + dims, data = data.split(b"\n", 1) + width, height = map(int, dims.split()) + while data[0] == 35: # Ignore comment lines starting with b'#' + data = data.split(b"\n", 1)[1] + max_val_b, data = data.split(b"\n", 1) # Assumes no comments after max val + max_value = int(max_val_b) + if max_value == 3: + format = GS2_HMSB + array_size = (width + 3) // 4 * height + elif max_value == 15: + format = GS4_HMSB + array_size = (width + 1) // 2 * height + elif max_value == 255: + format = GS8 + array_size = width * height + else: + raise ValueError(f"Unsupported max value {max_value}") + buffer = memoryview(bytearray(array_size)) + buffer[:] = data + return FrameBuffer(buffer, width, height, format) + + +def bmp_to_framebuffer(filename): + """ + Convert a BMP file to a RGB565 FrameBuffer. + First ensures planes is 1, bits per pixel is 16, and compression is 0. + + Args: + filename (str): Filename of the + """ + with open(filename, "rb") as f: + if f.read(2) != b"BM": + raise ValueError("Not a BMP file") + f.seek(10) + data_offset = struct.unpack("= canvas.width or \ + # y < -self.height or y >= canvas.height: + # return + # Go through each row of the character. + for char_y in range(self._font_height): + # Grab the byte for the current row of font data. + if not (line := self._read_line(char, char_y)): + continue # maybe character isnt there? go to next + # Go through each column in the row byte. + for char_x in range(self.width): + # Draw a pixel for each bit that's flipped on. + if (line >> (self.width - char_x - 1)) & 0x1: + canvas.fill_rect( + ( + x + char_x * scale + if not inverted + else x + (self._font_width - char_x - 1) * scale + ), + ( + y + char_y * scale + if not inverted + else y + (self._font_height - char_y - 1) * scale + ), + scale, + scale, + color, + ) + return Area(x, y, self._font_width * scale, self._font_height * scale) + + def text(self, canvas, string, x, y, color, scale=1, inverted=False): + """ + Draw text to the canvas. + + Args: + canvas (Canvas): The DisplayDriver, FrameBuffer, or other canvas-like object to draw on. + string (str): The text to draw. + x (int): The x position to start drawing the text. + y (int): The y position to start drawing the text. + color (int): The color to draw the text in. + scale (int): The scale factor to draw the text at. Default is 1. + inverted (bool): If True, draw the text inverted. Default is False. + + Returns: + (Area): The area that was drawn to. + """ + if inverted: + string = "".join(reversed(string)) + + char_y = y + largest_x = 0 # the last x position reached on the longest line + for chunk in string.split("\n"): + last_x = x # the last x position reached on the current line + for i, char in enumerate(chunk): + char_x = x + (i * self.width * scale) + if char_x < canvas.width if hasattr(canvas, "width") else True: + if char_y < canvas.height if hasattr(canvas, "height") else True: + if char_x + (self.width * scale) > 0: + if char_y + (self.height * scale) > 0: + self.draw_char( + char, + char_x, + char_y, + canvas, + color, + scale=scale, + inverted=inverted, + ) + last_x = char_x + (self.width * scale) + largest_x = max([largest_x, last_x]) # update the largest x position + char_y += self.height * scale + return Area(x, y, largest_x - x, char_y - y) + + def text_width(self, text, scale=1): + """ + Return the pixel width of the specified text message. + Takes into account the scale factor, but not any newlines. + + Args: + text (str): The text to measure. + scale (int): The scale factor to measure the text at. Default is 1. + """ + return len(text) * self._font_width * scale + + def _read_line(self, char, line): + """Read a line of font data for a character.""" + if self._cache: + return self._cache[(ord(char) * self.height) + line] + + self._font.seek((ord(char) * self.height) + line) + try: + return struct.unpack("B", self._font.read(1))[0] + except RuntimeError: # maybe character isnt there? go to next + return None + + def export(self, filename): + """ + Export the font data in self._cache to a .py file that can be imported. + The format is a single bytes object named _FONT. There are 256 lines, one for each character. + The last line is `FONT = memoryview(_FONT)`. + + Args: + filename (str): The path to save the file to. + """ + if not self._cache: + raise RuntimeError("Font data not cached, cannot export") + mv = memoryview(self._cache) + with open(filename, "w") as f: + f.write("_FONT =\\\n") + for i in range(256): + f.write("b'") + for j in range(self.height): + f.write(f"\\x{mv[(i * self.height) + j]:02x}") + f.write("'\\\n") + f.write("\nFONT = memoryview(_FONT)\n") + print(f"Font data saved to {filename}") diff --git a/micropython/pydisplay/graphics/graphics/_font_8x14.py b/micropython/pydisplay/graphics/graphics/_font_8x14.py new file mode 100644 index 000000000..a4810bc68 --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_font_8x14.py @@ -0,0 +1,259 @@ +_FONT = ( + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x7e\x81\xa5\x81\x81\xbd\x99\x81\x7e\x00\x00\x00" + b"\x00\x00\x7e\xff\xdb\xff\xff\xc3\xe7\xff\x7e\x00\x00\x00" + b"\x00\x00\x00\x6c\xfe\xfe\xfe\xfe\x7c\x38\x10\x00\x00\x00" + b"\x00\x00\x00\x10\x38\x7c\xfe\x7c\x38\x10\x00\x00\x00\x00" + b"\x00\x00\x18\x3c\x3c\xe7\xe7\xe7\x18\x18\x3c\x00\x00\x00" + b"\x00\x00\x18\x3c\x7e\xff\xff\x7e\x18\x18\x3c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x18\x3c\x3c\x18\x00\x00\x00\x00\x00" + b"\xff\xff\xff\xff\xff\xe7\xc3\xc3\xe7\xff\xff\xff\xff\xff" + b"\x00\x00\x00\x00\x3c\x66\x42\x42\x66\x3c\x00\x00\x00\x00" + b"\xff\xff\xff\xff\xc3\x99\xbd\xbd\x99\xc3\xff\xff\xff\xff" + b"\x00\x00\x1e\x0e\x1a\x32\x78\xcc\xcc\xcc\x78\x00\x00\x00" + b"\x00\x00\x3c\x66\x66\x66\x3c\x18\x7e\x18\x18\x00\x00\x00" + b"\x00\x00\x3f\x33\x3f\x30\x30\x30\x70\xf0\xe0\x00\x00\x00" + b"\x00\x00\x7f\x63\x7f\x63\x63\x63\x67\xe7\xe6\xc0\x00\x00" + b"\x00\x00\x18\x18\xdb\x3c\xe7\x3c\xdb\x18\x18\x00\x00\x00" + b"\x00\x00\x80\xc0\xe0\xf8\xfe\xf8\xe0\xc0\x80\x00\x00\x00" + b"\x00\x00\x02\x06\x0e\x3e\xfe\x3e\x0e\x06\x02\x00\x00\x00" + b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x00\x00\x00" + b"\x00\x00\x66\x66\x66\x66\x66\x66\x00\x66\x66\x00\x00\x00" + b"\x00\x00\x7f\xdb\xdb\xdb\x7b\x1b\x1b\x1b\x1b\x00\x00\x00" + b"\x00\x7c\xc6\x60\x38\x6c\xc6\xc6\x6c\x38\x0c\xc6\x7c\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\xfe\x00\x00\x00" + b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x7e\x00\x00" + b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x18\x18\x18\x00\x00\x00" + b"\x00\x00\x18\x18\x18\x18\x18\x18\x7e\x3c\x18\x00\x00\x00" + b"\x00\x00\x00\x00\x18\x0c\xfe\x0c\x18\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x30\x60\xfe\x60\x30\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc0\xc0\xc0\xfe\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x28\x6c\xfe\x6c\x28\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x10\x38\x38\x7c\x7c\xfe\xfe\x00\x00\x00\x00" + b"\x00\x00\x00\xfe\xfe\x7c\x7c\x38\x38\x10\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x18\x3c\x3c\x3c\x18\x18\x00\x18\x18\x00\x00\x00" + b"\x00\x66\x66\x66\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x6c\x6c\xfe\x6c\x6c\x6c\xfe\x6c\x6c\x00\x00\x00" + b"\x18\x18\x7c\xc6\xc2\xc0\x7c\x06\x86\xc6\x7c\x18\x18\x00" + b"\x00\x00\x00\x00\xc2\xc6\x0c\x18\x30\x66\xc6\x00\x00\x00" + b"\x00\x00\x38\x6c\x6c\x38\x76\xdc\xcc\xcc\x76\x00\x00\x00" + b"\x00\x30\x30\x30\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x0c\x18\x30\x30\x30\x30\x30\x18\x0c\x00\x00\x00" + b"\x00\x00\x30\x18\x0c\x0c\x0c\x0c\x0c\x18\x30\x00\x00\x00" + b"\x00\x00\x00\x00\x66\x3c\xff\x3c\x66\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x18\x30\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00" + b"\x00\x00\x02\x06\x0c\x18\x30\x60\xc0\x80\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\xce\xde\xf6\xe6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\x18\x38\x78\x18\x18\x18\x18\x18\x7e\x00\x00\x00" + b"\x00\x00\x7c\xc6\x06\x0c\x18\x30\x60\xc6\xfe\x00\x00\x00" + b"\x00\x00\x7c\xc6\x06\x06\x3c\x06\x06\xc6\x7c\x00\x00\x00" + b"\x00\x00\x0c\x1c\x3c\x6c\xcc\xfe\x0c\x0c\x1e\x00\x00\x00" + b"\x00\x00\xfe\xc0\xc0\xc0\xfc\x06\x06\xc6\x7c\x00\x00\x00" + b"\x00\x00\x38\x60\xc0\xc0\xfc\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\xfe\xc6\x06\x0c\x18\x30\x30\x30\x30\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\x7c\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\x7e\x06\x06\x0c\x78\x00\x00\x00" + b"\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x30\x00\x00\x00" + b"\x00\x00\x06\x0c\x18\x30\x60\x30\x18\x0c\x06\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x00\x00\x00\x00\x00" + b"\x00\x00\x60\x30\x18\x0c\x06\x0c\x18\x30\x60\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\x0c\x18\x18\x00\x18\x18\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xde\xde\xde\xdc\xc0\x7c\x00\x00\x00" + b"\x00\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x00\x00\x00" + b"\x00\x00\xfc\x66\x66\x66\x7c\x66\x66\x66\xfc\x00\x00\x00" + b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc2\x66\x3c\x00\x00\x00" + b"\x00\x00\xf8\x6c\x66\x66\x66\x66\x66\x6c\xf8\x00\x00\x00" + b"\x00\x00\xfe\x66\x62\x68\x78\x68\x62\x66\xfe\x00\x00\x00" + b"\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x60\xf0\x00\x00\x00" + b"\x00\x00\x3c\x66\xc2\xc0\xc0\xde\xc6\x66\x3a\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00" + b"\x00\x00\x3c\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x00\x1e\x0c\x0c\x0c\x0c\x0c\xcc\xcc\x78\x00\x00\x00" + b"\x00\x00\xe6\x66\x6c\x6c\x78\x6c\x6c\x66\xe6\x00\x00\x00" + b"\x00\x00\xf0\x60\x60\x60\x60\x60\x62\x66\xfe\x00\x00\x00" + b"\x00\x00\xc6\xee\xfe\xfe\xd6\xc6\xc6\xc6\xc6\x00\x00\x00" + b"\x00\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\x00\x00\x00" + b"\x00\x00\x38\x6c\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00" + b"\x00\x00\xfc\x66\x66\x66\x7c\x60\x60\x60\xf0\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\xc6\xd6\xde\x7c\x0c\x0e\x00\x00" + b"\x00\x00\xfc\x66\x66\x66\x7c\x6c\x66\x66\xe6\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\x60\x38\x0c\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\x7e\x7e\x5a\x18\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x6c\x38\x10\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xd6\xd6\xfe\x7c\x6c\x00\x00\x00" + b"\x00\x00\xc6\xc6\x6c\x38\x38\x38\x6c\xc6\xc6\x00\x00\x00" + b"\x00\x00\x66\x66\x66\x66\x3c\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x00\xfe\xc6\x8c\x18\x30\x60\xc2\xc6\xfe\x00\x00\x00" + b"\x00\x00\x3c\x30\x30\x30\x30\x30\x30\x30\x3c\x00\x00\x00" + b"\x00\x00\x80\xc0\xe0\x70\x38\x1c\x0e\x06\x02\x00\x00\x00" + b"\x00\x00\x3c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x3c\x00\x00\x00" + b"\x10\x38\x6c\xc6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00" + b"\x30\x30\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" + b"\x00\x00\xe0\x60\x60\x78\x6c\x66\x66\x66\x7c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc6\x7c\x00\x00\x00" + b"\x00\x00\x1c\x0c\x0c\x3c\x6c\xcc\xcc\xcc\x76\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" + b"\x00\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xf0\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\xcc\x78\x00" + b"\x00\x00\xe0\x60\x60\x6c\x76\x66\x66\x66\xe6\x00\x00\x00" + b"\x00\x00\x18\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x00\x06\x06\x00\x0e\x06\x06\x06\x06\x66\x66\x3c\x00" + b"\x00\x00\xe0\x60\x60\x66\x6c\x78\x6c\x66\xe6\x00\x00\x00" + b"\x00\x00\x38\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xec\xfe\xd6\xd6\xd6\xc6\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x7c\x60\x60\xf0\x00" + b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\x0c\x1e\x00" + b"\x00\x00\x00\x00\x00\xdc\x76\x66\x60\x60\xf0\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\x70\x1c\xc6\x7c\x00\x00\x00" + b"\x00\x00\x10\x30\x30\xfc\x30\x30\x30\x36\x1c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x66\x66\x66\x66\x3c\x18\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc6\xc6\xd6\xd6\xfe\x6c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc6\x6c\x38\x38\x6c\xc6\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\xf8\x00" + b"\x00\x00\x00\x00\x00\xfe\xcc\x18\x30\x66\xfe\x00\x00\x00" + b"\x00\x00\x0e\x18\x18\x18\x70\x18\x18\x18\x0e\x00\x00\x00" + b"\x00\x00\x18\x18\x18\x18\x00\x18\x18\x18\x18\x00\x00\x00" + b"\x00\x00\x70\x18\x18\x18\x0e\x18\x18\x18\x70\x00\x00\x00" + b"\x00\x00\x76\xdc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x10\x38\x6c\xc6\xc6\xfe\x00\x00\x00\x00" + b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc2\x66\x3c\x0c\x06\x7c\x00" + b"\x00\x00\xcc\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" + b"\x00\x0c\x18\x30\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" + b"\x00\x10\x38\x6c\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" + b"\x00\x00\xcc\xcc\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" + b"\x00\x38\x6c\x38\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" + b"\x00\x00\x00\x00\x3c\x66\x60\x66\x3c\x0c\x06\x3c\x00\x00" + b"\x00\x10\x38\x6c\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" + b"\x00\x00\xcc\xcc\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00" + b"\x00\x00\x66\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x18\x3c\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\xc6\xc6\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00" + b"\x38\x6c\x38\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00" + b"\x18\x30\x60\x00\xfe\x66\x60\x7c\x60\x66\xfe\x00\x00\x00" + b"\x00\x00\x00\x00\xcc\x76\x36\x7e\xd8\xd8\x6e\x00\x00\x00" + b"\x00\x00\x3e\x6c\xcc\xcc\xfe\xcc\xcc\xcc\xce\x00\x00\x00" + b"\x00\x10\x38\x6c\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\xc6\xc6\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x30\x78\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" + b"\x00\x60\x30\x18\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" + b"\x00\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\x78\x00" + b"\x00\xc6\xc6\x38\x6c\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00" + b"\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x18\x18\x3c\x66\x60\x60\x66\x3c\x18\x18\x00\x00\x00" + b"\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xe6\xfc\x00\x00\x00" + b"\x00\x00\x66\x66\x3c\x18\x7e\x18\x7e\x18\x18\x00\x00\x00" + b"\x00\xf8\xcc\xcc\xf8\xc4\xcc\xde\xcc\xcc\xc6\x00\x00\x00" + b"\x00\x0e\x1b\x18\x18\x18\x7e\x18\x18\x18\x18\xd8\x70\x00" + b"\x00\x18\x30\x60\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00" + b"\x00\x0c\x18\x30\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00" + b"\x00\x18\x30\x60\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x18\x30\x60\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00" + b"\x00\x00\x76\xdc\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00" + b"\x76\xdc\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\x00\x00\x00" + b"\x00\x3c\x6c\x6c\x3e\x00\x7e\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x38\x6c\x6c\x38\x00\x7c\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x30\x30\x00\x30\x30\x60\xc6\xc6\x7c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xfe\xc0\xc0\xc0\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xfe\x06\x06\x06\x00\x00\x00\x00" + b"\x00\xc0\xc0\xc6\xcc\xd8\x30\x60\xdc\x86\x0c\x18\x3e\x00" + b"\x00\xc0\xc0\xc6\xcc\xd8\x30\x66\xce\x9e\x3e\x06\x06\x00" + b"\x00\x00\x18\x18\x00\x18\x18\x3c\x3c\x3c\x18\x00\x00\x00" + b"\x00\x00\x00\x00\x36\x6c\xd8\x6c\x36\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\xd8\x6c\x36\x6c\xd8\x00\x00\x00\x00\x00" + b"\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44" + b"\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa" + b"\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77" + b"\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\xf8\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x18\x18\x18\x18\x18\x18" + b"\x36\x36\x36\x36\x36\x36\x36\xf6\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\x00\x00\xfe\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\xf8\x18\xf8\x18\x18\x18\x18\x18\x18" + b"\x36\x36\x36\x36\x36\xf6\x06\xf6\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\xfe\x06\xf6\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\xf6\x06\xfe\x00\x00\x00\x00\x00\x00" + b"\x36\x36\x36\x36\x36\x36\x36\xfe\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\xf8\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\x1f\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\x18\x18\xff\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\xff\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\x1f\x18\x18\x18\x18\x18\x18" + b"\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\x18\x18\xff\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x18\x18\x18\x18\x18\x18" + b"\x36\x36\x36\x36\x36\x36\x36\x37\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x37\x30\x3f\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x3f\x30\x37\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\xf7\x00\xff\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xff\x00\xf7\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x37\x30\x37\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00" + b"\x36\x36\x36\x36\x36\xf7\x00\xf7\x36\x36\x36\x36\x36\x36" + b"\x18\x18\x18\x18\x18\xff\x00\xff\x00\x00\x00\x00\x00\x00" + b"\x36\x36\x36\x36\x36\x36\x36\xff\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xff\x00\xff\x18\x18\x18\x18\x18\x18" + b"\x00\x00\x00\x00\x00\x00\x00\xff\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x36\x36\x3f\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x1f\x18\x1f\x18\x18\x18\x18\x18\x18" + b"\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x36\x36\xff\x36\x36\x36\x36\x36\x36" + b"\x18\x18\x18\x18\x18\xff\x18\xff\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\xf8\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x1f\x18\x18\x18\x18\x18\x18" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff" + b"\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0" + b"\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f" + b"\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x76\xdc\xd8\xd8\xdc\x76\x00\x00\x00" + b"\x00\x00\x00\x00\x7c\xc6\xfc\xc6\xc6\xfc\xc0\xc0\x40\x00" + b"\x00\x00\xfe\xc6\xc6\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00" + b"\x00\x00\x00\x00\xfe\x6c\x6c\x6c\x6c\x6c\x6c\x00\x00\x00" + b"\x00\x00\xfe\xc6\x60\x30\x18\x30\x60\xc6\xfe\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7e\xd8\xd8\xd8\xd8\x70\x00\x00\x00" + b"\x00\x00\x00\x00\x66\x66\x66\x66\x7c\x60\x60\xc0\x00\x00" + b"\x00\x00\x00\x00\x76\xdc\x18\x18\x18\x18\x18\x00\x00\x00" + b"\x00\x00\x7e\x18\x3c\x66\x66\x66\x3c\x18\x7e\x00\x00\x00" + b"\x00\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x6c\x38\x00\x00\x00" + b"\x00\x00\x38\x6c\xc6\xc6\xc6\x6c\x6c\x6c\xee\x00\x00\x00" + b"\x00\x00\x1e\x30\x18\x0c\x3e\x66\x66\x66\x3c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7e\xdb\xdb\x7e\x00\x00\x00\x00\x00" + b"\x00\x00\x03\x06\x7e\xdb\xdb\xf3\x7e\x60\xc0\x00\x00\x00" + b"\x00\x00\x1c\x30\x60\x60\x7c\x60\x60\x30\x1c\x00\x00\x00" + b"\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00" + b"\x00\x00\x00\xfe\x00\x00\xfe\x00\x00\xfe\x00\x00\x00\x00" + b"\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\xff\x00\x00\x00" + b"\x00\x00\x30\x18\x0c\x06\x0c\x18\x30\x00\x7e\x00\x00\x00" + b"\x00\x00\x0c\x18\x30\x60\x30\x18\x0c\x00\x7e\x00\x00\x00" + b"\x00\x00\x0e\x1b\x1b\x18\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\x18\xd8\xd8\x70\x00\x00\x00" + b"\x00\x00\x00\x18\x18\x00\x7e\x00\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x76\xdc\x00\x76\xdc\x00\x00\x00\x00\x00" + b"\x00\x38\x6c\x6c\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00" + b"\x00\x0f\x0c\x0c\x0c\x0c\x0c\xec\x6c\x3c\x1c\x00\x00\x00" + b"\x00\xd8\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x70\xd8\x30\x60\xc8\xf8\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x7c\x7c\x7c\x7c\x7c\x7c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +FONT = memoryview(_FONT) diff --git a/micropython/pydisplay/graphics/graphics/_font_8x16.py b/micropython/pydisplay/graphics/graphics/_font_8x16.py new file mode 100644 index 000000000..6ad3c7522 --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_font_8x16.py @@ -0,0 +1,259 @@ +_FONT = ( + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x7e\x81\xa5\x81\x81\xbd\x99\x81\x81\x7e\x00\x00\x00\x00" + b"\x00\x00\x7e\xff\xdb\xff\xff\xc3\xe7\xff\xff\x7e\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x6c\xfe\xfe\xfe\xfe\x7c\x38\x10\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x10\x38\x7c\xfe\x7c\x38\x10\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x18\x3c\x3c\xe7\xe7\xe7\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x00\x00\x18\x3c\x7e\xff\xff\x7e\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x18\x3c\x3c\x18\x00\x00\x00\x00\x00\x00" + b"\xff\xff\xff\xff\xff\xff\xe7\xc3\xc3\xe7\xff\xff\xff\xff\xff\xff" + b"\x00\x00\x00\x00\x00\x3c\x66\x42\x42\x66\x3c\x00\x00\x00\x00\x00" + b"\xff\xff\xff\xff\xff\xc3\x99\xbd\xbd\x99\xc3\xff\xff\xff\xff\xff" + b"\x00\x00\x1e\x0e\x1a\x32\x78\xcc\xcc\xcc\xcc\x78\x00\x00\x00\x00" + b"\x00\x00\x3c\x66\x66\x66\x66\x3c\x18\x7e\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x3f\x33\x3f\x30\x30\x30\x30\x70\xf0\xe0\x00\x00\x00\x00" + b"\x00\x00\x7f\x63\x7f\x63\x63\x63\x63\x67\xe7\xe6\xc0\x00\x00\x00" + b"\x00\x00\x00\x18\x18\xdb\x3c\xe7\x3c\xdb\x18\x18\x00\x00\x00\x00" + b"\x00\x80\xc0\xe0\xf0\xf8\xfe\xf8\xf0\xe0\xc0\x80\x00\x00\x00\x00" + b"\x00\x02\x06\x0e\x1e\x3e\xfe\x3e\x1e\x0e\x06\x02\x00\x00\x00\x00" + b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00\x00" + b"\x00\x00\x66\x66\x66\x66\x66\x66\x66\x00\x66\x66\x00\x00\x00\x00" + b"\x00\x00\x7f\xdb\xdb\xdb\x7b\x1b\x1b\x1b\x1b\x1b\x00\x00\x00\x00" + b"\x00\x7c\xc6\x60\x38\x6c\xc6\xc6\x6c\x38\x0c\xc6\x7c\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\xfe\xfe\x00\x00\x00\x00" + b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x7e\x00\x00\x00\x00" + b"\x00\x00\x18\x3c\x7e\x18\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x18\x18\x18\x18\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x18\x0c\xfe\x0c\x18\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x30\x60\xfe\x60\x30\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xc0\xc0\xc0\xfe\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x28\x6c\xfe\x6c\x28\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x10\x38\x38\x7c\x7c\xfe\xfe\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\xfe\xfe\x7c\x7c\x38\x38\x10\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x18\x3c\x3c\x3c\x18\x18\x18\x00\x18\x18\x00\x00\x00\x00" + b"\x00\x66\x66\x66\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x6c\x6c\xfe\x6c\x6c\x6c\xfe\x6c\x6c\x00\x00\x00\x00" + b"\x18\x18\x7c\xc6\xc2\xc0\x7c\x06\x06\x86\xc6\x7c\x18\x18\x00\x00" + b"\x00\x00\x00\x00\xc2\xc6\x0c\x18\x30\x60\xc6\x86\x00\x00\x00\x00" + b"\x00\x00\x38\x6c\x6c\x38\x76\xdc\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x30\x30\x30\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x0c\x18\x30\x30\x30\x30\x30\x30\x18\x0c\x00\x00\x00\x00" + b"\x00\x00\x30\x18\x0c\x0c\x0c\x0c\x0c\x0c\x18\x30\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x66\x3c\xff\x3c\x66\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x18\x30\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x02\x06\x0c\x18\x30\x60\xc0\x80\x00\x00\x00\x00" + b"\x00\x00\x38\x6c\xc6\xc6\xd6\xd6\xc6\xc6\x6c\x38\x00\x00\x00\x00" + b"\x00\x00\x18\x38\x78\x18\x18\x18\x18\x18\x18\x7e\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\x06\x0c\x18\x30\x60\xc0\xc6\xfe\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\x06\x06\x3c\x06\x06\x06\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x0c\x1c\x3c\x6c\xcc\xfe\x0c\x0c\x0c\x1e\x00\x00\x00\x00" + b"\x00\x00\xfe\xc0\xc0\xc0\xfc\x06\x06\x06\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x38\x60\xc0\xc0\xfc\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\xfe\xc6\x06\x06\x0c\x18\x30\x30\x30\x30\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\x7e\x06\x06\x06\x0c\x78\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x30\x00\x00\x00\x00" + b"\x00\x00\x00\x06\x0c\x18\x30\x60\x30\x18\x0c\x06\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x60\x30\x18\x0c\x06\x0c\x18\x30\x60\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\x0c\x18\x18\x18\x00\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x00\x7c\xc6\xc6\xde\xde\xde\xdc\xc0\x7c\x00\x00\x00\x00" + b"\x00\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x00\x00\xfc\x66\x66\x66\x7c\x66\x66\x66\x66\xfc\x00\x00\x00\x00" + b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc0\xc2\x66\x3c\x00\x00\x00\x00" + b"\x00\x00\xf8\x6c\x66\x66\x66\x66\x66\x66\x6c\xf8\x00\x00\x00\x00" + b"\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x62\x66\xfe\x00\x00\x00\x00" + b"\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x60\x60\xf0\x00\x00\x00\x00" + b"\x00\x00\x3c\x66\xc2\xc0\xc0\xde\xc6\xc6\x66\x3a\x00\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xfe\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x00\x00\x3c\x18\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x00\x1e\x0c\x0c\x0c\x0c\x0c\xcc\xcc\xcc\x78\x00\x00\x00\x00" + b"\x00\x00\xe6\x66\x66\x6c\x78\x78\x6c\x66\x66\xe6\x00\x00\x00\x00" + b"\x00\x00\xf0\x60\x60\x60\x60\x60\x60\x62\x66\xfe\x00\x00\x00\x00" + b"\x00\x00\xc6\xee\xfe\xfe\xd6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x00\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\xfc\x66\x66\x66\x7c\x60\x60\x60\x60\xf0\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xd6\xde\x7c\x0c\x0e\x00\x00" + b"\x00\x00\xfc\x66\x66\x66\x7c\x6c\x66\x66\x66\xe6\x00\x00\x00\x00" + b"\x00\x00\x7c\xc6\xc6\x60\x38\x0c\x06\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x7e\x7e\x5a\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x6c\x38\x10\x00\x00\x00\x00" + b"\x00\x00\xc6\xc6\xc6\xc6\xd6\xd6\xd6\xfe\xee\x6c\x00\x00\x00\x00" + b"\x00\x00\xc6\xc6\x6c\x7c\x38\x38\x7c\x6c\xc6\xc6\x00\x00\x00\x00" + b"\x00\x00\x66\x66\x66\x66\x3c\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x00\xfe\xc6\x86\x0c\x18\x30\x60\xc2\xc6\xfe\x00\x00\x00\x00" + b"\x00\x00\x3c\x30\x30\x30\x30\x30\x30\x30\x30\x3c\x00\x00\x00\x00" + b"\x00\x00\x00\x80\xc0\xe0\x70\x38\x1c\x0e\x06\x02\x00\x00\x00\x00" + b"\x00\x00\x3c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x3c\x00\x00\x00\x00" + b"\x10\x38\x6c\xc6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00" + b"\x00\x30\x18\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x00\xe0\x60\x60\x78\x6c\x66\x66\x66\x66\x7c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc0\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x1c\x0c\x0c\x3c\x6c\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x1c\x36\x32\x30\x78\x30\x30\x30\x30\x78\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\xcc\xcc\x7c\x0c\xcc\x78\x00" + b"\x00\x00\xe0\x60\x60\x6c\x76\x66\x66\x66\x66\xe6\x00\x00\x00\x00" + b"\x00\x00\x18\x18\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x00\x06\x06\x00\x0e\x06\x06\x06\x06\x06\x06\x66\x66\x3c\x00" + b"\x00\x00\xe0\x60\x60\x66\x6c\x78\x78\x6c\x66\xe6\x00\x00\x00\x00" + b"\x00\x00\x38\x18\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xec\xfe\xd6\xd6\xd6\xd6\xc6\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x66\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x7c\x60\x60\xf0\x00" + b"\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\xcc\xcc\x7c\x0c\x0c\x1e\x00" + b"\x00\x00\x00\x00\x00\xdc\x76\x66\x60\x60\x60\xf0\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\x60\x38\x0c\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x10\x30\x30\xfc\x30\x30\x30\x30\x36\x1c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc6\xc6\xd6\xd6\xd6\xfe\x6c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc6\x6c\x38\x38\x38\x6c\xc6\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7e\x06\x0c\xf8\x00" + b"\x00\x00\x00\x00\x00\xfe\xcc\x18\x30\x60\xc6\xfe\x00\x00\x00\x00" + b"\x00\x00\x0e\x18\x18\x18\x70\x18\x18\x18\x18\x0e\x00\x00\x00\x00" + b"\x00\x00\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x70\x18\x18\x18\x0e\x18\x18\x18\x18\x70\x00\x00\x00\x00" + b"\x00\x76\xdc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x10\x38\x6c\xc6\xc6\xc6\xfe\x00\x00\x00\x00\x00" + b"\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc0\xc2\x66\x3c\x18\x70\x00\x00" + b"\x00\x00\xcc\x00\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x0c\x18\x30\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" + b"\x00\x10\x38\x6c\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x00\xcc\x00\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x38\x6c\x38\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc0\xc6\x7c\x18\x70\x00\x00" + b"\x00\x10\x38\x6c\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\xc6\x00\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x7c\xc6\xfe\xc0\xc0\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x66\x00\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x18\x3c\x66\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\xc6\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x38\x6c\x38\x10\x38\x6c\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x0c\x18\x00\xfe\x66\x62\x68\x78\x68\x62\x66\xfe\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xec\x36\x36\x7e\xd8\xd8\x6e\x00\x00\x00\x00" + b"\x00\x00\x3e\x6c\xcc\xcc\xfe\xcc\xcc\xcc\xcc\xce\x00\x00\x00\x00" + b"\x00\x10\x38\x6c\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\xc6\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x60\x30\x18\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x30\x78\xcc\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x60\x30\x18\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x00\xc6\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7e\x06\x0c\x78\x00" + b"\x00\xc6\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\xc6\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x18\x18\x7c\xc6\xc0\xc0\xc0\xc6\x7c\x18\x18\x00\x00\x00\x00" + b"\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\x60\xe6\xfc\x00\x00\x00\x00" + b"\x00\x00\x66\x66\x3c\x18\x7e\x18\x7e\x18\x18\x18\x00\x00\x00\x00" + b"\x00\xf8\xcc\xcc\xf8\xc4\xcc\xde\xcc\xcc\xcc\xc6\x00\x00\x00\x00" + b"\x00\x0e\x1b\x18\x18\x18\x7e\x18\x18\x18\xd8\x70\x00\x00\x00\x00" + b"\x00\x18\x30\x60\x00\x78\x0c\x7c\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x0c\x18\x30\x00\x38\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00" + b"\x00\x18\x30\x60\x00\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x18\x30\x60\x00\xcc\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00" + b"\x00\x00\x76\xdc\x00\xdc\x66\x66\x66\x66\x66\x66\x00\x00\x00\x00" + b"\x76\xdc\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x00\x00\x3c\x6c\x6c\x3e\x00\x7e\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x38\x6c\x6c\x38\x00\x7c\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x30\x30\x00\x30\x30\x60\xc0\xc6\xc6\x7c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xfe\xc0\xc0\xc0\xc0\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\xfe\x06\x06\x06\x06\x00\x00\x00\x00\x00" + b"\x00\x60\xe0\x62\x66\x6c\x18\x30\x60\xdc\x86\x0c\x18\x3e\x00\x00" + b"\x00\x60\xe0\x62\x66\x6c\x18\x30\x66\xce\x9a\x3f\x06\x06\x00\x00" + b"\x00\x00\x18\x18\x00\x18\x18\x18\x3c\x3c\x3c\x18\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x36\x6c\xd8\x6c\x36\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xd8\x6c\x36\x6c\xd8\x00\x00\x00\x00\x00\x00" + b"\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44" + b"\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa" + b"\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77" + b"\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\xf8\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x36\x36\x36\x36\x36\x36\x36\xf6\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\x00\x00\xfe\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x36\x36\x36\x36\x36\xf6\x06\xf6\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\xfe\x06\xf6\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\xf6\x06\xfe\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x36\x36\x36\x36\x36\x36\x36\xfe\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\xf8\x18\xf8\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\xf8\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\x1f\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\x18\x18\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\xff\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\x1f\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\x18\x18\xff\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x36\x36\x36\x36\x36\x36\x36\x37\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x37\x30\x3f\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x3f\x30\x37\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\xf7\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xff\x00\xf7\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x37\x30\x37\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x36\x36\x36\x36\x36\xf7\x00\xf7\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x18\x18\x18\x18\x18\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x36\x36\x36\x36\x36\x36\x36\xff\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xff\x00\xff\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x00\x00\x00\x00\x00\x00\x00\xff\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x36\x36\x3f\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x18\x18\x18\x18\x18\x1f\x18\x1f\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x36\x36\xff\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x18\x18\x18\x18\x18\xff\x18\xff\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\xf8\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x1f\x18\x18\x18\x18\x18\x18\x18\x18" + b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff" + b"\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0" + b"\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f" + b"\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x76\xdc\xd8\xd8\xd8\xdc\x76\x00\x00\x00\x00" + b"\x00\x00\x78\xcc\xcc\xcc\xd8\xcc\xc6\xc6\xc6\xcc\x00\x00\x00\x00" + b"\x00\x00\xfe\xc6\xc6\xc0\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\xfe\x6c\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00" + b"\x00\x00\xfe\xc6\x60\x30\x18\x18\x30\x60\xc6\xfe\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7e\xd8\xd8\xd8\xd8\xd8\x70\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x66\x66\x66\x66\x66\x66\x7c\x60\x60\xc0\x00" + b"\x00\x00\x00\x00\x76\xdc\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00" + b"\x00\x00\x7e\x18\x3c\x66\x66\x66\x66\x3c\x18\x7e\x00\x00\x00\x00" + b"\x00\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00" + b"\x00\x00\x38\x6c\xc6\xc6\xc6\x6c\x6c\x6c\x6c\xee\x00\x00\x00\x00" + b"\x00\x00\x1e\x30\x18\x0c\x3e\x66\x66\x66\x66\x3c\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x7e\xdb\xdb\xdb\x7e\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x03\x06\x7e\xdb\xdb\xf3\x7e\x60\xc0\x00\x00\x00\x00" + b"\x00\x00\x1c\x30\x60\x60\x7c\x60\x60\x60\x30\x1c\x00\x00\x00\x00" + b"\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00" + b"\x00\x00\x00\x00\xfe\x00\x00\xfe\x00\x00\xfe\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x7e\x00\x00\x00\x00" + b"\x00\x00\x00\x30\x18\x0c\x06\x0c\x18\x30\x00\x7e\x00\x00\x00\x00" + b"\x00\x00\x00\x0c\x18\x30\x60\x30\x18\x0c\x00\x7e\x00\x00\x00\x00" + b"\x00\x00\x0e\x1b\x1b\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\x18\x18\x18\x18\xd8\xd8\xd8\x70\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x18\x00\x7e\x00\x18\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x76\xdc\x00\x76\xdc\x00\x00\x00\x00\x00\x00" + b"\x00\x38\x6c\x6c\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x0f\x0c\x0c\x0c\x0c\x0c\xec\x6c\x6c\x3c\x1c\x00\x00\x00\x00" + b"\x00\x6c\x36\x36\x36\x36\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x3c\x66\x0c\x18\x32\x7e\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x7e\x7e\x7e\x7e\x7e\x7e\x7e\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) +FONT = memoryview(_FONT) diff --git a/micropython/pydisplay/graphics/graphics/_font_8x8.py b/micropython/pydisplay/graphics/graphics/_font_8x8.py new file mode 100644 index 000000000..8b3bef231 --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_font_8x8.py @@ -0,0 +1,259 @@ +_FONT = ( + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x7e\x81\xa5\x81\xbd\x99\x81\x7e" + b"\x7e\xff\xdb\xff\xc3\xe7\xff\x7e" + b"\x6c\xfe\xfe\xfe\x7c\x38\x10\x00" + b"\x10\x38\x7c\xfe\x7c\x38\x10\x00" + b"\x38\x7c\x38\xfe\xfe\xd6\x10\x38" + b"\x10\x38\x7c\xfe\xfe\x7c\x10\x38" + b"\x00\x00\x18\x3c\x3c\x18\x00\x00" + b"\xff\xff\xe7\xc3\xc3\xe7\xff\xff" + b"\x00\x3c\x66\x42\x42\x66\x3c\x00" + b"\xff\xc3\x99\xbd\xbd\x99\xc3\xff" + b"\x0f\x07\x0f\x7d\xcc\xcc\xcc\x78" + b"\x3c\x66\x66\x66\x3c\x18\x7e\x18" + b"\x3f\x33\x3f\x30\x30\x70\xf0\xe0" + b"\x7f\x63\x7f\x63\x63\x67\xe6\xc0" + b"\x18\xdb\x3c\xe7\xe7\x3c\xdb\x18" + b"\x80\xe0\xf8\xfe\xf8\xe0\x80\x00" + b"\x02\x0e\x3e\xfe\x3e\x0e\x02\x00" + b"\x18\x3c\x7e\x18\x18\x7e\x3c\x18" + b"\x66\x66\x66\x66\x66\x00\x66\x00" + b"\x7f\xdb\xdb\x7b\x1b\x1b\x1b\x00" + b"\x3e\x61\x3c\x66\x66\x3c\x86\x7c" + b"\x00\x00\x00\x00\x7e\x7e\x7e\x00" + b"\x18\x3c\x7e\x18\x7e\x3c\x18\xff" + b"\x18\x3c\x7e\x18\x18\x18\x18\x00" + b"\x18\x18\x18\x18\x7e\x3c\x18\x00" + b"\x00\x18\x0c\xfe\x0c\x18\x00\x00" + b"\x00\x30\x60\xfe\x60\x30\x00\x00" + b"\x00\x00\xc0\xc0\xc0\xfe\x00\x00" + b"\x00\x24\x66\xff\x66\x24\x00\x00" + b"\x00\x18\x3c\x7e\xff\xff\x00\x00" + b"\x00\xff\xff\x7e\x3c\x18\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x18\x3c\x3c\x18\x18\x00\x18\x00" + b"\x66\x66\x24\x00\x00\x00\x00\x00" + b"\x6c\x6c\xfe\x6c\xfe\x6c\x6c\x00" + b"\x18\x3e\x60\x3c\x06\x7c\x18\x00" + b"\x00\xc6\xcc\x18\x30\x66\xc6\x00" + b"\x38\x6c\x38\x76\xdc\xcc\x76\x00" + b"\x18\x18\x30\x00\x00\x00\x00\x00" + b"\x0c\x18\x30\x30\x30\x18\x0c\x00" + b"\x30\x18\x0c\x0c\x0c\x18\x30\x00" + b"\x00\x66\x3c\xff\x3c\x66\x00\x00" + b"\x00\x18\x18\x7e\x18\x18\x00\x00" + b"\x00\x00\x00\x00\x00\x18\x18\x30" + b"\x00\x00\x00\x7e\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x18\x18\x00" + b"\x06\x0c\x18\x30\x60\xc0\x80\x00" + b"\x38\x6c\xc6\xd6\xc6\x6c\x38\x00" + b"\x18\x38\x18\x18\x18\x18\x7e\x00" + b"\x7c\xc6\x06\x1c\x30\x66\xfe\x00" + b"\x7c\xc6\x06\x3c\x06\xc6\x7c\x00" + b"\x1c\x3c\x6c\xcc\xfe\x0c\x1e\x00" + b"\xfe\xc0\xc0\xfc\x06\xc6\x7c\x00" + b"\x38\x60\xc0\xfc\xc6\xc6\x7c\x00" + b"\xfe\xc6\x0c\x18\x30\x30\x30\x00" + b"\x7c\xc6\xc6\x7c\xc6\xc6\x7c\x00" + b"\x7c\xc6\xc6\x7e\x06\x0c\x78\x00" + b"\x00\x18\x18\x00\x00\x18\x18\x00" + b"\x00\x18\x18\x00\x00\x18\x18\x30" + b"\x06\x0c\x18\x30\x18\x0c\x06\x00" + b"\x00\x00\x7e\x00\x00\x7e\x00\x00" + b"\x60\x30\x18\x0c\x18\x30\x60\x00" + b"\x7c\xc6\x0c\x18\x18\x00\x18\x00" + b"\x7c\xc6\xde\xde\xde\xc0\x78\x00" + b"\x38\x6c\xc6\xfe\xc6\xc6\xc6\x00" + b"\xfc\x66\x66\x7c\x66\x66\xfc\x00" + b"\x3c\x66\xc0\xc0\xc0\x66\x3c\x00" + b"\xf8\x6c\x66\x66\x66\x6c\xf8\x00" + b"\xfe\x62\x68\x78\x68\x62\xfe\x00" + b"\xfe\x62\x68\x78\x68\x60\xf0\x00" + b"\x3c\x66\xc0\xc0\xce\x66\x3a\x00" + b"\xc6\xc6\xc6\xfe\xc6\xc6\xc6\x00" + b"\x3c\x18\x18\x18\x18\x18\x3c\x00" + b"\x1e\x0c\x0c\x0c\xcc\xcc\x78\x00" + b"\xe6\x66\x6c\x78\x6c\x66\xe6\x00" + b"\xf0\x60\x60\x60\x62\x66\xfe\x00" + b"\xc6\xee\xfe\xfe\xd6\xc6\xc6\x00" + b"\xc6\xe6\xf6\xde\xce\xc6\xc6\x00" + b"\x7c\xc6\xc6\xc6\xc6\xc6\x7c\x00" + b"\xfc\x66\x66\x7c\x60\x60\xf0\x00" + b"\x7c\xc6\xc6\xc6\xc6\xce\x7c\x0e" + b"\xfc\x66\x66\x7c\x6c\x66\xe6\x00" + b"\x3c\x66\x30\x18\x0c\x66\x3c\x00" + b"\x7e\x7e\x5a\x18\x18\x18\x3c\x00" + b"\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00" + b"\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00" + b"\xc6\xc6\xc6\xd6\xd6\xfe\x6c\x00" + b"\xc6\xc6\x6c\x38\x6c\xc6\xc6\x00" + b"\x66\x66\x66\x3c\x18\x18\x3c\x00" + b"\xfe\xc6\x8c\x18\x32\x66\xfe\x00" + b"\x3c\x30\x30\x30\x30\x30\x3c\x00" + b"\xc0\x60\x30\x18\x0c\x06\x02\x00" + b"\x3c\x0c\x0c\x0c\x0c\x0c\x3c\x00" + b"\x10\x38\x6c\xc6\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\xff" + b"\x30\x18\x0c\x00\x00\x00\x00\x00" + b"\x00\x00\x78\x0c\x7c\xcc\x76\x00" + b"\xe0\x60\x7c\x66\x66\x66\xdc\x00" + b"\x00\x00\x7c\xc6\xc0\xc6\x7c\x00" + b"\x1c\x0c\x7c\xcc\xcc\xcc\x76\x00" + b"\x00\x00\x7c\xc6\xfe\xc0\x7c\x00" + b"\x3c\x66\x60\xf8\x60\x60\xf0\x00" + b"\x00\x00\x76\xcc\xcc\x7c\x0c\xf8" + b"\xe0\x60\x6c\x76\x66\x66\xe6\x00" + b"\x18\x00\x38\x18\x18\x18\x3c\x00" + b"\x06\x00\x06\x06\x06\x66\x66\x3c" + b"\xe0\x60\x66\x6c\x78\x6c\xe6\x00" + b"\x38\x18\x18\x18\x18\x18\x3c\x00" + b"\x00\x00\xec\xfe\xd6\xd6\xd6\x00" + b"\x00\x00\xdc\x66\x66\x66\x66\x00" + b"\x00\x00\x7c\xc6\xc6\xc6\x7c\x00" + b"\x00\x00\xdc\x66\x66\x7c\x60\xf0" + b"\x00\x00\x76\xcc\xcc\x7c\x0c\x1e" + b"\x00\x00\xdc\x76\x60\x60\xf0\x00" + b"\x00\x00\x7e\xc0\x7c\x06\xfc\x00" + b"\x30\x30\xfc\x30\x30\x36\x1c\x00" + b"\x00\x00\xcc\xcc\xcc\xcc\x76\x00" + b"\x00\x00\xc6\xc6\xc6\x6c\x38\x00" + b"\x00\x00\xc6\xd6\xd6\xfe\x6c\x00" + b"\x00\x00\xc6\x6c\x38\x6c\xc6\x00" + b"\x00\x00\xc6\xc6\xc6\x7e\x06\xfc" + b"\x00\x00\x7e\x4c\x18\x32\x7e\x00" + b"\x0e\x18\x18\x70\x18\x18\x0e\x00" + b"\x18\x18\x18\x18\x18\x18\x18\x00" + b"\x70\x18\x18\x0e\x18\x18\x70\x00" + b"\x76\xdc\x00\x00\x00\x00\x00\x00" + b"\x00\x10\x38\x6c\xc6\xc6\xfe\x00" + b"\x7c\xc6\xc0\xc0\xc6\x7c\x0c\x78" + b"\xcc\x00\xcc\xcc\xcc\xcc\x76\x00" + b"\x0c\x18\x7c\xc6\xfe\xc0\x7c\x00" + b"\x7c\x82\x78\x0c\x7c\xcc\x76\x00" + b"\xc6\x00\x78\x0c\x7c\xcc\x76\x00" + b"\x30\x18\x78\x0c\x7c\xcc\x76\x00" + b"\x30\x30\x78\x0c\x7c\xcc\x76\x00" + b"\x00\x00\x7e\xc0\xc0\x7e\x0c\x38" + b"\x7c\x82\x7c\xc6\xfe\xc0\x7c\x00" + b"\xc6\x00\x7c\xc6\xfe\xc0\x7c\x00" + b"\x30\x18\x7c\xc6\xfe\xc0\x7c\x00" + b"\x66\x00\x38\x18\x18\x18\x3c\x00" + b"\x7c\x82\x38\x18\x18\x18\x3c\x00" + b"\x30\x18\x00\x38\x18\x18\x3c\x00" + b"\xc6\x38\x6c\xc6\xfe\xc6\xc6\x00" + b"\x38\x6c\x7c\xc6\xfe\xc6\xc6\x00" + b"\x18\x30\xfe\xc0\xf8\xc0\xfe\x00" + b"\x00\x00\x7e\x18\x7e\xd8\x7e\x00" + b"\x3e\x6c\xcc\xfe\xcc\xcc\xce\x00" + b"\x7c\x82\x7c\xc6\xc6\xc6\x7c\x00" + b"\xc6\x00\x7c\xc6\xc6\xc6\x7c\x00" + b"\x30\x18\x7c\xc6\xc6\xc6\x7c\x00" + b"\x78\x84\x00\xcc\xcc\xcc\x76\x00" + b"\x60\x30\xcc\xcc\xcc\xcc\x76\x00" + b"\xc6\x00\xc6\xc6\xc6\x7e\x06\xfc" + b"\xc6\x38\x6c\xc6\xc6\x6c\x38\x00" + b"\xc6\x00\xc6\xc6\xc6\xc6\x7c\x00" + b"\x18\x18\x7e\xc0\xc0\x7e\x18\x18" + b"\x38\x6c\x64\xf0\x60\x66\xfc\x00" + b"\x66\x66\x3c\x7e\x18\x7e\x18\x18" + b"\xf8\xcc\xcc\xfa\xc6\xcf\xc6\xc7" + b"\x0e\x1b\x18\x3c\x18\xd8\x70\x00" + b"\x18\x30\x78\x0c\x7c\xcc\x76\x00" + b"\x0c\x18\x00\x38\x18\x18\x3c\x00" + b"\x0c\x18\x7c\xc6\xc6\xc6\x7c\x00" + b"\x18\x30\xcc\xcc\xcc\xcc\x76\x00" + b"\x76\xdc\x00\xdc\x66\x66\x66\x00" + b"\x76\xdc\x00\xe6\xf6\xde\xce\x00" + b"\x3c\x6c\x6c\x3e\x00\x7e\x00\x00" + b"\x38\x6c\x6c\x38\x00\x7c\x00\x00" + b"\x18\x00\x18\x18\x30\x63\x3e\x00" + b"\x00\x00\x00\xfe\xc0\xc0\x00\x00" + b"\x00\x00\x00\xfe\x06\x06\x00\x00" + b"\x63\xe6\x6c\x7e\x33\x66\xcc\x0f" + b"\x63\xe6\x6c\x7a\x36\x6a\xdf\x06" + b"\x18\x00\x18\x18\x3c\x3c\x18\x00" + b"\x00\x33\x66\xcc\x66\x33\x00\x00" + b"\x00\xcc\x66\x33\x66\xcc\x00\x00" + b"\x22\x88\x22\x88\x22\x88\x22\x88" + b"\x55\xaa\x55\xaa\x55\xaa\x55\xaa" + b"\x77\xdd\x77\xdd\x77\xdd\x77\xdd" + b"\x18\x18\x18\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\xf8\x18\x18\x18" + b"\x18\x18\xf8\x18\xf8\x18\x18\x18" + b"\x36\x36\x36\x36\xf6\x36\x36\x36" + b"\x00\x00\x00\x00\xfe\x36\x36\x36" + b"\x00\x00\xf8\x18\xf8\x18\x18\x18" + b"\x36\x36\xf6\x06\xf6\x36\x36\x36" + b"\x36\x36\x36\x36\x36\x36\x36\x36" + b"\x00\x00\xfe\x06\xf6\x36\x36\x36" + b"\x36\x36\xf6\x06\xfe\x00\x00\x00" + b"\x36\x36\x36\x36\xfe\x00\x00\x00" + b"\x18\x18\xf8\x18\xf8\x00\x00\x00" + b"\x00\x00\x00\x00\xf8\x18\x18\x18" + b"\x18\x18\x18\x18\x1f\x00\x00\x00" + b"\x18\x18\x18\x18\xff\x00\x00\x00" + b"\x00\x00\x00\x00\xff\x18\x18\x18" + b"\x18\x18\x18\x18\x1f\x18\x18\x18" + b"\x00\x00\x00\x00\xff\x00\x00\x00" + b"\x18\x18\x18\x18\xff\x18\x18\x18" + b"\x18\x18\x1f\x18\x1f\x18\x18\x18" + b"\x36\x36\x36\x36\x37\x36\x36\x36" + b"\x36\x36\x37\x30\x3f\x00\x00\x00" + b"\x00\x00\x3f\x30\x37\x36\x36\x36" + b"\x36\x36\xf7\x00\xff\x00\x00\x00" + b"\x00\x00\xff\x00\xf7\x36\x36\x36" + b"\x36\x36\x37\x30\x37\x36\x36\x36" + b"\x00\x00\xff\x00\xff\x00\x00\x00" + b"\x36\x36\xf7\x00\xf7\x36\x36\x36" + b"\x18\x18\xff\x00\xff\x00\x00\x00" + b"\x36\x36\x36\x36\xff\x00\x00\x00" + b"\x00\x00\xff\x00\xff\x18\x18\x18" + b"\x00\x00\x00\x00\xff\x36\x36\x36" + b"\x36\x36\x36\x36\x3f\x00\x00\x00" + b"\x18\x18\x1f\x18\x1f\x00\x00\x00" + b"\x00\x00\x1f\x18\x1f\x18\x18\x18" + b"\x00\x00\x00\x00\x3f\x36\x36\x36" + b"\x36\x36\x36\x36\xff\x36\x36\x36" + b"\x18\x18\xff\x18\xff\x18\x18\x18" + b"\x18\x18\x18\x18\xf8\x00\x00\x00" + b"\x00\x00\x00\x00\x1f\x18\x18\x18" + b"\xff\xff\xff\xff\xff\xff\xff\xff" + b"\x00\x00\x00\x00\xff\xff\xff\xff" + b"\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0" + b"\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f" + b"\xff\xff\xff\xff\x00\x00\x00\x00" + b"\x00\x00\x76\xdc\xc8\xdc\x76\x00" + b"\x78\xcc\xcc\xd8\xcc\xc6\xcc\x00" + b"\xfe\xc6\xc0\xc0\xc0\xc0\xc0\x00" + b"\x00\x00\xfe\x6c\x6c\x6c\x6c\x00" + b"\xfe\xc6\x60\x30\x60\xc6\xfe\x00" + b"\x00\x00\x7e\xd8\xd8\xd8\x70\x00" + b"\x00\x00\x66\x66\x66\x66\x7c\xc0" + b"\x00\x76\xdc\x18\x18\x18\x18\x00" + b"\x7e\x18\x3c\x66\x66\x3c\x18\x7e" + b"\x38\x6c\xc6\xfe\xc6\x6c\x38\x00" + b"\x38\x6c\xc6\xc6\x6c\x6c\xee\x00" + b"\x0e\x18\x0c\x3e\x66\x66\x3c\x00" + b"\x00\x00\x7e\xdb\xdb\x7e\x00\x00" + b"\x06\x0c\x7e\xdb\xdb\x7e\x60\xc0" + b"\x1e\x30\x60\x7e\x60\x30\x1e\x00" + b"\x00\x7c\xc6\xc6\xc6\xc6\xc6\x00" + b"\x00\xfe\x00\xfe\x00\xfe\x00\x00" + b"\x18\x18\x7e\x18\x18\x00\x7e\x00" + b"\x30\x18\x0c\x18\x30\x00\x7e\x00" + b"\x0c\x18\x30\x18\x0c\x00\x7e\x00" + b"\x0e\x1b\x1b\x18\x18\x18\x18\x18" + b"\x18\x18\x18\x18\x18\xd8\xd8\x70" + b"\x00\x18\x00\x7e\x00\x18\x00\x00" + b"\x00\x76\xdc\x00\x76\xdc\x00\x00" + b"\x38\x6c\x6c\x38\x00\x00\x00\x00" + b"\x00\x00\x00\x18\x18\x00\x00\x00" + b"\x00\x00\x00\x18\x00\x00\x00\x00" + b"\x0f\x0c\x0c\x0c\xec\x6c\x3c\x1c" + b"\x6c\x36\x36\x36\x36\x00\x00\x00" + b"\x78\x0c\x18\x30\x7c\x00\x00\x00" + b"\x00\x00\x3c\x3c\x3c\x3c\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00" +) +FONT = memoryview(_FONT) diff --git a/micropython/pydisplay/graphics/graphics/_framebuf.py b/micropython/pydisplay/graphics/graphics/_framebuf.py new file mode 100644 index 000000000..5e333cb2f --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_framebuf.py @@ -0,0 +1,603 @@ +# SPDX-FileCopyrightText: 2018 Kattni Rembor, Melissa LeBlanc-Williams +# and Tony DiCola, for Adafruit Industries. +# Copyright 2024 Brad Barnett +# Original file created by Damien P. George +# SPDX-License-Identifier: MIT + +""" +`graphics._framebuf` +==================================================== + +*Python framebuf module, based on the micropython framebuf module. +Will not be imported on MicroPython boards since framebuf is included +in the compiled firmware. + +framebuf on MicroPython does not return an Area object for methods that +write to the buffer, but BasicShapes does, so the native shape methods +here return an Area object in order to keep it consistent within this +library. It is recommended to use framebuf_plus.py instead of this if +you need to use the returned Areas so your code will be transferable. + +""" + +from . import _shapes +from . import _font + +try: + from ulab import numpy as np +except ImportError: + try: + import numpy as np + except ImportError: + np = None + + +# Framebuf format constants: +MONO_VLSB = 0 +MONO_HLSB = 3 +MONO_HMSB = 4 +RGB565 = 1 +GS2_HMSB = 5 +GS4_HMSB = 2 +GS8 = 6 + + +class MVLSBFormat: + depth = 1 + + @staticmethod + def set_pixel(framebuf, x, y, color): + index = (y >> 3) * framebuf._stride + x + offset = y & 0x07 + framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + @staticmethod + def get_pixel(framebuf, x, y): + index = (y >> 3) * framebuf._stride + x + offset = y & 0x07 + return (framebuf._buffer[index] >> offset) & 0x01 + + @staticmethod + def fill(framebuf, color): + if color: + fill = 0xFF + else: + fill = 0x00 + for i in range(len(framebuf._buffer)): # pylint: disable=consider-using-enumerate + framebuf._buffer[i] = fill + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + while height > 0: + index = (y >> 3) * framebuf._stride + x + offset = y & 0x07 + for w_w in range(width): + framebuf._buffer[index + w_w] = ( + framebuf._buffer[index + w_w] & ~(0x01 << offset) + ) | ((color != 0) << offset) + y += 1 + height -= 1 + + +class MHLSBFormat: + depth = 1 + + @staticmethod + def set_pixel(framebuf, x, y, color): + index = (y * framebuf._stride + x) >> 3 + offset = 7 - (x & 0x07) + framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + @staticmethod + def get_pixel(framebuf, x, y): + index = (x + y * framebuf._stride) >> 3 + offset = 7 - (x & 0x07) + return (framebuf._buffer[index] >> offset) & 0x01 + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + for _x in range(x, x + width): + offset = 7 - _x & 0x07 + for _y in range(y, y + height): + index = (_y * framebuf._stride + _x) >> 3 + framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + @staticmethod + def fill(framebuf, color): + if color: + fill = 0xFF + else: + fill = 0x00 + for i in range(len(framebuf._buffer)): + framebuf._buffer[i] = fill + + +class MHMSBFormat: + depth = 1 + + @staticmethod + def set_pixel(framebuf, x, y, color): + index = (y * framebuf._stride + x) >> 3 + offset = x & 0x07 + framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + @staticmethod + def get_pixel(framebuf, x, y): + index = (y * framebuf._stride + x) // 8 + offset = 7 - x & 0x07 + return (framebuf._buffer[index] >> offset) & 0x01 + + @staticmethod + def fill(framebuf, color): + if color: + fill = 0xFF + else: + fill = 0x00 + for i in range(len(framebuf._buffer)): # pylint: disable=consider-using-enumerate + framebuf._buffer[i] = fill + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + for _x in range(x, x + width): + offset = 7 - _x & 0x07 + for _y in range(y, y + height): + index = (_y * framebuf._stride + _x) // 8 + framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | ( + (color != 0) << offset + ) + + +class GS2HMSBFormat: + depth = 2 + + @staticmethod + def set_pixel(framebuf, x, y, color): + index = (y * framebuf._stride + x) >> 2 + pixel = framebuf._buffer[index] + + shift = (x & 0b11) << 1 + mask = 0b11 << shift + color = (color & 0b11) << shift + + framebuf._buffer[index] = color | (pixel & (~mask)) + + @staticmethod + def get_pixel(framebuf, x, y): + index = (y * framebuf._stride + x) >> 2 + pixel = framebuf._buffer[index] + + shift = (x & 0b11) << 1 + return (pixel >> shift) & 0b11 + + @staticmethod + def fill(framebuf, color): + if color: + bits = color & 0b11 + fill = (bits << 6) | (bits << 4) | (bits << 2) | (bits << 0) + else: + fill = 0x00 + + framebuf._buffer = [fill for i in range(len(framebuf._buffer))] + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + """Draw the outline and interior of a rectangle at the given location, size and color.""" + # pylint: disable=too-many-arguments + for _x in range(x, x + width): + for _y in range(y, y + height): + GS2HMSBFormat.set_pixel(framebuf, _x, _y, color) + + +class GS4HMSBFormat: + depth = 4 + + @staticmethod + def set_pixel(framebuf, x, y, color): + raise NotImplementedError + + @staticmethod + def get_pixel(framebuf, x, y): + raise NotImplementedError + + @staticmethod + def fill(framebuf, color): + raise NotImplementedError + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + raise NotImplementedError + + +class GS8Format: + depth = 8 + + @staticmethod + def set_pixel(framebuf, x, y, color): + index = y * framebuf._stride + x + framebuf._buffer[index] = color.to_bytes(1, "little") + + @staticmethod + def get_pixel(framebuf, x, y): + index = y * framebuf._stride + x + return int.from_bytes(framebuf._buffer[index : index + 1], "little") + + @staticmethod + def fill(framebuf, color): + framebuf._buffer = color.to_bytes(1, "little") * len(framebuf._buffer) + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + color = color.to_bytes(1, "little") + for _y in range(y, y + height): + offset = _y * framebuf._stride + for _x in range(x, x + width): + index = offset + _x + framebuf._buffer[index] = color + + +class RGB565Format: + depth = 16 + + @staticmethod + def set_pixel(framebuf, x, y, color): + index = (y * framebuf._stride + x) * 2 + framebuf._buffer[index : index + 2] = (color & 0xFFFF).to_bytes(2, "little") + + @staticmethod + def get_pixel(framebuf, x, y): + index = (y * framebuf._stride + x) * 2 + color = framebuf._buffer[index : index + 2] + color = int.from_bytes(color, "little") + return color + + @staticmethod + def fill(framebuf, color): + rgb565_color = (color & 0xFFFF).to_bytes(2, "little") + if False: + rgb565_color_int = int.from_bytes(rgb565_color, "little") + arr = np.frombuffer(framebuf._buffer, dtype=np.uint16) + arr[:] = rgb565_color_int + else: + for i in range(0, len(framebuf._buffer), 2): + framebuf._buffer[i : i + 2] = rgb565_color + + @staticmethod + def fill_rect(framebuf, x, y, width, height, color): + # make sure x, y, width, height are within the bounds of the framebuf + if x < 0: + width += x + x = 0 + if y < 0: + height += y + y = 0 + if x + width > framebuf.width: + width = framebuf.width - x + if y + height > framebuf.height: + height = framebuf.height - y + stride = framebuf._stride + rgb565_color = (color & 0xFFFF).to_bytes(2, "little") + if np: + rgb565_color_int = int.from_bytes(rgb565_color, "little") + arr = np.frombuffer(framebuf._buffer, dtype=np.uint16) + for _y in range(y, y + height): + arr[_y * stride + x : _y * stride + x + width] = rgb565_color_int + else: + for _y in range(y, y + height): + offset = _y * stride + for _x in range(x, x + width): + index = (offset + _x) * 2 + framebuf.buffer[index : index + 2] = rgb565_color + + +class FrameBuffer: + """ + FrameBuffer object. + + Args: + buffer (bytearray): The buffer to use for the frame buffer. + width (int): The width of the frame buffer in pixels. + height (int): The height of the frame buffer in pixels. + format (int): The format of the frame buffer. One of: + - ``MONO_VLSB``: Single bit displays (like SSD1306 OLED) + - ``MONO_HLSB``: Single bit files like PBM (Portable BitMap) + - ``MONO_HMSB``: Single bit displays where the bits in a byte are horizontally mapped + Each byte occupies 8 horizontal pixels with bit 0 being the leftmost. + - ``RGB565``: 16-bit color displays + - ``GS2_HMSB``: 2-bit color displays like the HT16K33 8x8 Matrix + - ``GS4_HMSB``: Unimplemented! + - ``GS8``: Unimplemented! + stride (int): The number of bytes between each horizontal line of the frame buffer + If not given, it is assumed to be equal to the width. + """ + + def __init__(self, buffer, width, height, format, stride=None): + self._buffer = buffer + self._width = width + self._height = height + self._stride = stride if stride is not None else width + self._font = None + if format == MONO_VLSB: + self._stride = (self._stride + 7) & ~7 + self._format = MVLSBFormat() + elif format == MONO_HLSB: + self._stride = (self._stride + 7) & ~7 + self._format = MHLSBFormat() + elif format == MONO_HMSB: + self._format = MHMSBFormat() + elif format == RGB565: + self._format = RGB565Format() + elif format == GS2_HMSB: + self._stride = (self._stride + 3) & ~3 + self._format = GS2HMSBFormat() + elif format == GS4_HMSB: + self._stride = (self._stride + 1) & ~1 + self._format = GS4HMSBFormat() + elif format == GS8: + self._format = GS8Format() + else: + raise ValueError("invalid format") + + @property + def width(self): + """ + The width of the FrameBuffer in pixels. + """ + return self._width + + @property + def height(self): + """ + The height of the FrameBuffer in pixels. + """ + return self._height + + def fill_rect(self, x, y, w, h, c): + """ + Draw a rectangle at the given location, size and color. + + Args: + x (int): The x-coordinate of the top left corner of the rectangle. + y (int): The y-coordinate of the top left corner of the rectangle. + w (int): The width of the rectangle. + h (int): The height of the rectangle. + c (int): The color of the rectangle. + + Returns: + (tuple): A tuple containing the x, y, width, and height of the rectangle + """ + self._format.fill_rect(self, x, y, w, h, c) + return (x, y, w, h) + + def pixel(self, x, y, c=None): + """ + Set or get the color of a given pixel. + + Args: + x (int): The x-coordinate of the pixel. + y (int): The y-coordinate of the pixel. + c (int): The color of the pixel. If not given, the color of the pixel is returned. + + Returns: + (int, tuple or None): If c is not given, the color of the pixel is returned. + If c is given and x and y are within the bounds of the FrameBuffer, + the x, y, width, and height of the pixel are returned. + If x and y are not within the bounds of the FrameBuffer, None is returned. + """ + if x < 0 or x >= self._width or y < 0 or y >= self._height: + return None + if c is None: + return self._format.get_pixel(self, x, y) + self._format.set_pixel(self, x, y, c) + return (x, y, 1, 1) + + def fill(self, c): + """ + Fill the entire FrameBuffer with the specified color. + + Args: + c (int): The color to fill the FrameBuffer with. + + Returns: + (tuple): A tuple containing the x, y, width, and height of the FrameBuffer. + """ + self._format.fill(self, c) + return (0, 0, self._width, self._height) + + def scroll(self, xstep, ystep): + """ + Shift the contents of the FrameBuffer by the given vector (xstep, ystep). + This may leave a footprint of the previous colors in the FrameBuffer. + + Args: + xstep (int): The number of pixels to shift the FrameBuffer in the x direction. + ystep (int): The number of pixels to shift the FrameBuffer in the y direction. + + Raises: + ValueError: If the FrameBuffer format depth is not a multiple of 8 + """ + # Check to make sure self._format.depth is a multiple of 8 + if self._format.depth % 8 != 0: + raise ValueError("Scrolling is only implemented for depths that are multiples of 8") + + BPP = self._format.depth // 8 # Bytes per pixel + + # Determine the width and height of the FrameBuffer + width = self._width + height = self._height + + # Calculate the number of bytes per row + bytes_per_row = width * BPP + + # Iterate over each row in the appropriate order (top to bottom for ystep > 0, bottom to top for ystep < 0) + if ystep > 0: + y_range = range(height - 1, -1, -1) + else: + y_range = range(height) + + # Iterate over each row in the appropriate order + for y in y_range: + # Calculate the new row index + new_y = y + ystep + + # Skip rows that go out of bounds + if new_y < 0 or new_y >= height: + continue + + # Calculate the byte offset for the current and new rows + offset = y * bytes_per_row + new_offset = new_y * bytes_per_row + + # Iterate over each column in the appropriate order (right to left for xstep > 0, left to right for xstep < 0) + if xstep > 0: + for i in range(bytes_per_row - 1, (xstep * BPP) - 1, -1): + self._buffer[new_offset + i] = self._buffer[offset + i - (xstep * BPP)] + elif xstep < 0: + for i in range(-xstep * BPP, bytes_per_row): + self._buffer[new_offset + i] = self._buffer[offset + i + (xstep * BPP)] + else: + # If there is no x shift, copy the row as is + self._buffer[new_offset : new_offset + bytes_per_row] = self._buffer[ + offset : offset + bytes_per_row + ] + + def blit(self, *args, **kwargs): + """ + Blit a source to the canvas at the specified x, y location. + + Args: + source (FrameBuffer): Source FrameBuffer object. + x (int): X-coordinate to blit to. + y (int): Y-coordinate to blit to. + key (int): Key value for transparency (default: -1). + palette (Palette): Palette object for color translation (default: None). + """ + _shapes.blit(self, *args, **kwargs) + + def ellipse(self, *args, **kwargs): + """ + Midpoint ellipse algorithm + Draw an ellipse at the given location. Radii r1 and r2 define the geometry; equal values cause a + circle to be drawn. The c parameter defines the color. + + The optional f parameter can be set to True to fill the ellipse. Otherwise just a one pixel outline + is drawn. + + The optional m parameter enables drawing to be restricted to certain quadrants of the ellipse. + The LS four bits determine which quadrants are to be drawn, with bit 0 specifying Q1, b1 Q2, + b2 Q3 and b3 Q4. Quadrants are numbered counterclockwise with Q1 being top right. + + Args: + x0 (int): Center x coordinate + y0 (int): Center y coordinate + r1 (int): x radius + r2 (int): y radius + c (int): color + f (bool): Fill the ellipse (default: False) + m (int): Bitmask to determine which quadrants to draw (default: 0b1111) + w (int): Width of the ellipse (default: None) + h (int): Height of the ellipse (default: None) + """ + _shapes.ellipse(self, *args, **kwargs) + + def hline(self, *args, **kwargs): + """ + Horizontal line drawing function. Will draw a single pixel wide line. + + Args: + x0 (int): X-coordinate of the start of the line. + y0 (int): Y-coordinate of the start of the line. + w (int): Width of the line. + c (int): color. + """ + _shapes.hline(self, *args, **kwargs) + + def line(self, *args, **kwargs): + """ + Line drawing function. Will draw a single pixel wide line starting at + x0, y0 and ending at x1, y1. + + Args: + x0 (int): X-coordinate of the start of the line. + y0 (int): Y-coordinate of the start of the line. + x1 (int): X-coordinate of the end of the line. + y1 (int): Y-coordinate of the end of the line. + c (int): color. + """ + _shapes.line(self, *args, **kwargs) + + def poly(self, *args, **kwargs): + """ + Given a list of coordinates, draw an arbitrary (convex or concave) closed polygon at the given x, y location + using the given color. + + The coords must be specified as an array of integers, e.g. array('h', [x0, y0, x1, y1, ... xn, yn]) or a + list or tuple of points, e.g. [(x0, y0), (x1, y1), ... (xn, yn)]. + + The optional f parameter can be set to True to fill the polygon. Otherwise, just a one-pixel outline is drawn. + + Args: + x (int): X-coordinate of the polygon's position. + y (int): Y-coordinate of the polygon's position. + coords (list): List of coordinates. + c (int): color. + f (bool): Fill the polygon (default: False). + """ + _shapes.poly(self, *args, **kwargs) + + def rect(self, *args, **kwargs): + """ + Rectangle drawing function. Will draw a single pixel wide rectangle starting at + x0, y0 and extending w, h pixels. + + Args: + x0 (int): X-coordinate of the top-left corner of the rectangle. + y0 (int): Y-coordinate of the top-left corner of the rectangle. + w (int): Width of the rectangle. + h (int): Height of the rectangle. + c (int): color. + f (bool): Fill the rectangle (default: False). + """ + _shapes.rect(self, *args, **kwargs) + + def vline(self, *args, **kwargs): + """ + Horizontal line drawing function. Will draw a single pixel wide line. + + Args: + x0 (int): X-coordinate of the start of the line. + y0 (int): Y-coordinate of the start of the line. + h (int): Height of the line. + c (int): color. + """ + _shapes.vline(self, *args, **kwargs) + + def text(self, *args, **kwargs): + """ + Place text on the canvas with an 8 pixel high font. + Breaks on \n to next line. Does not break on line going off canvas. + + Args: + s (str): The text to draw. + x (int): The x position to start drawing the text. + y (int): The y position to start drawing the text. + c (int): The color to draw the text in. Default is 1. + scale (int): The scale factor to draw the text at. Default is 1. + inverted (bool): If True, draw the text inverted. Default is False. + font_data (str): The path to the font file to use. Default is None. + """ + _font.text(self, *args, **kwargs) + + +def FrameBuffer1(buffer, width, height, format, stride=None): + """ + Create a new FrameBuffer object. Here only for historical reasons. + """ + return FrameBuffer(buffer, width, height, format, stride) diff --git a/micropython/pydisplay/graphics/graphics/_framebuf_plus.py b/micropython/pydisplay/graphics/graphics/_framebuf_plus.py new file mode 100644 index 000000000..a4097df3e --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_framebuf_plus.py @@ -0,0 +1,626 @@ +from ._area import Area +from . import _shapes +from . import _files +from . import _font + +try: # Try to import framebuf from MicroPython + from framebuf import ( + MONO_VLSB, + MONO_HLSB, + MONO_HMSB, + GS2_HMSB, + GS4_HMSB, + GS8, + RGB565, + FrameBuffer as _FrameBuffer, + ) +except ImportError: # If framebuf is not available, import from _framebuf.py + from ._framebuf import ( + MONO_VLSB, + MONO_HLSB, + MONO_HMSB, + GS2_HMSB, + GS4_HMSB, + GS8, + RGB565, + FrameBuffer as _FrameBuffer, + ) + + +class FrameBuffer(_FrameBuffer): + """ + An extension of MicroPython's framebuf.FrameBuffer that adds some useful methods for drawing shapes and text. + Each method returns a bounding box (x, y, w, h) of the drawn shape to indicate + the area of the display that was modified. This can be used to update only the + modified area of the display. Exposes attributes not exposed in the base class, such + as color_depth, width, height, buffer, and format. Also adds a save method to save + the framebuffer to a file, and a from_file method to load a framebuffer from a file. + + Inherits from frambuf.Framebuffer, which may be compiled into MicroPython + or may be from _framebuf.py. Methods should return an Area object, but + the MicroPython framebuf module returns None, so the methods inherited from + framebuf.FrameBuffer are overridden to return an Area object. + + Args: + buffer (bytearray): Framebuffer buffer + width (int): Width in pixels + height (int): Height in pixels + format (int): Framebuffer format + + Attributes: + buffer (bytearray): Framebuffer buffer + width (int): Width in pixels + height (int): Height in pixels + format (int): Framebuffer format + color_depth (int): Color depth + """ + + def __init__(self, buffer, width, height, format, *args, **kwargs): + super().__init__(buffer, width, height, format, *args, **kwargs) + self._width = width + self._height = height + self._fb_format = format + self._buffer = buffer + if format == MONO_VLSB: + self._color_depth = 1 + elif format == MONO_HLSB: + self._color_depth = 1 + elif format == MONO_HMSB: + self._color_depth = 1 + elif format == RGB565: + self._color_depth = 16 + elif format == GS2_HMSB: + self._color_depth = 2 + elif format == GS4_HMSB: + self._color_depth = 4 + elif format == GS8: + self._color_depth = 8 + else: + raise ValueError("invalid format") + + @property + def color_depth(self): + return self._color_depth + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def buffer(self): + return self._buffer + + @property + def format(self): + return self._fb_format + + def fill_rect(self, x, y, w, h, c): + """ + Fill the given rectangle with the given color. + + Args: + x (int): x coordinate + y (int): y coordinate + w (int): Width in pixels + h (int): Height in pixels + c (int): color + + Returns: + (Area): Bounding box of the filled rectangle + """ + super().fill_rect(x, y, w, h, c) + return Area(x, y, w, h) + + def pixel(self, x, y, c=None): + """ + Draw a single pixel at the given location and color. + + Args: + x (int): x coordinate + y (int): y coordinate + c (int): color (default: None) + + Returns: + (Area): Bounding box of the pixel + """ + if c is None: + return super().pixel(x, y) + super().pixel(x, y, c) + return Area(x, y, 1, 1) + + def fill(self, c): + """ + Fill the buffer with the given color. + + Args: + c (int): color + + Returns: + (Area): Bounding box of the filled buffer + """ + super().fill(c) + return Area(0, 0, self.width, self.height) + + def ellipse(self, x, y, rx, ry, c, f=False, m=0b1111): + """ + Draw an ellipse at the given location, radii and color. + + Args: + x (int): Center x coordinate + y (int): Center y coordinate + rx (int): X radius + ry (int): Y radius + c (int): color + f (bool): Fill the ellipse (default: False) + m (int): Bitmask to determine which quadrants to draw (default: 0b1111) + + Returns: + (Area): Bounding box of the ellipse + """ + super().ellipse(x, y, rx, ry, c, f, m) + return Area(x - rx, y - ry, 2 * rx, 2 * ry) + + def hline(self, x, y, w, c): + """ + Draw a horizontal line at the given location, width and color. + + Args: + x (int): x coordinate + y (int): y coordinate + w (int): Width in pixels + c (int): color + + Returns: + (Area): Bounding box of the horizontal line + """ + super().hline(x, y, w, c) + return Area(x, y, w, 1) + + def line(self, x1, y1, x2, y2, c): + """ + Draw a line between the given start and end points and color. + + Args: + x1 (int): Start x coordinate + y1 (int): Start y coordinate + x2 (int): End x coordinate + y2 (int): End y coordinate + c (int): color + + Returns: + (Area): Bounding box of the line + """ + super().line(x1, y1, x2, y2, c) + return Area(min(x1, x2), min(y1, y2), abs(x2 - x1) + 1, abs(y2 - y1) + 1) + + def poly(self, x, y, coords, c, f=False): + """ + Draw a polygon at the given location, coordinates and color. + + Args: + x (int): x coordinate + y (int): y coordinate + coords (array): Array of x, y coordinate tuples + c (int): color + f (bool): Fill the polygon (default: False) + + Returns: + (Area): Bounding box of the polygon + """ + super().poly(x, y, coords, c, f) + # Calculate the bounding box of the polygon + # Convert the coords to a list of x, y tuples if it is not already + if isinstance(coords, list): + vertices = coords + elif isinstance(coords, tuple): + vertices = list(coords) + else: + # Check that the coords array has an even number of elements + if len(coords) % 2 != 0: + raise ValueError("coords must have an even number of elements") + vertices = [(coords[i], coords[i + 1]) for i in range(0, len(coords), 2)] + # Find the min and max x and y values + min_x = min([v[0] for v in vertices]) + min_y = min([v[1] for v in vertices]) + max_x = max([v[0] for v in vertices]) + max_y = max([v[1] for v in vertices]) + return Area(min_x, min_y, max_x - min_x + 1, max_y - min_y + 1) + + def rect(self, x, y, w, h, c, f=False): + """ + Draw a rectangle at the given location, size and color. + + Args: + x (int): Top left corner x coordinate + y (int): Top left corner y coordinate + w (int): Width in pixels + h (int): Height in pixels + c (int): color + f (bool): Fill the rectangle (default: False) + + Returns: + (Area): Bounding box of the rectangle + """ + super().rect(x, y, w, h, c, f) + return Area(x, y, w, h) + + def vline(self, x, y, h, c): + """ + Draw a vertical line at the given location, height and color. + + Args: + x (int): x coordinate + y (int): y coordinate + h (int): Height in pixels + c (int): color + + Returns: + (Area): Bounding box of the vertical line + """ + super().vline(x, y, h, c) + return Area(x, y, 1, h) + + def text(self, s, x, y, c=1, scale=1, inverted=False, font_data=None, height=8): + """ + Draw text at the given location, using the given font and color. + + Args: + s (str): Text to draw + x (int): x coordinate + y (int): y coordinate + c (int): color + scale (int): Scale factor (default: 1) + inverted (bool): Invert the text (default: False) + font_data (str): Path to the font file (default: None) + height (int): Height of the font (default: 8) + + Returns: + (Area): Bounding box of the text + """ + _font.text( + self, s, x, y, c, scale=scale, inverted=inverted, font_data=font_data, height=height + ) + + def blit(self, buf, x, y, key=-1, palette=None): + """ + Blit the given buffer at the given location. + + Args: + buf (FrameBuffer): FrameBuffer to blit + x (int): x coordinate + y (int): y coordinate + key (int): Color key (default: -1) + palette (list): Palette (default: None) + + Returns: + (Area): Bounding box of the blitted buffer + """ + super().blit(buf, x, y, key, palette) + + ########### Additional methods + + def arc(self, *args, **kwargs): + """ + Arc drawing function. Will draw a single pixel wide arc with a radius r + centered at x, y from a0 to a1. + + Args: + x (int): X-coordinate of the arc's center. + y (int): Y-coordinate of the arc's center. + r (int): Radius of the arc. + a0 (float): Starting angle in degrees. + a1 (float): Ending angle in degrees. + c (int): color. + + Returns: + (Area): The bounding box of the arc. + """ + return _shapes.arc(self, *args, **kwargs) + + def blit_rect(self, buf, x, y, w, h): + """ + Blit a rectangular area from a buffer to the canvas. Uses the canvas's blit_rect method if available, + otherwise writes directly to the buffer. + + Args: + buf (memoryview): Buffer to blit. Must already be byte-swapped if necessary. + x (int): X-coordinate to blit to. + y (int): Y-coordinate to blit to. + w (int): Width of the area to blit. + h (int): Height of the area to blit. + + Returns: + (Area): The bounding box of the blitted area. + """ + BPP = 2 + + if x < 0 or y < 0 or x + w > self.width or y + h > self.height: + raise ValueError("The provided x, y, w, h values are out of range") + + if len(buf) != w * h * BPP: + print(f"len(buf)={len(buf)} w={w} h={h} self.color_depth={self.color_depth}") + raise ValueError("The source buffer is not the correct size") + + for row in range(h): + source_begin = row * w * BPP + source_end = source_begin + w * BPP + dest_begin = ((y + row) * self.width + x) * BPP + dest_end = dest_begin + w * BPP + self.buffer[dest_begin:dest_end] = buf[source_begin:source_end] + return Area(x, y, w, h) + + def blit_transparent(self, *args, **kwargs): + """ + Blit a buffer with transparency. + + Args: + buf (memoryview): Buffer to blit. + x (int): X-coordinate to blit to. + y (int): Y-coordinate to blit to. + w (int): Width of the area to blit. + h (int): Height of the area to blit. + key (int): Key value for transparency. + + Returns: + (Area): The bounding box of the blitted area. + """ + return _shapes.blit_transparent(self, *args, **kwargs) + + def circle(self, *args, **kwargs): + """ + Circle drawing function. Will draw a single pixel wide circle + centered at x0, y0 and the specified r. + + Args: + x0 (int): Center x coordinate + y0 (int): Center y coordinate + r (int): Radius + c (int): Color + f (bool): Fill the circle (default: False) + + Returns: + (Area): The bounding box of the circle. + """ + return _shapes.circle(self, *args, **kwargs) + + def gradient_rect(self, *args, **kwargs): + """ + Fill a rectangle with a gradient. + + Args: + x (int): X-coordinate of the top-left corner of the rectangle. + y (int): Y-coordinate of the top-left corner of the rectangle. + w (int): Width of the rectangle. + h (int): Height of the rectangle. + c1 (int): 565 encoded color for the top or left edge. + c2 (int): 565 encoded color for the bottom or right edge. If None or the same as c1, + fill_rect will be called instead. + vertical (bool): If True, the gradient will be vertical. If False, the gradient will be horizontal. + + Returns: + (Area): The bounding box of the filled area. + """ + return _shapes.gradient_rect(self, *args, **kwargs) + + def polygon(self, *args, **kwargs): + """ + Draw a polygon on the canvas. + + Args: + points (list): List of points to draw. + x (int): X-coordinate of the polygon's position. + y (int): Y-coordinate of the polygon's position. + color (int): color. + angle (float): Rotation angle in radians (default: 0). + center_x (int): X-coordinate of the rotation center (default: 0). + center_y (int): Y-coordinate of the rotation center (default: 0). + + Raises: + ValueError: If the polygon has less than 3 points. + + Returns: + (Area): The bounding box of the polygon. + """ + return _shapes.polygon(self, *args, **kwargs) + + def round_rect(self, *args, **kwargs): + """ + Rounded rectangle drawing function. Will draw a single pixel wide rounded rectangle starting at + x0, y0 and extending w, h pixels with the specified radius. + + Args: + x0 (int): X-coordinate of the top-left corner of the rectangle. + y0 (int): Y-coordinate of the top-left corner of the rectangle. + w (int): Width of the rectangle. + h (int): Height of the rectangle. + r (int): Radius of the corners. + c (int): color. + f (bool): Fill the rectangle (default: False). + + Returns: + (Area): The bounding box of the rectangle. + """ + return _shapes.round_rect(self, *args, **kwargs) + + def triangle(self, *args, **kwargs): + """ + Triangle drawing function. Draws a single pixel wide triangle with vertices at + (x0, y0), (x1, y1), and (x2, y2). + + Args: + x0 (int): X-coordinate of the first vertex. + y0 (int): Y-coordinate of the first vertex. + x1 (int): X-coordinate of the second vertex. + y1 (int): Y-coordinate of the second vertex. + x2 (int): X-coordinate of the third vertex. + y2 (int): Y-coordinate of the third vertex. + c (int): color. + f (bool): Fill the triangle (default: False). + + Returns: + (Area): The bounding box of the triangle. + """ + return _shapes.triangle(self, *args, **kwargs) + + def text8(self, *args, **kwargs): + """ + Place text on the canvas with an 8 pixel high font. + Breaks on \n to next line. Does not break on line going off canvas. + + Args: + canvas (Canvas): The DisplayDriver, FrameBuffer, or other canvas-like object to draw on. + s (str): The text to draw. + x (int): The x position to start drawing the text. + y (int): The y position to start drawing the text. + c (int): The color to draw the text in. Default is 1. + scale (int): The scale factor to draw the text at. Default is 1. + inverted (bool): If True, draw the text inverted. Default is False. + font_data (str): The path to the font file to use. Default is None. + + Returns: + Area: The area that was drawn to. + """ + return _font.text8(self, *args, **kwargs) + + def text14(self, *args, **kwargs): + """ + Place text on the canvas with a 14 pixel high font. + Breaks on \n to next line. Does not break on line going off canvas. + + Args: + canvas (Canvas): The DisplayDriver, FrameBuffer, or other canvas-like object to draw on. + s (str): The text to draw. + x (int): The x position to start drawing the text. + y (int): The y position to start drawing the text. + c (int): The color to draw the text in. Default is 1. + scale (int): The scale factor to draw the text at. Default is 1. + inverted (bool): If True, draw the text inverted. Default is False. + font_data (str): The path to the font file to use. Default is None. + + Returns: + Area: The area that was drawn to. + """ + return _font.text14(self, *args, **kwargs) + + def text16(self, *args, **kwargs): + """ + Place text on the canvas with a 16 pixel high font. + Breaks on \n to next line. Does not break on line going off canvas. + + Args: + canvas (Canvas): The DisplayDriver, FrameBuffer, or other canvas-like object to draw on. + s (str): The text to draw. + x (int): The x position to start drawing the text. + y (int): The y position to start drawing the text. + c (int): The color to draw the text in. Default is 1. + scale (int): The scale factor to draw the text at. Default is 1. + inverted (bool): If True, draw the text inverted. Default is False. + font_data (str): The path to the font file to use. Default is None. + + Returns: + Area: The area that was drawn to. + """ + return _font.text16(self, *args, **kwargs) + + def save(self, filename=None): + """ + Save the framebuffer to a file. The file extension must match the format, otherwise + the extension will be appended to the filename. + + Saves 1-bit formats as PBM, 2-bit formats as PGM with max value 3, 4-bit formats as PGM with max value 15, + 8-bit formats as PGM with max value 255, and 16-bit formats as BMP. + + Args: + filename (str): Filename to save to + """ + if filename is None: + filename = "screenshot" + file_ext = filename.split(".")[-1] + if self.format == MONO_HLSB: + if file_ext != "pbm": + filename += ".pbm" + with open(filename, "wb") as f: + f.write(b"P4\n") + f.write(f"{self.width} {self.height}\n".encode()) + f.write(self.buffer) + elif self.format == GS2_HMSB: + if file_ext != "pgm": + filename += ".pgm" + with open(filename, "wb") as f: + f.write(b"P5\n") + f.write(f"{self.width} {self.height}\n".encode()) + f.write(b"3\n") + f.write(self.buffer) + elif self.format == GS4_HMSB: + if file_ext != "pgm": + filename += ".pgm" + with open(filename, "wb") as f: + f.write(b"P5\n") + f.write(f"{self.width} {self.height}\n".encode()) + f.write(b"15\n") + f.write(self.buffer) + elif self.format == GS8: + if file_ext != "pgm": + filename += ".pgm" + with open(filename, "wb") as f: + f.write(b"P5\n") + f.write(f"{self.width} {self.height}\n".encode()) + f.write(b"255\n") + f.write(self.buffer) + elif self.format == RGB565: + if file_ext != "bmp": + filename += ".bmp" + with open(filename, "wb") as f: + f.write(b"BM") # Offset 0: Signature + f.write((54 + len(self.buffer)).to_bytes(4, "little")) # Offset 2: File size + f.write(b"\x00\x00\x00\x00") # Offset 6: Unused + f.write(b"\x36\x00\x00\x00") # Offset 10: Offset to image data + f.write(b"\x28\x00\x00\x00") # Offset 14: DIB header size + f.write(self.width.to_bytes(4, "little")) # Offset 18: Width + f.write(self.height.to_bytes(4, "little")) # Offset 22: Height + f.write(b"\x01\x00") # Offset 26: Planes + f.write(b"\x10\x00") # Offset 28: Bits per pixel + f.write(b"\x00\x00\x00\x00") # Offset 30: Compression + f.write(len(self.buffer).to_bytes(4, "little")) # Offset 34: Image size + f.write( + b"\x00\x00\x00\x00\x00\x00\x00\x00" + ) # Offset 38: Horizontal and vertical resolution + f.write(b"\x00\x00\x00\x00") # Offset 46: Colors in palette + f.write(b"\x00\x00\x00\x00") # Offset 50: Important colors + # The order of the lines is reversed. We need to reverse them back. + for i in range(self.height): + f.write( + self.buffer[ + (self.height - i - 1) * self.width * 2 : (self.height - i) + * self.width + * 2 + ] + ) + else: + raise ValueError(f"Save method not implemented for format {self.format}") + + @staticmethod + def from_file(filename): + """ + Load a framebuffer from a file. + + Args: + filename (str): Filename to load from + """ + # Read the first two bytes to determine the file type + f = open(filename, "rb") + header = f.read(2) + f.close() + + if header == b"P4": + return _files.pbm_to_framebuffer(filename) + elif header == b"P5": + return _files.pgm_to_framebuffer(filename) + elif header == b"BM": + return _files.bmp_to_framebuffer(filename) + else: + raise ValueError(f"Unsupported file type {header}") diff --git a/micropython/pydisplay/graphics/graphics/_shapes.py b/micropython/pydisplay/graphics/graphics/_shapes.py new file mode 100644 index 000000000..99cd19b0b --- /dev/null +++ b/micropython/pydisplay/graphics/graphics/_shapes.py @@ -0,0 +1,905 @@ +# SPDX-FileCopyrightText: 2018 Kattni Rembor for Adafruit Industries, 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`graphics._shapes` +==================================================== +Graphics primitives for drawing on a canvas. + +Heavily modified from gfx.py at: +https://github.com/adafruit/Adafruit_CircuitPython_GFX +* Author(s): Kattni Rembor, Tony DiCola, Jonah Yolles-Murphy, based on code by Phil Burgess + +Implementation Notes +-------------------- +.pixel(), .fill_rect(), .fill() and .blit_rect() will be called from the canvas object if the canvas +object has these methods. + +.pixel() and .blit_rect() assume 16-bit color depth. + +""" + +import math +from ._area import Area + + +def arc(canvas, x, y, r, a0, a1, c): + """ + Arc drawing function. Will draw a single pixel wide arc with a radius r + centered at x, y from a0 to a1. + + Args: + x (int): X-coordinate of the arc's center. + y (int): Y-coordinate of the arc's center. + r (int): Radius of the arc. + a0 (float): Starting angle in degrees. + a1 (float): Ending angle in degrees. + c (int): color. + + Returns: + (Area): The bounding box of the arc. + """ + resolution = 60 + a0 = math.radians(a0) + a1 = math.radians(a1) + x0 = x + int(r * math.cos(a0)) + y0 = y + int(r * math.sin(a0)) + if a1 > a0: + arc_range = range(int(a0 * resolution), int(a1 * resolution)) + else: + arc_range = range(int(a0 * resolution), int(a1 * resolution), -1) + + x_min = x_max = x0 + y_min = y_max = y0 + for a in arc_range: + ar = a / resolution + x1 = x + int(r * math.cos(ar)) + y1 = y + int(r * math.sin(ar)) + line(canvas, x0, y0, x1, y1, c) + x_min = min(x0, x1, x_min) + x_max = max(x0, x1, x_max) + y_min = min(y0, y1, y_min) + y_max = max(y0, y1, y_max) + x0 = x1 + y0 = y1 + return Area(x_min, y_min, x_max - x_min, y_max - y_min) + + +def blit(canvas, source, x, y, key=-1, palette=None): + """ + Blit a source to the canvas at the specified x, y location. + + Args: + source (FrameBuffer): Source FrameBuffer object. + x (int): X-coordinate to blit to. + y (int): Y-coordinate to blit to. + key (int): Key value for transparency (default: -1). + palette (Palette): Palette object for color translation (default: None). + + Returns: + (Area): The bounding box of the blitted area. + """ + if ( + (-x >= source.width) + or (-y >= source.height) + or (x >= canvas.width) + or (y >= canvas.height) + ): + # Out of bounds, no-op. + return + + # Clip. + x0 = max(0, x) + y0 = max(0, y) + x1 = max(0, -x) + y1 = max(0, -y) + x0end = min(canvas.width, x + source.width) + y0end = min(canvas.height, y + source.height) + + for cy0 in range(y0, y0end): + cx1 = x1 + for cx0 in range(x0, x0end): + col = source.pixel(cx1, y1) + if palette: + col = palette.pixel(col, 0) + if col != key: + pixel(canvas, cx0, cy0, col) + cx1 += 1 + y1 += 1 + return Area(x0, y0, x0end - x0, y0end - y0) + + +def blit_rect(canvas, buf, x, y, w, h): + """ + Blit a rectangular area from a buffer to the canvas. Uses the canvas's blit_rect method if available, + otherwise writes directly to the buffer. + + Args: + buf (memoryview): Buffer to blit. Must already be byte-swapped if necessary. + x (int): X-coordinate to blit to. + y (int): Y-coordinate to blit to. + w (int): Width of the area to blit. + h (int): Height of the area to blit. + + Returns: + (Area): The bounding box of the blitted area. + """ + if hasattr(canvas, "blit_rect"): + canvas.blit_rect(buf, x, y, w, h) + else: + BPP = 2 + + if x < 0 or y < 0 or x + w > canvas.width or y + h > canvas.height: + raise ValueError("The provided x, y, w, h values are out of range") + + if len(buf) != w * h * BPP: + print(f"len(buf)={len(buf)} w={w} h={h} self.color_depth={canvas.color_depth}") + raise ValueError("The source buffer is not the correct size") + + for row in range(h): + source_begin = row * w * BPP + source_end = source_begin + w * BPP + dest_begin = ((y + row) * canvas.width + x) * BPP + dest_end = dest_begin + w * BPP + canvas.buffer[dest_begin:dest_end] = buf[source_begin:source_end] + return Area(x, y, w, h) + + +def blit_transparent(canvas, buf, x, y, w, h, key): + """ + Blit a buffer with transparency. + + Args: + buf (memoryview): Buffer to blit. + x (int): X-coordinate to blit to. + y (int): Y-coordinate to blit to. + w (int): Width of the area to blit. + h (int): Height of the area to blit. + key (int): Key value for transparency. + + Returns: + (Area): The bounding box of the blitted area. + """ + BPP = canvas.color_depth // 8 + key_bytes = key.to_bytes(BPP, "little") + stride = w * BPP + for j in range(h): + rowstart = j * stride + colstart = 0 + # iterate over each pixel looking for the first non-key pixel + while colstart < stride: + startoffset = rowstart + colstart + if buf[startoffset : startoffset + BPP] != key_bytes: + # found a non-key pixel + # then iterate over each pixel looking for the next key pixel + colend = colstart + while colend < stride: + endoffset = rowstart + colend + if buf[endoffset : endoffset + BPP] == key_bytes: + break + colend += BPP + # blit the non-key pixels + blit_rect( + canvas, + buf[rowstart + colstart : rowstart + colend], + x + colstart // BPP, + y + j, + (colend - colstart) // BPP, + 1, + ) + colstart = colend + else: + colstart += BPP + return Area(x, y, w, h) + + +def circle(canvas, x0, y0, r, c, f=False): + """ + Circle drawing function. Will draw a single pixel wide circle + centered at x0, y0 and the specified r. + + Args: + x0 (int): Center x coordinate + y0 (int): Center y coordinate + r (int): Radius + c (int): Color + f (bool): Fill the circle (default: False) + + Returns: + (Area): The bounding box of the circle. + """ + if f: + return _fill_circle_helper(canvas, x0, y0, r, c, 0, 0) + + _circle_helper(canvas, x0, y0, r, c, 0, 0) + return Area(x0 - r, y0 - r, 2 * r, 2 * r) + + +def _circle_helper(canvas, x0, y0, r, c, x_offset, y_offset): + """ + Circle helper function. Draws the 4 quadrants of a circle with center at x0, y0 and the specified r + separated by the specified x_offset and y_offset. Draws a circle if offsets are 0. Draws the 4 corners of + a round_rect if an offset is greater than 0. + """ + f = 1 - r + ddF_x = 1 + ddF_y = -2 * r + x = 0 + y = r + while x < y: + if f >= 0: + y -= 1 + ddF_y += 2 + f += ddF_y + x += 1 + ddF_x += 2 + f += ddF_x + offset_x = x + x_offset + offset_y = y + y_offset + pixel(canvas, x0 + offset_x - 1, y0 - offset_y, c) # 90 to 45 + pixel(canvas, x0 - offset_x, y0 - offset_y, c) # 90 to 135 + pixel(canvas, x0 + offset_x - 1, y0 + offset_y - 1, c) # 270 to 315 + pixel(canvas, x0 - offset_x, y0 + offset_y - 1, c) # 270 to 225 + offset_x = y + x_offset + offset_y = x + y_offset + pixel(canvas, x0 + offset_x - 1, y0 + offset_y - 1, c) # 0 to 315 + pixel(canvas, x0 - offset_x, y0 + offset_y - 1, c) # 180 to 225 + pixel(canvas, x0 + offset_x - 1, y0 - offset_y, c) # 0 to 45 + pixel(canvas, x0 - offset_x, y0 - offset_y, c) # 180 to 135 + + +def _fill_circle_helper(canvas, x0, y0, r, c, x_offset, y_offset): + """ + Fill circle helper function. Draws the 4 quadrants of a filled circle with center at x0, y0 and the + specified r separated by the specified x_offset and y_offset. Fills a circle if offsets are 0. Fills the + 4 corners of a filled round_rect if an offset is greater than 0. + """ + # vline(canvas, x0, y0 - r, 2 * r + 1, c) + f = 1 - r + ddF_x = 1 + ddF_y = -2 * r + x = 0 + y = r + while x < y: + if f >= 0: + y -= 1 + ddF_y += 2 + f += ddF_y + x += 1 + ddF_x += 2 + f += ddF_x + offset_x = x + x_offset + offset_y = y + y_offset + vline(canvas, x0 - offset_x, y0 - offset_y, 2 * offset_y, c) + vline(canvas, x0 + offset_x - 1, y0 - offset_y, 2 * offset_y, c) + offset_x = y + x_offset + offset_y = x + y_offset + vline(canvas, x0 - offset_x, y0 - offset_y, 2 * offset_y, c) + vline(canvas, x0 + offset_x - 1, y0 - offset_y, 2 * offset_y, c) + + return Area(x0 - r, y0 - r, 2 * r, 2 * r) + + +def ellipse(canvas, x0, y0, r1, r2, c, f=False, m=0b1111, w=None, h=None): + """ + Midpoint ellipse algorithm + Draw an ellipse at the given location. Radii r1 and r2 define the geometry; equal values cause a + circle to be drawn. The c parameter defines the color. + + The optional f parameter can be set to True to fill the ellipse. Otherwise just a one pixel outline + is drawn. + + The optional m parameter enables drawing to be restricted to certain quadrants of the ellipse. + The LS four bits determine which quadrants are to be drawn, with bit 0 specifying Q1, b1 Q2, + b2 Q3 and b3 Q4. Quadrants are numbered counterclockwise with Q1 being top right. + + Args: + x0 (int): Center x coordinate + y0 (int): Center y coordinate + r1 (int): x radius + r2 (int): y radius + c (int): color + f (bool): Fill the ellipse (default: False) + m (int): Bitmask to determine which quadrants to draw (default: 0b1111) + w (int): Width of the ellipse (default: None) + h (int): Height of the ellipse (default: None) + + Returns: + (Area): The bounding box of the ellipse. + """ + if r1 < 1 or r2 < 1: + return + + x_side = w - 2 * r1 if w else 0 + y_side = h - 2 * r2 if h else 0 + x_offset = x_side // 2 if w else 0 + y_offset = y_side // 2 if h else 0 + + if f: + if y_offset > 0: + fill_rect(canvas, x0 - w // 2, y0 - y_offset, w, y_side, c) + if x_offset > 0: + fill_rect(canvas, x0 - x_offset, y0 - h // 2, x_side, r1, c) + fill_rect(canvas, x0 - x_offset, y0 + h // 2 - r1, x_side, r1, c) + + if x_offset > 0: + hline(canvas, x0 - x_offset, y0 - h // 2, x_side, c) + hline(canvas, x0 - x_offset, y0 + h // 2, x_side, c) + if y_offset > 0: + vline(canvas, x0 - w // 2, y0 - y_offset, y_side, c) + vline(canvas, x0 + w // 2, y0 - y_offset, y_side, c) + + a2 = r1 * r1 + b2 = r2 * r2 + fa2 = 4 * a2 + fb2 = 4 * b2 + + x1 = r1 + y1 = 0 + sigma = 2 * a2 + b2 * (1 - 2 * r1) + while a2 * y1 <= b2 * x1: + if f: + if m & 0x1: + hline(canvas, x0 + x_offset, y0 - y1 - y_offset, x1, c) + if m & 0x2: + hline(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, x1, c) + if m & 0x4: + hline(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, x1, c) + if m & 0x8: + hline(canvas, x0 + x_offset, y0 + y1 + y_offset, x1, c) + else: + if m & 0x1: + pixel(canvas, x0 + x1 + x_offset, y0 - y1 - y_offset, c) + if m & 0x2: + pixel(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, c) + if m & 0x4: + pixel(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, c) + if m & 0x8: + pixel(canvas, x0 + x1 + x_offset, y0 + y1 + y_offset, c) + if sigma >= 0: + sigma += fb2 * (1 - x1) + x1 -= 1 + sigma += a2 * ((4 * y1) + 6) + y1 += 1 + + x1 = 0 + y1 = r2 + sigma = 2 * b2 + a2 * (1 - 2 * r2) + while b2 * x1 <= a2 * y1: + if f: + if m & 0x1: + hline(canvas, x0 + x_offset, y0 - y1 - y_offset, x1, c) + if m & 0x2: + hline(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, x1, c) + if m & 0x4: + hline(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, x1, c) + if m & 0x8: + hline(canvas, x0 + x_offset, y0 + y1 + y_offset, x1, c) + else: + if m & 0x1: + pixel(canvas, x0 + x1 + x_offset, y0 - y1 - y_offset, c) + if m & 0x2: + pixel(canvas, x0 - x1 - x_offset, y0 - y1 - y_offset, c) + if m & 0x4: + pixel(canvas, x0 - x1 - x_offset, y0 + y1 + y_offset, c) + if m & 0x8: + pixel(canvas, x0 + x1 + x_offset, y0 + y1 + y_offset, c) + if sigma >= 0: + sigma += fa2 * (1 - y1) + y1 -= 1 + sigma += b2 * ((4 * x1) + 6) + x1 += 1 + return Area(x0 - r1 - x_offset, y0 - r2 - y_offset, 2 * (r1 + x_offset), 2 * (r2 + y_offset)) + + +def fill(canvas, c): + """ + Fill the entire canvas with a color. Uses the canvas's fill method if available, + otherwise calls the fill_rect function. + + Args: + c (int): color. + + Returns: + (Area): The bounding box of the filled area. + """ + if hasattr(canvas, "fill"): + canvas.fill(c) + return Area(0, 0, canvas.width, canvas.height) + else: + return fill_rect(canvas, 0, 0, canvas.width, canvas.height, c) + + +def fill_rect(canvas, x, y, w, h, c): + """ + Filled rectangle drawing function. Draws a filled rectangle starting at + x, y and extending w, h pixels. Uses the canvas's fill_rect method if available, + otherwise calls the pixel function for each pixel. + + Args: + x (int): X-coordinate of the top-left corner of the rectangle. + y (int): Y-coordinate of the top-left corner of the rectangle. + w (int): Width of the rectangle. + h (int): Height of the rectangle. + c (int): color + + Returns: + (Area): The bounding box of the filled area. + """ + if y < -h or y > canvas.height or x < -w or x > canvas.width: + return + if hasattr(canvas, "fill_rect"): + canvas.fill_rect(x, y, w, h, c) + else: + for j in range(y, y + h): + for i in range(x, x + w): + pixel(canvas, i, j, c) + return Area(x, y, w, h) + + +def gradient_rect(canvas, x, y, w, h, c1, c2=None, vertical=True): + """ + Fill a rectangle with a gradient. + + Args: + x (int): X-coordinate of the top-left corner of the rectangle. + y (int): Y-coordinate of the top-left corner of the rectangle. + w (int): Width of the rectangle. + h (int): Height of the rectangle. + c1 (int): 565 encoded color for the top or left edge. + c2 (int): 565 encoded color for the bottom or right edge. If None or the same as c1, + fill_rect will be called instead. + vertical (bool): If True, the gradient will be vertical. If False, the gradient will be horizontal. + + Returns: + (Area): The bounding box of the filled area. + """ + if c2 is None or c1 == c2: + return fill_rect(canvas, x, y, w, h, c1) + r1, g1, b1 = (c1 >> 8) & 0xF8, (c1 >> 3) & 0xFC, (c1 << 3) & 0xF8 + r2, g2, b2 = (c2 >> 8) & 0xF8, (c2 >> 3) & 0xFC, (c2 << 3) & 0xF8 + if vertical: + for j in range(h): + r = r1 + (r2 - r1) * j // h + g = g1 + (g2 - g1) * j // h + b = b1 + (b2 - b1) * j // h + c = (r & 0xF8) << 8 | (g & 0xFC) << 3 | (b & 0xF8) >> 3 + fill_rect(canvas, x, y + j, w, 1, c) + else: + for i in range(w): + r = r1 + (r2 - r1) * i // w + g = g1 + (g2 - g1) * i // w + b = b1 + (b2 - b1) * i // w + c = (r & 0xF8) << 8 | (g & 0xFC) << 3 | (b & 0xF8) >> 3 + fill_rect(canvas, x + i, y, 1, h, c) + return Area(x, y, w, h) + + +def hline(canvas, x0, y0, w, c): + """ + Horizontal line drawing function. Will draw a single pixel wide line. + + Args: + x0 (int): X-coordinate of the start of the line. + y0 (int): Y-coordinate of the start of the line. + w (int): Width of the line. + c (int): color. + + Returns: + (Area): The bounding box of the line. + """ + if y0 < 0 or y0 > canvas.height or x0 < -w or x0 > canvas.width: + return + fill_rect(canvas, x0, y0, w, 1, c) + return Area(x0, y0, w, 1) + + +def line(canvas, x0, y0, x1, y1, c): + """ + Line drawing function. Will draw a single pixel wide line starting at + x0, y0 and ending at x1, y1. + + Args: + x0 (int): X-coordinate of the start of the line. + y0 (int): Y-coordinate of the start of the line. + x1 (int): X-coordinate of the end of the line. + y1 (int): Y-coordinate of the end of the line. + c (int): color. + + Returns: + (Area): The bounding box of the line. + """ + if x0 == x1: + return vline(canvas, x0, y0, abs(y1 - y0) + 1, c) + if y0 == y1: + return hline(canvas, x0, y0, abs(x1 - x0) + 1, c) + + steep = abs(y1 - y0) > abs(x1 - x0) + if steep: + x0, y0 = y0, x0 + x1, y1 = y1, x1 + if x0 > x1: + x0, x1 = x1, x0 + y0, y1 = y1, y0 + dx = x1 - x0 + dy = abs(y1 - y0) + err = dx // 2 + ystep = 0 + if y0 < y1: + ystep = 1 + else: + ystep = -1 + while x0 <= x1: + if steep: + pixel(canvas, y0, x0, c) + else: + pixel(canvas, x0, y0, c) + err -= dy + if err < 0: + y0 += ystep + err += dx + x0 += 1 + return Area(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0)) + + +def pixel(canvas, x, y, c): + """ + Draw a single pixel at the specified x, y location. Uses the canvas's pixel method if available, + otherwise writes directly to the buffer. + + Args: + x (int): X-coordinate of the pixel. + y (int): Y-coordinate of the pixel. + c (int): color. + + Returns: + (Area): The bounding box of the pixel. + """ + if hasattr(canvas, "pixel"): + canvas.pixel(x, y, c) + else: + rgb565_color = (c & 0xFFFF).to_bytes(2, "little") + canvas.buffer[(y * canvas.width + x) * 2 : (y * canvas.width + x) * 2 + 2] = rgb565_color + return Area(x, y, 1, 1) + + +def poly(canvas, x, y, coords, c, f=False): + """ + Given a list of coordinates, draw an arbitrary (convex or concave) closed polygon at the given x, y location + using the given color. + + The coords must be specified as an array of integers, e.g. array('h', [x0, y0, x1, y1, ... xn, yn]) or a + list or tuple of points, e.g. [(x0, y0), (x1, y1), ... (xn, yn)]. + + The optional f parameter can be set to True to fill the polygon. Otherwise, just a one-pixel outline is drawn. + + Args: + x (int): X-coordinate of the polygon's position. + y (int): Y-coordinate of the polygon's position. + coords (list): List of coordinates. + c (int): color. + f (bool): Fill the polygon (default: False). + + Returns: + (Area): The bounding box of the polygon. + """ + + # Convert the coords to a list of x, y tuples if it is not already + if isinstance(coords, list): + vertices = coords + elif isinstance(coords, tuple): + vertices = list(coords) + else: + # Check that the coords array has an even number of elements + if len(coords) % 2 != 0: + raise ValueError("coords must have an even number of elements") + vertices = [(coords[i], coords[i + 1]) for i in range(0, len(coords), 2)] + + # Check that the polygon has at least 3 vertices + if len(vertices) < 3: + raise ValueError("polygon must have at least 3 vertices") + + # Close the polygon if it is not already closed + if vertices[0] != vertices[-1]: + vertices.append(vertices[0]) + + # Offset vertices by (x, y) + vertices = [(x + vertex[0], y + vertex[1]) for vertex in vertices] + + # Find the rectangle bounding box of the polygon + left = min(vertex[0] for vertex in vertices) + right = max(vertex[0] for vertex in vertices) + top = min(vertex[1] for vertex in vertices) + bottom = max(vertex[1] for vertex in vertices) + + if f: + # Fill the polygon using scanline algorithm + # Calculate the minimum and maximum y-coordinates in the polygon + y_min = min(vertex[1] for vertex in vertices) + y_max = max(vertex[1] for vertex in vertices) + + # Iterate through each y-coordinate within the bounding box + for y_scan in range(y_min, y_max + 1): + # Determine intersections with the polygon edges + intersections = [] + for i in range(len(vertices) - 1): + x1, y1 = vertices[i] + x2, y2 = vertices[i + 1] + # Check if the scanline intersects the edge + if y1 <= y_scan < y2 or y2 <= y_scan < y1: + # Calculate the intersection point using linear interpolation + x_intersection = x1 + ((y_scan - y1) / (y2 - y1)) * (x2 - x1) + intersections.append(x_intersection) + + # Sort intersections in increasing order + intersections.sort() + + # Draw horizontal lines between pairs of intersection points + for i in range(0, len(intersections), 2): + x_start = int(intersections[i]) + x_end = int(intersections[i + 1]) + hline(canvas, x_start, y_scan, x_end - x_start, c) + else: + for i in range(len(vertices) - 1): + line( + canvas, + vertices[i][0], + vertices[i][1], + vertices[i + 1][0], + vertices[i + 1][1], + c, + ) + return Area(left, top, right - left, bottom - top) + + +def polygon(canvas, points, x, y, color, angle=0, center_x=0, center_y=0): + """ + Draw a polygon on the canvas. + + Args: + points (list): List of points to draw. + x (int): X-coordinate of the polygon's position. + y (int): Y-coordinate of the polygon's position. + color (int): color. + angle (float): Rotation angle in radians (default: 0). + center_x (int): X-coordinate of the rotation center (default: 0). + center_y (int): Y-coordinate of the rotation center (default: 0). + + Raises: + ValueError: If the polygon has less than 3 points. + + Returns: + (Area): The bounding box of the polygon. + """ + # MIT License + # Copyright (c) 2024 Brad Barnett + # Copyright (c) 2020-2023 Russ Hughes + # Copyright (c) 2019 Ivan Belokobylskiy + if len(points) < 3: + raise ValueError("Polygon must have at least 3 points.") + + # fmt: off + if angle: + cos_a = math.cos(angle) + sin_a = math.sin(angle) + rotated = [ + (x + center_x + int((point[0] - center_x) * cos_a - (point[1] - center_y) * sin_a), + y + center_y + int((point[0] - center_x) * sin_a + (point[1] - center_y) * cos_a)) + for point in points + ] + else: + rotated = [(x + int((point[0])), y + int((point[1]))) for point in points] + + # Find the rectangle bounding box of the polygon + left = min(vertex[0] for vertex in rotated) + right = max(vertex[0] for vertex in rotated) + top = min(vertex[1] for vertex in rotated) + bottom = max(vertex[1] for vertex in rotated) + + for i in range(1, len(rotated)): + line(canvas, rotated[i - 1][0], rotated[i - 1][1], rotated[i][0], rotated[i][1], color) + # fmt: on + return Area(left, top, right - left, bottom - top) + + +def rect(canvas, x0, y0, w, h, c, f=False): + """ + Rectangle drawing function. Will draw a single pixel wide rectangle starting at + x0, y0 and extending w, h pixels. + + Args: + x0 (int): X-coordinate of the top-left corner of the rectangle. + y0 (int): Y-coordinate of the top-left corner of the rectangle. + w (int): Width of the rectangle. + h (int): Height of the rectangle. + c (int): color. + f (bool): Fill the rectangle (default: False). + + Returns: + (Area): The bounding box of the rectangle. + """ + if f: + return fill_rect(canvas, x0, y0, w, h, c) + if y0 < -h or y0 > canvas.height or x0 < -w or x0 > canvas.width: + return + hline(canvas, x0, y0, w, c) + hline(canvas, x0, y0 + h - 1, w, c) + vline(canvas, x0, y0, h, c) + vline(canvas, x0 + w - 1, y0, h, c) + return Area(x0, y0, w, h) + + +def round_rect(canvas, x0, y0, w, h, r, c, f=False): + """ + Rounded rectangle drawing function. Will draw a single pixel wide rounded rectangle starting at + x0, y0 and extending w, h pixels with the specified radius. + + Args: + x0 (int): X-coordinate of the top-left corner of the rectangle. + y0 (int): Y-coordinate of the top-left corner of the rectangle. + w (int): Width of the rectangle. + h (int): Height of the rectangle. + r (int): Radius of the corners. + c (int): color. + f (bool): Fill the rectangle (default: False). + + Returns: + (Area): The bounding box of the rectangle. + """ + # If the radius is 0, just draw a rectangle + if r == 0: + return rect(canvas, x0, y0, w, h, c, f) + + # If filled, draw the rounded rectangle using the _fill_round_rect function + if f: + return _fill_round_rect(canvas, x0, y0, w, h, r, c) + + # ensure that the r will only ever half of the shortest side or less + r = int(min(r, w / 2, h / 2)) + + hline(canvas, x0 + r, y0, w - 2 * r, c) # top + hline(canvas, x0 + r, y0 + h - 1, w - 2 * r, c) # bottom + vline(canvas, x0, y0 + r, h - 2 * r, c) # left + vline(canvas, x0 + w - 1, y0 + r, h - 2 * r, c) # right + _circle_helper(canvas, x0 + w // 2, y0 + h // 2, r, c, w // 2 - r, h // 2 - r) + return Area(x0, y0, w, h) + + +def _fill_round_rect(canvas, x0, y0, w, h, r, c): + """ + Filled rounded rectangle drawing function. Will draw a filled rounded rectangle starting at + x0, y0 and extending w, h pixels with the specified radius. + """ + + # ensure that the r will only ever be half of the shortest side or less + r = int(min(r, w / 2, h / 2)) + fill_rect(canvas, x0 + r, y0, w - 2 * r, h, c) # center + _fill_circle_helper(canvas, x0 + w // 2, y0 + h // 2, r, c, w // 2 - r, h // 2 - r) + return Area(x0, y0, w, h) + + +def triangle(canvas, x0, y0, x1, y1, x2, y2, c, f=False): + # pylint: disable=too-many-arguments + """ + Triangle drawing function. Draws a single pixel wide triangle with vertices at + (x0, y0), (x1, y1), and (x2, y2). + + Args: + x0 (int): X-coordinate of the first vertex. + y0 (int): Y-coordinate of the first vertex. + x1 (int): X-coordinate of the second vertex. + y1 (int): Y-coordinate of the second vertex. + x2 (int): X-coordinate of the third vertex. + y2 (int): Y-coordinate of the third vertex. + c (int): color. + f (bool): Fill the triangle (default: False). + + Returns: + (Area): The bounding box of the triangle. + """ + if f: + return _fill_triangle(canvas, x0, y0, x1, y1, x2, y2, c) + line(canvas, x0, y0, x1, y1, c) + line(canvas, x1, y1, x2, y2, c) + line(canvas, x2, y2, x0, y0, c) + left = min(x0, x1, x2) + top = min(y0, y1, y2) + right = max(x0, x1, x2) + bottom = max(y0, y1, y2) + return Area(left, top, right - left, bottom - top) + + +def _fill_triangle(canvas, x0, y0, x1, y1, x2, y2, c): + # pylint: disable=too-many-arguments + """ + Filled triangle drawing function. Will draw a filled triangle with vertices at + (x0, y0), (x1, y1), and (x2, y2). + """ + if y0 > y1: + y0, y1 = y1, y0 + x0, x1 = x1, x0 + if y1 > y2: + y2, y1 = y1, y2 + x2, x1 = x1, x2 + if y0 > y1: + y0, y1 = y1, y0 + x0, x1 = x1, x0 + a = 0 + b = 0 + last = 0 + if y0 == y2: + a = x0 + b = x0 + if x1 < a: + a = x1 + elif x1 > b: + b = x1 + if x2 < a: + a = x2 + elif x2 > b: + b = x2 + hline(canvas, a, y0, b - a + 1, c) + return + dx01 = x1 - x0 + dy01 = y1 - y0 + dx02 = x2 - x0 + dy02 = y2 - y0 + dx12 = x2 - x1 + dy12 = y2 - y1 + if dy01 == 0: + dy01 = 1 + if dy02 == 0: + dy02 = 1 + if dy12 == 0: + dy12 = 1 + sa = 0 + sb = 0 + y = y0 + if y0 == y1: + last = y1 - 1 + else: + last = y1 + while y <= last: + a = x0 + sa // dy01 + b = x0 + sb // dy02 + sa += dx01 + sb += dx02 + if a > b: + a, b = b, a + hline(canvas, a, y, b - a + 1, c) + y += 1 + sa = dx12 * (y - y1) + sb = dx02 * (y - y0) + while y <= y2: + a = x1 + sa // dy12 + b = x0 + sb // dy02 + sa += dx12 + sb += dx02 + if a > b: + a, b = b, a + hline(canvas, a, y, b - a + 1, c) + y += 1 + left = min(x0, x1, x2) + top = min(y0, y1, y2) + right = max(x0, x1, x2) + bottom = max(y0, y1, y2) + return Area(left, top, right - left, bottom - top) + + +def vline(canvas, x0, y0, h, c): + """ + Horizontal line drawing function. Will draw a single pixel wide line. + + Args: + x0 (int): X-coordinate of the start of the line. + y0 (int): Y-coordinate of the start of the line. + h (int): Height of the line. + c (int): color. + + Returns: + (Area): The bounding box of the line. + """ + if y0 < -h or y0 > canvas.height or x0 < 0 or x0 > canvas.width: + return + fill_rect(canvas, x0, y0, 1, h, c) + return Area(x0, y0, 1, h) diff --git a/micropython/pydisplay/graphics/manifest.py b/micropython/pydisplay/graphics/manifest.py new file mode 100644 index 000000000..24e620bcf --- /dev/null +++ b/micropython/pydisplay/graphics/manifest.py @@ -0,0 +1,8 @@ +metadata( + description="PyDisplay graphics", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="graphics", +) +package("graphics") diff --git a/micropython/pydisplay/multimer/README.md b/micropython/pydisplay/multimer/README.md new file mode 100644 index 000000000..1a01afa0c --- /dev/null +++ b/micropython/pydisplay/multimer/README.md @@ -0,0 +1,151 @@ +logo + +

pydisplay

+ +

Cross-platform User Interface and Event Drivers for *Python

+ +

+ About • + Key Features • + Getting Started • + Running Your First App • + API • + Roadmap • + Contributing • + Thanks • + Screenshots +

+ +| ![peterhinch's active.py](screenshots/active.gif) | ![russhughes's tiny_toasters.py](screenshots/tiny_toasters.gif) | +|-------------------------|--------------------------------| +| @peterhinch's active.py | @russhughes's tiny_toasters.py | + +## About + +WARNINGS: pydisplay is currently alpha quality. Every effort has been made to test on as many platforms as possible, but I need your help and feedback to get it to its inital release. A lot has changed and I am working on catching up the documentation. + +pydisplay is a universal display, event and device driver framework for multiple flavors of Python, including MicroPython, CircuitPython and CPython (big Python). It may be used as-is to create graphic frontends to your apps, or may be used as a foundation with GUI libraries such as [LVGL](https://github.com/lvgl/lv_micropython), [MicroPython-touch](https://github.com/peterhinch/micropython-touch) or maybe even a GUI framework you've been thinking of developing. Its primary purpose is to provide display and touch drivers for MicroPython, but it is equally useful for developers who may never touch MicroPython. + +It is important to note that pydisplay is meant to be a foundation for GUI libraries and is not itself a GUI library. It doesn't provide widgets, such as buttons, checkboxes or sliders, and it doesn't provide a timing mechanism. You will need a GUI library to provide those if necessary, although many apps won't need them. (There is a cross-platform repository [multimer](https://github.com/PyDevices/pydisplay/tree/main/src/lib/multimer) you can use if you want to used scheduled interrupts. It works with CPython and MicroPython, but doesn't work with CircuitPython. You can also use asyncio for timing.) + +## Key Features + +- May be used without additional libraries to add graphics capabilities to MicroPython, CircuitPython and CPython, with a consistent API across them all. +- Enables moving from one platform to another, for example MicroPython on ESP32-S3 to CPython on Windows without changing your code. Do your graphics development on your desktop, laptop or ChromeBook and then move to a microcontroller when you are ready to interface with your sensors and devices. CPython has much better error messages than MicroPython making it easier to troubleshoot when things go wrong! +- Built around devices available on microcontrollers but not necessarily available on desktop operating systems. For instance, rotary encoders and mouse scroll wheels show up as the same device type and yield the same events. Touchscreens on microcontrollers yield the same events as mice on desktops. Likewise with keypads / keyboards. +- Easily extensible. Use the primitives provided by pydisplay and add your own libraries, classes and functions to have even greater functionality. +- Provides several built-in color palettes and a mechanism to generate your own palettes. +- Lots of examples included, whether developed specifically for pydisplay or ported from [Russ Hughes's st7789py_mpy](https://github.com/russhughes/st7789py_mpy). Also works with all of the examples from Peter Hinch's MicroPython GUI libraries [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) and [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) on MicroPython. +- Support MicroPython on microcontrollers and on Unix(-like) operating systems. +- On MicroPython, can be configured to work with [kdschlosser's lvgl_micropython bus drivers](https://github.com/kdschlosser/lvgl_micropython), which are very fast bus drivers written in C. +- Works with CircuitPython's FourWire and ParallelBus bus drivers, as well as FrameBufferDisplay based interfaces such as dotclockframebuffer, usb_video and rgbmatrix + +## Getting Started + +This section is under construction. For now, see [Getting Started](GETTING-STARTED.md) for more information. + + +## Running your first app + +You will need to import the `path.py` file before running any of the examples. + +On desktop operating systems, `cd` into the `mp` directory (or wherever you have the files staged) and type: +``` +python3 -i path.py +``` +or +``` +micropython -i path.py +``` + +On microcontrollers, either add the following to your `boot.py` (MicroPython) or `code.py` (CircuitPython), or simply import it at the REPL before importing your desired app: +``` +import lib.path +``` + +The [examples](examples) directory will be on the system path, so to run an app from it, you just need to type: +``` +import calculator # substitute `calculator` with the file OR directory you want to run, omitting the .py extension +``` + +To run any of the examples from MicroPython-Touch (remember, its for MicroPython only) type: +``` +import gui.demos.various # substitute `various` with the file you want to run, omitting the .py extension +``` + +## API + +Where possible, existing, proven APIs were used. + +- There are currently 5 display classes, and hopefully another `EPaperDisplay` display class will be added soon, although I will need help from the community for this. + - BusDisplay is for microcontrollers, both on MicroPython and CircuitPython. CircuitPython provides the required bus drivers, as mentioned elsewhere in this README, but MicroPython doesn't have display bus drivers. The [buses](src/lib/buses) packages are included with the installer. It is my hope that community members will create other C bus drivers similar to @kdschlosser's bus drivers in [lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython). + - SDL2Display - the preferred class for desktop operating systems as it is faster than PGDisplay. It uses an SDL `texture` in place of an LCD's Graphics RAM (GRAM). + - PGDisplay - an optional class for desktop operating systems. It uses a pygame `surface` in place of an LCD's GRAM. It can be benificial in a couple of instances: + - SDL2Display "glitches" on my ChromeBook, but PGDisplay doesn't + - On Windows, it is easier to install PyGame than SDL2 + - FBDisplay works with CircuitPython framebufferio.FramebufferDisplay objects, such as dotclockframebuffer (RGB displays), usb_video and rgbmatrix. (usb_video may be the coolest thing you can do with displaysys, although I'm not sure how practical or useful it is. It allows your board to function as a webcam, even without a camera, and to render the display through USB to any application on your host PC that can open a webcam! My Windows machine sees it as an unsupported device, so it will not work, but it does work on my ChromeBook. Currently it is limited to RP2040 only and is hardcoded to a 128 x 96 resolution, but that likely will change. See the [screen capture](examples/circuitpython_usb_video_chromebook.gif) and the [board_config.py](board_configs/circuitpython/usb_video/board_config.py) for more details.) + - JNDisplay for Jupyter Notebooks. No input devices are currently supported. + - PSDisplay for PyScript. Only touchscreens are currently supported. +- Names of events and Devices in [eventsys](src/lib/eventsys/) are taken from PyGame and/or SDL2 to keep the API consistent. +- All drawing targets, sometimes referred to as `canvas` in the code, may be written to using the API from MicroPython's framebuf.FrameBuffer API + - CPython and CircuitPython don't have a `framebuf` module that is API compliant with MicroPython's `framebuf`, so [framebuf.py](add_ons/framebuf.py) is provided for those platforms. It is not used in MicroPython unless framebuf wasn't compiled in. + - A `graphics` module is provided that subclasses `FrameBuffer` (either built-in or from framebuf.py) and provides additional drawing tools, such as `round_rect`. All methods in graphics return an Area object with x, y, w and h attributes describing a bounding box of what was changed. This can be used by applications to only update the part of the display that needs it. That functionality is implemented in DisplayBuffer and will likely be required by EPaperDisplay when it is implemented. + - Canvases include, but are not limited to, the display itself, framebuf bytearrays, bmp565 (16-bit Windows Bitmap files) and displaybuf.DisplayBuffer objects. + - displaybuf.DisplayBuffer implements @peterhinch's API that represents the full display as a framebuffer and allows for 4-, 8- and 16-bit bytearrays while still drawing to the screen as 16-bit. It is required for `MicroPython-Touch` and is very useful outside of that library as well, especially when memory is constrained. +- Display drivers for MicroPython BusDisplay use the constructor API of CircuitPython's DisplayIO drivers. This includes rotation = 0, 90, 180, 270 instead of 0, 1, 2, 3. +- BusDisplay can communicate with the underlying bus driver using either CircuitPython's DisplayIO method calls or @kdschlosser's [lvgl_micropython] method calls. +- There are 3 primary mechanism's for fonts: the graphics.Font class, tft_text.text() and tft_write.write() methods. All 3 of these return an Area object as mentioned earlier. A fourth font mechanism called EZFont is included in the utils folder, but it doesn't return an Area object, which is why it isn't in the lib folder. + - Font is derived from Tony DiCola's 5x7 font class and reads 8x8, 8x14 and 8x16 .bin files from [@spaceraces romfont repo](https://github.com/spacerace/romfont) + - .text() is written by @russhughes and uses fonts generated by his [text_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/text_font_converter.py) It reads 8 and 16bit wide fonts in heights that are multiples of 8. + - .write() is written by @russhughes and uses fonts generated by his [write_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/write_font_converter.py) + - EZFont is a subclass of [@easytarget's microPyEZfonts](https://github.com/easytarget/microPyEZfonts) which uses fonts generated from [@peterhinch's font-to-py](https://github.com/peterhinch/micropython-font-to-py). + - NOTE: @peterhinch's Writer class is inlcuded in MicroPyton-Touch and may be used on MicroPython platforms, but, like EZFont, it doesn't return an Area object. +- Graphics files may be used by 3 mechanisms: + - bmp565.BM565 is a class that can read and write Windows Bitmap files saved with RGB565 color encoding. GIMP supports exporting RGB565 .BMPs. The BMP565 class can open a file and read it's entire contents into memory, or with the `streamed = True` flag, it will only read the slice requested, allowing progressive rendering of files much too large to fit into memory. The slice can be 2 dimensional (BMP565[1:5, 6:10] gets pixels 1 through 5 on rows 6 through 10) or 1 dimensional (BMP565[6:10] gets all pixels in rows 6 through 10). This slicing mechanism is very useful when rendering sprites. It can reverse the order of pixels in a row with `mirrored = False`, which is needed when rendering a background image when rotation is 90 or 270 and (horizontal) scrolling is desired. Finally, it can use an existing bytearray as its buffer instead of reading from a file, which allows saving screenshots from existing canvases such as a FrameBuffer or DisplayBuffer. + - .bitmap() is written by @russhughes and reads .py graphics files encoded with his [image_converter.py utiliity](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or his [sprite_converter.py utility](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py). It renders the entire image to a buffer, and then copies that buffer to the display. + - .pbitmap() is also written by @russhughes and renders the same fonts as .bitmap(), but it does it progressively, one line at a time using a one line buffer. +- Config files - All files that are intended for you to edit to customize your configuration are in the [configs](src/configs/) directory. They are: + - `board_config.py` - required in all circumstances. Feel free to add your own setup code here, such as for real-time clocks, wifi, sensors, etc. + - `path.py` - required in all circumstances + - `color_setup.py` - required for [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) + - `hardware_setup.py` - required for [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) + - `lv_config.py` - required for LVGL + - `tft_config.py` - required for @russhughes's examples. I had to do some search and replace to get those examples to work. + + +## Roadmap + +- [ ] Much more documentation on Github +- [ ] Document the files to produce output for ReadTheDocs +- [ ] Implement EPaperDisplay +- [ ] Optimize with more Numpy and Viper code +- [ ] Decrease the memory footprint where possible +- [ ] Test with frozen modules +- [ ] On MicroPython on Unix, the screen gets cleared when the display is rotated. Microcontroller displays don't do this. It's not an issue unless you want to draw to the display, rotate it, then draw more on top. This functionality allow drawing text in all four 90 degree orientations. +- [ ] Scrolling vertically on desktop operating sytems works correctly, but not when rotated. When rotated, it show scroll horizontally, but continues to scroll vertically. +- [ ] Scrolling on microcontrollers has issues when trying to write spanning the cutoff line. For instance, if drawing a 16 pixel high image at the 8th line from the cutoff line, the bottom 8 lines don't end up where you expect. See the [bmp565_sprite](examples/bmp565_sprite.py) example. +- [ ] Ensure multiple displays work at the same time +- [ ] Implement color depths other than 16 bit +- [ ] Add a Joystick class to eventsys +- [ ] Test with CircuitPython Blinka on SBC's such as Raspberry Pi 4 +- [ ] Need C bus drivers from the community, especially for STM32H7 and MIMXRT + +## Contributing + +This is a community project and I need your help! If you have a suggestion that would make this better, please fork the repo and create a pull request. +Don't forget to give the project a star! Thanks again! + +1. Fork the project +2. Clone it open the repository in command line +3. Create your feature branch (`git checkout -b feature/amazing-feature`) +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a pull request from your feature branch from your repository into this repository main branch, and provide a description of your changes + +## Thanks + +I very much appreciate @peterhinch, @russhughes and the team at Adafruit for their contributions to the Python on microcontrollers community. + +## Why + +I started out just wanting to create drivers that worked with MicroPython the way DisplayIO drivers work for CircuitPython, except without DisplayIO and instead usable by any GUI framework like, but not limited to, LVGL. That snowballed into adding more platforms and then adding drawing primitives, font classes, palettes, an event system, a barebones SDL2 library, a Bitmap 565 reader/writer and supporting as many platforms as possible. I stopped short of creating a full fledged GUI and plan to leave it as a very capable graphics library. I think this is a great foundation for building a GUI framework with widgets and a task scheduler, although it is very usable and useful without one. @peterhinch has a great GUI for MicroPython that works on top of pydisplay, and I'm hoping someone will make a GUI that works across platforms. diff --git a/micropython/pydisplay/multimer/manifest.py b/micropython/pydisplay/multimer/manifest.py new file mode 100644 index 000000000..d471312af --- /dev/null +++ b/micropython/pydisplay/multimer/manifest.py @@ -0,0 +1,8 @@ +metadata( + description="PyDisplay multimer", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="multimer", +) +package("multimer") diff --git a/micropython/pydisplay/multimer/multimer/__init__.py b/micropython/pydisplay/multimer/multimer/__init__.py new file mode 100644 index 000000000..5fde589c5 --- /dev/null +++ b/micropython/pydisplay/multimer/multimer/__init__.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`multimer` +==================================================== + +Cross-platform Timer class for *Python. + +Enables using 'from multimer import Timer' on MicroPython on microcontrollers, +on MicroPython on Unix (which doesn't have a machine.Timer) and CPython (ditto). + +_librt.py uses uses MicroPython ffi to connect to libc and librt, while _sdl2.py uses +SDL2 on CPython to connect to libSDL2. No compatibility for CircuitPython yet. + +Returns None if the platform is not supported rather than raising an ImportError so that +the client can handle the error more gracefully (e.g. by using `if Timer is not None:`). + +Usage: + from multimer import Timer + tim = Timer() + tim.init(mode=Timer.PERIODIC, period=500, callback=lambda t: print(".")) + .... + tim.deinit() +""" + +import sys + +try: + from machine import Timer # MicroPython on microcontrollers +except ImportError: + if sys.implementation.name == "micropython": # MicroPython on Unix + from ._librt import Timer + elif sys.implementation.name == "cpython": # Big Python + from ._sdl2 import Timer + else: + Timer = None + +_next_timer_id = 1 + + +def get_timer(callback, period=33): + """ + Creates and returns a timer to periodically call the callback function + + Args: + callback (function): The function to call periodically + period (int): The period in milliseconds, default is 33ms (30fps) + """ + global _next_timer_id + if sys.platform == "rp2": + id = -1 + else: + id = _next_timer_id + _next_timer_id += 1 + t = Timer(id) + t.init(mode=Timer.PERIODIC, period=period, callback=lambda t: callback()) + print(f"Timer: timer started ({id=}, {period=})") + return t diff --git a/micropython/pydisplay/multimer/multimer/_librt.py b/micropython/pydisplay/multimer/multimer/_librt.py new file mode 100644 index 000000000..4e7a0273c --- /dev/null +++ b/micropython/pydisplay/multimer/multimer/_librt.py @@ -0,0 +1,155 @@ +# Timer that matches machine.Timer (https://docs.micropython.org/en/latest/library/machine.Timer.html) +# for the unix port. +# +# MIT license; Copyright (c) 2021 Amir Gonnen, 2024 Brad Barnett +# +# Based on timer.py from micropython-lib (https://github.com/micropython/micropython-lib/blob/master/unix-ffi/machine/machine/timer.py) + +from ._timerbase import _TimerBase +import ffi +import uctypes +import array +import os + +# FFI libraries + +libc = ffi.open("libc.so.6") +try: + librt = ffi.open("librt.so") +except OSError: + librt = libc + + +# C constants + +CLOCK_REALTIME = 0 +CLOCK_MONOTONIC = 1 +SIGEV_SIGNAL = 0 + +# C structs + +sigaction_t = { + "sa_handler": (0 | uctypes.UINT64), + "sa_mask": (8 | uctypes.ARRAY, 16 | uctypes.UINT64), + "sa_flags": (136 | uctypes.INT32), + "sa_restorer": (144 | uctypes.PTR, uctypes.UINT8), +} + +sigval_t = { + "sival_int": 0 | uctypes.INT32, + "sival_ptr": (0 | uctypes.PTR, uctypes.UINT8), +} + +sigevent_t = { + "sigev_value": (0, sigval_t), + "sigev_signo": uctypes.sizeof(sigval_t) | uctypes.INT32, + "sigev_notify": (uctypes.sizeof(sigval_t) + 4) | uctypes.INT32, +} + +timespec_t = { + "tv_sec": 0 | uctypes.INT32, + "tv_nsec": 8 | uctypes.INT64, +} + +itimerspec_t = { + "it_interval": (0, timespec_t), + "it_value": (16, timespec_t), +} + +# C functions + +__libc_current_sigrtmin = libc.func("i", "__libc_current_sigrtmin", "") +SIGRTMIN = __libc_current_sigrtmin() + +timer_create_ = librt.func("i", "timer_create", "ipp") +timer_delete_ = librt.func("i", "timer_delete", "i") +timer_settime_ = librt.func("i", "timer_settime", "PiPp") + +sigaction_ = libc.func("i", "sigaction", "iPp") + +# Create a new C struct + + +def new(sdesc): + buf = bytearray(uctypes.sizeof(sdesc)) + s = uctypes.struct(uctypes.addressof(buf), sdesc, uctypes.NATIVE) + return s + + +# Posix Signal handling + + +def sigaction(signum, handler, flags=0): + sa = new(sigaction_t) + sa_old = new(sigaction_t) + cb = ffi.callback("v", handler, "i", lock=True) + sa.sa_handler = cb.cfun() + sa.sa_flags = flags + r = sigaction_(signum, sa, sa_old) + if r != 0: + raise RuntimeError("sigaction_ error: %d (errno = %d)" % (r, os.errno())) + return cb # sa_old.sa_handler + + +# Posix Timer handling + + +def timer_create(sig_id): + sev = new(sigevent_t) + # print(sev) + sev.sigev_notify = SIGEV_SIGNAL + sev.sigev_signo = SIGRTMIN + sig_id + timerid = array.array("P", [0]) + r = timer_create_(CLOCK_MONOTONIC, sev, timerid) + if r != 0: + raise RuntimeError("timer_create_ error: %d (errno = %d)" % (r, os.errno())) + # print("timerid", hex(timerid[0])) + return timerid[0] + + +def timer_delete(tid): + r = timer_delete_(tid) + if r != 0: + raise RuntimeError("timer_delete_ error: %d (errno = %d)" % (r, os.errno())) + + +def timer_settime(tid, period_ms, periodic): + period_ns = (period_ms * 1000000) % 1000000000 + period_sec = (period_ms * 1000000) // 1000000000 + + new_val = new(itimerspec_t) + new_val.it_value.tv_sec = period_sec + new_val.it_value.tv_nsec = period_ns + if periodic: + new_val.it_interval.tv_sec = period_sec + new_val.it_interval.tv_nsec = period_ns + # print("new_val:", bytes(new_val)) + old_val = new(itimerspec_t) + # print(new_val, old_val) + r = timer_settime_(tid, 0, new_val, old_val) + if r != 0: + raise RuntimeError("timer_settime_ error: %d (errno = %d)" % (r, os.errno())) + # print("old_val:", bytes(old_val)) + + +# Timer class + + +class Timer(_TimerBase): + """librt Timer class""" + + def _start(self): + self.id = ( + self.id if self.id != -1 else 0xF + ) # id must be non-negative, so we use 0xF as a default + self._timer = timer_create(self.id) + timer_settime(self._timer, self._interval, self._mode == Timer.PERIODIC) + self._handler_ref = self._handler + self._action = sigaction(SIGRTMIN + self.id, self._handler_ref) + + def _stop(self): + # timer_settime(self._timer, 0, False) + timer_delete(self._timer) + self._timer = None + self._action = None + self._handler_ref = None diff --git a/micropython/pydisplay/multimer/multimer/_sdl2.py b/micropython/pydisplay/multimer/multimer/_sdl2.py new file mode 100644 index 000000000..e011662ca --- /dev/null +++ b/micropython/pydisplay/multimer/multimer/_sdl2.py @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +Timer using SDL2 for CPython with the same API as machine.Timer in MicroPython. +""" + +from ._timerbase import _TimerBase +import ctypes +from sys import platform + + +if platform == "win32": + _libSDL2 = ctypes.CDLL("SDL2.dll") +else: + _libSDL2 = ctypes.CDLL("libSDL2-2.0.so.0") + +SDL_INIT_TIMER = 0x00000001 + +_libSDL2.SDL_Init.argtypes = [ctypes.c_uint] +_libSDL2.SDL_Init.restype = ctypes.c_int +SDL_Init = _libSDL2.SDL_Init + +_libSDL2.SDL_AddTimer.argtypes = [ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p] +_libSDL2.SDL_AddTimer.restype = ctypes.c_void_p +SDL_AddTimer = _libSDL2.SDL_AddTimer + +_libSDL2.SDL_RemoveTimer.argtypes = [ctypes.c_void_p] +_libSDL2.SDL_RemoveTimer.restype = ctypes.c_int +SDL_RemoveTimer = _libSDL2.SDL_RemoveTimer + +SDL_TimerCallback = ctypes.CFUNCTYPE(ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p) + + +class Timer(_TimerBase): + """SDL2 Timer class""" + + def _start(self): + SDL_Init(SDL_INIT_TIMER) + self._handler_ref = self._handler + self._tcb = SDL_TimerCallback(self._handler_ref) + self._timer = SDL_AddTimer(self._interval, self._tcb, None) + + def _stop(self): + if self._timer: + SDL_RemoveTimer(self._timer) + self._timer = None + self._tcb = None + self._handler_ref = None diff --git a/micropython/pydisplay/multimer/multimer/_timerbase.py b/micropython/pydisplay/multimer/multimer/_timerbase.py new file mode 100644 index 000000000..5562abf9b --- /dev/null +++ b/micropython/pydisplay/multimer/multimer/_timerbase.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +try: + from micropython import const, schedule +except ImportError: + + def const(x): + return x + + def schedule(cb, interval): + cb(interval) + + +class _TimerBase: + """ + A class to create a timer with the same API and similar functionality to + MicroPython's machine.Timer class. + """ + + PERIODIC = const(0) + ONE_SHOT = const(1) + + def __init__(self, id=-1, **kwargs): + """ + Initializes the timer with the given parameters. + + Args: + id (int): The timer ID (default is -1). + **kwargs: Additional keyword arguments. + """ + self.id = id + self._busy = False + self._timer = None + if kwargs: + self.init(**kwargs) + + def init(self, *, mode, freq=-1, period=-1, callback=None): + """ + Initialize the timer. + + Args: + mode (int): Timer mode (Timer.ONE_SHOT or Timer.PERIODIC). + freq (int, optional): Timer frequency in Hz. Defaults to -1. + period (int, optional): Timer period in milliseconds. Ignored if freq is specified. Defaults to -1. + callback (callable, optional): Callable to execute upon timer expiration. Defaults to None. + + Raises: + ValueError: If an invalid timer mode or interval is provided. + """ + if mode in (self.ONE_SHOT, self.PERIODIC): + self._mode = mode + else: + raise ValueError("Invalid timer mode") + + self._interval = int(1000 / freq) if freq > 0 else period + if self._interval < 1: + raise ValueError("Invalid freq or period") + + self._callback = callback + self._start() # _start() is implemented in subclasses + + def deinit(self): + """ + Deinitializes the timer. + """ + while self._busy: + pass + + self._stop() # _stop() is implemented in subclasses + self._mode = None + self._interval = 0 + self._callback = None + self._timer = None + + def _handler(self, interval, param=None): + """ + Internal callback function called when the timer expires. + SDL2 timers call the handler with the interval and a user-defined parameter, + while librt timers call the handler with the interval only. + They are ignored here. + + Args: + interval (int): The interval at which the timer expires. + param: User-defined parameter (ignored). + + Returns: + int: The next interval for SDL2 timers, 0 for one-shot timers. + """ + if self._busy: + return + + self._busy = True + try: + schedule(self._callback, 0) + except RuntimeError: # MicroPython raises RuntimeError if the schedule queue is full + pass + self._busy = False + + if self._mode == self.ONE_SHOT: + self.deinit() + return 0 # SDL2 expects the callback to return the next interval, 0 for one-shot + return self._interval + + def _start(self): + """ + Starts the timer. Must be implemented by subclasses. + + Raises: + NotImplementedError: If not implemented by subclass. + """ + raise NotImplementedError("Subclasses must implement this method") + + def _stop(self): + """ + Stops the timer. Must be implemented by subclasses. + + Raises: + NotImplementedError: If not implemented by subclass. + """ + raise NotImplementedError("Subclasses must implement this method") diff --git a/micropython/pydisplay/palettes/README.md b/micropython/pydisplay/palettes/README.md new file mode 100644 index 000000000..1a01afa0c --- /dev/null +++ b/micropython/pydisplay/palettes/README.md @@ -0,0 +1,151 @@ +logo + +

pydisplay

+ +

Cross-platform User Interface and Event Drivers for *Python

+ +

+ About • + Key Features • + Getting Started • + Running Your First App • + API • + Roadmap • + Contributing • + Thanks • + Screenshots +

+ +| ![peterhinch's active.py](screenshots/active.gif) | ![russhughes's tiny_toasters.py](screenshots/tiny_toasters.gif) | +|-------------------------|--------------------------------| +| @peterhinch's active.py | @russhughes's tiny_toasters.py | + +## About + +WARNINGS: pydisplay is currently alpha quality. Every effort has been made to test on as many platforms as possible, but I need your help and feedback to get it to its inital release. A lot has changed and I am working on catching up the documentation. + +pydisplay is a universal display, event and device driver framework for multiple flavors of Python, including MicroPython, CircuitPython and CPython (big Python). It may be used as-is to create graphic frontends to your apps, or may be used as a foundation with GUI libraries such as [LVGL](https://github.com/lvgl/lv_micropython), [MicroPython-touch](https://github.com/peterhinch/micropython-touch) or maybe even a GUI framework you've been thinking of developing. Its primary purpose is to provide display and touch drivers for MicroPython, but it is equally useful for developers who may never touch MicroPython. + +It is important to note that pydisplay is meant to be a foundation for GUI libraries and is not itself a GUI library. It doesn't provide widgets, such as buttons, checkboxes or sliders, and it doesn't provide a timing mechanism. You will need a GUI library to provide those if necessary, although many apps won't need them. (There is a cross-platform repository [multimer](https://github.com/PyDevices/pydisplay/tree/main/src/lib/multimer) you can use if you want to used scheduled interrupts. It works with CPython and MicroPython, but doesn't work with CircuitPython. You can also use asyncio for timing.) + +## Key Features + +- May be used without additional libraries to add graphics capabilities to MicroPython, CircuitPython and CPython, with a consistent API across them all. +- Enables moving from one platform to another, for example MicroPython on ESP32-S3 to CPython on Windows without changing your code. Do your graphics development on your desktop, laptop or ChromeBook and then move to a microcontroller when you are ready to interface with your sensors and devices. CPython has much better error messages than MicroPython making it easier to troubleshoot when things go wrong! +- Built around devices available on microcontrollers but not necessarily available on desktop operating systems. For instance, rotary encoders and mouse scroll wheels show up as the same device type and yield the same events. Touchscreens on microcontrollers yield the same events as mice on desktops. Likewise with keypads / keyboards. +- Easily extensible. Use the primitives provided by pydisplay and add your own libraries, classes and functions to have even greater functionality. +- Provides several built-in color palettes and a mechanism to generate your own palettes. +- Lots of examples included, whether developed specifically for pydisplay or ported from [Russ Hughes's st7789py_mpy](https://github.com/russhughes/st7789py_mpy). Also works with all of the examples from Peter Hinch's MicroPython GUI libraries [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) and [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) on MicroPython. +- Support MicroPython on microcontrollers and on Unix(-like) operating systems. +- On MicroPython, can be configured to work with [kdschlosser's lvgl_micropython bus drivers](https://github.com/kdschlosser/lvgl_micropython), which are very fast bus drivers written in C. +- Works with CircuitPython's FourWire and ParallelBus bus drivers, as well as FrameBufferDisplay based interfaces such as dotclockframebuffer, usb_video and rgbmatrix + +## Getting Started + +This section is under construction. For now, see [Getting Started](GETTING-STARTED.md) for more information. + + +## Running your first app + +You will need to import the `path.py` file before running any of the examples. + +On desktop operating systems, `cd` into the `mp` directory (or wherever you have the files staged) and type: +``` +python3 -i path.py +``` +or +``` +micropython -i path.py +``` + +On microcontrollers, either add the following to your `boot.py` (MicroPython) or `code.py` (CircuitPython), or simply import it at the REPL before importing your desired app: +``` +import lib.path +``` + +The [examples](examples) directory will be on the system path, so to run an app from it, you just need to type: +``` +import calculator # substitute `calculator` with the file OR directory you want to run, omitting the .py extension +``` + +To run any of the examples from MicroPython-Touch (remember, its for MicroPython only) type: +``` +import gui.demos.various # substitute `various` with the file you want to run, omitting the .py extension +``` + +## API + +Where possible, existing, proven APIs were used. + +- There are currently 5 display classes, and hopefully another `EPaperDisplay` display class will be added soon, although I will need help from the community for this. + - BusDisplay is for microcontrollers, both on MicroPython and CircuitPython. CircuitPython provides the required bus drivers, as mentioned elsewhere in this README, but MicroPython doesn't have display bus drivers. The [buses](src/lib/buses) packages are included with the installer. It is my hope that community members will create other C bus drivers similar to @kdschlosser's bus drivers in [lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython). + - SDL2Display - the preferred class for desktop operating systems as it is faster than PGDisplay. It uses an SDL `texture` in place of an LCD's Graphics RAM (GRAM). + - PGDisplay - an optional class for desktop operating systems. It uses a pygame `surface` in place of an LCD's GRAM. It can be benificial in a couple of instances: + - SDL2Display "glitches" on my ChromeBook, but PGDisplay doesn't + - On Windows, it is easier to install PyGame than SDL2 + - FBDisplay works with CircuitPython framebufferio.FramebufferDisplay objects, such as dotclockframebuffer (RGB displays), usb_video and rgbmatrix. (usb_video may be the coolest thing you can do with displaysys, although I'm not sure how practical or useful it is. It allows your board to function as a webcam, even without a camera, and to render the display through USB to any application on your host PC that can open a webcam! My Windows machine sees it as an unsupported device, so it will not work, but it does work on my ChromeBook. Currently it is limited to RP2040 only and is hardcoded to a 128 x 96 resolution, but that likely will change. See the [screen capture](examples/circuitpython_usb_video_chromebook.gif) and the [board_config.py](board_configs/circuitpython/usb_video/board_config.py) for more details.) + - JNDisplay for Jupyter Notebooks. No input devices are currently supported. + - PSDisplay for PyScript. Only touchscreens are currently supported. +- Names of events and Devices in [eventsys](src/lib/eventsys/) are taken from PyGame and/or SDL2 to keep the API consistent. +- All drawing targets, sometimes referred to as `canvas` in the code, may be written to using the API from MicroPython's framebuf.FrameBuffer API + - CPython and CircuitPython don't have a `framebuf` module that is API compliant with MicroPython's `framebuf`, so [framebuf.py](add_ons/framebuf.py) is provided for those platforms. It is not used in MicroPython unless framebuf wasn't compiled in. + - A `graphics` module is provided that subclasses `FrameBuffer` (either built-in or from framebuf.py) and provides additional drawing tools, such as `round_rect`. All methods in graphics return an Area object with x, y, w and h attributes describing a bounding box of what was changed. This can be used by applications to only update the part of the display that needs it. That functionality is implemented in DisplayBuffer and will likely be required by EPaperDisplay when it is implemented. + - Canvases include, but are not limited to, the display itself, framebuf bytearrays, bmp565 (16-bit Windows Bitmap files) and displaybuf.DisplayBuffer objects. + - displaybuf.DisplayBuffer implements @peterhinch's API that represents the full display as a framebuffer and allows for 4-, 8- and 16-bit bytearrays while still drawing to the screen as 16-bit. It is required for `MicroPython-Touch` and is very useful outside of that library as well, especially when memory is constrained. +- Display drivers for MicroPython BusDisplay use the constructor API of CircuitPython's DisplayIO drivers. This includes rotation = 0, 90, 180, 270 instead of 0, 1, 2, 3. +- BusDisplay can communicate with the underlying bus driver using either CircuitPython's DisplayIO method calls or @kdschlosser's [lvgl_micropython] method calls. +- There are 3 primary mechanism's for fonts: the graphics.Font class, tft_text.text() and tft_write.write() methods. All 3 of these return an Area object as mentioned earlier. A fourth font mechanism called EZFont is included in the utils folder, but it doesn't return an Area object, which is why it isn't in the lib folder. + - Font is derived from Tony DiCola's 5x7 font class and reads 8x8, 8x14 and 8x16 .bin files from [@spaceraces romfont repo](https://github.com/spacerace/romfont) + - .text() is written by @russhughes and uses fonts generated by his [text_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/text_font_converter.py) It reads 8 and 16bit wide fonts in heights that are multiples of 8. + - .write() is written by @russhughes and uses fonts generated by his [write_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/write_font_converter.py) + - EZFont is a subclass of [@easytarget's microPyEZfonts](https://github.com/easytarget/microPyEZfonts) which uses fonts generated from [@peterhinch's font-to-py](https://github.com/peterhinch/micropython-font-to-py). + - NOTE: @peterhinch's Writer class is inlcuded in MicroPyton-Touch and may be used on MicroPython platforms, but, like EZFont, it doesn't return an Area object. +- Graphics files may be used by 3 mechanisms: + - bmp565.BM565 is a class that can read and write Windows Bitmap files saved with RGB565 color encoding. GIMP supports exporting RGB565 .BMPs. The BMP565 class can open a file and read it's entire contents into memory, or with the `streamed = True` flag, it will only read the slice requested, allowing progressive rendering of files much too large to fit into memory. The slice can be 2 dimensional (BMP565[1:5, 6:10] gets pixels 1 through 5 on rows 6 through 10) or 1 dimensional (BMP565[6:10] gets all pixels in rows 6 through 10). This slicing mechanism is very useful when rendering sprites. It can reverse the order of pixels in a row with `mirrored = False`, which is needed when rendering a background image when rotation is 90 or 270 and (horizontal) scrolling is desired. Finally, it can use an existing bytearray as its buffer instead of reading from a file, which allows saving screenshots from existing canvases such as a FrameBuffer or DisplayBuffer. + - .bitmap() is written by @russhughes and reads .py graphics files encoded with his [image_converter.py utiliity](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or his [sprite_converter.py utility](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py). It renders the entire image to a buffer, and then copies that buffer to the display. + - .pbitmap() is also written by @russhughes and renders the same fonts as .bitmap(), but it does it progressively, one line at a time using a one line buffer. +- Config files - All files that are intended for you to edit to customize your configuration are in the [configs](src/configs/) directory. They are: + - `board_config.py` - required in all circumstances. Feel free to add your own setup code here, such as for real-time clocks, wifi, sensors, etc. + - `path.py` - required in all circumstances + - `color_setup.py` - required for [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) + - `hardware_setup.py` - required for [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) + - `lv_config.py` - required for LVGL + - `tft_config.py` - required for @russhughes's examples. I had to do some search and replace to get those examples to work. + + +## Roadmap + +- [ ] Much more documentation on Github +- [ ] Document the files to produce output for ReadTheDocs +- [ ] Implement EPaperDisplay +- [ ] Optimize with more Numpy and Viper code +- [ ] Decrease the memory footprint where possible +- [ ] Test with frozen modules +- [ ] On MicroPython on Unix, the screen gets cleared when the display is rotated. Microcontroller displays don't do this. It's not an issue unless you want to draw to the display, rotate it, then draw more on top. This functionality allow drawing text in all four 90 degree orientations. +- [ ] Scrolling vertically on desktop operating sytems works correctly, but not when rotated. When rotated, it show scroll horizontally, but continues to scroll vertically. +- [ ] Scrolling on microcontrollers has issues when trying to write spanning the cutoff line. For instance, if drawing a 16 pixel high image at the 8th line from the cutoff line, the bottom 8 lines don't end up where you expect. See the [bmp565_sprite](examples/bmp565_sprite.py) example. +- [ ] Ensure multiple displays work at the same time +- [ ] Implement color depths other than 16 bit +- [ ] Add a Joystick class to eventsys +- [ ] Test with CircuitPython Blinka on SBC's such as Raspberry Pi 4 +- [ ] Need C bus drivers from the community, especially for STM32H7 and MIMXRT + +## Contributing + +This is a community project and I need your help! If you have a suggestion that would make this better, please fork the repo and create a pull request. +Don't forget to give the project a star! Thanks again! + +1. Fork the project +2. Clone it open the repository in command line +3. Create your feature branch (`git checkout -b feature/amazing-feature`) +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a pull request from your feature branch from your repository into this repository main branch, and provide a description of your changes + +## Thanks + +I very much appreciate @peterhinch, @russhughes and the team at Adafruit for their contributions to the Python on microcontrollers community. + +## Why + +I started out just wanting to create drivers that worked with MicroPython the way DisplayIO drivers work for CircuitPython, except without DisplayIO and instead usable by any GUI framework like, but not limited to, LVGL. That snowballed into adding more platforms and then adding drawing primitives, font classes, palettes, an event system, a barebones SDL2 library, a Bitmap 565 reader/writer and supporting as many platforms as possible. I stopped short of creating a full fledged GUI and plan to leave it as a very capable graphics library. I think this is a great foundation for building a GUI framework with widgets and a task scheduler, although it is very usable and useful without one. @peterhinch has a great GUI for MicroPython that works on top of pydisplay, and I'm hoping someone will make a GUI that works across platforms. diff --git a/micropython/pydisplay/palettes/examples/palettes_cube.py b/micropython/pydisplay/palettes/examples/palettes_cube.py new file mode 100644 index 000000000..71c983993 --- /dev/null +++ b/micropython/pydisplay/palettes/examples/palettes_cube.py @@ -0,0 +1,48 @@ +from board_config import display_drv +from palettes import get_palette +from graphics import FrameBuffer, RGB565 +from time import sleep + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +display_drv.rotation = 0 + +palette = get_palette(name="cube", size=5, color_depth=16, swapped=needs_swap) +# palette = get_palette(name="wheel", length=522, color_depth=16, swapped=needs_swap) + +line_height = 20 +last_line = display_drv.height - line_height +scroll = 0 +y = 0 + +BPP = display_drv.color_depth // 8 # Bytes per pixel +ba = bytearray(display_drv.width * line_height * BPP) +fb = FrameBuffer(ba, display_drv.width, line_height, RGB565) + + +def main(): + global y, scroll + for index, color in enumerate(palette): + if y - scroll - last_line > 0: + scroll = (y - last_line) % display_drv.height + display_drv.vscsad(scroll) + name = f"{index} - {palette.color_name(index)}" + text_color = palette.WHITE if palette.brightness(index) < 0.4 else palette.BLACK # noqa: PLR2004 + fb.fill(color) + fb.text16(name, 2, 2, text_color) + display_drv.blit_rect(ba, 0, y % display_drv.height, display_drv.width, line_height) + y += line_height + sleep(0.1) + + +def loop(): + while True: + main() + + +main() diff --git a/micropython/pydisplay/palettes/examples/palettes_material.py b/micropython/pydisplay/palettes/examples/palettes_material.py new file mode 100644 index 000000000..e717fe261 --- /dev/null +++ b/micropython/pydisplay/palettes/examples/palettes_material.py @@ -0,0 +1,24 @@ +from board_config import display_drv +from palettes import get_palette +from graphics import text16 + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +display_drv.rotation = 0 + +palette = get_palette(name="material_design", color_depth=16, swapped=needs_swap) + + +def main(): + line_height = 1 + for i, color in enumerate(palette): + display_drv.fill_rect(0, i * line_height, display_drv.width, line_height, color) + + +while True: + main() diff --git a/micropython/pydisplay/palettes/examples/palettes_wheel.py b/micropython/pydisplay/palettes/examples/palettes_wheel.py new file mode 100644 index 000000000..c8c73b16a --- /dev/null +++ b/micropython/pydisplay/palettes/examples/palettes_wheel.py @@ -0,0 +1,35 @@ +from board_config import display_drv +from palettes import get_palette + +# If byte swapping is required and the display bus is capable of having byte swapping disabled, +# disable it and set a flag so we can swap the color bytes as they are created. +if display_drv.requires_byteswap: + needs_swap = display_drv.disable_auto_byteswap(True) +else: + needs_swap = False + +display_drv.rotation = 0 + +palette = get_palette(name="wheel", swapped=needs_swap, length=256, saturation=1.0) +# palette = get_palette(name="wheel", color_depth=16, length=256) + +line_height = 2 + +i = 0 + + +def main(): + global i + for color in palette: + if i >= display_drv.height: + display_drv.vscsad((line_height + i) % display_drv.height) + display_drv.fill_rect(0, i % display_drv.height, display_drv.width, line_height, color) + i += line_height + + +def loop(): + while True: + main() + + +loop() diff --git a/micropython/pydisplay/palettes/manifest.py b/micropython/pydisplay/palettes/manifest.py new file mode 100644 index 000000000..8fb3416c3 --- /dev/null +++ b/micropython/pydisplay/palettes/manifest.py @@ -0,0 +1,8 @@ +metadata( + description="PyDisplay palettes", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="palettes", +) +package("palettes") diff --git a/micropython/pydisplay/palettes/palettes/__init__.py b/micropython/pydisplay/palettes/palettes/__init__.py new file mode 100644 index 000000000..58064ff57 --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/__init__.py @@ -0,0 +1,178 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`palettes` +==================================================== +""" + +# The 16 colors of the standard Windows 16-color palette +WIN16 = { + 0x000000: "Black", + 0x000080: "Navy", + 0x0000FF: "Blue", + 0x008000: "Green", + 0x008080: "Teal", + 0x00FF00: "Lime", + 0x00FFFF: "Cyan", + 0x800000: "Maroon", + 0x800080: "Purple", + 0x808000: "Olive", + 0x808080: "Grey", + 0xC0C0C0: "Silver", + 0xFF0000: "Red", + 0xFF00FF: "Magenta", + 0xFFFF00: "Yellow", + 0xFFFFFF: "White", +} + + +def get_palette(name="default", **kwargs): + if name == "wheel": + from .wheel import WheelPalette as MyPalette + elif name == "material_design": + from .material_design import MDPalette as MyPalette + elif name == "cube": + from .cube import CubePalette as MyPalette + else: + MyPalette = Palette + return MyPalette(name, **kwargs) + + +class Palette: + """ + A class to represent a color palette. + """ + + def __init__(self, name="", color_depth=16, swapped=False, cached=False): + self._name = name + self._color_depth = color_depth + self._swapped = swapped + self._cache = {} if cached else None + + if not hasattr(self, "_names"): + self._names = WIN16 + if not hasattr(self, "_length"): + self._length = len(self._names) + + self._define_named_colors() + + def _define_named_colors(self): + for color, name in self._names.items(): + if self._color_depth == 16: + color = self.color565(color) + elif self._color_depth == 8: + color = self.color332(color) + elif self._color_depth == 4: + color = list(self._names.keys()).index(color) + setattr(self, name.replace(" ", "_").upper(), color) + + @property + def name(self): + return self._name + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __len__(self): + return self._length + + def __getitem__(self, index): + index = self._normalize(index) + + if self._cache is not None: + if index in self._cache: + return self._cache[index] + + r, g, b = self._get_rgb(index) + if self._color_depth == 24 or self._color_depth == 4: + return r << 16 | g << 8 | b + elif self._color_depth == 16: + return self.color565(r, g, b) + elif self._color_depth == 8: + return self.color332(r, g, b) + raise ValueError("Invalid color depth") + + def _normalize(self, index): + while index < 0: + index += len(self) + if index >= len(self): + index %= len(self) + return index + + def color565(self, r, g=None, b=None): + if isinstance(r, (tuple, list)): + # r is a tuple or list + r, g, b = r + elif g is None: + # r is a 24-bit color + r, g, b = r >> 16 & 0xFF, r >> 8 & 0xFF, r & 0xFF + + color = (r & 0xF8) << 8 | (g & 0xFC) << 3 | b >> 3 + if self._swapped: + return (color & 0xFF) << 8 | (color & 0xFF00) >> 8 + else: + return color + + def color332(self, r, g=None, b=None): + # Convert r, g, b to 8-bit + if isinstance(r, (tuple, list)): + # r is a tuple or list + r, g, b = r + elif g is None: + # r is a 24-bit color + r, g, b = r >> 16 & 0xFF, r >> 8 & 0xFF, r & 0xFF + + color = (r & 0xE0) | (g & 0xE0) >> 3 | (b & 0xC0) >> 6 + return color + + def color_rgb(self, color): + """ + color can be an 16-bit integer or a tuple, list or bytearray of length 2 or 3. + """ + if isinstance(color, int): + # convert 16-bit int color to 2 bytes + color = (color & 0xFF, color >> 8) + if len(color) == 2: + r = color[1] & 0xF8 | (color[1] >> 5) & 0x7 # 5 bit to 8 bit red + g = color[1] << 5 & 0xE0 | (color[0] >> 3) & 0x1F # 6 bit to 8 bit green + b = color[0] << 3 & 0xF8 | (color[0] >> 2) & 0x7 # 5 bit to 8 bit blue + else: + r, g, b = color + return (r, g, b) + + def color_name(self, index): + return self.rgb_name(self._get_rgb(self._normalize(index))) + + def rgb_name(self, r, g=None, b=None): + if isinstance(r, (tuple, list)): + r, g, b = r + return self._names.get(r << 16 | g << 8 | b, f"#{r:02X}{g:02X}{b:02X}") + + def luminance(self, index): + r, g, b = self._get_rgb(index) + return 0.299 * r + 0.587 * g + 0.114 * b + + def brightness(self, index): + r, g, b = self._get_rgb(index) + return (r + g + b) / 3 / 255 + + def _get_rgb(self, index): + color = list(self._names.keys())[index] + return color >> 16 & 0xFF, color >> 8 & 0xFF, color & 0xFF + + +class MappedPalette(Palette): + """ + A class to represent a color palette with a color map. + """ + + def __init__(self, name, color_depth, swapped, color_map): + self._color_map = color_map + self._length = len(color_map) // 3 + super().__init__(name, color_depth, swapped) + + def _get_rgb(self, index): + r, g, b = self._color_map[index * 3 : index * 3 + 3] + return r, g, b diff --git a/micropython/pydisplay/palettes/palettes/_cube125.py b/micropython/pydisplay/palettes/palettes/_cube125.py new file mode 100644 index 000000000..02b595e62 --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/_cube125.py @@ -0,0 +1,131 @@ +# Color names based on https://www.chilliant.com/colournames.html + +# A color cube with 5 levels of red, green, and blue (125 colors in total) +# (values 0x00, 0x40, 0x80, 0xC0, and 0xFF for each of the red, green, and blue components) +CUBE125 = { + 0x000000: "Black", + 0x000040: "Stratos", + 0x000080: "Navy", + 0x0000C0: "Zaffre", + 0x0000FF: "Blue", + 0x004000: "Vine", + 0x004040: "Cyprus", + 0x004080: "Congress", + 0x0040C0: "Cobalt", + 0x0040FF: "Majorelle", + 0x008000: "Green", + 0x008040: "Salem", + 0x008080: "Teal", + 0x0080C0: "Lochmara", + 0x0080FF: "Azure", + 0x00C000: "Leaf", + 0x00C040: "Apple", + 0x00C080: "Shamrock", + 0x00C0C0: "Java", + 0x00C0FF: "Picton", + 0x00FF00: "Lime", + 0x00FF40: "Erin", + 0x00FF80: "Spring", + 0x00FFC0: "Lagoon", + 0x00FFFF: "Cyan", + 0x400000: "Aubergine", + 0x400040: "Loulou", + 0x400080: "Deep Purple", + 0x4000C0: "Felicia", + 0x4000FF: "Indigo", + 0x404000: "Waiouru", + 0x404040: "Masala", + 0x404080: "Astronaut", + 0x4040C0: "Ocean", + 0x4040FF: "Iris", + 0x408000: "Bilbao", + 0x408040: "Goblin", + 0x408080: "Blue Grey", + 0x4080C0: "Glaucous", + 0x4080FF: "Blueberry", + 0x40C000: "Kelly", + 0x40C040: "Mantis", + 0x40C080: "Emerald", + 0x40C0C0: "Tradewind", + 0x40C0FF: "Aero", + 0x40FF00: "Harlequin", + 0x40FF40: "Frog", + 0x40FF80: "Lettuce", + 0x40FFC0: "Padua", + 0x40FFFF: "Riptide", + 0x800000: "Maroon", + 0x800040: "Siren", + 0x800080: "Purple", + 0x8000C0: "Rebecca", + 0x8000FF: "Violet", + 0x804000: "Cinnamon", + 0x804040: "Lotus", + 0x804080: "Affair", + 0x8040C0: "Studio", + 0x8040FF: "Veronica", + 0x808000: "Olive", + 0x808040: "Pesto", + 0x808080: "Grey", + 0x8080C0: "Ube", + 0x8080FF: "Light Blue", + 0x80C000: "Lima", + 0x80C040: "Atlantis", + 0x80C080: "Fern", + 0x80C0C0: "Neptune", + 0x80C0FF: "Jordy", + 0x80FF00: "Chartreuse", + 0x80FF40: "Lawn", + 0x80FF80: "Light Green", + 0x80FFC0: "Aquamarine", + 0x80FFFF: "Light Cyan", + 0xC00000: "Guardsman", + 0xC00040: "Cardinal", + 0xC00080: "Hibiscus", + 0xC000C0: "Roxo", + 0xC000FF: "Sororia", + 0xC04000: "Rust", + 0xC04040: "Brown", + 0xC04080: "Mulberry", + 0xC040C0: "Byzantine", + 0xC040FF: "Phlox", + 0xC08000: "Meteor", + 0xC08040: "Peru", + 0xC08080: "Contessa", + 0xC080C0: "Bouquet", + 0xC080FF: "Heliotrope", + 0xC0C000: "Celery", + 0xC0C040: "Turmeric", + 0xC0C080: "Gimblet", + 0xC0C0C0: "Silver", + 0xC0C0FF: "Melrose", + 0xC0FF00: "Limon", + 0xC0FF40: "Mindaro", + 0xC0FF80: "Sulu", + 0xC0FFC0: "Madang", + 0xC0FFFF: "Scandal", + 0xFF0000: "Red", + 0xFF0040: "Alizarin", + 0xFF0080: "Rose", + 0xFF00C0: "Pizzazz", + 0xFF00FF: "Magenta", + 0xFF4000: "Deep Orange", + 0xFF4040: "Cinnabar", + 0xFF4080: "Watermelon", + 0xFF40C0: "Dazzle", + 0xFF40FF: "Kovidar", + 0xFF8000: "Orange", + 0xFF8040: "Coral", + 0xFF8080: "Salmon", + 0xFF80C0: "Shocking", + 0xFF80FF: "Orchid", + 0xFFC000: "Amber", + 0xFFC040: "Mango", + 0xFFC080: "Rajah", + 0xFFC0C0: "Pink", + 0xFFC0FF: "Chantilly", + 0xFFFF00: "Yellow", + 0xFFFF40: "Fizz", + 0xFFFF80: "Dolly", + 0xFFFFC0: "Shalimar", + 0xFFFFFF: "White", +} diff --git a/micropython/pydisplay/palettes/palettes/_cube27.py b/micropython/pydisplay/palettes/palettes/_cube27.py new file mode 100644 index 000000000..7dbd44eef --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/_cube27.py @@ -0,0 +1,35 @@ +# Color names based on https://www.chilliant.com/colournames.html + +# A color cube with 3 levels of red, green, and blue (27 colors in total) +# (values 0x00, 0x80, and 0xFF for each of the red, green, and blue components) +# A 28th color, "Silver", is added to the end of the list for compatibility with other palettes +CUBE27 = { + 0x000000: "Black", + 0x000080: "Navy", + 0x0000FF: "Blue", + 0x008000: "Green", + 0x008080: "Teal", + 0x0080FF: "Azure", + 0x00FF00: "Lime", + 0x00FF80: "Spring", + 0x00FFFF: "Cyan", + 0x800000: "Maroon", + 0x800080: "Purple", + 0x8000FF: "Violet", + 0x808000: "Olive", + 0x808080: "Grey", + 0x8080FF: "Light Blue", + 0x80FF00: "Chartreuse", + 0x80FF80: "Light Green", + 0x80FFFF: "Light Cyan", + 0xFF0000: "Red", + 0xFF0080: "Rose", + 0xFF00FF: "Magenta", + 0xFF8000: "Orange", + 0xFF8080: "Salmon", + 0xFF80FF: "Orchid", + 0xFFFF00: "Yellow", + 0xFFFF80: "Dolly", + 0xFFFFFF: "White", + 0xC0C0C0: "Silver", +} diff --git a/micropython/pydisplay/palettes/palettes/_cube64.py b/micropython/pydisplay/palettes/palettes/_cube64.py new file mode 100644 index 000000000..786ef8e51 --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/_cube64.py @@ -0,0 +1,70 @@ +# Color names based on https://www.chilliant.com/colournames.html + +# A color cube with 4 levels of red, green, and blue (64 colors in total) +# (values 0x00, 0x55, 0xAA, and 0xFF for each of the red, green, and blue components) +CUBE64 = { + 0x000000: "Black", + 0x000055: "Navy", + 0x0000AA: "Blue", + 0x0000FF: "Vivid Blue", + 0x005500: "Dark Green", + 0x005555: "Teal", + 0x0055AA: "Dark Azure", + 0x0055FF: "Denim", + 0x00AA00: "Green", + 0x00AA55: "Jade", + 0x00AAAA: "Cyan", + 0x00AAFF: "Cerulean", + 0x00FF00: "Lime", + 0x00FF55: "Erin", + 0x00FFAA: "Spring", + 0x00FFFF: "Vivid Cyan", + 0x550000: "Maroon", + 0x550055: "Dark Magenta", + 0x5500AA: "Deep Purple", + 0x5500FF: "Indigo", + 0x555500: "Olive", + 0x555555: "Grey", + 0x5555AA: "Liberty", + 0x5555FF: "Light Blue", + 0x55AA00: "Dark Chartreuse", + 0x55AA55: "Apple", + 0x55AAAA: "Blue Grey", + 0x55AAFF: "Azure", + 0x55FF00: "Harlequin", + 0x55FF55: "Light Green", + 0x55FFAA: "Aquamarine", + 0x55FFFF: "Light Cyan", + 0xAA0000: "Red", + 0xAA0055: "Plum", + 0xAA00AA: "Magenta", + 0xAA00FF: "Violet", + 0xAA5500: "Brown", + 0xAA5555: "Deep Orange", + 0xAA55AA: "Orchid", + 0xAA55FF: "Purple", + 0xAAAA00: "Dark Yellow", + 0xAAAA55: "Light Olive", + 0xAAAAAA: "Silver", + 0xAAAAFF: "Bluebell", + 0xAAFF00: "Inchworm", + 0xAAFF55: "Chartreuse", + 0xAAFFAA: "Mint", + 0xAAFFFF: "Celeste", + 0xFF0000: "Vivid Red", + 0xFF0055: "Rose", + 0xFF00AA: "Cerise", + 0xFF00FF: "Vivid Magenta", + 0xFF5500: "Orange", + 0xFF5555: "Salmon", + 0xFF55AA: "Light Plum", + 0xFF55FF: "Light Magenta", + 0xFFAA00: "Amber", + 0xFFAA55: "Light Brown", + 0xFFAAAA: "Pink", + 0xFFAAFF: "Mauve", + 0xFFFF00: "Vivid Yellow", + 0xFFFF55: "Yellow", + 0xFFFFAA: "Dolly", + 0xFFFFFF: "White", +} diff --git a/micropython/pydisplay/palettes/palettes/_cube8.py b/micropython/pydisplay/palettes/palettes/_cube8.py new file mode 100644 index 000000000..c38c2ef5c --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/_cube8.py @@ -0,0 +1,12 @@ +# A color cube with 2 levels of red, green, and blue (8 colors in total) +# (values 0x00 and 0xFF for each of the red, green, and blue components) +CUBE8 = { + 0x000000: "Black", + 0x0000FF: "Blue", + 0x00FF00: "Green", + 0x00FFFF: "Cyan", + 0xFF0000: "Red", + 0xFF00FF: "Magenta", + 0xFFFF00: "Yellow", + 0xFFFFFF: "White", +} diff --git a/micropython/pydisplay/palettes/palettes/_material_design.py b/micropython/pydisplay/palettes/palettes/_material_design.py new file mode 100644 index 000000000..ccc2e91e1 --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/_material_design.py @@ -0,0 +1,329 @@ +_colors = ( + # black + b"\x00\x00\x00" + # white + b"\xff\xff\xff" + # red + b"\xff\xeb\xee" # 50 + b"\xff\xcd\xd2" # 100 + b"\xef\x9a\x9a" # 200 + b"\xe5\x73\x73" # 300 + b"\xef\x53\x50" # 400 + b"\xf4\x43\x36" # 500 + b"\xe5\x39\x35" # 600 + b"\xd3\x2f\x2f" # 700 + b"\xc6\x28\x28" # 800 + b"\xb7\x1c\x1c" # 900 + b"\xff\x8a\x80" # A100 + b"\xff\x52\x52" # A200 + b"\xff\x17\x44" # A400 + b"\xd5\x00\x00" # A700 + # pink + b"\xfc\xeb\xee" # 50 + b"\xf8\xbb\xd0" # 100 + b"\xf4\x8f\xb1" # 200 + b"\xf0\x62\x92" # 300 + b"\xec\x40\x7a" # 400 + b"\xe9\x1e\x63" # 500 + b"\xd8\x1b\x60" # 600 + b"\xc2\x18\x5b" # 700 + b"\xad\x14\x57" # 800 + b"\x88\x0e\x4f" # 900 + b"\xff\x80\xab" # A100 + b"\xff\x40\x81" # A200 + b"\xf5\x00\x57" # A400 + b"\xc5\x11\x62" # A700 + # purple + b"\xf3\xe5\xf5" # 50 + b"\xe1\xbe\xe7" # 100 + b"\xce\x93\xd8" # 200 + b"\xba\x68\xc8" # 300 + b"\xab\x47\xbc" # 400 + b"\x9c\x27\xb0" # 500 + b"\x8e\x24\xaa" # 600 + b"\x7b\x1f\xa2" # 700 + b"\x6a\x1b\x9a" # 800 + b"\x4a\x14\x8c" # 900 + b"\xea\x80\xfc" # A100 + b"\xe0\x40\xfb" # A200 + b"\xd5\x00\xf9" # A400 + b"\xaa\x00\xff" # A700 + # deep_purple + b"\xed\xe7\xf6" # 50 + b"\xd1\xc4\xe9" # 100 + b"\xb3\x9d\xdb" # 200 + b"\x95\x75\xcd" # 300 + b"\x7e\x57\xc2" # 400 + b"\x67\x3a\xb7" # 500 + b"\x5e\x35\xb1" # 600 + b"\x51\x2d\xa8" # 700 + b"\x45\x27\xa0" # 800 + b"\x31\x1b\x92" # 900 + b"\xb3\x88\xff" # A100 + b"\x7c\x4d\xff" # A200 + b"\x65\x1f\xff" # A400 + b"\x62\x00\xea" # A700 + # indigo + b"\xe8\xea\xf6" # 50 + b"\xc5\xca\xe9" # 100 + b"\x9f\xa8\xda" # 200 + b"\x79\x86\xcb" # 300 + b"\x5c\x6b\xc0" # 400 + b"\x3f\x51\xb5" # 500 + b"\x39\x49\xab" # 600 + b"\x30\x3f\x9f" # 700 + b"\x28\x35\x93" # 800 + b"\x1a\x23\x7e" # 900 + b"\x8c\x9e\xff" # A100 + b"\x53\x6d\xfe" # A200 + b"\x3d\x5a\xfe" # A400 + b"\x30\x4f\xfe" # A700 + # blue + b"\xe3\xf2\xfd" # 50 + b"\xbb\xde\xfb" # 100 + b"\x90\xca\xf9" # 200 + b"\x64\xb5\xf6" # 300 + b"\x42\xa5\xf5" # 400 + b"\x21\x96\xf3" # 500 + b"\x1e\x88\xe5" # 600 + b"\x19\x76\xd2" # 700 + b"\x15\x65\xc0" # 800 + b"\x0d\x47\xa1" # 900 + b"\x82\xb1\xff" # A100 + b"\x44\x8a\xff" # A200 + b"\x29\x79\xff" # A400 + b"\x29\x62\xff" # A700 + # light_blue + b"\xe1\xf5\xfe" # 50 + b"\xb3\xe5\xfc" # 100 + b"\x81\xd4\xfa" # 200 + b"\x4f\xc3\xf7" # 300 + b"\x29\xb6\xf6" # 400 + b"\x03\xa9\xf4" # 500 + b"\x03\x9b\xe5" # 600 + b"\x02\x88\xd1" # 700 + b"\x02\x77\xbd" # 800 + b"\x01\x57\x9b" # 900 + b"\x80\xd8\xff" # A100 + b"\x40\xc4\xff" # A200 + b"\x00\xb0\xff" # A400 + b"\x00\x91\xea" # A700 + # cyan + b"\xe0\xf7\xfa" # 50 + b"\xb2\xeb\xf2" # 100 + b"\x80\xde\xea" # 200 + b"\x4d\xd0\xe1" # 300 + b"\x26\xc6\xda" # 400 + b"\x00\xbc\xd4" # 500 + b"\x00\xac\xc1" # 600 + b"\x00\x97\xa7" # 700 + b"\x00\x83\x8f" # 800 + b"\x00\x60\x64" # 900 + b"\x84\xff\xff" # A100 + b"\x18\xff\xff" # A200 + b"\x00\xe5\xff" # A400 + b"\x00\xb8\xd4" # A700 + # teal + b"\xe0\xf2\xf1" # 50 + b"\xb2\xdf\xdb" # 100 + b"\x80\xcb\xc4" # 200 + b"\x4d\xb6\xac" # 300 + b"\x26\xa6\x9a" # 400 + b"\x00\x96\x88" # 500 + b"\x00\x89\x7b" # 600 + b"\x00\x79\x6b" # 700 + b"\x00\x69\x5c" # 800 + b"\x00\x4d\x40" # 900 + b"\xa7\xff\xeb" # A100 + b"\x64\xff\xda" # A200 + b"\x1d\xe9\xb6" # A400 + b"\x00\xbf\xa5" # A700 + # green + b"\xe8\xf5\xe9" # 50 + b"\xc8\xe6\xc9" # 100 + b"\xa5\xd6\xa7" # 200 + b"\x81\xc7\x84" # 300 + b"\x66\xbb\x6a" # 400 + b"\x4c\xaf\x50" # 500 + b"\x43\xa0\x47" # 600 + b"\x38\x8e\x3c" # 700 + b"\x2e\x7d\x32" # 800 + b"\x1b\x5e\x20" # 900 + b"\xb9\xf6\xca" # A100 + b"\x69\xf0\xae" # A200 + b"\x00\xe6\x76" # A400 + b"\x00\xc8\x53" # A700 + # light_green + b"\xf1\xf8\xe9" # 50 + b"\xdc\xed\xc8" # 100 + b"\xc5\xe1\xa5" # 200 + b"\xae\xd5\x81" # 300 + b"\x9c\xcc\x65" # 400 + b"\x8b\xc3\x4a" # 500 + b"\x7c\xb3\x42" # 600 + b"\x68\x9f\x38" # 700 + b"\x55\x8b\x2f" # 800 + b"\x33\x69\x1e" # 900 + b"\xcc\xff\x90" # A100 + b"\xb2\xff\x59" # A200 + b"\x76\xff\x03" # A400 + b"\x64\xdd\x17" # A700 + # lime + b"\xf9\xfb\xe7" # 50 + b"\xf0\xf4\xc3" # 100 + b"\xe6\xee\x9c" # 200 + b"\xdc\xe7\x75" # 300 + b"\xd4\xe1\x57" # 400 + b"\xcd\xdc\x39" # 500 + b"\xc0\xca\x33" # 600 + b"\xaf\xb4\x2b" # 700 + b"\x9e\x9d\x24" # 800 + b"\x82\x77\x17" # 900 + b"\xf4\xff\x81" # A100 + b"\xee\xff\x41" # A200 + b"\xc6\xff\x00" # A400 + b"\xae\xea\x00" # A700 + # yellow + b"\xff\xfd\xe7" # 50 + b"\xff\xf9\xc4" # 100 + b"\xff\xf5\x9d" # 200 + b"\xff\xf1\x76" # 300 + b"\xff\xee\x58" # 400 + b"\xff\xeb\x3b" # 500 + b"\xfd\xd8\x35" # 600 + b"\xfb\xc0\x2d" # 700 + b"\xf9\xa8\x25" # 800 + b"\xf5\x7f\x17" # 900 + b"\xff\xff\x8d" # A100 + b"\xff\xff\x00" # A200 + b"\xff\xea\x00" # A400 + b"\xff\xd6\x00" # A700 + # amber + b"\xff\xf8\xe1" # 50 + b"\xff\xec\xb3" # 100 + b"\xff\xe0\x82" # 200 + b"\xff\xd5\x4f" # 300 + b"\xff\xca\x28" # 400 + b"\xff\xc1\x07" # 500 + b"\xff\xb3\x00" # 600 + b"\xff\xa0\x00" # 700 + b"\xff\x8f\x00" # 800 + b"\xff\x6f\x00" # 900 + b"\xff\xe5\x7f" # A100 + b"\xff\xd7\x40" # A200 + b"\xff\xc4\x00" # A400 + b"\xff\xab\x00" # A700 + # orange + b"\xff\xf3\xe0" # 50 + b"\xff\xe0\xb2" # 100 + b"\xff\xcc\x80" # 200 + b"\xff\xb7\x4d" # 300 + b"\xff\xa7\x26" # 400 + b"\xff\x98\x00" # 500 + b"\xfb\x8c\x00" # 600 + b"\xf5\x7c\x00" # 700 + b"\xef\x6c\x00" # 800 + b"\xe6\x51\x00" # 900 + b"\xff\xd1\x80" # A100 + b"\xff\xab\x40" # A200 + b"\xff\x91\x00" # A400 + b"\xff\x6d\x00" # A700 + # deep_orange + b"\xfb\xe9\xe7" # 50 + b"\xff\xcc\xbc" # 100 + b"\xff\xab\x91" # 200 + b"\xff\x8a\x65" # 300 + b"\xff\x70\x43" # 400 + b"\xff\x57\x22" # 500 + b"\xf4\x51\x1e" # 600 + b"\xe6\x4a\x19" # 700 + b"\xd8\x43\x15" # 800 + b"\xbf\x36\x0c" # 900 + b"\xff\x9e\x80" # A100 + b"\xff\x6e\x40" # A200 + b"\xff\x3d\x00" # A400 + b"\xdd\x2c\x00" # A700 + # brown + b"\xef\xeb\xe9" # 50 + b"\xd7\xcc\xc8" # 100 + b"\xbc\xaa\xa4" # 200 + b"\xa1\x88\x7f" # 300 + b"\x8d\x6e\x63" # 400 + b"\x79\x55\x48" # 500 + b"\x6d\x4c\x41" # 600 + b"\x5d\x40\x37" # 700 + b"\x4e\x34\x2e" # 800 + b"\x3e\x27\x23" # 900 + # grey + b"\xfa\xfa\xfa" # 50 + b"\xf5\xf5\xf5" # 100 + b"\xee\xee\xee" # 200 + b"\xe0\xe0\xe0" # 300 + b"\xbd\xbd\xbd" # 400 + b"\x9e\x9e\x9e" # 500 + b"\x75\x75\x75" # 600 + b"\x61\x61\x61" # 700 + b"\x42\x42\x42" # 800 + b"\x21\x21\x21" # 900 + # blue_grey + b"\xec\xef\xf1" # 50 + b"\xcf\xd8\xdc" # 100 + b"\xb0\xbe\xc5" # 200 + b"\x90\xa4\xae" # 300 + b"\x78\x90\x9c" # 400 + b"\x60\x7d\x8b" # 500 + b"\x54\x6e\x7a" # 600 + b"\x45\x5a\x64" # 700 + b"\x37\x47\x4f" # 800 + b"\x26\x32\x38" # 900 +) + +FAMILIES = [ + "black", + "white", + "red", + "pink", + "purple", + "deep_purple", + "indigo", + "blue", + "light_blue", + "cyan", + "teal", + "green", + "light_green", + "lime", + "yellow", + "amber", + "orange", + "deep_orange", + "brown", + "grey", + "blue_grey", +] + +LENGTHS = [ + 1, + 1, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 14, + 10, + 10, + 10, +] + +COLORS = memoryview(_colors) diff --git a/micropython/pydisplay/palettes/palettes/cube.py b/micropython/pydisplay/palettes/palettes/cube.py new file mode 100644 index 000000000..c463b6351 --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/cube.py @@ -0,0 +1,54 @@ +# SPDIX:# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`palettes.cube` +==================================================== +Makes a color cube palette. + +Usage: + from palettes import get_palette + palette = get_palette(name="cube", size=5, color_depth=16, swapped=False) + # OR + palette = get_palette(name="cube") + + # OR + from palettes.cube import CubePalette + palette = CubePalette(size=5, color_depth=24) + + print(f"Palette: {palette.name}, Length: {len(palette)}") + for i, color in enumerate(palette): + for i, color in enumerate(palette): print(f"{i}. {color:#06X} {palette.color_name(i)}") +""" + +from . import Palette as _Palette + + +class CubePalette(_Palette): + """ + A color cube palette. The size of the cube is determined by the size parameter. + """ + + def __init__(self, name="", color_depth=16, swapped=False, cached=True, size=5): + self._size = size + self._length = size**3 + self._values = [round(i * (255 / (size - 1)) + 0.25) for i in range(size)] + + if self._size == 2: + from ._cube8 import CUBE8 as NAMES + elif self._size == 3: + from ._cube27 import CUBE27 as NAMES + elif self._size == 4: + from ._cube64 import CUBE64 as NAMES + else: + from ._cube125 import CUBE125 as NAMES + self._names = NAMES + super().__init__(name + str(self._length), color_depth, swapped, cached) + + def _get_rgb(self, index): + z = index % self._size + index //= self._size + y = index % self._size + index //= self._size + x = index % self._size + return self._values[x], self._values[y], self._values[z] diff --git a/micropython/pydisplay/palettes/palettes/material_design.py b/micropython/pydisplay/palettes/palettes/material_design.py new file mode 100644 index 000000000..78d3d6f9e --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/material_design.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2024 Brad Barnett +# +# SPDX-License-Identifier: MIT +""" +`palettes.material_design` +==================================================== +This module contains the Material Design color palette as a class object. + + +Usage: + from palettes import get_palette + palette = get_palette(name="material_design", color_depth=16, swapped=False) + # OR + palette = get_palette("material_design") + + # OR + from palettes.material_design import MDPalette + palette = MDPalette(size=5, color_depth=24) + + # to access the primary variant of a color family by name: + x = palette.RED + x = palette.BLACK + + # to access all 256 colors directly: + x = palette[127] # color at index 127 + + # to access a shade by name: + x = palette.RED_S500 # shade 500 + x = palette.RED_S900 # shade 900 + x = palette.RED_S50 # shade 50 + + # to access an accent of a color family by name: + x = palette.RED_A100 + x = palette.RED_A700 + + # to iterate over all 256 colors: + for x in palette: + pass +""" + +from . import MappedPalette +from ._material_design import COLORS, FAMILIES, LENGTHS + + +class MDPalette(MappedPalette): + """ + A class to represent the Material Design color palette. + """ + + _shades = [ + "S50", + "S100", + "S200", + "S300", + "S400", + "S500", + "S600", + "S700", + "S800", + "S900", + ] + + _accents = ["A100", "A200", "A400", "A700"] + + def __init__(self, name="", color_depth=16, swapped=False, color_map=COLORS): + super().__init__(name, color_depth, swapped, color_map) + self._name = name if name else "MaterialDesign" + + def _define_named_colors(self): + # The colors are already available as pal[0], pal[1], etc. + # Now we want to add pal.BLACK = pal[0], pal.WHITE = pal[1], etc. + color_index = 0 + for name, length in zip(FAMILIES, LENGTHS): + if length == 1: # black or white + setattr(self, name.upper(), self[color_index]) + color_index += 1 + else: + for shade in self._shades: + setattr(self, f"{name}_{shade}".upper(), self[color_index]) + # S500 is the default shade for each family, so add it to the palette + # without the _S500 suffix + if shade == "S500": + setattr(self, name.upper(), self[color_index]) + color_index += 1 + if length == 14: + for accent in self._accents: + setattr(self, f"{name}_{accent}".upper(), self[color_index]) + color_index += 1 diff --git a/micropython/pydisplay/palettes/palettes/wheel.py b/micropython/pydisplay/palettes/palettes/wheel.py new file mode 100644 index 000000000..b32fe8123 --- /dev/null +++ b/micropython/pydisplay/palettes/palettes/wheel.py @@ -0,0 +1,107 @@ +""" +`pypalette.wheel` +==================================================== +This module contains the cool wheel color palette as a class object. + + +Usage: + from palettes import get_palette + palette = get_palette(name="wheel", color_depth=16, swapped=False, length=256) + # OR + palette = get_palette(name="wheel") + + # OR + from palettes.wheel import WheelPalette + palette = WheelPalette(color_depth=16, swapped=False, length=256) + + print(f"Palette: {palette.name}, Length: {len(palette)}") + for i, color in enumerate(palette): + for i, color in enumerate(palette): print(f"{i}. {color:#06X} {palette.color_name(i)}") + + # to access the named colors directly: + x = palette.RED + x = palette.BLACK + +""" + +from . import Palette as _Palette + + +class WheelPalette(_Palette): + """ + A class to represent a color wheel as a palette. + """ + + def __init__( + self, + name="", + color_depth=16, + swapped=False, + cached=True, + length=256, + saturation=1.0, + value=None, + ): + self._length = length + + if saturation is None and value is None: + self._mode = "wheel" + self._one_third = self._length // 3 + self._two_thirds = 2 * self._length // 3 + self._spacing = 256 * 3 / self._length + else: + self._mode = "hsv" + self._saturation = saturation if saturation is not None else 1.0 + self._value = value if value is not None else 1.0 + if not 0 <= self._saturation <= 1 or not 0 <= self._value <= 1: + raise ValueError("Saturation and value must be in the range of 0-1") + from . import WIN16 as NAMES + + self._names = NAMES + + super().__init__(name + str(self._length), color_depth, swapped, cached) + + def _get_rgb(self, index): + if self._mode == "wheel": + return self._wheel_to_rgb(index) + else: + return self._hsv_to_rgb(index / self._length, self._saturation, self._value) + + def _wheel_to_rgb(self, index): + # incoming index is in the range of 0-self._length + index = self._length - 1 - index # reverse the order + + if index < self._one_third: + return (255 - int(index * self._spacing), 0, int(index * self._spacing)) + elif index < self._two_thirds: + index -= self._one_third + return (0, int(index * self._spacing), 255 - int(index * self._spacing)) + else: + index -= self._two_thirds + return (int(index * self._spacing), 255 - int(index * self._spacing), 0) + + def _hsv_to_rgb(self, h, s, v): + # incoming values are in the range of 0-1 + # returns r, g, b values in the range of 0-255 + if s == 0: # when s=0, all values are shades of gray + return int(v * 255), int(v * 255), int(v * 255) + i = int(h * 6.0) + f = (h * 6.0) - i + p = int(255 * v * (1.0 - s)) + q = int(255 * v * (1.0 - s * f)) + t = int(255 * v * (1.0 - s * (1.0 - f))) + v = int(255 * v) + i = i % 6 + if i == 0: # red + return v, t, p + if i == 1: # yellow + return q, v, p + if i == 2: # green + return p, v, t + if i == 3: # cyan + return p, q, v + if i == 4: # blue + return t, p, v + if i == 5: # magenta + return v, p, q + return 0, 0, 0 diff --git a/micropython/pydisplay/pydisplay-bundle/README.md b/micropython/pydisplay/pydisplay-bundle/README.md new file mode 100644 index 000000000..1a01afa0c --- /dev/null +++ b/micropython/pydisplay/pydisplay-bundle/README.md @@ -0,0 +1,151 @@ +logo + +

pydisplay

+ +

Cross-platform User Interface and Event Drivers for *Python

+ +

+ About • + Key Features • + Getting Started • + Running Your First App • + API • + Roadmap • + Contributing • + Thanks • + Screenshots +

+ +| ![peterhinch's active.py](screenshots/active.gif) | ![russhughes's tiny_toasters.py](screenshots/tiny_toasters.gif) | +|-------------------------|--------------------------------| +| @peterhinch's active.py | @russhughes's tiny_toasters.py | + +## About + +WARNINGS: pydisplay is currently alpha quality. Every effort has been made to test on as many platforms as possible, but I need your help and feedback to get it to its inital release. A lot has changed and I am working on catching up the documentation. + +pydisplay is a universal display, event and device driver framework for multiple flavors of Python, including MicroPython, CircuitPython and CPython (big Python). It may be used as-is to create graphic frontends to your apps, or may be used as a foundation with GUI libraries such as [LVGL](https://github.com/lvgl/lv_micropython), [MicroPython-touch](https://github.com/peterhinch/micropython-touch) or maybe even a GUI framework you've been thinking of developing. Its primary purpose is to provide display and touch drivers for MicroPython, but it is equally useful for developers who may never touch MicroPython. + +It is important to note that pydisplay is meant to be a foundation for GUI libraries and is not itself a GUI library. It doesn't provide widgets, such as buttons, checkboxes or sliders, and it doesn't provide a timing mechanism. You will need a GUI library to provide those if necessary, although many apps won't need them. (There is a cross-platform repository [multimer](https://github.com/PyDevices/pydisplay/tree/main/src/lib/multimer) you can use if you want to used scheduled interrupts. It works with CPython and MicroPython, but doesn't work with CircuitPython. You can also use asyncio for timing.) + +## Key Features + +- May be used without additional libraries to add graphics capabilities to MicroPython, CircuitPython and CPython, with a consistent API across them all. +- Enables moving from one platform to another, for example MicroPython on ESP32-S3 to CPython on Windows without changing your code. Do your graphics development on your desktop, laptop or ChromeBook and then move to a microcontroller when you are ready to interface with your sensors and devices. CPython has much better error messages than MicroPython making it easier to troubleshoot when things go wrong! +- Built around devices available on microcontrollers but not necessarily available on desktop operating systems. For instance, rotary encoders and mouse scroll wheels show up as the same device type and yield the same events. Touchscreens on microcontrollers yield the same events as mice on desktops. Likewise with keypads / keyboards. +- Easily extensible. Use the primitives provided by pydisplay and add your own libraries, classes and functions to have even greater functionality. +- Provides several built-in color palettes and a mechanism to generate your own palettes. +- Lots of examples included, whether developed specifically for pydisplay or ported from [Russ Hughes's st7789py_mpy](https://github.com/russhughes/st7789py_mpy). Also works with all of the examples from Peter Hinch's MicroPython GUI libraries [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) and [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) on MicroPython. +- Support MicroPython on microcontrollers and on Unix(-like) operating systems. +- On MicroPython, can be configured to work with [kdschlosser's lvgl_micropython bus drivers](https://github.com/kdschlosser/lvgl_micropython), which are very fast bus drivers written in C. +- Works with CircuitPython's FourWire and ParallelBus bus drivers, as well as FrameBufferDisplay based interfaces such as dotclockframebuffer, usb_video and rgbmatrix + +## Getting Started + +This section is under construction. For now, see [Getting Started](GETTING-STARTED.md) for more information. + + +## Running your first app + +You will need to import the `path.py` file before running any of the examples. + +On desktop operating systems, `cd` into the `mp` directory (or wherever you have the files staged) and type: +``` +python3 -i path.py +``` +or +``` +micropython -i path.py +``` + +On microcontrollers, either add the following to your `boot.py` (MicroPython) or `code.py` (CircuitPython), or simply import it at the REPL before importing your desired app: +``` +import lib.path +``` + +The [examples](examples) directory will be on the system path, so to run an app from it, you just need to type: +``` +import calculator # substitute `calculator` with the file OR directory you want to run, omitting the .py extension +``` + +To run any of the examples from MicroPython-Touch (remember, its for MicroPython only) type: +``` +import gui.demos.various # substitute `various` with the file you want to run, omitting the .py extension +``` + +## API + +Where possible, existing, proven APIs were used. + +- There are currently 5 display classes, and hopefully another `EPaperDisplay` display class will be added soon, although I will need help from the community for this. + - BusDisplay is for microcontrollers, both on MicroPython and CircuitPython. CircuitPython provides the required bus drivers, as mentioned elsewhere in this README, but MicroPython doesn't have display bus drivers. The [buses](src/lib/buses) packages are included with the installer. It is my hope that community members will create other C bus drivers similar to @kdschlosser's bus drivers in [lvgl_micropython](https://github.com/kdschlosser/lvgl_micropython). + - SDL2Display - the preferred class for desktop operating systems as it is faster than PGDisplay. It uses an SDL `texture` in place of an LCD's Graphics RAM (GRAM). + - PGDisplay - an optional class for desktop operating systems. It uses a pygame `surface` in place of an LCD's GRAM. It can be benificial in a couple of instances: + - SDL2Display "glitches" on my ChromeBook, but PGDisplay doesn't + - On Windows, it is easier to install PyGame than SDL2 + - FBDisplay works with CircuitPython framebufferio.FramebufferDisplay objects, such as dotclockframebuffer (RGB displays), usb_video and rgbmatrix. (usb_video may be the coolest thing you can do with displaysys, although I'm not sure how practical or useful it is. It allows your board to function as a webcam, even without a camera, and to render the display through USB to any application on your host PC that can open a webcam! My Windows machine sees it as an unsupported device, so it will not work, but it does work on my ChromeBook. Currently it is limited to RP2040 only and is hardcoded to a 128 x 96 resolution, but that likely will change. See the [screen capture](examples/circuitpython_usb_video_chromebook.gif) and the [board_config.py](board_configs/circuitpython/usb_video/board_config.py) for more details.) + - JNDisplay for Jupyter Notebooks. No input devices are currently supported. + - PSDisplay for PyScript. Only touchscreens are currently supported. +- Names of events and Devices in [eventsys](src/lib/eventsys/) are taken from PyGame and/or SDL2 to keep the API consistent. +- All drawing targets, sometimes referred to as `canvas` in the code, may be written to using the API from MicroPython's framebuf.FrameBuffer API + - CPython and CircuitPython don't have a `framebuf` module that is API compliant with MicroPython's `framebuf`, so [framebuf.py](add_ons/framebuf.py) is provided for those platforms. It is not used in MicroPython unless framebuf wasn't compiled in. + - A `graphics` module is provided that subclasses `FrameBuffer` (either built-in or from framebuf.py) and provides additional drawing tools, such as `round_rect`. All methods in graphics return an Area object with x, y, w and h attributes describing a bounding box of what was changed. This can be used by applications to only update the part of the display that needs it. That functionality is implemented in DisplayBuffer and will likely be required by EPaperDisplay when it is implemented. + - Canvases include, but are not limited to, the display itself, framebuf bytearrays, bmp565 (16-bit Windows Bitmap files) and displaybuf.DisplayBuffer objects. + - displaybuf.DisplayBuffer implements @peterhinch's API that represents the full display as a framebuffer and allows for 4-, 8- and 16-bit bytearrays while still drawing to the screen as 16-bit. It is required for `MicroPython-Touch` and is very useful outside of that library as well, especially when memory is constrained. +- Display drivers for MicroPython BusDisplay use the constructor API of CircuitPython's DisplayIO drivers. This includes rotation = 0, 90, 180, 270 instead of 0, 1, 2, 3. +- BusDisplay can communicate with the underlying bus driver using either CircuitPython's DisplayIO method calls or @kdschlosser's [lvgl_micropython] method calls. +- There are 3 primary mechanism's for fonts: the graphics.Font class, tft_text.text() and tft_write.write() methods. All 3 of these return an Area object as mentioned earlier. A fourth font mechanism called EZFont is included in the utils folder, but it doesn't return an Area object, which is why it isn't in the lib folder. + - Font is derived from Tony DiCola's 5x7 font class and reads 8x8, 8x14 and 8x16 .bin files from [@spaceraces romfont repo](https://github.com/spacerace/romfont) + - .text() is written by @russhughes and uses fonts generated by his [text_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/text_font_converter.py) It reads 8 and 16bit wide fonts in heights that are multiples of 8. + - .write() is written by @russhughes and uses fonts generated by his [write_font_converter](https://github.com/russhughes/st7789py_mpy/blob/master/utils/write_font_converter.py) + - EZFont is a subclass of [@easytarget's microPyEZfonts](https://github.com/easytarget/microPyEZfonts) which uses fonts generated from [@peterhinch's font-to-py](https://github.com/peterhinch/micropython-font-to-py). + - NOTE: @peterhinch's Writer class is inlcuded in MicroPyton-Touch and may be used on MicroPython platforms, but, like EZFont, it doesn't return an Area object. +- Graphics files may be used by 3 mechanisms: + - bmp565.BM565 is a class that can read and write Windows Bitmap files saved with RGB565 color encoding. GIMP supports exporting RGB565 .BMPs. The BMP565 class can open a file and read it's entire contents into memory, or with the `streamed = True` flag, it will only read the slice requested, allowing progressive rendering of files much too large to fit into memory. The slice can be 2 dimensional (BMP565[1:5, 6:10] gets pixels 1 through 5 on rows 6 through 10) or 1 dimensional (BMP565[6:10] gets all pixels in rows 6 through 10). This slicing mechanism is very useful when rendering sprites. It can reverse the order of pixels in a row with `mirrored = False`, which is needed when rendering a background image when rotation is 90 or 270 and (horizontal) scrolling is desired. Finally, it can use an existing bytearray as its buffer instead of reading from a file, which allows saving screenshots from existing canvases such as a FrameBuffer or DisplayBuffer. + - .bitmap() is written by @russhughes and reads .py graphics files encoded with his [image_converter.py utiliity](https://github.com/russhughes/st7789py_mpy/blob/master/utils/image_converter.py) or his [sprite_converter.py utility](https://github.com/russhughes/st7789py_mpy/blob/master/utils/sprites_converter.py). It renders the entire image to a buffer, and then copies that buffer to the display. + - .pbitmap() is also written by @russhughes and renders the same fonts as .bitmap(), but it does it progressively, one line at a time using a one line buffer. +- Config files - All files that are intended for you to edit to customize your configuration are in the [configs](src/configs/) directory. They are: + - `board_config.py` - required in all circumstances. Feel free to add your own setup code here, such as for real-time clocks, wifi, sensors, etc. + - `path.py` - required in all circumstances + - `color_setup.py` - required for [Nano-GUI](https://github.com/peterhinch/micropython-nano-gui) + - `hardware_setup.py` - required for [MicroPython-Touch](https://github.com/peterhinch/micropython-touch) + - `lv_config.py` - required for LVGL + - `tft_config.py` - required for @russhughes's examples. I had to do some search and replace to get those examples to work. + + +## Roadmap + +- [ ] Much more documentation on Github +- [ ] Document the files to produce output for ReadTheDocs +- [ ] Implement EPaperDisplay +- [ ] Optimize with more Numpy and Viper code +- [ ] Decrease the memory footprint where possible +- [ ] Test with frozen modules +- [ ] On MicroPython on Unix, the screen gets cleared when the display is rotated. Microcontroller displays don't do this. It's not an issue unless you want to draw to the display, rotate it, then draw more on top. This functionality allow drawing text in all four 90 degree orientations. +- [ ] Scrolling vertically on desktop operating sytems works correctly, but not when rotated. When rotated, it show scroll horizontally, but continues to scroll vertically. +- [ ] Scrolling on microcontrollers has issues when trying to write spanning the cutoff line. For instance, if drawing a 16 pixel high image at the 8th line from the cutoff line, the bottom 8 lines don't end up where you expect. See the [bmp565_sprite](examples/bmp565_sprite.py) example. +- [ ] Ensure multiple displays work at the same time +- [ ] Implement color depths other than 16 bit +- [ ] Add a Joystick class to eventsys +- [ ] Test with CircuitPython Blinka on SBC's such as Raspberry Pi 4 +- [ ] Need C bus drivers from the community, especially for STM32H7 and MIMXRT + +## Contributing + +This is a community project and I need your help! If you have a suggestion that would make this better, please fork the repo and create a pull request. +Don't forget to give the project a star! Thanks again! + +1. Fork the project +2. Clone it open the repository in command line +3. Create your feature branch (`git checkout -b feature/amazing-feature`) +4. Commit your changes (`git commit -m 'Add some amazing feature'`) +5. Push to the branch (`git push origin feature/amazing-feature`) +6. Open a pull request from your feature branch from your repository into this repository main branch, and provide a description of your changes + +## Thanks + +I very much appreciate @peterhinch, @russhughes and the team at Adafruit for their contributions to the Python on microcontrollers community. + +## Why + +I started out just wanting to create drivers that worked with MicroPython the way DisplayIO drivers work for CircuitPython, except without DisplayIO and instead usable by any GUI framework like, but not limited to, LVGL. That snowballed into adding more platforms and then adding drawing primitives, font classes, palettes, an event system, a barebones SDL2 library, a Bitmap 565 reader/writer and supporting as many platforms as possible. I stopped short of creating a full fledged GUI and plan to leave it as a very capable graphics library. I think this is a great foundation for building a GUI framework with widgets and a task scheduler, although it is very usable and useful without one. @peterhinch has a great GUI for MicroPython that works on top of pydisplay, and I'm hoping someone will make a GUI that works across platforms. diff --git a/micropython/pydisplay/pydisplay-bundle/manifest.py b/micropython/pydisplay/pydisplay-bundle/manifest.py new file mode 100644 index 000000000..cc346af3a --- /dev/null +++ b/micropython/pydisplay/pydisplay-bundle/manifest.py @@ -0,0 +1,12 @@ +metadata( + description="PyDisplay bundle", + version="0.0.1", + author="Brad Barnett ", + license="MIT", + pypi_publish="pydisplay-bundle", +) +require("eventsys") +require("graphics") +require("multimer") +require("palettes") +require("displaysys")