├── doc ├── CYD1.jpg ├── CYD_Chart.jpg ├── CYD_Simple.jpg ├── CYD_MPY_Only.jpg ├── CYD_Portrait.jpg ├── color_test_correct.jpg └── Font_Converter_Settings.jpg ├── demo_lvgl ├── img │ └── hamster.png ├── fonts │ ├── Symbols_40.bin │ ├── LCD_Font_120.bin │ ├── montserrat-22.bin │ └── font_awesome_18.bin ├── log │ └── counter_log.csv ├── lib │ ├── display_driver.py │ └── xpt2046_cyd.py ├── demo_load_font.py ├── demo_load_png.py ├── demo_load_icon_font.py ├── demo_simple.py ├── demo_more_icons.py ├── demo_portrait_mode.py └── demo_multi_screen.py ├── lvgl9_firmware ├── lvgl_micropython_cyd.bin └── touch_color_test.py ├── .github └── ISSUE_TEMPLATE │ └── question---help-needed.md ├── demo_no_lvgl ├── Pins.py ├── demo_no_lvgl.py ├── xpt2046.py └── ili9341.py ├── index.md ├── LVGL8.md └── README.md /doc/CYD1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/doc/CYD1.jpg -------------------------------------------------------------------------------- /doc/CYD_Chart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/doc/CYD_Chart.jpg -------------------------------------------------------------------------------- /doc/CYD_Simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/doc/CYD_Simple.jpg -------------------------------------------------------------------------------- /doc/CYD_MPY_Only.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/doc/CYD_MPY_Only.jpg -------------------------------------------------------------------------------- /doc/CYD_Portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/doc/CYD_Portrait.jpg -------------------------------------------------------------------------------- /demo_lvgl/img/hamster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/demo_lvgl/img/hamster.png -------------------------------------------------------------------------------- /doc/color_test_correct.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/doc/color_test_correct.jpg -------------------------------------------------------------------------------- /demo_lvgl/fonts/Symbols_40.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/demo_lvgl/fonts/Symbols_40.bin -------------------------------------------------------------------------------- /doc/Font_Converter_Settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/doc/Font_Converter_Settings.jpg -------------------------------------------------------------------------------- /demo_lvgl/fonts/LCD_Font_120.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/demo_lvgl/fonts/LCD_Font_120.bin -------------------------------------------------------------------------------- /demo_lvgl/fonts/montserrat-22.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/demo_lvgl/fonts/montserrat-22.bin -------------------------------------------------------------------------------- /demo_lvgl/fonts/font_awesome_18.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/demo_lvgl/fonts/font_awesome_18.bin -------------------------------------------------------------------------------- /lvgl9_firmware/lvgl_micropython_cyd.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/HEAD/lvgl9_firmware/lvgl_micropython_cyd.bin -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question---help-needed.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question / Help needed 3 | about: Use this template if you have questions or need help seeting up your CYD. 4 | title: "[Question / Help]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | > [!IMPORTANT] 11 | > Make sure you read the complete README file before asking questions or asking for help. 12 | -------------------------------------------------------------------------------- /demo_no_lvgl/Pins.py: -------------------------------------------------------------------------------- 1 | from micropython import const 2 | 3 | LDR = const(34) 4 | 5 | RGB_LED_R = const(4) 6 | RGB_LED_G = const(16) 7 | RGB_LED_B = const(17) 8 | 9 | TOUCH_CS = const(33) 10 | TOUCH_INT = const(36) 11 | 12 | TOUCH_SPI_SLOT = const(2) 13 | TOUCH_SPI_SCK = const(25) 14 | TOUCH_SPI_MOSI = const(32) 15 | TOUCH_SPI_MISO = const(39) 16 | 17 | TFT_WIDTH = const(320) 18 | TFT_HEIGHT = const(240) 19 | 20 | TFT_BLK = const(21) 21 | TFT_DC = const(2) 22 | TFT_CS = const(15) 23 | TFT_RST = const(15) 24 | 25 | TFT_SPI_SLOT = const(1) 26 | TFT_SPI_SCK = const(14) 27 | TFT_SPI_MOSI = const(13) 28 | 29 | SD_SLOT = const(2) 30 | SD_CS = const(5) 31 | SD_SCK = const(18) 32 | SD_MISO = const(19) 33 | SD_MOSI = const(23) -------------------------------------------------------------------------------- /demo_lvgl/log/counter_log.csv: -------------------------------------------------------------------------------- 1 | 2024-09-01 14:06,11500 2 | 2024-09-02 14:08,20770 3 | 2024-09-03 14:12,19970 4 | 2024-09-04 14:06,11230 5 | 2024-09-05 14:08,20450 6 | 2024-09-06 14:12,29650 7 | 2024-09-07 14:12,29650 8 | 2024-09-08 14:12,29650 9 | 2024-09-09 14:12,29650 10 | 2024-09-10 14:12,29650 11 | 2024-09-11 14:06,11500 12 | 2024-09-12 14:08,20770 13 | 2024-09-13 14:12,19970 14 | 2024-09-14 14:06,11230 15 | 2024-09-15 14:08,20450 16 | 2024-09-16 14:12,29650 17 | 2024-09-17 14:12,29650 18 | 2024-09-18 14:12,29650 19 | 2024-09-19 14:12,29650 20 | 2024-09-20 14:12,29650 21 | 2024-09-21 14:06,11500 22 | 2024-09-22 14:08,20770 23 | 2024-09-23 14:12,19970 24 | 2024-09-24 14:06,11230 25 | 2024-09-25 14:08,20450 26 | 2024-09-26 14:12,29650 27 | 2024-09-27 14:12,29650 28 | 2024-09-28 14:12,29650 29 | 2024-09-29 14:12,29650 30 | 2024-09-30 14:12,29650 -------------------------------------------------------------------------------- /demo_lvgl/lib/display_driver.py: -------------------------------------------------------------------------------- 1 | from machine import SoftSPI, Pin 2 | import ili9XXX 3 | from xpt2046_cyd import xpt2046 4 | 5 | ''' Initialize CYD 6 | LVGL is initialized via display driver ''' 7 | disp = ili9XXX.ili9341(clk=14, cs=15, 8 | dc=2, rst=12, power=23, miso=12, 9 | mosi=13, width = 320, height = 240, 10 | rot = 0xC0, colormode=ili9XXX.COLOR_MODE_RGB, 11 | double_buffer = False, factor = 16) 12 | 13 | spiTouch = SoftSPI(baudrate = 2500000, sck = Pin(25), 14 | mosi = Pin(32), miso = Pin(39)) 15 | 16 | touch = xpt2046(spi = spiTouch, cs = Pin(33), cal_x0 = 3700, 17 | cal_y0 = 3820, cal_x1 = 180, cal_y1 = 250, transpose=False) 18 | 19 | ''' Backlight may be controlled via PWM to adjust brightness ''' 20 | backlight = Pin(21, Pin.OUT) 21 | backlight(1) -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | WIP! 2 | 3 | 4 | # Cheap-Yellow-Display Micropython LVGL Guide and Examples 5 | 6 | ## Introduction 7 | CYDs 8 | 9 | 10 | ## Display And Touch Screen Driver Setup 11 | Although CYDs from different vendors look almost identical, they may require varying display and touch screen initialization setups. 12 | The main differences are 13 | - MADCTL configuration (display roration and mirroring) / Display Orientation Table 14 | - Color modes: RGB vs. BGR 15 | - Touch screen calibration: mirroring the x/y-axis or swapping the axes may be neccessary. 16 | 17 | The correct configuration for a specific display can only be found by trial and error. 18 | 19 | [https://github.com/lvgl-micropython/lvgl_micropython/discussions/281](https://github.com/lvgl-micropython/lvgl_micropython/discussions/281) 20 | 21 | ## LVGL9 22 | I compiled a firmware for the CYD using lvgl9. 23 | I also uploaded a test program which might help to find the correct display orientation, colormode and touchscreen settings. 24 | 25 | ## LVGL8 26 | Firmware 27 | Examples 28 | 29 | ## No LVGL 30 | Examples 31 | -------------------------------------------------------------------------------- /demo_lvgl/demo_load_font.py: -------------------------------------------------------------------------------- 1 | ''' /font folder must be uploaded to the esp32 via Thonny ''' 2 | import lvgl as lv 3 | import time 4 | import display_driver 5 | import fs_driver # important 6 | 7 | 8 | # Register file system driver 9 | fs_drive_letter = 'S' # Can be any letter 10 | fs_font_driver = lv.fs_drv_t() 11 | fs_driver.fs_register(fs_font_driver, fs_drive_letter) 12 | 13 | # Multiple fonts may be loaded after fs driver is set up 14 | # Text font 15 | custom_font = lv.font_load(fs_drive_letter + ':' + 'fonts/montserrat-22.bin') 16 | 17 | # LCD display font, contains only numbers 0 - 9 and : 18 | # I use it to display time of day 19 | LCD_font = lv.font_load(fs_drive_letter + ':' + 'fonts/LCD_Font_120.bin') 20 | # Get reference to active screen for drawing 21 | scr = lv.scr_act() 22 | scr.set_style_bg_color(lv.color_white(), lv.PART.MAIN) 23 | 24 | # Create label with custom font 25 | customFontLbl = lv.label(scr) 26 | customFontLbl.set_style_text_font(custom_font, 0) 27 | customFontLbl.set_text('Custom Text Font') 28 | customFontLbl.align(lv.ALIGN.TOP_MID, 0, 30) 29 | 30 | LCDFontLbl = lv.label(scr) 31 | LCDFontLbl.set_style_text_font(LCD_font, 0) 32 | LCDFontLbl.set_text('12:00') 33 | LCDFontLbl.align(lv.ALIGN.TOP_MID, 0, 80) 34 | 35 | 36 | while True: 37 | time.sleep(1) -------------------------------------------------------------------------------- /demo_lvgl/demo_load_png.py: -------------------------------------------------------------------------------- 1 | ''' /img folder must be uploaded to the esp32 via Thonny ''' 2 | import lvgl as lv 3 | import lv_utils 4 | import time 5 | import display_driver 6 | 7 | 8 | # Create Image decoder 9 | from imagetools import get_png_info, open_png 10 | decoder = lv.img.decoder_create() 11 | decoder.info_cb = get_png_info 12 | decoder.open_cb = open_png 13 | 14 | 15 | # Simple class for loading images 16 | class ImgSimple(lv.img): 17 | def __init__(self, parent, src, w, h): 18 | super().__init__(parent) 19 | 20 | try: 21 | with open(src, 'rb') as f: 22 | png_data = f.read() 23 | except: 24 | print('Image not found.') 25 | #sys.exit() 26 | 27 | img_data = lv.img_dsc_t({ 28 | 'data_size': len(png_data), 29 | 'data': png_data 30 | }) 31 | 32 | self.set_src(img_data) 33 | self.set_size(w, h) 34 | 35 | 36 | scr = lv.scr_act() 37 | scr.set_style_bg_color(lv.color_white(), lv.PART.MAIN) 38 | 39 | # Load image 40 | hamsterImg = ImgSimple(scr, 'img/hamster.png', 50, 34) 41 | hamsterImg.align(lv.ALIGN.CENTER, -70, 0) 42 | 43 | # Create label next to image 44 | hamsterLbl = lv.label(scr) 45 | hamsterLbl.set_style_text_color(lv.color_black(), 0) 46 | hamsterLbl.set_style_text_font(lv.font_montserrat_16, 0) 47 | hamsterLbl.set_text("It's a Hamster!") 48 | hamsterLbl.align_to(hamsterImg, lv.ALIGN.OUT_RIGHT_MID, 10, 0) 49 | 50 | 51 | while True: 52 | time.sleep(1) -------------------------------------------------------------------------------- /demo_lvgl/demo_load_icon_font.py: -------------------------------------------------------------------------------- 1 | ''' /font folder must be uploaded to the esp32 via Thonny ''' 2 | import lvgl as lv 3 | import time 4 | import display_driver 5 | import fs_driver # important 6 | 7 | 8 | 9 | def utf8Bytes(hexStr): 10 | ''' Used for custom icon fonts 11 | Returns six digit utf8 bytecode from four digit hex code 12 | as shown on font awesome website e.g. 13 | 'F287' -> b'\0xEF\0x8A\0x87' 14 | 15 | Use: obj.set_text(utf8Bytes('F287'))''' 16 | 17 | hexCode = int(hexStr, 16) 18 | unicodeStr= chr(hexCode) 19 | utf8Bytecode = unicodeStr.encode('utf-8') 20 | return utf8Bytecode 21 | 22 | 23 | # Register file system driver 24 | fs_drive_letter = 'S' # Can be any letter 25 | fs_font_driver = lv.fs_drv_t() 26 | fs_driver.fs_register(fs_font_driver, fs_drive_letter) 27 | 28 | # Multiple fonts may be loaded after fs driver is set up 29 | # Icon font, containing five icons: e801 - e804 30 | font_icons = lv.font_load('S:' + 'fonts/Symbols_40.bin') 31 | 32 | # Get reference to active screen for drawing 33 | scr = lv.scr_act() 34 | scr.set_style_bg_color(lv.color_white(), lv.PART.MAIN) 35 | 36 | # Create label with custom font 37 | iconLbl1 = lv.label(scr) 38 | iconLbl1.set_style_text_font(font_icons, 0) 39 | iconLbl1.set_text(utf8Bytes('e801')) 40 | iconLbl1.align(lv.ALIGN.TOP_MID, 0, 30) 41 | 42 | iconLbl2 = lv.label(scr) 43 | iconLbl2.set_style_text_font(font_icons, 0) 44 | iconLbl2.set_text(utf8Bytes('e802')) 45 | iconLbl2.align(lv.ALIGN.TOP_MID, 0, 100) 46 | 47 | while True: 48 | time.sleep(1) -------------------------------------------------------------------------------- /demo_lvgl/demo_simple.py: -------------------------------------------------------------------------------- 1 | '''Custom Driver xpt2046_cyd and MPY-LVGL build from 2 | https://stefan.box2code.de/2023/11/18/esp32-grafik-mit-lvgl-und-micropython/ 3 | 4 | Running on cheap yellow display with TWO USB Ports 5 | --> https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/cyd.md 6 | ''' 7 | import lvgl as lv 8 | import time 9 | import display_driver 10 | 11 | num = 0 # Used for callback 12 | 13 | def enable(el): 14 | if el.has_state(lv.STATE.DISABLED): 15 | el.clear_state(lv.STATE.DISABLED) 16 | 17 | def disable(el): 18 | if not el.has_state(lv.STATE.DISABLED): 19 | el.add_state(lv.STATE.DISABLED) 20 | 21 | def largeFont(el): 22 | el.set_style_text_font(lv.font_montserrat_16, 0) 23 | 24 | def callback(s): 25 | global num 26 | if s == 'Prev': 27 | num -= 1 28 | elif s == 'Next': 29 | num += 1 30 | valueLbl.set_text('Count: ' + str(num)) 31 | 32 | 33 | 34 | 35 | 36 | ''' Get reference to active screen for drawing ''' 37 | scr = lv.scr_act() 38 | scr.set_style_bg_color(lv.color_white(), lv.PART.MAIN) 39 | 40 | 41 | ''' Create label and center it on screen ''' 42 | valueLbl = lv.label(scr) 43 | largeFont(valueLbl) 44 | valueLbl.set_text('Count: ' + str(num)) 45 | valueLbl.center() 46 | 47 | 48 | ''' Create Button with icon and text and add callback ''' 49 | prevBtn = lv.btn(scr) 50 | prevBtn.set_size(80, 40) 51 | prevBtn.align_to(valueLbl, lv.ALIGN.OUT_LEFT_MID, -20, 0) 52 | prevBtn.add_event_cb(lambda e: callback('Prev'), lv.EVENT.CLICKED, None) 53 | ''' Assign label to button ''' 54 | prevBtnLbl = lv.label(prevBtn) 55 | prevBtnLbl.set_text(lv.SYMBOL.PREV + ' Prev') 56 | prevBtnLbl.center() 57 | 58 | 59 | nextBtn = lv.btn(scr) 60 | nextBtn.set_size(80, 40) 61 | nextBtn.align_to(valueLbl, lv.ALIGN.OUT_RIGHT_MID, 20, 0) 62 | nextBtn.add_event_cb(lambda e: callback('Next'), lv.EVENT.CLICKED, None) 63 | 64 | nextBtnLbl = lv.label(nextBtn) 65 | nextBtnLbl.set_text('Next ' + lv.SYMBOL.NEXT) 66 | nextBtnLbl.center() 67 | 68 | 69 | while True: 70 | time.sleep(1) -------------------------------------------------------------------------------- /demo_lvgl/demo_more_icons.py: -------------------------------------------------------------------------------- 1 | ''' /font folder must be uploaded to the esp32 via Thonny ''' 2 | import lvgl as lv 3 | import time 4 | import display_driver 5 | import fs_driver 6 | 7 | 8 | 9 | def convert_to_utf8_bytes(hex_str): 10 | code_point = int(hex_str, 16) 11 | unicode_str = chr(code_point) 12 | utf8_bytes = unicode_str.encode('utf-8') 13 | return utf8_bytes 14 | 15 | try: 16 | 17 | style = lv.style_t() 18 | style.init() 19 | style.set_border_width(1) 20 | style.set_border_color(lv.palette_main(lv.PALETTE.ORANGE)) 21 | style.set_pad_all(2) 22 | 23 | fs_drv = lv.fs_drv_t() 24 | fs_driver.fs_register(fs_drv, 'S') 25 | font_custom = lv.font_load('S:' + 'fonts/font_awesome_18.bin') 26 | 27 | style.set_text_color(lv.color_black()) 28 | style.set_text_font(font_custom) 29 | 30 | 31 | spans = lv.spangroup(lv.scr_act()) 32 | spans.set_width(320) 33 | spans.set_height(240) 34 | spans.center() 35 | spans.add_style(style, 0) 36 | 37 | spans.set_align(lv.TEXT_ALIGN.LEFT) 38 | spans.set_overflow(lv.SPAN_OVERFLOW.CLIP) 39 | spans.set_indent(20) 40 | spans.set_mode(lv.SPAN_MODE.BREAK) 41 | 42 | glyphs = ['shutdown', 'gear', 'menue', 'file', 'folder', 'gears', 'keyboard', 43 | 'fileText','eyeOpen','eyeStrikeThrough','floppyDisk','calendarMonths','bell', 44 | 'calendarEmpty','clock','world','letter','eyeStrikeThrough2','user', 45 | 'image','message','camera','redo','pieChart','language','wifi','download', 46 | 'upload','arrowLeftLong','confirm','close','trash','arrowLeft', 47 | 'bar','arrowRight','menue2','chain','lock','key','lockOpen','deny', 48 | 'lightbulb','birthdayCake'] 49 | 50 | chars = ['f011','f013','f0c9','f15b','f07b','f085','f11c','f15c','f06e', 51 | 'f070','f0c7','f073','f0f3','f133','f017','f0ac','f0e0','f070', 52 | 'f007','f03e','f075','f030','f064','f200','f1ab','f1eb','f019', 53 | 'f093','f178','f00c','f00d','f1f8','f178','f068','f177','f141', 54 | 'f0c1','f023','f084','f09c','f05e','f0eb','f1fd'] 55 | 56 | print(len(glyphs), len(chars)) 57 | 58 | def getGlyph(name): 59 | global glyphs, chars 60 | 61 | for i, glyph in enumerate(glyphs): 62 | if name == glyph: 63 | return convert_to_utf8_bytes(chars[i]) 64 | 65 | print('Glyph not found!') 66 | return convert_to_utf8_bytes('f05e') 67 | 68 | 69 | for char in chars: 70 | span = spans.new_span() 71 | span.set_text(convert_to_utf8_bytes(char)) 72 | 73 | 74 | spans.refr_mode() 75 | 76 | while True: 77 | time.sleep(1) 78 | 79 | except KeyboardInterrupt: 80 | print('Exiting...') 81 | #finally: 82 | bgl(0) 83 | disp.deinit() 84 | touch.deinit() 85 | spi2.deinit() 86 | -------------------------------------------------------------------------------- /demo_lvgl/demo_portrait_mode.py: -------------------------------------------------------------------------------- 1 | from machine import SoftSPI, Pin 2 | import lvgl as lv 3 | import time 4 | import ili9XXX 5 | from xpt2046_cyd import xpt2046 6 | 7 | try: 8 | lv.init() 9 | 10 | spiTouch = SoftSPI(baudrate = 2500000, sck = Pin(25), 11 | mosi = Pin(32), miso = Pin(39)) 12 | 13 | 14 | ''' Set Display Orientation: 15 | DISP_PORTRAIT = True --> Portrait Mode 16 | DISP_PORTRAIT = False --> Landscape Mode 17 | 18 | The following parameters need to be adjusted according 19 | to the selected display orientation: 20 | disp: rot, width, height 21 | touch: width, height, transpose, portrait, (calibration) 22 | ''' 23 | DISP_PORTRAIT = True 24 | #DISP_PORTRAIT = False 25 | 26 | if DISP_PORTRAIT: 27 | ''' Portrait Mode ''' 28 | disp = ili9XXX.ili9341(clk=14, cs=15, 29 | dc=2, rst=12, power=23, miso=12, 30 | mosi=13, width = 240, height = 320, 31 | rot = 0xA0, colormode=ili9XXX.COLOR_MODE_RGB, 32 | double_buffer = False, factor = 16) 33 | 34 | touch = xpt2046(spi = spiTouch, cs = Pin(33), cal_x0 = 3722, 35 | cal_y0 = 3738, cal_x1 = 250, cal_y1 = 245, portrait = True, transpose = False) 36 | else: 37 | ''' Landscape Mode ''' 38 | disp = ili9XXX.ili9341(clk=14, cs=15, 39 | dc=2, rst=12, power=23, miso=12, 40 | mosi=13, width = 320, height = 240, 41 | rot = 0xC0, colormode=ili9XXX.COLOR_MODE_RGB, 42 | double_buffer = False, factor = 16) 43 | 44 | 45 | touch = xpt2046(spi = spiTouch, cs = Pin(33), cal_x0 = 3700, 46 | cal_y0 = 3820, cal_x1 = 180, cal_y1 = 250, transpose=False) 47 | 48 | backlight = Pin(21, Pin.OUT) 49 | backlight(1) 50 | 51 | group = lv.group_create() 52 | group.set_default() 53 | 54 | ''' Create Screen Object ''' 55 | scr = lv.obj() 56 | scr.set_style_bg_color(lv.color_white(), lv.PART.MAIN) 57 | 58 | 59 | ''' Add Screen Content ''' 60 | statusLbl = lv.label(scr) 61 | statusLbl.set_text('Portrait Mode' if DISP_PORTRAIT else 'Landscape Mode') 62 | statusLbl.center() 63 | 64 | testBtn = lv.btn(scr) 65 | testBtn.set_size(110, 40) 66 | testBtn.align_to(statusLbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) 67 | 68 | testBtnLbl = lv.label(testBtn) 69 | testBtnLbl.set_text(lv.SYMBOL.RIGHT + ' Click Me! ' + lv.SYMBOL.LEFT) 70 | testBtnLbl.center() 71 | 72 | testBtn.add_event_cb(lambda e: statusLbl.set_text('Mid Button clicked!'), lv.EVENT.CLICKED, None) 73 | 74 | testBtn2 = lv.btn(scr) 75 | testBtn2.align(lv.ALIGN.TOP_LEFT, 5, 5) 76 | 77 | testBtnLbl2 = lv.label(testBtn2) 78 | testBtnLbl2.set_text('TOP LEFT') 79 | testBtnLbl2.center() 80 | 81 | testBtn2.add_event_cb(lambda e: statusLbl.set_text('Top Button clicked!'), lv.EVENT.CLICKED, None) 82 | 83 | 84 | testBtn3 = lv.btn(scr) 85 | testBtn3.align(lv.ALIGN.BOTTOM_RIGHT, -5, -5) 86 | 87 | testBtnLbl3 = lv.label(testBtn3) 88 | testBtnLbl3.set_text('BOTTOM RIGHT') 89 | testBtnLbl3.center() 90 | 91 | testBtn3.add_event_cb(lambda e: statusLbl.set_text('Bottom Button clicked!'), lv.EVENT.CLICKED, None) 92 | 93 | ''' Load Screen ''' 94 | lv.scr_load(scr) 95 | 96 | while True: 97 | time.sleep(1) 98 | 99 | except KeyboardInterrupt: 100 | print('Programm terminated by user.') 101 | #finally: -------------------------------------------------------------------------------- /demo_no_lvgl/demo_no_lvgl.py: -------------------------------------------------------------------------------- 1 | """ILI9341 XPT2046 touch demo for cheap yellow display.""" 2 | from ili9341 import Display, color565 3 | from xpt2046 import Touch 4 | from machine import Pin, SPI, PWM 5 | import time 6 | 7 | 8 | def color_rgb(r, g, b): 9 | return color565(r, g, b) 10 | 11 | ''' 12 | TouchScreen (0, 0) = corner diagonal to usb port 13 | ''' 14 | 15 | class TouchScreen(object): 16 | """Touchscreen simple demo.""" 17 | CYAN = color_rgb(0, 255, 255) 18 | PURPLE = color_rgb(255, 0, 255) 19 | WHITE = color_rgb(255, 0, 0) 20 | 21 | def __init__(self): 22 | self.mark_touch = True # True, False --> Show touch coordinates 23 | self.Touch_items = [] 24 | self.Touch_callbacks = [] 25 | 26 | self.spi_display = SPI(1, baudrate=10000000, 27 | sck=Pin(14), mosi=Pin(13)) 28 | 29 | self.Screen = Display(self.spi_display, dc=Pin(2), cs=Pin(15), 30 | rst=Pin(15), width = 320, height = 240, 31 | bgr = False, gamma = True) 32 | 33 | self.backlight = Pin(21, Pin.OUT) 34 | self.backlight.on() 35 | 36 | self.spi_touch = SPI(2, baudrate=1000000, sck=Pin(25), 37 | mosi=Pin(32), miso=Pin(39)) 38 | 39 | self.Touch = Touch(self.spi_touch, cs=Pin(33), int_pin=Pin(36), 40 | int_handler=self.touchscreen_press) 41 | 42 | self.Screen.clear(color565(255,255,255)) 43 | 44 | #self.backlightPWM = PWM(self.backlight, freq=5000, duty_u16=32768) 45 | 46 | # Assign touch callbacks and draw elements on screen 47 | self.draw() 48 | 49 | 50 | def draw(self): 51 | ''' Draw text and assign a callback to touch event at rectangular area ''' 52 | 53 | text = 'Touch Area 1' 54 | self.Screen.draw_text8x8(108, 108, text, 55 | color565(0, 0, 0), color565(255, 255, 255)) 56 | item = self.TouchArea(self, 100, 100, 15 + 8 * len(text), 20, True) 57 | self.addTouchItem(item, lambda x: print('Area 1')) 58 | 59 | 60 | text = 'Touch Area 2' 61 | self.Screen.draw_text8x8(168, 168, text, 62 | color565(0, 0, 0), color565(255, 255, 255)) 63 | item2 = self.TouchArea(self, 160, 160, 15 + 8 * len(text), 20, True) 64 | self.addTouchItem(item2, lambda x: print('Area 2')) 65 | 66 | 67 | ''' Draw shutdown icon and bind callback to it ''' 68 | a = 5 # Move the icon 69 | b = 190 # Move the icon 70 | self.Screen.fill_circle(300 - a, 220 - b, 18, color565(0, 0, 0)) 71 | self.Screen.fill_circle(300 - a, 220 - b, 15, color565(255, 255, 255)) 72 | self.Screen.fill_rectangle(295 - a, 197 - b, 10, 20, color565(255, 255, 255)) 73 | self.Screen.fill_rectangle(298 - a, 197 - b, 4, 20, color565(0, 0, 0)) 74 | 75 | item3 = self.TouchArea(self, 300 - a - 20, 220 - b - 20, 40, 40) 76 | self.addTouchItem(item3, lambda x: self.shutdown()) 77 | 78 | 79 | def addTouchItem(self, item, cb): 80 | self.Touch_items.append(item) 81 | self.Touch_callbacks.append(cb) 82 | 83 | def shutdown(self): 84 | print('Shutdown.') 85 | self.spi_touch.deinit() 86 | #self.backlightPWM.deinit() 87 | 88 | self.backlight.off() 89 | self.Screen.cleanup() 90 | sys.exit(0) 91 | 92 | def touchscreen_press(self, x, y): 93 | """Process touchscreen press events.""" 94 | x, y = y, x 95 | 96 | if self.mark_touch: 97 | self.Screen.fill_circle(x, y, 4, color_rgb(155, 155, 155)) 98 | self.Screen.draw_circle(x, y, 4, color_rgb(255, 255, 255)) 99 | 100 | 101 | if len(self.Touch_items) > 0: 102 | for i, item in enumerate(self.Touch_items): 103 | if x in item.TouchX and y in item.TouchY: 104 | self.Touch_callbacks[i](i) 105 | 106 | class TouchArea: 107 | ''' Bind callback to rectangular touch area''' 108 | def __init__(self, parent, x, y, w, h, draw = False): 109 | self.parent = parent 110 | self.x = x 111 | self.y = y 112 | self.width = w 113 | self.height = h 114 | 115 | self.TouchX = range(self.x, self.x + self.width) 116 | self.TouchY = range(self.y, self.y + self.height) 117 | 118 | if draw is True: 119 | self.draw() 120 | 121 | ''' Draw the outline of the touch area ''' 122 | def draw(self): 123 | self.color = color_rgb(70, 0, 0) 124 | 125 | for i in range(2): 126 | self.parent.Screen.draw_rectangle(self.x+i, self.y+i, 127 | self.width-2*i, self.height-2*i, 128 | color_rgb(255, 0, 0)) 129 | 130 | 131 | try: 132 | x = TouchScreen() 133 | 134 | while True: 135 | time.sleep(1) 136 | 137 | except KeyboardInterrupt: 138 | print("\nCtrl-C pressed. Cleaning up and exiting...") 139 | #finally: 140 | x.shutdown() 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /demo_lvgl/lib/xpt2046_cyd.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Modified xpt2046 driver for use with lvgl on ESP32-2432S028 3 | aka CYD with two usb ports. 4 | 5 | Original source: Stefan Scholz, 6 | https://stefan.box2code.de/2023/11/18/esp32-grafik-mit-lvgl-und-micropython/ 7 | 8 | Modified by D. Hennig for use in portrait mode. 9 | ''' 10 | from machine import SPI 11 | import lvgl as lv 12 | import espidf as esp 13 | 14 | class xpt2046: 15 | CMD_X_READ = const(0x90) 16 | CMD_Y_READ = const(0xd0) 17 | CMD_Z1_READ = const(0xb8) 18 | CMD_Z2_READ = const(0xc8) 19 | 20 | MAX_RAW_COORD = const((1<<12) - 1) 21 | 22 | def __init__(self, spi, cs, max_cmds=16, cal_x0 = 3783, cal_y0 = 3948, cal_x1 = 242, cal_y1 = 423, 23 | transpose = True, samples = 3, portrait = False): 24 | 25 | # Initializations 26 | 27 | if not lv.is_initialized(): 28 | lv.init() 29 | 30 | self.portrait = portrait 31 | 32 | disp = lv.disp_t.__cast__(None) 33 | 34 | self.screen_width = 240 if self.portrait else 320 35 | self.screen_height = 320 if self.portrait else 240 36 | 37 | self.spi = spi 38 | self.cs = cs 39 | self.recv = bytearray(3) 40 | self.xmit = bytearray(3) 41 | 42 | self.max_cmds = max_cmds 43 | self.cal_x0 = cal_x0 44 | self.cal_y0 = cal_y0 45 | self.cal_x1 = cal_x1 46 | self.cal_y1 = cal_y1 47 | self.transpose = transpose 48 | self.samples = samples 49 | 50 | self.touch_count = 0 51 | self.touch_cycles = 0 52 | 53 | indev_drv = lv.indev_drv_t() 54 | indev_drv.init() 55 | indev_drv.type = lv.INDEV_TYPE.POINTER 56 | indev_drv.read_cb = self.read 57 | indev_drv.register() 58 | 59 | def calibrate(self, x0, y0, x1, y1): 60 | self.cal_x0 = x0 61 | self.cal_y0 = y0 62 | self.cal_x1 = x1 63 | self.cal_y1 = y1 64 | 65 | def deinit(self): 66 | print('Deinitializing XPT2046...') 67 | 68 | def touch_talk(self, cmd, bits): 69 | if self.cs is not None: 70 | self.cs(0) 71 | self.xmit[0] = cmd 72 | self.spi.write_readinto(self.xmit, self.recv) 73 | if self.cs is not None: 74 | self.cs(1) 75 | return (self.recv[1] * 256 + self.recv[2]) >> (15 - bits) 76 | 77 | # @micropython.viper 78 | def xpt_cmds(self, cmds): 79 | result = [] 80 | for cmd in cmds: 81 | value = self.touch_talk(cmd, 12) 82 | if value == int(self.MAX_RAW_COORD): 83 | value = 0 84 | result.append(value) 85 | return tuple(result) 86 | 87 | # @micropython.viper 88 | def get_med_coords(self, count : int): 89 | mid = count//2 90 | values = [] 91 | for i in range(0, count): 92 | values.append(self.xpt_cmds([self.CMD_X_READ, self.CMD_Y_READ])) 93 | x_values = sorted([x for x,y in values]) 94 | y_values = sorted([y for x,y in values]) 95 | if int(x_values[0]) == 0 or int(y_values[0]) == 0 : return None 96 | #print(x_values[mid], y_values[mid]) 97 | return x_values[mid], y_values[mid] 98 | 99 | # @micropython.viper 100 | def get_coords(self): 101 | med_coords = self.get_med_coords(int(self.samples)) 102 | if not med_coords: return None 103 | if self.transpose: 104 | raw_y, raw_x = med_coords 105 | else: 106 | raw_x, raw_y = med_coords 107 | 108 | if int(raw_x) != 0 and int(raw_y) != 0: 109 | if self.portrait: 110 | x = self.screen_width - (((int(raw_y) - int(self.cal_y0)) * int(self.screen_width)) // (int(self.cal_y1) - int(self.cal_y0))) 111 | y = ((int(raw_x) - int(self.cal_x0)) * int(self.screen_height)) // (int(self.cal_x1) - int(self.cal_x0)) 112 | else: 113 | x = ((int(raw_x) - int(self.cal_x0)) * int(self.screen_width)) // (int(self.cal_x1) - int(self.cal_x0)) 114 | y = ((int(raw_y) - int(self.cal_y0)) * int(self.screen_height)) // (int(self.cal_y1) - int(self.cal_y0)) 115 | return x,y 116 | else: return None 117 | 118 | # @micropython.native 119 | def get_pressure(self, factor : int) -> int: 120 | z1, z2, x = self.xpt_cmds([self.CMD_Z1_READ, self.CMD_Z2_READ, self.CMD_X_READ]) 121 | if int(z1) == 0: return -1 122 | return ( (int(x)*factor) / 4096)*( int(z2)/int(z1) - 1) 123 | 124 | start_time_ptr = esp.C_Pointer() 125 | end_time_ptr = esp.C_Pointer() 126 | cycles_in_ms = esp.esp_clk_cpu_freq() // 1000 127 | 128 | # @micropython.native 129 | def read(self, indev_drv, data) -> int: 130 | 131 | esp.get_ccount(self.start_time_ptr) 132 | coords = self.get_coords() 133 | #print(coords) 134 | esp.get_ccount(self.end_time_ptr) 135 | 136 | if self.end_time_ptr.int_val > self.start_time_ptr.int_val: 137 | self.touch_cycles += self.end_time_ptr.int_val - self.start_time_ptr.int_val 138 | self.touch_count += 1 139 | 140 | if coords: 141 | data.point.x ,data.point.y = coords 142 | data.state = lv.INDEV_STATE.PRESSED 143 | return False 144 | data.state = lv.INDEV_STATE.RELEASED 145 | return False 146 | 147 | def stat(self): 148 | return self.touch_cycles / (self.touch_count * self.cycles_in_ms) -------------------------------------------------------------------------------- /demo_no_lvgl/xpt2046.py: -------------------------------------------------------------------------------- 1 | """XPT2046 Touch module. 2 | Source: https://github.com/rdagger/micropython-ili9341/blob/master/xpt2046.py 3 | MIT License""" 4 | from time import sleep 5 | 6 | 7 | class Touch(object): 8 | """Serial interface for XPT2046 Touch Screen Controller.""" 9 | 10 | # Command constants from ILI9341 datasheet 11 | GET_X = const(0b11010000) # X position 12 | GET_Y = const(0b10010000) # Y position 13 | GET_Z1 = const(0b10110000) # Z1 position 14 | GET_Z2 = const(0b11000000) # Z2 position 15 | GET_TEMP0 = const(0b10000000) # Temperature 0 16 | GET_TEMP1 = const(0b11110000) # Temperature 1 17 | GET_BATTERY = const(0b10100000) # Battery monitor 18 | GET_AUX = const(0b11100000) # Auxiliary input to ADC 19 | 20 | def __init__(self, spi, cs, int_pin=None, int_handler=None, 21 | width=240, height=320, 22 | x_min=100, x_max=1962, y_min=100, y_max=1900): 23 | """Initialize touch screen controller. 24 | 25 | Args: 26 | spi (Class Spi): SPI interface for OLED 27 | cs (Class Pin): Chip select pin 28 | int_pin (Class Pin): Touch controller interrupt pin 29 | int_handler (function): Handler for screen interrupt 30 | width (int): Width of LCD screen 31 | height (int): Height of LCD screen 32 | x_min (int): Minimum x coordinate 33 | x_max (int): Maximum x coordinate 34 | y_min (int): Minimum Y coordinate 35 | y_max (int): Maximum Y coordinate 36 | """ 37 | self.spi = spi 38 | self.cs = cs 39 | self.cs.init(self.cs.OUT, value=1) 40 | self.rx_buf = bytearray(3) # Receive buffer 41 | self.tx_buf = bytearray(3) # Transmit buffer 42 | self.width = width 43 | self.height = height 44 | # Set calibration 45 | self.x_min = x_min 46 | self.x_max = x_max 47 | self.y_min = y_min 48 | self.y_max = y_max 49 | self.x_multiplier = width / (x_max - x_min) 50 | self.x_add = x_min * -self.x_multiplier 51 | self.y_multiplier = height / (y_max - y_min) 52 | self.y_add = y_min * -self.y_multiplier 53 | 54 | if int_pin is not None: 55 | self.int_pin = int_pin 56 | self.int_pin.init(int_pin.IN) 57 | self.int_handler = int_handler 58 | self.int_locked = False 59 | int_pin.irq(trigger=int_pin.IRQ_FALLING | int_pin.IRQ_RISING, 60 | handler=self.int_press) 61 | 62 | def get_touch(self): 63 | """Take multiple samples to get accurate touch reading.""" 64 | timeout = 2 # set timeout to 2 seconds 65 | confidence = 5 66 | buff = [[0, 0] for x in range(confidence)] 67 | buf_length = confidence # Require a confidence of 5 good samples 68 | buffptr = 0 # Track current buffer position 69 | nsamples = 0 # Count samples 70 | while timeout > 0: 71 | if nsamples == buf_length: 72 | meanx = sum([c[0] for c in buff]) // buf_length 73 | meany = sum([c[1] for c in buff]) // buf_length 74 | dev = sum([(c[0] - meanx)**2 + 75 | (c[1] - meany)**2 for c in buff]) / buf_length 76 | if dev <= 50: # Deviation should be under margin of 50 77 | return self.normalize(meanx, meany) 78 | # get a new value 79 | sample = self.raw_touch() # get a touch 80 | if sample is None: 81 | nsamples = 0 # Invalidate buff 82 | else: 83 | buff[buffptr] = sample # put in buff 84 | buffptr = (buffptr + 1) % buf_length # Incr, until rollover 85 | nsamples = min(nsamples + 1, buf_length) # Incr. until max 86 | 87 | sleep(.05) 88 | timeout -= .05 89 | return None 90 | 91 | def int_press(self, pin): 92 | """Send X,Y values to passed interrupt handler.""" 93 | if not pin.value() and not self.int_locked: 94 | self.int_locked = True # Lock Interrupt 95 | buff = self.raw_touch() 96 | 97 | if buff is not None: 98 | x, y = self.normalize(*buff) 99 | self.int_handler(x, y) 100 | sleep(.1) # Debounce falling edge 101 | elif pin.value() and self.int_locked: 102 | sleep(.1) # Debounce rising edge 103 | self.int_locked = False # Unlock interrupt 104 | 105 | def normalize(self, x, y): 106 | """Normalize mean X,Y values to match LCD screen.""" 107 | x = int(self.x_multiplier * x + self.x_add) 108 | y = int(self.y_multiplier * y + self.y_add) 109 | return x, y 110 | 111 | def raw_touch(self): 112 | """Read raw X,Y touch values. 113 | 114 | Returns: 115 | tuple(int, int): X, Y 116 | """ 117 | x = self.send_command(self.GET_X) 118 | y = self.send_command(self.GET_Y) 119 | if self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max: 120 | return (x, y) 121 | else: 122 | return None 123 | 124 | def send_command(self, command): 125 | """Write command to XT2046 (MicroPython). 126 | 127 | Args: 128 | command (byte): XT2046 command code. 129 | Returns: 130 | int: 12 bit response 131 | """ 132 | self.tx_buf[0] = command 133 | self.cs(0) 134 | self.spi.write_readinto(self.tx_buf, self.rx_buf) 135 | self.cs(1) 136 | 137 | return (self.rx_buf[1] << 4) | (self.rx_buf[2] >> 4) 138 | -------------------------------------------------------------------------------- /LVGL8.md: -------------------------------------------------------------------------------- 1 | > [!WARNING] 2 | > This document is not maintained. 3 | > All demo programs in the `demo_lvgl` were tested with the pre-built firmware linked in this repositry using **LVGL version 8.3.6.** and **Micropython version 1.19.1.** 4 | > The demo programs are incompatible with the LVGL9 firmware. 5 | 6 | # CYD2-MPY-LVGL 7 | 8 | ## Introduction 9 | 10 | This repositry is meant to show you how to quickly set up [LVGL](https://github.com/rzeldent/platformio-espressif32-sunton) under [Micropython](https://github.com/micropython/micropython) on the [Cheap Yellow Display](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/tree/main) and save you some time and pain. 11 | Everyone is welcome to contribute to this repositry and share his / her knowledge. 12 | 13 | 14 | 15 | The demo programms demonstrate the following functions of LVGL on the CYD(2): 16 | 17 | - simple demo with buttons and callback functions 18 | - using CYD2 in portrait mode 19 | - loading a png image (without the image converter) 20 | - loading a custom text font 21 | - loading a custom icon font 22 | - advanced demo with multiple screens, a chart with data imported from a .csv file and asyncio usage 23 | 24 | [Two similar versions of CYD are available](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/cyd.md). 25 | The first version has one USB port (i call this "CYD") and the second version features two USB ports (i call this "CYD2"). 26 | Although the remaining components are identical, there is a difference in the display drivers color management. 27 | 28 | 29 | 30 | 31 | ## CYD2 and LVGL + Micropython 32 | 33 | > [!IMPORTANT] 34 | > Summary of the steps needed to make the LVGL demo programms work on your CYD/CYD2: 35 | > - Download the LVGL-MPY firmware from the link below and flash it to your CYD using esptool.py. 36 | > - Upload the complete content of the `/demo-lvgl` folder to your CYD's root (not the folder itself). 37 | > - Run demo programm (which most likely looks wrong at this point). 38 | > - Open `demo_lvgl/lib/display_driver.py` and adjust display color mode and rotation settings (you have to test the different settings until you find the correct ones). 39 | 40 | 41 | ### Drivers and Firmware 42 | After getting CYD2 to work with standard MPY firmware and the corresponding drivers, 43 | I figured that the display driver is slow and has very limited capabilities for use. 44 | 45 | A [prebuild version of the lvgl firmware 8.3.6. for CYD](https://stefan.box2code.de/2023/11/18/esp32-grafik-mit-lvgl-und-micropython/) is provided for download by Stefan Scholz in his awesome blog post. 46 | Furthermore, Stefan Scholz modified the xpt2046 driver used in his blog. 47 | I further modified the driver to support portrait mode (included in the `lvgl_demo/lib folder`). 48 | 49 | The prebuild version of the MPY-LVGL firmware needs to be downloaded from the aforementioned site. 50 | I didn't upload them since I don't hold the copyright. 51 | 52 | Here are direct download links from Stefan's Blog for non-german users (use one of the first three versions for CYD/CYD2): 53 | 54 | - [Esp32WROOM](https://stefan.box2code.de/wp-content/uploads/2023/11/lv_micropython-WROOM.zip) 55 | - [Esp32WROOM + espnow](https://stefan.box2code.de/wp-content/uploads/2024/04/lv_micropython-WROOM_EspNow.zip) 56 | - [Esp32WROOM + async espnow](https://stefan.box2code.de/wp-content/uploads/2024/04/lv_micropython-WROOM_AOIEspNow.zip) 57 | - [Esp32WROVER](https://stefan.box2code.de/huge_files/lv_micropython-WROVER.zip) * 58 | 59 | *: Can be used to connect an ESP32-WROVER (more PSRAM) to a ILI9341 + xpt2046 Touchscreen. Also useful when [adding PSRAM to the CYD](https://github.com/hexeguitar/ESP32_TFT_PIO). 60 | 61 | The .zip archives already contain a `flash.sh` file for flashing with esptool.py under unix. 62 | You might need to change `python` to `python3` and `-p /dev/ttyUSB0` to `--port COMXX` (XX = your COM address) if you use esptool.py with windows command line. 63 | Open command line and navigate to the folder containing the source files (use the `cd` command, e. g. `cd Desktop/lv_micropython-WROOM`). 64 | Then run the esptool command. 65 | In my case I had to use the following command: 66 | ``` 67 | python -m esptool --chip esp32 --port COM13 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 80m --flash_size 4MB 0x1000 bootloader/bootloader.bin 0x10000 micropython.bin 0x8000 partition_table/partition-table.bin 68 | ``` 69 | 70 | ### Adjusting the display settings for CYD2 71 | 72 | Although my CYD2's look all the same, some require adjustments for the initialization of the display driver (thanks to Stefan Scholz for the help). 73 | Some of my boards only needed the colormode to be changed, other also required different rotation settings. 74 | You just have to figure it out by trying. 75 | 76 | Open `demo_lvgl/lib/display_driver.py` and look for the display initialization command: 77 | 78 | ```python 79 | disp = ili9XXX.ili9341(clk=14, cs=15, dc=2, rst=12, power=23, miso=12, mosi=13, width = 320, height = 240, 80 | rot = 0xC0, colormode=ili9XXX.COLOR_MODE_RGB, double_buffer = False, factor = 16) 81 | ``` 82 | 83 | If your colors are inverted, replace `colormode=ili9XXX.COLOR_MODE_RGB` with `colormode=ili9XXX.COLOR_MODE_BGR`. 84 | If the rotation is wrong, change `rot = 0xC0` to `rot = 0xXX` according to the table below. 85 | Try the different values until you get the right one. 86 | 87 | ```python 88 | # Excerpt from the ILI9341 driver MADCTL configurations for rotation and mirroring. 89 | # First value stands for mirroring, second value stands for rotation. 90 | MIRROR_ROTATE = { 91 | (False, 0): 0x80, 92 | (False, 90): 0xE0, 93 | (False, 180): 0x40, 94 | (False, 270): 0x20, 95 | (True, 0): 0xC0, 96 | (True, 90): 0x60, 97 | (True, 180): 0x00, 98 | (True, 270): 0xA0 99 | } 100 | ``` 101 | ### Demo Programms 102 | 103 | Several demos can be found in the `/demo-lvgl` folder. Flash the prebuild firmware with esptool.py and **upload the complete content** of the `/demo-lvgl` folder to your CYD (not the folder itself). 104 | The modified xpt2046 driver is included in the `lib` folder. Display and touchscreen are initialized in the `display_driver.py` file in the `lib` folder. 105 | 106 | 107 | 108 | 109 | The demo programms demonstrate the following functions of lvgl on CYD(2): 110 | 111 | - simple demo with buttons and callback functions 112 | - using CYD2 in portrait mode 113 | - loading a png image 114 | - loading a custom text font 115 | - loading a custom icon font 116 | - advanced demo with multiple screens, a chart with data imported from a .csv file and asyncio usage 117 | -------------------------------------------------------------------------------- /lvgl9_firmware/touch_color_test.py: -------------------------------------------------------------------------------- 1 | from micropython import const 2 | import lvgl as lv 3 | import time 4 | import machine 5 | import lcd_bus 6 | import ili9341 7 | import xpt2046 8 | import touch_cal_data 9 | import task_handler 10 | from machine import Timer 11 | 12 | 13 | # ============== Customize settings ============== # 14 | # The following values need to be customized. 15 | 16 | # Switch width and height for portrait mode. 17 | _DISPLAY_WIDTH = const(320) 18 | _DISPLAY_HEIGHT = const(240) 19 | # Try different values from rotation table, see below. 20 | _DISPLAY_ROT = const(0xE0) 21 | # Set to True if red and blue are switched. 22 | _DISPLAY_BGR = const(1) 23 | # May have to be set to 0 if both RGB / BGR mode give bad results. 24 | _DISPLAY_RGB565_BYTE_SWAP = const(1) 25 | # Allow touch calibration. Set to True when display works correctly. 26 | _ALLOW_TOUCH_CAL = const(0) 27 | # Show marker at current touch coordinates. 28 | _DISPLAY_SHOW_TOUCH_INDICATOR = const(1) 29 | 30 | ''' 31 | MADCTL_TABLE = { 32 | (False, 0): 0x80, # mirroring = False 33 | (False, 90): 0xE0, 34 | (False, 180): 0x40, 35 | (False, 270): 0x20, 36 | (True, 0): 0xC0, # mirroring = True 37 | (True, 90): 0x60, 38 | (True, 180): 0x00, 39 | (True, 270): 0xA0 40 | } 41 | ''' 42 | 43 | 44 | # ============== Display / Indev initialization ============== # 45 | # no need to change anything below here 46 | _SPI_BUS_HOST = const(1) 47 | _SPI_BUS_MOSI = const(13) 48 | _SPI_BUS_MISO = const(12) 49 | _SPI_BUS_SCK = const(14) 50 | _INDEV_BUS_HOST = const(2) 51 | _INDEV_BUS_MOSI = const(32) 52 | _INDEV_BUS_MISO = const(39) 53 | _INDEV_BUS_SCK = const(25) 54 | _INDEV_DEVICE_FREQ = const(2000000) 55 | _INDEV_DEVICE_CS = const(33) 56 | _DISPLAY_BUS_FREQ = const(24000000) 57 | _DISPLAY_BUS_DC = const(2) 58 | _DISPLAY_BUS_CS = const(15) 59 | _DISPLAY_BACKLIGHT_PIN = const(21) 60 | 61 | spi_bus = machine.SPI.Bus( 62 | host=_SPI_BUS_HOST, 63 | mosi=_SPI_BUS_MOSI, 64 | miso=_SPI_BUS_MISO, 65 | sck=_SPI_BUS_SCK 66 | ) 67 | 68 | indev_bus = machine.SPI.Bus( 69 | host=_INDEV_BUS_HOST, 70 | mosi=_INDEV_BUS_MOSI, 71 | miso=_INDEV_BUS_MISO, 72 | sck=_INDEV_BUS_SCK 73 | ) 74 | 75 | indev_device = machine.SPI.Device( 76 | spi_bus=indev_bus, 77 | freq=_INDEV_DEVICE_FREQ, 78 | cs=_INDEV_DEVICE_CS 79 | ) 80 | 81 | display_bus = lcd_bus.SPIBus( 82 | spi_bus=spi_bus, 83 | freq=_DISPLAY_BUS_FREQ, 84 | dc=_DISPLAY_BUS_DC, 85 | cs=_DISPLAY_BUS_CS 86 | ) 87 | 88 | display = ili9341.ILI9341( 89 | data_bus=display_bus, 90 | display_width=_DISPLAY_WIDTH, 91 | display_height=_DISPLAY_HEIGHT, 92 | backlight_pin=_DISPLAY_BACKLIGHT_PIN, 93 | backlight_on_state=ili9341.STATE_PWM, 94 | color_space=lv.COLOR_FORMAT.RGB565, 95 | color_byte_order=ili9341.BYTE_ORDER_BGR if _DISPLAY_BGR else ili9341.BYTE_ORDER_RGB, 96 | rgb565_byte_swap=_DISPLAY_RGB565_BYTE_SWAP 97 | ) 98 | 99 | # The rotation table MUST be defined 100 | display._ORIENTATION_TABLE = ( 101 | _DISPLAY_ROT, # this value sets the rotation 102 | 0x0, # placeholder 103 | 0x0, # placeholder 104 | 0x0 # placeholder 105 | ) 106 | 107 | # lv.DISPLAY_ROTATION._0 uses the first value from the 108 | # display._ORIENTATION_TABLE to set display rotation 109 | display.set_rotation(lv.DISPLAY_ROTATION._0) 110 | display.set_power(True) 111 | display.init(1) 112 | display.set_backlight(100) 113 | 114 | 115 | indev = xpt2046.XPT2046(device=indev_device) 116 | 117 | # Calibration data is stored in the non-volatile storage (NVS) of the Esp32 118 | if not indev.is_calibrated and _ALLOW_TOUCH_CAL: 119 | indev.calibrate() 120 | indev._cal.save() 121 | 122 | 123 | task_handler.TaskHandler() 124 | 125 | # ============== End of display / touch (indev) setup ============== # 126 | 127 | 128 | def palette_color(c, shade = 0): 129 | ''' 130 | Returns a color from LVGL's main palette and 131 | lightens or darkens the color by a specified shade. 132 | 133 | Palette Colors: 134 | RED, PINK, PURPLE, DEEP_PURPLE, INDIGO, BLUE, 135 | LIGHT_BLUE, CYAN, TEAL, GREEN, LIGHT_GREEN, LIME, 136 | YELLOW, AMBER, ORANGE, DEEP_ORANGE, BROWN, BLUE_GREY, GREY 137 | ''' 138 | attr = getattr(lv.PALETTE, c.upper(), 'Undefined') 139 | if attr != 'Undefined': 140 | if not (shade in range(-4, 6)): return lv.color_black() 141 | if shade == 0: 142 | return lv.palette_main(attr) 143 | elif shade > 0: 144 | return lv.palette_lighten(attr, shade) 145 | elif shade < 0: 146 | return lv.palette_darken(attr, abs(shade)) 147 | else: 148 | return lv.color_black() 149 | 150 | class RectStyle(lv.style_t): 151 | def __init__(self, bg_color=lv.color_black()): 152 | super().__init__() 153 | self.set_bg_opa(lv.OPA._100) 154 | self.set_bg_color(bg_color) 155 | self.set_text_opa(lv.OPA._100) 156 | self.set_text_color(lv.color_black()) 157 | 158 | 159 | class Rect(): 160 | def __init__(self, align, color, parent): 161 | self.align = align 162 | self.color = palette_color(color) 163 | self.parent = parent 164 | 165 | self.lvalign = getattr(lv.ALIGN, self.align, 'Undefined') 166 | 167 | s = self.align.split('_') # Remove undersore from align value and 168 | self.text = s[0][0] + s[1][0] # converts e.g. TOP_LEFT to TL as shortcut 169 | 170 | self.rect = lv.obj(parent) 171 | self.rect.remove_style_all() 172 | self.rect.set_size(35, 35) 173 | self.rect.align(self.lvalign, 0, 0) 174 | self.rect.add_style(RectStyle(bg_color = self.color), lv.PART.MAIN) 175 | self.rect.add_style(RectStyle(bg_color = lv.color_white()), lv.PART.MAIN | lv.STATE.PRESSED) 176 | self.rect.add_event_cb(lambda e: self._cb(), lv.EVENT.CLICKED, None) 177 | 178 | self.lbl = lv.label(self.rect) 179 | self.lbl.remove_style_all() 180 | self.lbl.set_text(self.text) 181 | self.lbl.center() 182 | 183 | 184 | def _cb(self): 185 | # Get touch coordinates 186 | point = lv.point_t() 187 | indev.get_point(point) 188 | 189 | status_lbl.set_text(f'{self.align.replace("_", " ")} obj clicked!\n(x: {point.x}, y: {point.y})') 190 | status_lbl.set_style_text_color(self.color, 0) 191 | 192 | class FlexRowStyle(lv.style_t): 193 | def __init__(self): 194 | super().__init__() 195 | 196 | self.set_text_align(lv.TEXT_ALIGN.CENTER) 197 | 198 | self.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP) 199 | self.set_flex_main_place(lv.FLEX_ALIGN.SPACE_EVENLY) 200 | self.set_layout(lv.LAYOUT.FLEX) 201 | 202 | 203 | # ============== Touch Indicator Setup ============== # 204 | class CircleStyle(lv.style_t): 205 | def __init__(self): 206 | super().__init__() 207 | 208 | self.set_bg_opa(lv.OPA._40) 209 | self.set_bg_color(lv.color_hex3(0x0F0)) 210 | self.set_radius(lv.RADIUS_CIRCLE) 211 | self.set_border_opa(lv.OPA._100) 212 | self.set_border_width(2) 213 | self.set_border_color(lv.color_hex3(0x0F0)) 214 | 215 | class TouchIndicator(): 216 | def __init__(self, position_x, position_y): 217 | 218 | _size = 10 219 | self.circle = lv.obj(lv.screen_active()) 220 | self.circle.remove_style_all() 221 | self.circle.set_size(_size, _size) 222 | self.circle.set_pos(int(position_x - _size / 2), int(position_y - _size / 2)) 223 | self.circle.add_style(CircleStyle(), lv.PART.MAIN) 224 | 225 | def delete(self): 226 | self.circle.delete() 227 | 228 | 229 | ti = None # Store TouchIndicator object between calls of touch_cb 230 | def touch_cb(e = None): 231 | global ti 232 | code = e.get_code() 233 | 234 | if code == lv.EVENT.CLICKED: 235 | if ti is not None: 236 | ti.delete() 237 | 238 | point = lv.point_t() 239 | indev.get_point(point) 240 | 241 | ti = TouchIndicator(point.x, point.y) 242 | else: 243 | pass 244 | 245 | if _DISPLAY_SHOW_TOUCH_INDICATOR: 246 | indev.add_event_cb(touch_cb, lv.EVENT.ALL, None) 247 | 248 | 249 | # ========== Start UI ========== # 250 | 251 | # Create a screen 252 | scr = lv.screen_active() 253 | scr.set_style_bg_color(lv.color_black(), lv.PART.MAIN) 254 | scr.remove_flag(lv.obj.FLAG.SCROLLABLE) 255 | 256 | # This label will display touch coordinates / clicked object 257 | status_lbl = lv.label(scr) 258 | status_lbl.set_text('Touch Test,\nClick Anywhere.') 259 | status_lbl.align(lv.ALIGN.CENTER, 0, -40) 260 | status_lbl.set_style_text_color(lv.color_white(), 0) 261 | status_lbl.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) 262 | 263 | # Container provides margin from display edge for the rects 264 | rect_container = lv.obj(scr) 265 | rect_container.remove_style_all() 266 | rect_container.set_size(lv.pct(100), lv.pct(100)) 267 | rect_container.align(lv.ALIGN.TOP_LEFT, 0, 0) 268 | rect_container.set_style_pad_all(10, 0) 269 | 270 | # Create rectangular objets as touch targets 271 | align = ['BOTTOM_LEFT', 'BOTTOM_MID', 'BOTTOM_RIGHT', 'LEFT_MID', 272 | 'RIGHT_MID', 'TOP_LEFT', 'TOP_MID', 'TOP_RIGHT'] 273 | 274 | colors = ['CYAN', 'DEEP_PURPLE', 'GREEN', 'ORANGE', 275 | 'PINK', 'LIME', 'RED', 'YELLOW'] 276 | 277 | for a, c in zip(align, colors): 278 | r = Rect(a, c, rect_container) 279 | 280 | # Container for color display test 281 | # It will display three rects in red, green and blue with labels 282 | color_container = lv.obj(rect_container) 283 | color_container.remove_style_all() 284 | color_container.set_size(132, 40) 285 | color_container.align(lv.ALIGN.CENTER, 0, 30) 286 | color_container.add_style(RectStyle(bg_color = lv.color_black()), lv.PART.MAIN) 287 | color_container.add_style(FlexRowStyle(), lv.PART.MAIN) 288 | 289 | for l, c in (zip('RGB', (0xF00, 0x0F0, 0x00F))): 290 | color_rect = lv.obj(color_container) 291 | color_rect.remove_style_all() 292 | color_rect.set_size(lv.pct(30), lv.pct(100)) 293 | color_rect.add_style(RectStyle(bg_color = lv.color_hex3(c)), lv.PART.MAIN) 294 | 295 | color_lbl = lv.label(color_rect) 296 | color_lbl.remove_style_all() 297 | color_lbl.set_text(l) 298 | color_lbl.center() 299 | color_lbl.set_style_text_color(lv.color_black(), 0) 300 | 301 | 302 | color_container_lbl = lv.label(rect_container) 303 | color_container_lbl.remove_style_all() 304 | color_container_lbl.set_text('Color Test.') 305 | color_container_lbl.align_to(color_container, lv.ALIGN.OUT_TOP_MID, 0, -5) 306 | color_container_lbl.set_style_text_color(lv.color_white(), 0) 307 | 308 | # Print LVGL version info and available fonts 309 | print(f'--- LVGL Version: {lv.version_major()}.{lv.version_minor()}. ---') 310 | print('Available LVGL fonts:') 311 | print(', '.join([f'lv.{m}' for m in dir(lv) if 'font_montserrat' in m or 'font_unscii' in m])) 312 | 313 | 314 | while True: 315 | time.sleep(1) -------------------------------------------------------------------------------- /demo_lvgl/demo_multi_screen.py: -------------------------------------------------------------------------------- 1 | import uasyncio as asyncio 2 | from machine import Pin, PWM, RTC, SoftSPI 3 | import time 4 | import lv_utils 5 | import lvgl as lv 6 | from xpt2046_cyd import xpt2046 7 | import ili9XXX 8 | import gc 9 | 10 | class Updater: 11 | def __init__(self, app): 12 | self.app = app 13 | self.rtc = RTC() 14 | self.idle_state = 0 15 | self.min_old = -1 16 | self.day_old = -1 17 | 18 | self.time = None 19 | self.date = None 20 | 21 | def update_time(self, now): 22 | self.min_old = now[6] 23 | self.time = '{:02d}:{:02d}:{:02d}'.format(now[4], now[5], now[6]) 24 | self.app.update_ui('Time', self.time) 25 | 26 | def update_date(self, now): 27 | self.day_old = now[2] 28 | self.date = '{:02d}.{:02d}.{:04d}'.format(now[2], now[1], now[0]) 29 | self.app.update_ui('Date', self.date) 30 | 31 | async def update(self): 32 | try: 33 | while True: 34 | now = self.rtc.datetime() 35 | 36 | # Update time every minute 37 | if self.min_old != now[6]: 38 | self.update_time(now) 39 | 40 | # Update date every day 41 | if self.day_old != now[2]: 42 | self.update_date(now) 43 | 44 | # Power off display when idle for 60s 45 | idle_time = lv.disp_get_default().get_inactive_time() 46 | if idle_time > (60 * 1000) and self.idle_state == 0: 47 | self.idle_state = 1 48 | self.app.backlight.duty(0) 49 | elif idle_time < (2 * 1000) and self.idle_state == 1: 50 | self.idle_state = 0 51 | self.app.backlight.duty(1023) 52 | 53 | # Main loop delay 54 | await asyncio.sleep(1) 55 | except asyncio.CancelledError: 56 | print('Updater task cancelled.') 57 | 58 | 59 | class App(Updater): 60 | screens = {} 61 | screen_order = [] 62 | 63 | @classmethod 64 | def register_screen(cls, order): 65 | '''Decorator function to register a screen with a navigation order.''' 66 | def decorator(screen_cls): 67 | cls.screens[screen_cls.__name__] = screen_cls 68 | cls.screen_order.append((order, screen_cls.__name__)) 69 | cls.screen_order.sort(key=lambda x: x[0]) 70 | 71 | original_init = screen_cls.__init__ 72 | 73 | def new_init(self, *args, **kwargs): 74 | original_init(self, *args, **kwargs) 75 | self.set_style_bg_color(lv.color_white(), lv.PART.MAIN) 76 | app = args[0] 77 | 78 | screen_names = [screen[1] for screen in cls.screen_order] 79 | current_idx = screen_names.index(screen_cls.__name__) 80 | 81 | # Add Next button if not the last screen 82 | if current_idx < len(screen_names) - 1: 83 | next_screen = screen_names[current_idx + 1] 84 | next_btn = lv.btn(self) 85 | next_btn.set_size(30, 30) 86 | next_btn.align(lv.ALIGN.BOTTOM_RIGHT, -5, -5) 87 | next_btn_lbl = lv.label(next_btn) 88 | next_btn_lbl.set_text(lv.SYMBOL.RIGHT) 89 | next_btn_lbl.center() 90 | next_btn.add_event_cb(lambda e: app.load_screen(next_screen), lv.EVENT.CLICKED, None) 91 | 92 | # Add Previous button if not the first screen 93 | if current_idx > 0: 94 | prev_screen = screen_names[current_idx - 1] 95 | prev_btn = lv.btn(self) 96 | prev_btn.set_size(30, 30) 97 | prev_btn.align(lv.ALIGN.BOTTOM_LEFT, 5, -5) 98 | prev_btn_lbl = lv.label(prev_btn) 99 | prev_btn_lbl.set_text(lv.SYMBOL.LEFT) 100 | prev_btn_lbl.center() 101 | prev_btn.add_event_cb(lambda e: app.load_screen(prev_screen), lv.EVENT.CLICKED, None) 102 | 103 | # Replace the original __init__ with the new one 104 | screen_cls.__init__ = new_init 105 | 106 | return screen_cls 107 | return decorator 108 | 109 | def __init__(self): 110 | super().__init__(self) 111 | self.act_screen = None 112 | self.prev_screen = None 113 | self.act_screen_id = None 114 | 115 | try: 116 | self.disp = ili9XXX.ili9341(clk=14, cs=15, 117 | dc=2, rst=12, power=23, miso=12, 118 | mosi=13, width=320, height=240, 119 | rot=0xC0, colormode=ili9XXX.COLOR_MODE_RGB, 120 | double_buffer=False, factor=16, asynchronous=True) 121 | except Exception as e: 122 | print(f'Display initialization failed: {e}') 123 | 124 | try: 125 | self.spiTouch = SoftSPI(baudrate=2500000, sck=Pin(25), 126 | mosi=Pin(32), miso=Pin(39)) 127 | self.touch = xpt2046(spi=self.spiTouch, cs=Pin(33), cal_x0=3700, 128 | cal_y0=3820, cal_x1=180, cal_y1=250, transpose=False) 129 | except Exception as e: 130 | print(f'Touch initialization failed: {e}') 131 | 132 | try: 133 | self.backlight = PWM(Pin(21), freq=1000) 134 | self.backlight.duty(1023) 135 | except Exception as e: 136 | print(f'Backlight initialization failed: {e}') 137 | 138 | self.group = lv.group_create() 139 | self.group.set_default() 140 | 141 | # Load the initial screen 142 | self.load_screen('ScreenMain') 143 | 144 | def update_ui(self, element_id, text): 145 | elements = {'Time': 'timeLbl', 146 | 'Date': 'dateLbl'} 147 | 148 | if not (element_id in elements): 149 | print(f'Label {element_id} is not registered for update.') 150 | return False 151 | 152 | try: 153 | attr = getattr(self.act_screen, elements[element_id]) 154 | except AttributeError: 155 | attr = None 156 | finally: 157 | if attr is not None: 158 | attr.set_text(str(text)) 159 | 160 | def load_screen(self, scr_id): 161 | #Loads a screen by its class name provided as string 162 | gc.collect() 163 | print(f'Loading screen {scr_id}\nFree memory: {gc.mem_free()}') 164 | 165 | if scr_id not in self.screens: 166 | print('Unknown screen!') 167 | return False 168 | 169 | if self.prev_screen is not None: 170 | self.prev_screen.del_async() 171 | gc.collect() 172 | 173 | if self.act_screen is not None: 174 | self.prev_screen = self.act_screen 175 | 176 | try: 177 | # Get the screen class from the scr_id and instantiate it 178 | self.act_screen = self.screens[scr_id](self) 179 | self.act_screen_id = scr_id 180 | lv.scr_load(self.act_screen) 181 | gc.collect() 182 | except Exception as e: 183 | print(f'Error loading screen {scr_id}: {e}') 184 | gc.collect() 185 | 186 | 187 | 188 | # Screens defined and registered via the @App.register_screen decorator 189 | @App.register_screen(1) 190 | class ScreenMain(lv.obj): 191 | def __init__(self, app, *args, **kwds): 192 | self.app = app 193 | super().__init__(*args, **kwds) 194 | 195 | self.titleLbl = lv.label(self) 196 | self.titleLbl.set_style_text_font(lv.font_montserrat_16, 0) 197 | self.titleLbl.set_style_text_color(lv.color_black(), 0) 198 | self.titleLbl.set_text('Main Screen') 199 | self.titleLbl.center() 200 | 201 | self.timeLbl = lv.label(self) 202 | t = '-' if self.app.time is None else self.app.time 203 | self.timeLbl.set_text(t) 204 | self.timeLbl.align(lv.ALIGN.TOP_LEFT, 255, 3) 205 | 206 | self.dateLbl = lv.label(self) 207 | d = '-' if self.app.date is None else self.app.date 208 | self.dateLbl.set_text(d) 209 | self.dateLbl.align(lv.ALIGN.TOP_LEFT, 5, 3) 210 | 211 | 212 | @App.register_screen(2) 213 | class ScreenChart(lv.obj): 214 | def __init__(self, app, *args, **kwds): 215 | self.app = app 216 | super().__init__(*args, **kwds) 217 | 218 | file_path = 'log/counter_log.csv' 219 | x_data, y_data = read_csv(file_path) 220 | 221 | 222 | chart = lv.chart(self) 223 | chart.set_size(275, 150) 224 | chart.align(lv.ALIGN.TOP_LEFT, 35, 30) 225 | 226 | chart.set_style_line_width(0, lv.PART.ITEMS) # Remove the lines 227 | chart.set_style_line_width(0, lv.PART.MAIN) 228 | chart.set_type(lv.chart.TYPE.SCATTER) 229 | 230 | #lv_chart_set_axis_tick(chart, axis, major_len, minor_len, major_cnt, minor_cnt, label_en, draw_size) 231 | chart.set_axis_tick(lv.chart.AXIS.PRIMARY_X, 5, 2, 7, 5, True, 30) 232 | chart.set_axis_tick(lv.chart.AXIS.PRIMARY_Y, 10, 5, 4, 5, True, 30) 233 | 234 | chart.set_range(lv.chart.AXIS.PRIMARY_X, x_data[0], x_data[0] + 29) 235 | chart.set_range(lv.chart.AXIS.PRIMARY_Y, 1, 30) 236 | 237 | chart.set_point_count(len(x_data)) 238 | 239 | ser = chart.add_series(lv.palette_main(lv.PALETTE.RED), lv.chart.AXIS.PRIMARY_Y) 240 | 241 | ser.x_points = x_data 242 | ser.y_points = y_data 243 | 244 | yLbl = lv.label(self) 245 | yLbl.set_text('d / km') 246 | yLbl.align_to(chart, lv.ALIGN.OUT_TOP_LEFT, -30, 0) 247 | 248 | xLbl = lv.label(self) 249 | xLbl.set_text('t / days') 250 | xLbl.align_to(chart, lv.ALIGN.OUT_BOTTOM_MID, 0, 25) 251 | print(f'Chart loaded \nFree memory: {gc.mem_free()}') 252 | 253 | 254 | 255 | @App.register_screen(3) 256 | class Screen3(lv.obj): 257 | def __init__(self, app, *args, **kwds): 258 | self.app = app 259 | super().__init__(*args, **kwds) 260 | 261 | self.title = lv.label(self) 262 | self.title.set_style_text_font(lv.font_montserrat_16, 0) 263 | self.title.set_style_text_color(lv.color_black(), 0) 264 | self.title.set_text('Screen 3') 265 | self.title.center() 266 | 267 | 268 | def calculate_distance(meters): 269 | distance = meters / 1000 270 | return distance 271 | 272 | def parse_timestamp(timestamp_str): 273 | date_str = timestamp_str.split(' ')[0] 274 | year, month, day = [int(x) for x in date_str.split('-')] 275 | return year, month, day 276 | 277 | 278 | '''Read data from .csv-file and parse data for display in chart 279 | 280 | - counter_log.csv contains a timestamp and a measured distance in meters 281 | - the distance is converted to km 282 | - the timestamps are compared and the days elapsed since the first 283 | timestamp are shown in the diagram 284 | ''' 285 | def read_csv(file_path): 286 | x_values = [] 287 | y_values = [] 288 | max_values = 30 289 | 290 | try: 291 | with open(file_path, 'r') as file: 292 | lines = file.readlines() 293 | 294 | first_line = lines[0].strip().split(',') 295 | first_timestamp_str = first_line[0] 296 | 297 | first_year, first_month, first_day = parse_timestamp(first_timestamp_str) 298 | first_date = time.mktime((first_year, first_month, first_day, 0, 0, 0, 0, 0)) 299 | 300 | for line in lines: 301 | parts = line.strip().split(',') 302 | if len(parts) == 2: 303 | timestamp_str = parts[0] 304 | distance_str = parts[1] 305 | 306 | year, month, day = parse_timestamp(timestamp_str) 307 | date = time.mktime((year, month, day, 0, 0, 0, 0, 0)) 308 | 309 | days_since_first = int((date - first_date) / (24 * 3600)) + 1 310 | 311 | # Calculate distance in km and round it 312 | distance = int(round(calculate_distance(int(distance_str)))) 313 | 314 | x_values.append(days_since_first) 315 | y_values.append(distance) 316 | 317 | 318 | if len(x_values) > max_values: 319 | x_values = x_values[-max_values:] 320 | y_values = y_values[-max_values:] 321 | 322 | return x_values, y_values 323 | except Exception as e: 324 | print('Error loading file: ', e) 325 | 326 | 327 | async def main(): 328 | app = App() 329 | gc.collect() 330 | 331 | update_task = asyncio.create_task(app.update()) 332 | 333 | if not lv_utils.event_loop.is_running(): 334 | eventloop = lv_utils.event_loop(asynchronous=True) 335 | 336 | loop = asyncio.get_event_loop() 337 | loop.run_forever() 338 | 339 | asyncio.run(main()) 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Cheap Yellow Display and LVGL 2 | 3 | The family of Esp32-S2432028Rs or Cheap Yellow Displays (CYDs) comprises of various boards with similar hardware configuration including 4 | 5 | - an Esp32- WROOM 6 | - one or two USB ports 7 | - an ILI9341 2.8' (320 x 240, RGB565) display 8 | - a xpt2046 resistive touch interface 9 | - sdcard adapter, I2S interface, RGB Led and a photoresistor (LDR) 10 | - some GPIO pins / I2C interface 11 | 12 | This makes the CYDs ideal candidates for the development of small GUI projects using LVGL and MicroPython. 13 | 14 | An integrated Esp32S3 display module with more power is the [JC3248W535 aka Cheap Black Display (CBD)](https://github.com/de-dh/ESP32-JC3248W535-Micropython-LVGL/tree/main). 15 | It has onboard PSRAM which supports more complex LVGL programs. 16 | 17 | 18 | 19 | 20 | When it comes to development of GUIs which allow user Input via touch, MPY's primitive draw functions reach their limit pretty fast. It might be possible in theory to make some nice looking GUIs with MPYs primitive draw functions, but the required work would be enormous. This is where LVGL comes into play: 21 | 22 | LVGL enables the development of professionally looking GUIs which accept user input with reasonable effort. LVGL offers predefined widgets like labels, buttons, lists, textareas etc. All objects are styled using css-like style properties, e. g. text-color, background-color, shadow, padding. Objects can be aligned relative to each other and complex layouts can be designed using flexbox and grid like positioning. Even animations are supported. 23 | 24 | The major drawback of LVGL is that it requires a custom MPY firmware build and setting up the cofiguration for a specific touch / display combination can be tricky. [Kdschlosser's Micropython Bindings](https://github.com/lvgl-micropython/lvgl_micropython) for MPY aims to make the compilation of the firmware as easy as possible. I used this binding to compile the firmware for the CYD which is provided for download in this repositry. 25 | 26 | ## LVGL8 - Deprecated Documentation and Examples 27 | 28 | The old [LVGL8 documentation and examples](LVGL8.md) can be found here. 29 | It is not maintained and the programs are incompatible with LVGL9. 30 | 31 | ## LVGL9 32 | 33 | ### Precompiled Firmware 34 | 35 | The `/lvgl9_firmware` folder contains a prebuilt firmware for the Cheap Yellow Display (CYD) using LVGL 9.3 and MicroPython 1.25.0. 36 | 37 | 38 | The firmware was compiled from commit [15a414b](https://github.com/lvgl-micropython/lvgl_micropython/commit/15a414bc03486017235234882ce7415532c6325e) from [Kdschlosser's Micropython Bindings](https://github.com/lvgl-micropython/lvgl_micropython). All drivers for the CYD are included in the firmware, no additional drivers are needed. 39 | 40 | 41 | The firmware had to be compiled from a previous version of the bindings since the current version has a bug which puts the CYD in a boot loop. 42 | The firmware includes the touch fix kdschlosser/lvgl_micropython#454 for correct touch calibration. 43 | The previous version of the precompiled firmware required a modified touch driver due to a bug in the calibration routine. This is obsolete now. 44 | 45 | 46 | The following command is used to flash the firmware (esptool required): 47 | 48 | ```bash 49 | python -m esptool --chip esp32 --port COMXX -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 4MB --flash_freq 40m --erase-all 0x0 lvgl_micropython_cyd.bin 50 | ``` 51 | 52 | 53 |
54 | 55 | Included LVGL Fonts 56 | 57 | 58 | ```bash 59 | lv.font_montserrat_10, lv.font_montserrat_12, lv.font_montserrat_14, lv.font_montserrat_16, lv.font_montserrat_24, lv.font_montserrat_32, lv.font_montserrat_40, lv.font_montserrat_48, lv.font_unscii_16, lv.font_unscii_8 60 | ``` 61 | 62 |
63 | 64 | 65 | 66 | 67 | ### Finding the correct display settings 68 | 69 | Although the different versions of CYDs all look alike, they require varying parameters for display and touchscreen initialization. 70 | The file `/lvgl9_firmware/color_test.py` can be used to find the correct display driver's rotation and color settings. 71 | The figure below shows how the program should be displayed (the colors are more intense in reality). 72 | All neccessary settings can be customized at the top of the file. 73 | 74 | 75 | 76 | ```python 77 | # ============== Customize settings ============== # 78 | # The following values need to be customized. 79 | 80 | # Switch width and height for portrait mode. 81 | _DISPLAY_WIDTH = const(320) 82 | _DISPLAY_HEIGHT = const(240) 83 | # Try different values from rotation table, see below. 84 | _DISPLAY_ROT = const(0xE0) 85 | # Set to True if red and blue are switched. 86 | _DISPLAY_BGR = const(1) 87 | # May have to be set to 0 if both RGB / BGR mode give bad results. 88 | _DISPLAY_RGB565_BYTE_SWAP = const(1) 89 | # Allow touch calibration. Set to True when display works correctly. 90 | _ALLOW_TOUCH_CAL = const(0) 91 | # Show marker at current touch coordinates. 92 | _DISPLAY_SHOW_TOUCH_INDICATOR = const(1) 93 | ``` 94 | 95 | 96 | 97 | The following steps have to be followed to correctly set up the CYD for LVGL9. 98 | 99 | A **hard reset is required after every execution of the program** since the hardware might not work correctly otherwise. 100 | 101 | 102 | 103 | 1. Depending on the displays orientation the **display's width and height** might need to be edited before running the file. 104 | For use in landscape mode, `_DISPLAY_WIDTH = 240` and `_DISPLAY_HEIGHT = 320` have to be used. Switch the values for use in portrait mode. 105 | 2. Start the program now. If the displayed content is distorted the correct **display rotation** `_DISPLAY_ROT` needs to be found. 106 | The following `MADCTL` values for rotation need to be tested by try and error. 107 | 108 | ``` 109 | # Part from rdagger's micropython ili9341 driver provided under MIT license. 110 | # https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py 111 | 112 | MADCTL_TABLE = { 113 | (False, 0): 0x80, # mirroring = False 114 | (False, 90): 0xE0, 115 | (False, 180): 0x40, 116 | (False, 270): 0x20, 117 | (True, 0): 0xC0, # mirroring = True 118 | (True, 90): 0x60, 119 | (True, 180): 0x00, 120 | (True, 270): 0xA0 121 | } 122 | ``` 123 | 3. Next, the correct **colormode** has to be found 124 | E.g. if the red square is rendered in blue then BGR mode must be used by setting `_DISPLAY_BGR = const(1)`. If RGB mode is required set `_DISPLAY_BGR = const(0)`. 125 | 4. The last step is the **calibration of the touchscreen**. Set `_ALLOW_TOUCH_CAL = const(1)` and follow the instructions on screen. 126 | The calibration data is stored in the non volatile storage (NVS) of the Esp32 so calibration has to be performed only once. 127 | The program detects automatically if calibration data is available. 128 | 129 | 130 | 131 | ## LVGL Tips 132 | 133 | ### Examples from the LVGL 8.3 Documentation 134 | 135 | The [Documentation for LVGL 8.3](https://docs.lvgl.io/8.3/examples.html) contains Micropython examples for most widgets which are missing in other versions of the documentation. 136 | Most examples can be used with LVGL9 with small modifications. 137 | 138 | ### LVGL9 Commands 139 | 140 | Some commands changed during the upgrade from LVGL8 to LVGL9. E. g., `lv.btn()` becomes `lv.button()`. 141 | The `dir()` method can be used to inspect the attributes of the `lv` class, `lv.obj` class, widget classes etc. to find correct commands. 142 | To inspect the `lv.obj` class and print it's attributes to the shell, the following command can be used: 143 | 144 | ```python 145 | print(', '.join([m for m in dir(lv.obj) if not m.startswith('__')])) 146 | ``` 147 | 148 | The list can be copied to a text program and commands can be found using search function. 149 | 150 |
151 | 152 | Attributes of `lv` 153 | 154 | 155 | ```python 156 | ['list', 'map', 'pow', 'ALIGN', 'ANIM', 'ANIM_IMAGE_PART', 'ANIM_PLAYTIME_INFINITE', 'ANIM_REPEAT_INFINITE', 'BASE_DIR', 'BLEND_MODE', 'BORDER_SIDE', 'BUTTONMATRIX_BUTTON_NONE', 'CACHE_RESERVE_COND', 'CHART_POINT_NONE', 'COLOR_DEPTH', 'COLOR_FORMAT', 'COORD', 'COVER_RES', 'C_Pointer', 'DIR', 'DISPLAY_RENDER_MODE', 'DISPLAY_ROTATION', 'DPI_DEF', 'DRAW_BUF_ALIGN', 'DRAW_BUF_STRIDE_ALIGN', 'DRAW_SW_MASK_LINE_SIDE', 'DRAW_SW_MASK_RES', 'DRAW_SW_MASK_TYPE', 'DRAW_TASK_STATE', 'DRAW_TASK_TYPE', 'DROPDOWN_POS_LAST', 'EVENT', 'FLEX_ALIGN', 'FLEX_FLOW', 'FONT_FMT_TXT', 'FONT_FMT_TXT_CMAP', 'FONT_GLYPH_FORMAT', 'FONT_KERNING', 'FONT_SUBPX', 'FS_MODE', 'FS_RES', 'FS_SEEK', 'GRAD_DIR', 'GRIDNAV_CTRL', 'GRID_ALIGN', 'GRID_CONTENT', 'GRID_TEMPLATE_LAST', 'GROUP_REFOCUS_POLICY', 'IMAGE_HEADER_MAGIC', 'INDEV_MODE', 'INDEV_STATE', 'INDEV_TYPE', 'KEY', 'LABEL_DOT_NUM', 'LABEL_POS_LAST', 'LABEL_TEXT_SELECTION_OFF', 'LAYER_TYPE', 'LAYOUT', 'LOG_LEVEL', 'LvReferenceError', 'OPA', 'PALETTE', 'PART', 'PART_TEXTAREA', 'RADIUS_CIRCLE', 'RB_COLOR', 'RESULT', 'SCALE_LABEL_ENABLED_DEFAULT', 'SCALE_MAJOR_TICK_EVERY_DEFAULT', 'SCALE_NONE', 'SCALE_TOTAL_TICK_COUNT_DEFAULT', 'SCROLLBAR_MODE', 'SCROLL_SNAP', 'SCR_LOAD_ANIM', 'SIZE_CONTENT', 'SPAN_MODE', 'SPAN_OVERFLOW', 'STATE', 'STRIDE_AUTO', 'STYLE', 'STYLE_RES', 'SUBJECT_TYPE', 'SYMBOL', 'TABLE_CELL_NONE', 'TEXTAREA_CURSOR_LAST', 'TEXT_ALIGN', 'TEXT_DECOR', 'TEXT_FLAG', 'THREAD_PRIO', '_lv_draw_sw_mask_common_dsc_t', '_lv_draw_sw_mask_radius_circle_dsc_t', '_lv_mp_int_wrapper', '_nesting', 'anim_bezier3_para_t', 'anim_count_running', 'anim_delete', 'anim_delete_all', 'anim_get', 'anim_get_timer', 'anim_parameter_t', 'anim_refr_now', 'anim_speed', 'anim_speed_clamped', 'anim_state_t', 'anim_t', 'anim_timeline_create', 'anim_timeline_t', 'animimg', 'animimg_class', 'arc', 'arc_class', 'area_t', 'array_t', 'async_call', 'async_call_cancel', 'atan2', 'bar', 'bar_class', 'barcode', 'barcode_class', 'bezier3', 'bidi_calculate_align', 'bin_decoder_close', 'bin_decoder_get_area', 'bin_decoder_info', 'bin_decoder_init', 'bin_decoder_open', 'binfont_create', 'binfont_destroy', 'bmp_deinit', 'bmp_init', 'button', 'button_class', 'buttonmatrix', 'buttonmatrix_class', 'cache_class_lru_rb_count', 'cache_class_lru_rb_size', 'cache_class_t', 'cache_entry_alloc', 'cache_entry_get_entry', 'cache_entry_get_size', 'cache_entry_t', 'cache_ops_t', 'cache_t', 'calendar', 'calendar_class', 'calendar_date_t', 'calendar_header_arrow', 'calendar_header_arrow_class', 'calendar_header_dropdown', 'calendar_header_dropdown_class', 'canvas', 'canvas_class', 'chart', 'chart_class', 'chart_cursor_t', 'chart_series_t', 'checkbox', 'checkbox_class', 'clamp_height', 'clamp_width', 'color16_t', 'color32_make', 'color32_t', 'color_16_16_mix', 'color_black', 'color_filter_dsc_t', 'color_filter_shade', 'color_format_get_bpp', 'color_format_get_size', 'color_format_has_alpha', 'color_hex', 'color_hex3', 'color_hsv_t', 'color_hsv_to_rgb', 'color_make', 'color_rgb_to_hsv', 'color_t', 'color_white', 'cubic_bezier', 'deinit', 'delay_ms', 'delay_set_cb', 'display_create', 'display_get_default', 'display_t', 'dpx', 'draw_add_task', 'draw_arc', 'draw_arc_dsc_t', 'draw_arc_get_area', 'draw_border_dsc_t', 'draw_box_shadow_dsc_t', 'draw_buf_align', 'draw_buf_create', 'draw_buf_get_handlers', 'draw_buf_handlers_t', 'draw_buf_t', 'draw_buf_width_to_stride', 'draw_character', 'draw_create_unit', 'draw_deinit', 'draw_dispatch', 'draw_dispatch_request', 'draw_dispatch_wait_for_request', 'draw_dsc_base_t', 'draw_fill_dsc_t', 'draw_finalize_task_creation', 'draw_get_next_available_task', 'draw_global_info_t', 'draw_glyph_dsc_t', 'draw_image', 'draw_image_dsc_t', 'draw_image_sup_t', 'draw_init', 'draw_label', 'draw_label_dsc_t', 'draw_label_hint_t', 'draw_layer', 'draw_layer_alloc_buf', 'draw_layer_create', 'draw_layer_go_to_xy', 'draw_line', 'draw_line_dsc_t', 'draw_mask_rect', 'draw_mask_rect_dsc_t', 'draw_rect', 'draw_rect_dsc_t', 'draw_sw_blend_dsc_t', 'draw_sw_deinit', 'draw_sw_init', 'draw_sw_mask_angle_param_cfg_t', 'draw_sw_mask_angle_param_t', 'draw_sw_mask_apply', 'draw_sw_mask_deinit', 'draw_sw_mask_fade_param_cfg_t', 'draw_sw_mask_fade_param_t', 'draw_sw_mask_free_param', 'draw_sw_mask_init', 'draw_sw_mask_line_param_cfg_t', 'draw_sw_mask_line_param_t', 'draw_sw_mask_map_param_cfg_t', 'draw_sw_mask_map_param_t', 'draw_sw_mask_radius_param_cfg_t', 'draw_sw_mask_radius_param_t', 'draw_sw_rgb565_swap', 'draw_sw_rotate', 'draw_task_t', 'draw_triangle', 'draw_triangle_dsc_t', 'draw_unit_t', 'dropdown', 'dropdown_class', 'dropdownlist_class', 'event_dsc_t', 'event_list_t', 'event_register_id', 'event_t', 'flex_init', 'font_default', 'font_glyph_dsc_t', 'font_montserrat_12', 'font_montserrat_14', 'font_montserrat_16', 'font_t', 'free', 'free_core', 'fs_dir_t', 'fs_drv_t', 'fs_file_cache_t', 'fs_file_t', 'fs_get_drv', 'fs_get_ext', 'fs_get_last', 'fs_get_letters', 'fs_is_ready', 'fs_path_ex_t', 'fs_up', 'gd_GCE', 'gd_GIF', 'gd_Palette', 'gd_open_gif_data', 'gd_open_gif_file', 'gif', 'gif_class', 'grad_dsc_t', 'grad_t', 'gradient_stop_t', 'grid_fr', 'grid_init', 'gridnav_add', 'gridnav_remove', 'gridnav_set_focused', 'group_by_index', 'group_create', 'group_focus_obj', 'group_get_count', 'group_get_default', 'group_remove_obj', 'group_swap_obj', 'group_t', 'hit_test_info_t', 'image', 'image_class', 'image_decoder_args_t', 'image_decoder_dsc_t', 'image_decoder_t', 'image_dsc_t', 'image_header_t', 'imagebutton', 'imagebutton_class', 'ime_pinyin', 'ime_pinyin_class', 'imgfont_create', 'imgfont_destroy', 'indev_active', 'indev_create', 'indev_data_t', 'indev_get_active_obj', 'indev_keypad_t', 'indev_pointer_t', 'indev_read_timer_cb', 'indev_search_obj', 'indev_t', 'init', 'is_initialized', 'keyboard', 'keyboard_class', 'label', 'label_class', 'layer_bottom', 'layer_sys', 'layer_t', 'layer_top', 'layout_dsc_t', 'layout_register', 'led', 'led_class', 'line', 'line_class', 'list_button_class', 'list_class', 'list_text_class', 'll_t', 'lodepng_deinit', 'lodepng_init', 'malloc', 'malloc_core', 'malloc_zeroed', 'mem_add_pool', 'mem_deinit', 'mem_init', 'mem_monitor_t', 'mem_remove_pool', 'mem_test', 'mem_test_core', 'memcpy', 'memmove', 'memset', 'memzero', 'menu', 'menu_class', 'menu_cont', 'menu_cont_class', 'menu_main_cont_class', 'menu_main_header_cont_class', 'menu_page', 'menu_page_class', 'menu_section', 'menu_section_class', 'menu_separator', 'menu_separator_class', 'menu_sidebar_cont_class', 'menu_sidebar_header_cont_class', 'mp_lv_init_gc', 'msgbox', 'msgbox_backdrop_class', 'msgbox_class', 'msgbox_content_class', 'msgbox_footer_button_class', 'msgbox_footer_class', 'msgbox_header_button_class', 'msgbox_header_class', 'mutex_delete', 'mutex_init', 'mutex_lock', 'mutex_lock_isr', 'mutex_unlock', 'obj', 'obj_class', 'obj_class_t', 'objid_builtin_destroy', 'observer_t', 'palette_darken', 'palette_lighten', 'palette_main', 'pct', 'pct_to_px', 'pinyin_dict_t', 'point_precise_t', 'point_t', 'qrcode', 'qrcode_class', 'rand', 'rand_set_seed', 'rb_node_t', 'rb_t', 'realloc', 'realloc_core', 'refr_now', 'roller', 'roller_class', 'scale', 'scale_class', 'scale_section_t', 'screen_active', 'screen_load', 'screen_load_anim', 'slider', 'slider_class', 'snapshot_create_draw_buf', 'snapshot_free', 'snapshot_reshape_draw_buf', 'snapshot_take', 'snapshot_take_to_buf', 'snapshot_take_to_draw_buf', 'span_stack_deinit', 'span_stack_init', 'span_t', 'spangroup', 'spangroup_class', 'spinbox', 'spinbox_class', 'spinner', 'spinner_class', 'sqrt', 'sqrt_res_t', 'strcmp', 'strcpy', 'strdup', 'strlen', 'strncpy', 'style_const_prop_id_inv', 'style_get_num_custom_props', 'style_prop_get_default', 'style_prop_has_flag', 'style_register_prop', 'style_t', 'style_transition_dsc_t', 'style_value_t', 'subject_t', 'subject_value_t', 'switch', 'switch_class', 'table', 'table_class', 'tabview', 'tabview_class', 'task_handler', 'text_get_size', 'text_get_width', 'textarea', 'textarea_class', 'theme_apply', 'theme_default_deinit', 'theme_default_get', 'theme_default_init', 'theme_default_is_inited', 'theme_get_color_primary', 'theme_get_color_secondary', 'theme_get_font_large', 'theme_get_font_normal', 'theme_get_font_small', 'theme_get_from_obj', 'theme_simple_deinit', 'theme_simple_get', 'theme_simple_init', 'theme_simple_is_inited', 'theme_t', 'thread_delete', 'thread_init', 'thread_sync_delete', 'thread_sync_init', 'thread_sync_signal', 'thread_sync_wait', 'tick_elaps', 'tick_get', 'tick_inc', 'tick_set_cb', 'tick_state_t', 'tileview', 'tileview_class', 'tileview_tile_class', 'timer_create', 'timer_create_basic', 'timer_enable', 'timer_get_idle', 'timer_get_time_until_next', 'timer_handler', 'timer_handler_run_in_period', 'timer_handler_set_resume_cb', 'timer_periodic_handler', 'timer_state_t', 'timer_t', 'tjpgd_deinit', 'tjpgd_init', 'trigo_cos', 'trigo_sin', 'version_info', 'version_major', 'version_minor', 'version_patch', 'win', 'win_class'] 157 | ``` 158 |
159 | 160 | 161 | 162 |
163 | 164 | Attributes of `lv.obj` 165 | 166 | 167 | ```python 168 | ['CLASS_EDITABLE', 'CLASS_GROUP_DEF', 'CLASS_THEME_INHERITABLE', 'FLAG', 'POINT_TRANSFORM_FLAG', 'TREE_WALK', 'add_event_cb', 'add_flag', 'add_state', 'add_style', 'align', 'align_to', 'allocate_spec_attr', 'area_is_visible', 'assign_id', 'bind_flag_if_eq', 'bind_flag_if_not_eq', 'bind_state_if_eq', 'bind_state_if_not_eq', 'calculate_ext_draw_size', 'calculate_style_text_align', 'center', 'check_type', 'class_create_obj', 'class_init_obj', 'clean', 'delete', 'delete_anim_completed_cb', 'delete_async', 'delete_delayed', 'dump_tree', 'enable_style_refresh', 'event_base', 'fade_in', 'fade_out', 'free_id', 'get_child', 'get_child_by_type', 'get_child_count', 'get_child_count_by_type', 'get_class', 'get_click_area', 'get_content_coords', 'get_content_height', 'get_content_width', 'get_coords', 'get_display', 'get_event_count', 'get_event_dsc', 'get_group', 'get_height', 'get_index', 'get_index_by_type', 'get_local_style_prop', 'get_parent', 'get_screen', 'get_scroll_bottom', 'get_scroll_dir', 'get_scroll_end', 'get_scroll_left', 'get_scroll_right', 'get_scroll_snap_x', 'get_scroll_snap_y', 'get_scroll_top', 'get_scroll_x', 'get_scroll_y', 'get_scrollbar_area', 'get_scrollbar_mode', 'get_self_height', 'get_self_width', 'get_sibling', 'get_sibling_by_type', 'get_state', 'get_style_align', 'get_style_anim', 'get_style_anim_duration', 'get_style_arc_color', 'get_style_arc_color_filtered', 'get_style_arc_image_src', 'get_style_arc_opa', 'get_style_arc_rounded', 'get_style_arc_width', 'get_style_base_dir', 'get_style_bg_color', 'get_style_bg_color_filtered', 'get_style_bg_grad', 'get_style_bg_grad_color', 'get_style_bg_grad_color_filtered', 'get_style_bg_grad_dir', 'get_style_bg_grad_opa', 'get_style_bg_grad_stop', 'get_style_bg_image_opa', 'get_style_bg_image_recolor', 'get_style_bg_image_recolor_filtered', 'get_style_bg_image_recolor_opa', 'get_style_bg_image_src', 'get_style_bg_image_tiled', 'get_style_bg_main_opa', 'get_style_bg_main_stop', 'get_style_bg_opa', 'get_style_bitmap_mask_src', 'get_style_blend_mode', 'get_style_border_color', 'get_style_border_color_filtered', 'get_style_border_opa', 'get_style_border_post', 'get_style_border_side', 'get_style_border_width', 'get_style_clip_corner', 'get_style_color_filter_dsc', 'get_style_color_filter_opa', 'get_style_flex_cross_place', 'get_style_flex_flow', 'get_style_flex_grow', 'get_style_flex_main_place', 'get_style_flex_track_place', 'get_style_grid_cell_column_pos', 'get_style_grid_cell_column_span', 'get_style_grid_cell_row_pos', 'get_style_grid_cell_row_span', 'get_style_grid_cell_x_align', 'get_style_grid_cell_y_align', 'get_style_grid_column_align', 'get_style_grid_column_dsc_array', 'get_style_grid_row_align', 'get_style_grid_row_dsc_array', 'get_style_height', 'get_style_image_opa', 'get_style_image_recolor', 'get_style_image_recolor_filtered', 'get_style_image_recolor_opa', 'get_style_layout', 'get_style_length', 'get_style_line_color', 'get_style_line_color_filtered', 'get_style_line_dash_gap', 'get_style_line_dash_width', 'get_style_line_opa', 'get_style_line_rounded', 'get_style_line_width', 'get_style_margin_bottom', 'get_style_margin_left', 'get_style_margin_right', 'get_style_margin_top', 'get_style_max_height', 'get_style_max_width', 'get_style_min_height', 'get_style_min_width', 'get_style_opa', 'get_style_opa_layered', 'get_style_opa_recursive', 'get_style_outline_color', 'get_style_outline_color_filtered', 'get_style_outline_opa', 'get_style_outline_pad', 'get_style_outline_width', 'get_style_pad_bottom', 'get_style_pad_column', 'get_style_pad_left', 'get_style_pad_right', 'get_style_pad_row', 'get_style_pad_top', 'get_style_prop', 'get_style_radius', 'get_style_rotary_sensitivity', 'get_style_shadow_color', 'get_style_shadow_color_filtered', 'get_style_shadow_offset_x', 'get_style_shadow_offset_y', 'get_style_shadow_opa', 'get_style_shadow_spread', 'get_style_shadow_width', 'get_style_space_bottom', 'get_style_space_left', 'get_style_space_right', 'get_style_space_top', 'get_style_text_align', 'get_style_text_color', 'get_style_text_color_filtered', 'get_style_text_decor', 'get_style_text_font', 'get_style_text_letter_space', 'get_style_text_line_space', 'get_style_text_opa', 'get_style_transform_height', 'get_style_transform_pivot_x', 'get_style_transform_pivot_y', 'get_style_transform_rotation', 'get_style_transform_scale_x', 'get_style_transform_scale_x_safe', 'get_style_transform_scale_y', 'get_style_transform_scale_y_safe', 'get_style_transform_skew_x', 'get_style_transform_skew_y', 'get_style_transform_width', 'get_style_transition', 'get_style_translate_x', 'get_style_translate_y', 'get_style_width', 'get_style_x', 'get_style_y', 'get_transformed_area', 'get_user_data', 'get_width', 'get_x', 'get_x2', 'get_x_aligned', 'get_y', 'get_y2', 'get_y_aligned', 'has_class', 'has_flag', 'has_flag_any', 'has_state', 'has_style_prop', 'hit_test', 'init_draw_arc_dsc', 'init_draw_image_dsc', 'init_draw_label_dsc', 'init_draw_line_dsc', 'init_draw_rect_dsc', 'invalidate', 'invalidate_area', 'is_editable', 'is_group_def', 'is_layout_positioned', 'is_scrolling', 'is_valid', 'is_visible', 'mark_layout_as_dirty', 'move_background', 'move_children_by', 'move_foreground', 'move_to', 'move_to_index', 'readjust_scroll', 'redraw', 'refr_pos', 'refr_size', 'refresh_ext_draw_size', 'refresh_self_size', 'refresh_style', 'remove_event', 'remove_event_cb', 'remove_event_cb_with_user_data', 'remove_event_dsc', 'remove_flag', 'remove_local_style_prop', 'remove_state', 'remove_style', 'remove_style_all', 'replace_style', 'report_style_change', 'scroll_by', 'scroll_by_bounded', 'scroll_to', 'scroll_to_view', 'scroll_to_view_recursive', 'scroll_to_x', 'scroll_to_y', 'scrollbar_invalidate', 'send_event', 'set_align', 'set_content_height', 'set_content_width', 'set_ext_click_area', 'set_flex_align', 'set_flex_flow', 'set_flex_grow', 'set_grid_align', 'set_grid_cell', 'set_grid_dsc_array', 'set_height', 'set_layout', 'set_local_style_prop', 'set_parent', 'set_pos', 'set_scroll_dir', 'set_scroll_snap_x', 'set_scroll_snap_y', 'set_scrollbar_mode', 'set_size', 'set_state', 'set_style_align', 'set_style_anim', 'set_style_anim_duration', 'set_style_arc_color', 'set_style_arc_image_src', 'set_style_arc_opa', 'set_style_arc_rounded', 'set_style_arc_width', 'set_style_base_dir', 'set_style_bg_color', 'set_style_bg_grad', 'set_style_bg_grad_color', 'set_style_bg_grad_dir', 'set_style_bg_grad_opa', 'set_style_bg_grad_stop', 'set_style_bg_image_opa', 'set_style_bg_image_recolor', 'set_style_bg_image_recolor_opa', 'set_style_bg_image_src', 'set_style_bg_image_tiled', 'set_style_bg_main_opa', 'set_style_bg_main_stop', 'set_style_bg_opa', 'set_style_bitmap_mask_src', 'set_style_blend_mode', 'set_style_border_color', 'set_style_border_opa', 'set_style_border_post', 'set_style_border_side', 'set_style_border_width', 'set_style_clip_corner', 'set_style_color_filter_dsc', 'set_style_color_filter_opa', 'set_style_flex_cross_place', 'set_style_flex_flow', 'set_style_flex_grow', 'set_style_flex_main_place', 'set_style_flex_track_place', 'set_style_grid_cell_column_pos', 'set_style_grid_cell_column_span', 'set_style_grid_cell_row_pos', 'set_style_grid_cell_row_span', 'set_style_grid_cell_x_align', 'set_style_grid_cell_y_align', 'set_style_grid_column_align', 'set_style_grid_column_dsc_array', 'set_style_grid_row_align', 'set_style_grid_row_dsc_array', 'set_style_height', 'set_style_image_opa', 'set_style_image_recolor', 'set_style_image_recolor_opa', 'set_style_layout', 'set_style_length', 'set_style_line_color', 'set_style_line_dash_gap', 'set_style_line_dash_width', 'set_style_line_opa', 'set_style_line_rounded', 'set_style_line_width', 'set_style_margin_all', 'set_style_margin_bottom', 'set_style_margin_hor', 'set_style_margin_left', 'set_style_margin_right', 'set_style_margin_top', 'set_style_margin_ver', 'set_style_max_height', 'set_style_max_width', 'set_style_min_height', 'set_style_min_width', 'set_style_opa', 'set_style_opa_layered', 'set_style_outline_color', 'set_style_outline_opa', 'set_style_outline_pad', 'set_style_outline_width', 'set_style_pad_all', 'set_style_pad_bottom', 'set_style_pad_column', 'set_style_pad_gap', 'set_style_pad_hor', 'set_style_pad_left', 'set_style_pad_right', 'set_style_pad_row', 'set_style_pad_top', 'set_style_pad_ver', 'set_style_radius', 'set_style_rotary_sensitivity', 'set_style_shadow_color', 'set_style_shadow_offset_x', 'set_style_shadow_offset_y', 'set_style_shadow_opa', 'set_style_shadow_spread', 'set_style_shadow_width', 'set_style_size', 'set_style_text_align', 'set_style_text_color', 'set_style_text_decor', 'set_style_text_font', 'set_style_text_letter_space', 'set_style_text_line_space', 'set_style_text_opa', 'set_style_transform_height', 'set_style_transform_pivot_x', 'set_style_transform_pivot_y', 'set_style_transform_rotation', 'set_style_transform_scale', 'set_style_transform_scale_x', 'set_style_transform_scale_y', 'set_style_transform_skew_x', 'set_style_transform_skew_y', 'set_style_transform_width', 'set_style_transition', 'set_style_translate_x', 'set_style_translate_y', 'set_style_width', 'set_style_x', 'set_style_y', 'set_user_data', 'set_width', 'set_x', 'set_y', 'stringify_id', 'style_get_selector_part', 'style_get_selector_state', 'swap', 'transform_point', 'transform_point_array', 'tree_walk', 'update_flag', 'update_layout', 'update_snap'] 169 | ``` 170 |
171 | 172 | ### Loading images 173 | 174 | Png images can be loaded directly as shown [here](https://github.com/lvgl-micropython/lvgl_micropython/discussions/317#discussioncomment-13230539). 175 | 176 | ### Font Converter for custom fonts 177 | 178 | The [font converter](https://lvgl.io/tools/fontconverter) can be used to compile custom fonts for LVGL. 179 | The image shows the settings used to compile fonts which can be loaded by the following code. 180 | 181 | ```python 182 | import lvgl as lv 183 | import fs_driver #important 184 | 185 | fs_drive_letter = 'S' 186 | fs_font_driver = lv.fs_drv_t() 187 | fs_driver.fs_register(fs_font_driver, fs_drive_letter) 188 | 189 | teko_20 = lv.binfont_create(fs_drive_letter + ':' + 'Teko_20.bin') 190 | teko_48 = lv.binfont_create(fs_drive_letter + ':' + 'Teko_48.bin') 191 | 192 | label_small.set_style_text_font(teko_36, 0) 193 | label_large.set_style_text_font(teko_48, 0) 194 | ``` 195 | 196 | I have tested several fonts and [Lexend](https://fonts.google.com/specimen/Lexend) is one of my favourites. 197 | It's clearly readable on the CYD with medium or semi-bold font-weight. 198 | 199 | 200 | 201 | ### Icon fonts 202 | 203 | The `utf8Bytes` function is useful for displaying icons from icon fonts. 204 | It converts the character specific Unicode which is used on font collection websites to a six digit UTF-8 code [required by LVGL](https://docs.lvgl.io/8.3/overview/font.html#add-new-symbols). 205 | 206 | ```python 207 | def utf8Bytes(hexStr: str): 208 | ''' Helper function used to display icons. 209 | Returns the six digit utf8 bytecode from four digit Unicode 210 | as shown on font collection websites (e.g. font awesome, fontello) 211 | for direct use in lvgl. 212 | 'F287' -> b'\0xEF\0x8A\0x87' 213 | 214 | Use: 215 | obj.set_style_text_font(icon_font, 0) 216 | obj.set_text(utf8Bytes('F287'))''' 217 | 218 | hexCode = int(hexStr, 16) 219 | unicodeStr= chr(hexCode) 220 | utf8Bytecode = unicodeStr.encode('utf-8') 221 | return utf8Bytecode 222 | ``` 223 | 224 | ### Multitasking 225 | 226 | @kdschlosser suggests that the `_thread` module should be used to achieve concurrent tasks as described in [this forum post](https://forum.lvgl.io/t/jc3248w535en-event-problem/21586/23). 227 | 228 | ### Selecting all children of an object 229 | 230 | The `obj.get_child()` function returns only direct children (first level) of an object. 231 | The `get_all_children(obj)` function can be used to get all children of an object. 232 | 233 | ```python 234 | def get_all_children(parent_obj, exclude_parent = True): 235 | child_list = [] 236 | 237 | def walk(child_obj, data = None): 238 | if child_obj == parent_obj and exclude_parent: 239 | pass 240 | else: 241 | child_list.append(child_obj) 242 | 243 | return lv.obj.TREE_WALK.NEXT 244 | 245 | parent_obj.tree_walk(walk, None) 246 | 247 | return child_list 248 | 249 | # Example use: Return all children of 'obj' 250 | _children = get_all_children(obj) 251 | for el in _children: 252 | el.add_flag(lv.obj.FLAG.EVENT_BUBBLE) 253 | ``` 254 | 255 | 256 | 257 | ## CYD2 and MicroPython 258 | 259 | ### Drivers and Firmware 260 | 261 | The standard release of ESP32 MPY-Firmware can be installed on the CYD-2 as described [here](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/Examples/Micropython/Micropython.md). 262 | The ILI9341 driver and the xpt2046 driver can be found in the `/demo_no_lvgl` folder. 263 | 264 | ### Color Mode for CYD2 265 | 266 | During display initialization in pure Micropython, bgr-mode needs to be disabled: 267 | ```python 268 | Display(self.spi_display, dc=Pin(2), cs=Pin(15), rst=Pin(15), width = 320, height = 240, bgr = False) 269 | ``` 270 | 271 | Another solution can be disabling gamma-correction by passing `gamma = False` during Display initialization (see [#2](https://github.com/de-dh/ESP32-Cheap-Yellow-Display-Micropython-LVGL/issues/2#issuecomment-2558521839) ) 272 | 273 | ### Demo program 274 | 275 | A working demo and the drivers can be found in the `/demo_no_lvgl` folder. 276 | Draw functions can be used and touch actions can be assigned to multiple areas on screen in the demo program. 277 | 278 | 279 | 280 | 281 | ## Links 282 | 283 | CYD links: 284 | 285 | - [Micropython example for the CYD](https://github.com/witnessmenow/ESP32-Cheap-Yellow-Display/blob/main/Examples/Micropython/Micropython.md): Large repositry about the CYD mainly with `C` examples. 286 | - [Modifiying the CYD's hardware](https://github.com/hexeguitar/ESP32_TFT_PIO): Adding PSRAM, freeing GPIO pins, attaching a speaker. 287 | - [Overview of the different CYD versions](https://github.com/rzeldent/platformio-espressif32-sunton): Overview of different CYD boards and board definitions for PlatformIO. 288 | 289 | LVGL / Micropython links: 290 | 291 | - [JC3248W535 aka Cheap Black Display (CBD)](https://github.com/de-dh/ESP32-JC3248W535-Micropython-LVGL/tree/main) 292 | - [LVGL](https://github.com/rzeldent/platformio-espressif32-sunton): LVGL main repositry. 293 | - [Micropython](https://github.com/micropython/micropython): Micropython main repositry. 294 | - [LVGL Forum](https://forum.lvgl.io/): You can find help at the LVGL forum. Has a micropython category. 295 | - [Kdschlosser's Micropython Bindings](https://github.com/lvgl-micropython/lvgl_micropython): Micropython bindings from kdschlosser make building the firmware easier and support additional displays. 296 | - [Stefan's Blog](https://stefan.box2code.de/2023/11/18/esp32-grafik-mit-lvgl-und-micropython/): German blog with instructions on how to use LVGL on the CYD. Also contains prebuild firmware for LVGL 8.3. 297 | - [Documentation for LVGL 8.3](https://docs.lvgl.io/8.3/examples.html): Documentation for LVGL 8.3 with Micropython examples. 298 | - [LVGL Font Converter](https://lvgl.io/tools/fontconverter): Online font converter for LVGL. 299 | -------------------------------------------------------------------------------- /demo_no_lvgl/ili9341.py: -------------------------------------------------------------------------------- 1 | """ILI9341 LCD/Touch module. 2 | Source: https://github.com/rdagger/micropython-ili9341/blob/master/ili9341.py 3 | MIT License 4 | """ 5 | from time import sleep 6 | from math import cos, sin, pi, radians 7 | from sys import implementation 8 | from framebuf import FrameBuffer, RGB565 # type: ignore 9 | from micropython import const # type: ignore 10 | 11 | 12 | def color565(r, g, b): 13 | """Return RGB565 color value. 14 | 15 | Args: 16 | r (int): Red value. 17 | g (int): Green value. 18 | b (int): Blue value. 19 | """ 20 | return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3 21 | 22 | 23 | class Display(object): 24 | """Serial interface for 16-bit color (5-6-5 RGB) IL9341 display. 25 | 26 | Note: All coordinates are zero based. 27 | """ 28 | 29 | # Command constants from ILI9341 datasheet 30 | NOP = const(0x00) # No-op 31 | SWRESET = const(0x01) # Software reset 32 | RDDID = const(0x04) # Read display ID info 33 | RDDST = const(0x09) # Read display status 34 | SLPIN = const(0x10) # Enter sleep mode 35 | SLPOUT = const(0x11) # Exit sleep mode 36 | PTLON = const(0x12) # Partial mode on 37 | NORON = const(0x13) # Normal display mode on 38 | RDMODE = const(0x0A) # Read display power mode 39 | RDMADCTL = const(0x0B) # Read display MADCTL 40 | RDPIXFMT = const(0x0C) # Read display pixel format 41 | RDIMGFMT = const(0x0D) # Read display image format 42 | RDSELFDIAG = const(0x0F) # Read display self-diagnostic 43 | INVOFF = const(0x20) # Display inversion off 44 | INVON = const(0x21) # Display inversion on 45 | GAMMASET = const(0x26) # Gamma set 46 | DISPLAY_OFF = const(0x28) # Display off 47 | DISPLAY_ON = const(0x29) # Display on 48 | SET_COLUMN = const(0x2A) # Column address set 49 | SET_PAGE = const(0x2B) # Page address set 50 | WRITE_RAM = const(0x2C) # Memory write 51 | READ_RAM = const(0x2E) # Memory read 52 | PTLAR = const(0x30) # Partial area 53 | VSCRDEF = const(0x33) # Vertical scrolling definition 54 | MADCTL = const(0x36) # Memory access control 55 | VSCRSADD = const(0x37) # Vertical scrolling start address 56 | PIXFMT = const(0x3A) # COLMOD: Pixel format set 57 | WRITE_DISPLAY_BRIGHTNESS = const(0x51) # Brightness hardware dependent! 58 | READ_DISPLAY_BRIGHTNESS = const(0x52) 59 | WRITE_CTRL_DISPLAY = const(0x53) 60 | READ_CTRL_DISPLAY = const(0x54) 61 | WRITE_CABC = const(0x55) # Write Content Adaptive Brightness Control 62 | READ_CABC = const(0x56) # Read Content Adaptive Brightness Control 63 | WRITE_CABC_MINIMUM = const(0x5E) # Write CABC Minimum Brightness 64 | READ_CABC_MINIMUM = const(0x5F) # Read CABC Minimum Brightness 65 | FRMCTR1 = const(0xB1) # Frame rate control (In normal mode/full colors) 66 | FRMCTR2 = const(0xB2) # Frame rate control (In idle mode/8 colors) 67 | FRMCTR3 = const(0xB3) # Frame rate control (In partial mode/full colors) 68 | INVCTR = const(0xB4) # Display inversion control 69 | DFUNCTR = const(0xB6) # Display function control 70 | PWCTR1 = const(0xC0) # Power control 1 71 | PWCTR2 = const(0xC1) # Power control 2 72 | PWCTRA = const(0xCB) # Power control A 73 | PWCTRB = const(0xCF) # Power control B 74 | VMCTR1 = const(0xC5) # VCOM control 1 75 | VMCTR2 = const(0xC7) # VCOM control 2 76 | RDID1 = const(0xDA) # Read ID 1 77 | RDID2 = const(0xDB) # Read ID 2 78 | RDID3 = const(0xDC) # Read ID 3 79 | RDID4 = const(0xDD) # Read ID 4 80 | GMCTRP1 = const(0xE0) # Positive gamma correction 81 | GMCTRN1 = const(0xE1) # Negative gamma correction 82 | DTCA = const(0xE8) # Driver timing control A 83 | DTCB = const(0xEA) # Driver timing control B 84 | POSC = const(0xED) # Power on sequence control 85 | ENABLE3G = const(0xF2) # Enable 3 gamma control 86 | PUMPRC = const(0xF7) # Pump ratio control 87 | 88 | MIRROR_ROTATE = { # MADCTL configurations for rotation and mirroring 89 | (False, 0): 0x80, # 1000 0000 90 | (False, 90): 0xE0, # 1110 0000 91 | (False, 180): 0x40, # 0100 0000 92 | (False, 270): 0x20, # 0010 0000 93 | (True, 0): 0xC0, # 1100 0000 94 | (True, 90): 0x60, # 0110 0000 95 | (True, 180): 0x00, # 0000 0000 96 | (True, 270): 0xA0 # 1010 0000 97 | } 98 | 99 | def __init__(self, spi, cs, dc, rst, width=240, height=320, rotation=0, 100 | mirror=False, bgr=True, gamma=True): 101 | """Initialize OLED. 102 | 103 | Args: 104 | spi (Class Spi): SPI interface for OLED 105 | cs (Class Pin): Chip select pin 106 | dc (Class Pin): Data/Command pin 107 | rst (Class Pin): Reset pin 108 | width (Optional int): Screen width (default 240) 109 | height (Optional int): Screen height (default 320) 110 | rotation (Optional int): Rotation must be 0 default, 90. 180 or 270 111 | mirror (Optional bool): Mirror display (default False) 112 | bgr (Optional bool): Swaps red and blue colors (default True) 113 | gamma (Optional bool): Custom gamma correction (default True) 114 | """ 115 | self.spi = spi 116 | self.cs = cs 117 | self.dc = dc 118 | self.rst = rst 119 | self.width = width 120 | self.height = height 121 | if (mirror, rotation) not in self.MIRROR_ROTATE: 122 | raise ValueError('Rotation must be 0, 90, 180 or 270.') 123 | else: 124 | self.rotation = self.MIRROR_ROTATE[mirror, rotation] 125 | if bgr: # Set BGR bit 126 | self.rotation |= 0b00001000 127 | 128 | # Initialize GPIO pins and set implementation specific methods 129 | if implementation.name == 'circuitpython': 130 | self.cs.switch_to_output(value=True) 131 | self.dc.switch_to_output(value=False) 132 | self.rst.switch_to_output(value=True) 133 | self.reset = self.reset_cpy 134 | self.write_cmd = self.write_cmd_cpy 135 | self.write_data = self.write_data_cpy 136 | else: 137 | self.cs.init(self.cs.OUT, value=1) 138 | self.dc.init(self.dc.OUT, value=0) 139 | self.rst.init(self.rst.OUT, value=1) 140 | self.reset = self.reset_mpy 141 | self.write_cmd = self.write_cmd_mpy 142 | self.write_data = self.write_data_mpy 143 | self.reset() 144 | # Send initialization commands 145 | self.write_cmd(self.SWRESET) # Software reset 146 | sleep(.1) 147 | self.write_cmd(self.PWCTRB, 0x00, 0xC1, 0x30) # Pwr ctrl B 148 | self.write_cmd(self.POSC, 0x64, 0x03, 0x12, 0x81) # Pwr on seq. ctrl 149 | self.write_cmd(self.DTCA, 0x85, 0x00, 0x78) # Driver timing ctrl A 150 | self.write_cmd(self.PWCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02) # Pwr ctrl A 151 | self.write_cmd(self.PUMPRC, 0x20) # Pump ratio control 152 | self.write_cmd(self.DTCB, 0x00, 0x00) # Driver timing ctrl B 153 | self.write_cmd(self.PWCTR1, 0x23) # Pwr ctrl 1 154 | self.write_cmd(self.PWCTR2, 0x10) # Pwr ctrl 2 155 | self.write_cmd(self.VMCTR1, 0x3E, 0x28) # VCOM ctrl 1 156 | self.write_cmd(self.VMCTR2, 0x86) # VCOM ctrl 2 157 | self.write_cmd(self.MADCTL, self.rotation) # Memory access ctrl 158 | self.write_cmd(self.VSCRSADD, 0x00) # Vertical scrolling start address 159 | self.write_cmd(self.PIXFMT, 0x55) # COLMOD: Pixel format 160 | self.write_cmd(self.FRMCTR1, 0x00, 0x18) # Frame rate ctrl 161 | self.write_cmd(self.DFUNCTR, 0x08, 0x82, 0x27) 162 | self.write_cmd(self.ENABLE3G, 0x00) # Enable 3 gamma ctrl 163 | self.write_cmd(self.GAMMASET, 0x01) # Gamma curve selected 164 | if gamma: # Use custom gamma correction values 165 | self.write_cmd(self.GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 166 | 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 167 | 0x00) 168 | self.write_cmd(self.GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 169 | 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 170 | 0x0F) 171 | self.write_cmd(self.SLPOUT) # Exit sleep 172 | sleep(.1) 173 | self.write_cmd(self.DISPLAY_ON) # Display on 174 | sleep(.1) 175 | self.write_cmd(self.GAMMASET, 0x02) 176 | sleep(.1) 177 | self.write_cmd(self.GAMMASET, 0x01) 178 | # tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected 179 | # tft.writedata(2); 180 | # delay(120); 181 | # tft.writecommand(ILI9341_GAMMASET); //Gamma curve selected 182 | # tft.writedata(1); 183 | self.clear() 184 | 185 | def block(self, x0, y0, x1, y1, data): 186 | """Write a block of data to display. 187 | 188 | Args: 189 | x0 (int): Starting X position. 190 | y0 (int): Starting Y position. 191 | x1 (int): Ending X position. 192 | y1 (int): Ending Y position. 193 | data (bytes): Data buffer to write. 194 | """ 195 | self.write_cmd(self.SET_COLUMN, 196 | x0 >> 8, x0 & 0xff, x1 >> 8, x1 & 0xff) 197 | self.write_cmd(self.SET_PAGE, 198 | y0 >> 8, y0 & 0xff, y1 >> 8, y1 & 0xff) 199 | self.write_cmd(self.WRITE_RAM) 200 | self.write_data(data) 201 | 202 | def cleanup(self): 203 | """Clean up resources.""" 204 | self.clear() 205 | self.display_off() 206 | self.spi.deinit() 207 | print('display off') 208 | 209 | def clear(self, color=0, hlines=8): 210 | """Clear display. 211 | 212 | Args: 213 | color (Optional int): RGB565 color value (Default: 0 = Black). 214 | hlines (Optional int): # of horizontal lines per chunk (Default: 8) 215 | Note: 216 | hlines was introduced to deal with memory allocation on some 217 | boards. Smaller values allocate less memory but take longer 218 | to execute. hlines must be a factor of the display height. 219 | For example, for a 240 pixel height, valid values for hline 220 | would be 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 16, 20, 24, 30, 40, etc. 221 | Higher values may result in memory allocation errors. 222 | """ 223 | w = self.width 224 | h = self.height 225 | assert hlines > 0 and h % hlines == 0, ( 226 | "hlines must be a non-zero factor of height.") 227 | # Clear display 228 | if color: 229 | line = color.to_bytes(2, 'big') * (w * hlines) 230 | else: 231 | line = bytearray(w * 2 * hlines) 232 | for y in range(0, h, hlines): 233 | self.block(0, y, w - 1, y + hlines - 1, line) 234 | 235 | def display_off(self): 236 | """Turn display off.""" 237 | self.write_cmd(self.DISPLAY_OFF) 238 | 239 | def display_on(self): 240 | """Turn display on.""" 241 | self.write_cmd(self.DISPLAY_ON) 242 | 243 | def draw_circle(self, x0, y0, r, color): 244 | """Draw a circle. 245 | 246 | Args: 247 | x0 (int): X coordinate of center point. 248 | y0 (int): Y coordinate of center point. 249 | r (int): Radius. 250 | color (int): RGB565 color value. 251 | """ 252 | f = 1 - r 253 | dx = 1 254 | dy = -r - r 255 | x = 0 256 | y = r 257 | self.draw_pixel(x0, y0 + r, color) 258 | self.draw_pixel(x0, y0 - r, color) 259 | self.draw_pixel(x0 + r, y0, color) 260 | self.draw_pixel(x0 - r, y0, color) 261 | while x < y: 262 | if f >= 0: 263 | y -= 1 264 | dy += 2 265 | f += dy 266 | x += 1 267 | dx += 2 268 | f += dx 269 | self.draw_pixel(x0 + x, y0 + y, color) 270 | self.draw_pixel(x0 - x, y0 + y, color) 271 | self.draw_pixel(x0 + x, y0 - y, color) 272 | self.draw_pixel(x0 - x, y0 - y, color) 273 | self.draw_pixel(x0 + y, y0 + x, color) 274 | self.draw_pixel(x0 - y, y0 + x, color) 275 | self.draw_pixel(x0 + y, y0 - x, color) 276 | self.draw_pixel(x0 - y, y0 - x, color) 277 | 278 | def draw_ellipse(self, x0, y0, a, b, color): 279 | """Draw an ellipse. 280 | 281 | Args: 282 | x0, y0 (int): Coordinates of center point. 283 | a (int): Semi axis horizontal. 284 | b (int): Semi axis vertical. 285 | color (int): RGB565 color value. 286 | Note: 287 | The center point is the center of the x0,y0 pixel. 288 | Since pixels are not divisible, the axes are integer rounded 289 | up to complete on a full pixel. Therefore the major and 290 | minor axes are increased by 1. 291 | """ 292 | a2 = a * a 293 | b2 = b * b 294 | twoa2 = a2 + a2 295 | twob2 = b2 + b2 296 | x = 0 297 | y = b 298 | px = 0 299 | py = twoa2 * y 300 | # Plot initial points 301 | self.draw_pixel(x0 + x, y0 + y, color) 302 | self.draw_pixel(x0 - x, y0 + y, color) 303 | self.draw_pixel(x0 + x, y0 - y, color) 304 | self.draw_pixel(x0 - x, y0 - y, color) 305 | # Region 1 306 | p = round(b2 - (a2 * b) + (0.25 * a2)) 307 | while px < py: 308 | x += 1 309 | px += twob2 310 | if p < 0: 311 | p += b2 + px 312 | else: 313 | y -= 1 314 | py -= twoa2 315 | p += b2 + px - py 316 | self.draw_pixel(x0 + x, y0 + y, color) 317 | self.draw_pixel(x0 - x, y0 + y, color) 318 | self.draw_pixel(x0 + x, y0 - y, color) 319 | self.draw_pixel(x0 - x, y0 - y, color) 320 | # Region 2 321 | p = round(b2 * (x + 0.5) * (x + 0.5) + 322 | a2 * (y - 1) * (y - 1) - a2 * b2) 323 | while y > 0: 324 | y -= 1 325 | py -= twoa2 326 | if p > 0: 327 | p += a2 - py 328 | else: 329 | x += 1 330 | px += twob2 331 | p += a2 - py + px 332 | self.draw_pixel(x0 + x, y0 + y, color) 333 | self.draw_pixel(x0 - x, y0 + y, color) 334 | self.draw_pixel(x0 + x, y0 - y, color) 335 | self.draw_pixel(x0 - x, y0 - y, color) 336 | 337 | def draw_hline(self, x, y, w, color): 338 | """Draw a horizontal line. 339 | 340 | Args: 341 | x (int): Starting X position. 342 | y (int): Starting Y position. 343 | w (int): Width of line. 344 | color (int): RGB565 color value. 345 | """ 346 | if self.is_off_grid(x, y, x + w - 1, y): 347 | return 348 | line = color.to_bytes(2, 'big') * w 349 | self.block(x, y, x + w - 1, y, line) 350 | 351 | def draw_image(self, path, x=0, y=0, w=320, h=240): 352 | """Draw image from flash. 353 | 354 | Args: 355 | path (string): Image file path. 356 | x (int): X coordinate of image left. Default is 0. 357 | y (int): Y coordinate of image top. Default is 0. 358 | w (int): Width of image. Default is 320. 359 | h (int): Height of image. Default is 240. 360 | """ 361 | x2 = x + w - 1 362 | y2 = y + h - 1 363 | if self.is_off_grid(x, y, x2, y2): 364 | return 365 | with open(path, "rb") as f: 366 | chunk_height = 1024 // w 367 | chunk_count, remainder = divmod(h, chunk_height) 368 | chunk_size = chunk_height * w * 2 369 | chunk_y = y 370 | if chunk_count: 371 | for c in range(0, chunk_count): 372 | buf = f.read(chunk_size) 373 | self.block(x, chunk_y, 374 | x2, chunk_y + chunk_height - 1, 375 | buf) 376 | chunk_y += chunk_height 377 | if remainder: 378 | buf = f.read(remainder * w * 2) 379 | self.block(x, chunk_y, 380 | x2, chunk_y + remainder - 1, 381 | buf) 382 | 383 | def draw_letter(self, x, y, letter, font, color, background=0, 384 | landscape=False, rotate_180=False): 385 | """Draw a letter. 386 | 387 | Args: 388 | x (int): Starting X position. 389 | y (int): Starting Y position. 390 | letter (string): Letter to draw. 391 | font (XglcdFont object): Font. 392 | color (int): RGB565 color value. 393 | background (int): RGB565 background color (default: black) 394 | landscape (bool): Orientation (default: False = portrait) 395 | rotate_180 (bool): Rotate text by 180 degrees 396 | """ 397 | buf, w, h = font.get_letter(letter, color, background, landscape) 398 | if rotate_180: 399 | # Manually rotate the buffer by 180 degrees 400 | # ensure bytes pairs for each pixel retain color565 401 | new_buf = bytearray(len(buf)) 402 | num_pixels = len(buf) // 2 403 | for i in range(num_pixels): 404 | # The index for the new buffer's byte pair 405 | new_idx = (num_pixels - 1 - i) * 2 406 | # The index for the original buffer's byte pair 407 | old_idx = i * 2 408 | # Swap the pixels 409 | new_buf[new_idx], new_buf[new_idx + 1] = buf[old_idx], buf[old_idx + 1] 410 | buf = new_buf 411 | 412 | # Check for errors (Font could be missing specified letter) 413 | if w == 0: 414 | return w, h 415 | 416 | if landscape: 417 | y -= w 418 | if self.is_off_grid(x, y, x + h - 1, y + w - 1): 419 | return 0, 0 420 | self.block(x, y, 421 | x + h - 1, y + w - 1, 422 | buf) 423 | else: 424 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 425 | return 0, 0 426 | self.block(x, y, 427 | x + w - 1, y + h - 1, 428 | buf) 429 | return w, h 430 | 431 | def draw_line(self, x1, y1, x2, y2, color): 432 | """Draw a line using Bresenham's algorithm. 433 | 434 | Args: 435 | x1, y1 (int): Starting coordinates of the line 436 | x2, y2 (int): Ending coordinates of the line 437 | color (int): RGB565 color value. 438 | """ 439 | # Check for horizontal line 440 | if y1 == y2: 441 | if x1 > x2: 442 | x1, x2 = x2, x1 443 | self.draw_hline(x1, y1, x2 - x1 + 1, color) 444 | return 445 | # Check for vertical line 446 | if x1 == x2: 447 | if y1 > y2: 448 | y1, y2 = y2, y1 449 | self.draw_vline(x1, y1, y2 - y1 + 1, color) 450 | return 451 | # Confirm coordinates in boundary 452 | if self.is_off_grid(min(x1, x2), min(y1, y2), 453 | max(x1, x2), max(y1, y2)): 454 | return 455 | # Changes in x, y 456 | dx = x2 - x1 457 | dy = y2 - y1 458 | # Determine how steep the line is 459 | is_steep = abs(dy) > abs(dx) 460 | # Rotate line 461 | if is_steep: 462 | x1, y1 = y1, x1 463 | x2, y2 = y2, x2 464 | # Swap start and end points if necessary 465 | if x1 > x2: 466 | x1, x2 = x2, x1 467 | y1, y2 = y2, y1 468 | # Recalculate differentials 469 | dx = x2 - x1 470 | dy = y2 - y1 471 | # Calculate error 472 | error = dx >> 1 473 | ystep = 1 if y1 < y2 else -1 474 | y = y1 475 | for x in range(x1, x2 + 1): 476 | # Had to reverse HW ???? 477 | if not is_steep: 478 | self.draw_pixel(x, y, color) 479 | else: 480 | self.draw_pixel(y, x, color) 481 | error -= abs(dy) 482 | if error < 0: 483 | y += ystep 484 | error += dx 485 | 486 | def draw_lines(self, coords, color): 487 | """Draw multiple lines. 488 | 489 | Args: 490 | coords ([[int, int],...]): Line coordinate X, Y pairs 491 | color (int): RGB565 color value. 492 | """ 493 | # Starting point 494 | x1, y1 = coords[0] 495 | # Iterate through coordinates 496 | for i in range(1, len(coords)): 497 | x2, y2 = coords[i] 498 | self.draw_line(x1, y1, x2, y2, color) 499 | x1, y1 = x2, y2 500 | 501 | def draw_pixel(self, x, y, color): 502 | """Draw a single pixel. 503 | 504 | Args: 505 | x (int): X position. 506 | y (int): Y position. 507 | color (int): RGB565 color value. 508 | """ 509 | if self.is_off_grid(x, y, x, y): 510 | return 511 | self.block(x, y, x, y, color.to_bytes(2, 'big')) 512 | 513 | def draw_polygon(self, sides, x0, y0, r, color, rotate=0): 514 | """Draw an n-sided regular polygon. 515 | 516 | Args: 517 | sides (int): Number of polygon sides. 518 | x0, y0 (int): Coordinates of center point. 519 | r (int): Radius. 520 | color (int): RGB565 color value. 521 | rotate (Optional float): Rotation in degrees relative to origin. 522 | Note: 523 | The center point is the center of the x0,y0 pixel. 524 | Since pixels are not divisible, the radius is integer rounded 525 | up to complete on a full pixel. Therefore diameter = 2 x r + 1. 526 | """ 527 | coords = [] 528 | theta = radians(rotate) 529 | n = sides + 1 530 | for s in range(n): 531 | t = 2.0 * pi * s / sides + theta 532 | coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) 533 | 534 | # Cast to python float first to fix rounding errors 535 | self.draw_lines(coords, color=color) 536 | 537 | def draw_rectangle(self, x, y, w, h, color): 538 | """Draw a rectangle. 539 | 540 | Args: 541 | x (int): Starting X position. 542 | y (int): Starting Y position. 543 | w (int): Width of rectangle. 544 | h (int): Height of rectangle. 545 | color (int): RGB565 color value. 546 | """ 547 | x2 = x + w - 1 548 | y2 = y + h - 1 549 | self.draw_hline(x, y, w, color) 550 | self.draw_hline(x, y2, w, color) 551 | self.draw_vline(x, y, h, color) 552 | self.draw_vline(x2, y, h, color) 553 | 554 | def draw_sprite(self, buf, x, y, w, h): 555 | """Draw a sprite (optimized for horizontal drawing). 556 | 557 | Args: 558 | buf (bytearray): Buffer to draw. 559 | x (int): Starting X position. 560 | y (int): Starting Y position. 561 | w (int): Width of drawing. 562 | h (int): Height of drawing. 563 | """ 564 | x2 = x + w - 1 565 | y2 = y + h - 1 566 | if self.is_off_grid(x, y, x2, y2): 567 | return 568 | self.block(x, y, x2, y2, buf) 569 | 570 | def draw_text(self, x, y, text, font, color, background=0, 571 | landscape=False, rotate_180=False, spacing=1): 572 | """Draw text. 573 | 574 | Args: 575 | x (int): Starting X position 576 | y (int): Starting Y position 577 | text (string): Text to draw 578 | font (XglcdFont object): Font 579 | color (int): RGB565 color value 580 | background (int): RGB565 background color (default: black) 581 | landscape (bool): Orientation (default: False = portrait) 582 | rotate_180 (bool): Rotate text by 180 degrees 583 | spacing (int): Pixels between letters (default: 1) 584 | """ 585 | iterable_text = reversed(text) if rotate_180 else text 586 | for letter in iterable_text: 587 | # Get letter array and letter dimensions 588 | w, h = self.draw_letter(x, y, letter, font, color, background, 589 | landscape, rotate_180) 590 | # Stop on error 591 | if w == 0 or h == 0: 592 | print('Invalid width {0} or height {1}'.format(w, h)) 593 | return 594 | 595 | if landscape: 596 | # Fill in spacing 597 | if spacing: 598 | self.fill_hrect(x, y - w - spacing, h, spacing, background) 599 | # Position y for next letter 600 | y -= (w + spacing) 601 | else: 602 | # Fill in spacing 603 | if spacing: 604 | self.fill_hrect(x + w, y, spacing, h, background) 605 | # Position x for next letter 606 | x += (w + spacing) 607 | 608 | # # Fill in spacing 609 | # if spacing: 610 | # self.fill_vrect(x + w, y, spacing, h, background) 611 | # # Position x for next letter 612 | # x += w + spacing 613 | 614 | def draw_text8x8(self, x, y, text, color, background=0, 615 | rotate=0): 616 | """Draw text using built-in MicroPython 8x8 bit font. 617 | 618 | Args: 619 | x (int): Starting X position. 620 | y (int): Starting Y position. 621 | text (string): Text to draw. 622 | color (int): RGB565 color value. 623 | background (int): RGB565 background color (default: black). 624 | rotate(int): 0, 90, 180, 270 625 | """ 626 | w = len(text) * 8 627 | h = 8 628 | # Confirm coordinates in boundary 629 | if self.is_off_grid(x, y, x + 7, y + 7): 630 | return 631 | buf = bytearray(w * 16) 632 | fbuf = FrameBuffer(buf, w, h, RGB565) 633 | if background != 0: 634 | # Swap background color bytes to correct for framebuf endianness 635 | b_color = ((background & 0xFF) << 8) | ((background & 0xFF00) >> 8) 636 | fbuf.fill(b_color) 637 | # Swap text color bytes to correct for framebuf endianness 638 | t_color = ((color & 0xFF) << 8) | ((color & 0xFF00) >> 8) 639 | fbuf.text(text, 0, 0, t_color) 640 | if rotate == 0: 641 | self.block(x, y, x + w - 1, y + (h - 1), buf) 642 | elif rotate == 90: 643 | buf2 = bytearray(w * 16) 644 | fbuf2 = FrameBuffer(buf2, h, w, RGB565) 645 | for y1 in range(h): 646 | for x1 in range(w): 647 | fbuf2.pixel(y1, x1, 648 | fbuf.pixel(x1, (h - 1) - y1)) 649 | self.block(x, y, x + (h - 1), y + w - 1, buf2) 650 | elif rotate == 180: 651 | buf2 = bytearray(w * 16) 652 | fbuf2 = FrameBuffer(buf2, w, h, RGB565) 653 | for y1 in range(h): 654 | for x1 in range(w): 655 | fbuf2.pixel(x1, y1, 656 | fbuf.pixel((w - 1) - x1, (h - 1) - y1)) 657 | self.block(x, y, x + w - 1, y + (h - 1), buf2) 658 | elif rotate == 270: 659 | buf2 = bytearray(w * 16) 660 | fbuf2 = FrameBuffer(buf2, h, w, RGB565) 661 | for y1 in range(h): 662 | for x1 in range(w): 663 | fbuf2.pixel(y1, x1, 664 | fbuf.pixel((w - 1) - x1, y1)) 665 | self.block(x, y, x + (h - 1), y + w - 1, buf2) 666 | 667 | def draw_vline(self, x, y, h, color): 668 | """Draw a vertical line. 669 | 670 | Args: 671 | x (int): Starting X position. 672 | y (int): Starting Y position. 673 | h (int): Height of line. 674 | color (int): RGB565 color value. 675 | """ 676 | # Confirm coordinates in boundary 677 | if self.is_off_grid(x, y, x, y + h - 1): 678 | return 679 | line = color.to_bytes(2, 'big') * h 680 | self.block(x, y, x, y + h - 1, line) 681 | 682 | def fill_circle(self, x0, y0, r, color): 683 | """Draw a filled circle. 684 | 685 | Args: 686 | x0 (int): X coordinate of center point. 687 | y0 (int): Y coordinate of center point. 688 | r (int): Radius. 689 | color (int): RGB565 color value. 690 | """ 691 | f = 1 - r 692 | dx = 1 693 | dy = -r - r 694 | x = 0 695 | y = r 696 | self.draw_vline(x0, y0 - r, 2 * r + 1, color) 697 | while x < y: 698 | if f >= 0: 699 | y -= 1 700 | dy += 2 701 | f += dy 702 | x += 1 703 | dx += 2 704 | f += dx 705 | self.draw_vline(x0 + x, y0 - y, 2 * y + 1, color) 706 | self.draw_vline(x0 - x, y0 - y, 2 * y + 1, color) 707 | self.draw_vline(x0 - y, y0 - x, 2 * x + 1, color) 708 | self.draw_vline(x0 + y, y0 - x, 2 * x + 1, color) 709 | 710 | def fill_ellipse(self, x0, y0, a, b, color): 711 | """Draw a filled ellipse. 712 | 713 | Args: 714 | x0, y0 (int): Coordinates of center point. 715 | a (int): Semi axis horizontal. 716 | b (int): Semi axis vertical. 717 | color (int): RGB565 color value. 718 | Note: 719 | The center point is the center of the x0,y0 pixel. 720 | Since pixels are not divisible, the axes are integer rounded 721 | up to complete on a full pixel. Therefore the major and 722 | minor axes are increased by 1. 723 | """ 724 | a2 = a * a 725 | b2 = b * b 726 | twoa2 = a2 + a2 727 | twob2 = b2 + b2 728 | x = 0 729 | y = b 730 | px = 0 731 | py = twoa2 * y 732 | # Plot initial points 733 | self.draw_line(x0, y0 - y, x0, y0 + y, color) 734 | # Region 1 735 | p = round(b2 - (a2 * b) + (0.25 * a2)) 736 | while px < py: 737 | x += 1 738 | px += twob2 739 | if p < 0: 740 | p += b2 + px 741 | else: 742 | y -= 1 743 | py -= twoa2 744 | p += b2 + px - py 745 | self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) 746 | self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) 747 | # Region 2 748 | p = round(b2 * (x + 0.5) * (x + 0.5) + 749 | a2 * (y - 1) * (y - 1) - a2 * b2) 750 | while y > 0: 751 | y -= 1 752 | py -= twoa2 753 | if p > 0: 754 | p += a2 - py 755 | else: 756 | x += 1 757 | px += twob2 758 | p += a2 - py + px 759 | self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) 760 | self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) 761 | 762 | def fill_hrect(self, x, y, w, h, color): 763 | """Draw a filled rectangle (optimized for horizontal drawing). 764 | 765 | Args: 766 | x (int): Starting X position. 767 | y (int): Starting Y position. 768 | w (int): Width of rectangle. 769 | h (int): Height of rectangle. 770 | color (int): RGB565 color value. 771 | """ 772 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 773 | return 774 | chunk_height = 1024 // w 775 | chunk_count, remainder = divmod(h, chunk_height) 776 | chunk_size = chunk_height * w 777 | chunk_y = y 778 | if chunk_count: 779 | buf = color.to_bytes(2, 'big') * chunk_size 780 | for c in range(0, chunk_count): 781 | self.block(x, chunk_y, 782 | x + w - 1, chunk_y + chunk_height - 1, 783 | buf) 784 | chunk_y += chunk_height 785 | 786 | if remainder: 787 | buf = color.to_bytes(2, 'big') * remainder * w 788 | self.block(x, chunk_y, 789 | x + w - 1, chunk_y + remainder - 1, 790 | buf) 791 | 792 | def fill_rectangle(self, x, y, w, h, color): 793 | """Draw a filled rectangle. 794 | 795 | Args: 796 | x (int): Starting X position. 797 | y (int): Starting Y position. 798 | w (int): Width of rectangle. 799 | h (int): Height of rectangle. 800 | color (int): RGB565 color value. 801 | """ 802 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 803 | return 804 | if w > h: 805 | self.fill_hrect(x, y, w, h, color) 806 | else: 807 | self.fill_vrect(x, y, w, h, color) 808 | 809 | def fill_polygon(self, sides, x0, y0, r, color, rotate=0): 810 | """Draw a filled n-sided regular polygon. 811 | 812 | Args: 813 | sides (int): Number of polygon sides. 814 | x0, y0 (int): Coordinates of center point. 815 | r (int): Radius. 816 | color (int): RGB565 color value. 817 | rotate (Optional float): Rotation in degrees relative to origin. 818 | Note: 819 | The center point is the center of the x0,y0 pixel. 820 | Since pixels are not divisible, the radius is integer rounded 821 | up to complete on a full pixel. Therefore diameter = 2 x r + 1. 822 | """ 823 | # Determine side coordinates 824 | coords = [] 825 | theta = radians(rotate) 826 | n = sides + 1 827 | for s in range(n): 828 | t = 2.0 * pi * s / sides + theta 829 | coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) 830 | # Starting point 831 | x1, y1 = coords[0] 832 | # Minimum Maximum X dict 833 | xdict = {y1: [x1, x1]} 834 | # Iterate through coordinates 835 | for row in coords[1:]: 836 | x2, y2 = row 837 | xprev, yprev = x2, y2 838 | # Calculate perimeter 839 | # Check for horizontal side 840 | if y1 == y2: 841 | if x1 > x2: 842 | x1, x2 = x2, x1 843 | if y1 in xdict: 844 | xdict[y1] = [min(x1, xdict[y1][0]), max(x2, xdict[y1][1])] 845 | else: 846 | xdict[y1] = [x1, x2] 847 | x1, y1 = xprev, yprev 848 | continue 849 | # Non horizontal side 850 | # Changes in x, y 851 | dx = x2 - x1 852 | dy = y2 - y1 853 | # Determine how steep the line is 854 | is_steep = abs(dy) > abs(dx) 855 | # Rotate line 856 | if is_steep: 857 | x1, y1 = y1, x1 858 | x2, y2 = y2, x2 859 | # Swap start and end points if necessary 860 | if x1 > x2: 861 | x1, x2 = x2, x1 862 | y1, y2 = y2, y1 863 | # Recalculate differentials 864 | dx = x2 - x1 865 | dy = y2 - y1 866 | # Calculate error 867 | error = dx >> 1 868 | ystep = 1 if y1 < y2 else -1 869 | y = y1 870 | # Calcualte minimum and maximum x values 871 | for x in range(x1, x2 + 1): 872 | if is_steep: 873 | if x in xdict: 874 | xdict[x] = [min(y, xdict[x][0]), max(y, xdict[x][1])] 875 | else: 876 | xdict[x] = [y, y] 877 | else: 878 | if y in xdict: 879 | xdict[y] = [min(x, xdict[y][0]), max(x, xdict[y][1])] 880 | else: 881 | xdict[y] = [x, x] 882 | error -= abs(dy) 883 | if error < 0: 884 | y += ystep 885 | error += dx 886 | x1, y1 = xprev, yprev 887 | # Fill polygon 888 | for y, x in xdict.items(): 889 | self.draw_hline(x[0], y, x[1] - x[0] + 2, color) 890 | 891 | def fill_vrect(self, x, y, w, h, color): 892 | """Draw a filled rectangle (optimized for vertical drawing). 893 | 894 | Args: 895 | x (int): Starting X position. 896 | y (int): Starting Y position. 897 | w (int): Width of rectangle. 898 | h (int): Height of rectangle. 899 | color (int): RGB565 color value. 900 | """ 901 | if self.is_off_grid(x, y, x + w - 1, y + h - 1): 902 | return 903 | chunk_width = 1024 // h 904 | chunk_count, remainder = divmod(w, chunk_width) 905 | chunk_size = chunk_width * h 906 | chunk_x = x 907 | if chunk_count: 908 | buf = color.to_bytes(2, 'big') * chunk_size 909 | for c in range(0, chunk_count): 910 | self.block(chunk_x, y, 911 | chunk_x + chunk_width - 1, y + h - 1, 912 | buf) 913 | chunk_x += chunk_width 914 | 915 | if remainder: 916 | buf = color.to_bytes(2, 'big') * remainder * h 917 | self.block(chunk_x, y, 918 | chunk_x + remainder - 1, y + h - 1, 919 | buf) 920 | 921 | def invert(self, enable=True): 922 | """Enables or disables inversion of display colors. 923 | 924 | Args: 925 | enable (Optional bool): True=enable, False=disable 926 | """ 927 | if enable: 928 | self.write_cmd(self.INVON) 929 | else: 930 | self.write_cmd(self.INVOFF) 931 | 932 | def is_off_grid(self, xmin, ymin, xmax, ymax): 933 | """Check if coordinates extend past display boundaries. 934 | 935 | Args: 936 | xmin (int): Minimum horizontal pixel. 937 | ymin (int): Minimum vertical pixel. 938 | xmax (int): Maximum horizontal pixel. 939 | ymax (int): Maximum vertical pixel. 940 | Returns: 941 | boolean: False = Coordinates OK, True = Error. 942 | """ 943 | if xmin < 0: 944 | print('x-coordinate: {0} below minimum of 0.'.format(xmin)) 945 | return True 946 | if ymin < 0: 947 | print('y-coordinate: {0} below minimum of 0.'.format(ymin)) 948 | return True 949 | if xmax >= self.width: 950 | print('x-coordinate: {0} above maximum of {1}.'.format( 951 | xmax, self.width - 1)) 952 | return True 953 | if ymax >= self.height: 954 | print('y-coordinate: {0} above maximum of {1}.'.format( 955 | ymax, self.height - 1)) 956 | return True 957 | return False 958 | 959 | def load_sprite(self, path, w, h): 960 | """Load sprite image. 961 | 962 | Args: 963 | path (string): Image file path. 964 | w (int): Width of image. 965 | h (int): Height of image. 966 | Notes: 967 | w x h cannot exceed 2048 on boards w/o PSRAM 968 | """ 969 | buf_size = w * h * 2 970 | with open(path, "rb") as f: 971 | return f.read(buf_size) 972 | 973 | def reset_cpy(self): 974 | """Perform reset: Low=initialization, High=normal operation. 975 | 976 | Notes: CircuitPython implemntation 977 | """ 978 | self.rst.value = False 979 | sleep(.05) 980 | self.rst.value = True 981 | sleep(.05) 982 | 983 | def reset_mpy(self): 984 | """Perform reset: Low=initialization, High=normal operation. 985 | 986 | Notes: MicroPython implemntation 987 | """ 988 | self.rst(0) 989 | sleep(.05) 990 | self.rst(1) 991 | sleep(.05) 992 | 993 | def scroll(self, y): 994 | """Scroll display vertically. 995 | 996 | Args: 997 | y (int): Number of pixels to scroll display. 998 | """ 999 | self.write_cmd(self.VSCRSADD, y >> 8, y & 0xFF) 1000 | 1001 | def set_scroll(self, top, bottom): 1002 | """Set the height of the top and bottom scroll margins. 1003 | 1004 | Args: 1005 | top (int): Height of top scroll margin 1006 | bottom (int): Height of bottom scroll margin 1007 | """ 1008 | if top + bottom <= self.height: 1009 | middle = self.height - (top + bottom) 1010 | self.write_cmd(self.VSCRDEF, 1011 | top >> 8, 1012 | top & 0xFF, 1013 | middle >> 8, 1014 | middle & 0xFF, 1015 | bottom >> 8, 1016 | bottom & 0xFF) 1017 | 1018 | def sleep(self, enable=True): 1019 | """Enters or exits sleep mode. 1020 | 1021 | Args: 1022 | enable (bool): True (default)=Enter sleep mode, False=Exit sleep 1023 | """ 1024 | if enable: 1025 | self.write_cmd(self.SLPIN) 1026 | else: 1027 | self.write_cmd(self.SLPOUT) 1028 | 1029 | def write_cmd_mpy(self, command, *args): 1030 | """Write command to OLED (MicroPython). 1031 | 1032 | Args: 1033 | command (byte): ILI9341 command code. 1034 | *args (optional bytes): Data to transmit. 1035 | """ 1036 | self.dc(0) 1037 | self.cs(0) 1038 | self.spi.write(bytearray([command])) 1039 | self.cs(1) 1040 | # Handle any passed data 1041 | if len(args) > 0: 1042 | self.write_data(bytearray(args)) 1043 | 1044 | def write_cmd_cpy(self, command, *args): 1045 | """Write command to OLED (CircuitPython). 1046 | 1047 | Args: 1048 | command (byte): ILI9341 command code. 1049 | *args (optional bytes): Data to transmit. 1050 | """ 1051 | self.dc.value = False 1052 | self.cs.value = False 1053 | # Confirm SPI locked before writing 1054 | while not self.spi.try_lock(): 1055 | pass 1056 | self.spi.write(bytearray([command])) 1057 | self.spi.unlock() 1058 | self.cs.value = True 1059 | # Handle any passed data 1060 | if len(args) > 0: 1061 | self.write_data(bytearray(args)) 1062 | 1063 | def write_data_mpy(self, data): 1064 | """Write data to OLED (MicroPython). 1065 | 1066 | Args: 1067 | data (bytes): Data to transmit. 1068 | """ 1069 | self.dc(1) 1070 | self.cs(0) 1071 | self.spi.write(data) 1072 | self.cs(1) 1073 | 1074 | def write_data_cpy(self, data): 1075 | """Write data to OLED (CircuitPython). 1076 | 1077 | Args: 1078 | data (bytes): Data to transmit. 1079 | """ 1080 | self.dc.value = True 1081 | self.cs.value = False 1082 | # Confirm SPI locked before writing 1083 | while not self.spi.try_lock(): 1084 | pass 1085 | self.spi.write(data) 1086 | self.spi.unlock() 1087 | self.cs.value = True 1088 | --------------------------------------------------------------------------------