├── 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 |
--------------------------------------------------------------------------------