diff --git a/micropython.mk b/micropython.mk index 6fa0a34ed..5288c80c9 100644 --- a/micropython.mk +++ b/micropython.mk @@ -3,7 +3,7 @@ # LVGL unix optional libraries # Update CFLAGS_EXTMOD and LDFLAGS_EXTMOD for LVGL extenral library, # but do that only on the unix port, for unix specific dependencies - +ifeq ($(notdir $(CURDIR)),unix) ifneq ($(UNAME_S),Darwin) CFLAGS_EXTMOD += -DMICROPY_FB=1 endif @@ -36,6 +36,7 @@ ifneq ($(FFMPEG_LDFLAGS_EXTMOD),) CFLAGS_EXTMOD += $(FFMPEG_CFLAGS_EXTMOD) -DMICROPY_FFMPEG=1 LDFLAGS_EXTMOD += $(FFMPEG_LDFLAGS_EXTMOD) endif +endif ################################################################################ diff --git a/ports/stm32/manifest.py b/ports/stm32/manifest.py new file mode 100644 index 000000000..58dc529a6 --- /dev/null +++ b/ports/stm32/manifest.py @@ -0,0 +1 @@ +module("lv_utils.py", base_path="../../lib") diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..46383acb5 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,46 @@ + +### Tests: + +- `api/`: These tests are to test MicroPython-LVGL bindings. They can be +automated/included in CI. +To run from `micropython/tests`: +``` + ./run-tests.py ../../user_modules/lv_binding_micropython/tests/api/basic*.py -r . +``` + +- `display/`: These are to test the `api` + display driver. Intended for HIL +(Hardware in the loop) testing. Display only, no touch interface, touch is +automated and simulated in software. +To run from `micropython/tests`: +``` +./run-tests.py ../../user_modules/lv_binding_micropython/tests/display/basic*.py -r . +``` +e.g. in unix port a display will appear to provide visual feedback. + + +- `indev/`: These are to test the `display` + indev (touch) driver. Intended for +interactive HIL testing, e.g. they expect user input to complete the test. + +To run from `micropython/tests`: +``` +./run-tests.py ../../user_modules/lv_binding_micropython/tests/indev/basic*.py -r . +``` +e.g. in unix port a display will appear to allow user input. + +All tests are intended/expected to be run both in desktop (unix port) and in devices with the same result. + +For devices `testrunner.py`, `testdisplay.py` and `display_mode.py` need to be +uploaded. Also for display/indev testing a `hwdisplay.py` with a display driver +called `display` is expected. This `display` driver is expected to have at least a +```py + + def blit(self, x1, y1, w, h, buff): +``` +method or handle the lv display setup by itself (e.g setting buffers, `flush_cb`, etc) + +For interactive indev tests, it is required to have a +```py + + def read_cb(self, indev, data): +``` +method too, or handle indev creation by itself. diff --git a/tests/api/basic.py b/tests/api/basic.py new file mode 100644 index 000000000..703249234 --- /dev/null +++ b/tests/api/basic.py @@ -0,0 +1,60 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + _btn = lv.button(scr) + _btn.set_size(lv.pct(25), lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), 0) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name): + print(f"{name} PRESSED") + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name: button_cb(event, button_name), + lv.EVENT.CLICKED, + None, + ) + + await asyncio.sleep_ms(500) # await so the frame can be rendered + print("EVENT TEST:") + for _btn, name in _all_btns: + _btn.send_event(lv.EVENT.CLICKED, None) + await asyncio.sleep_ms(200) + + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode +except Exception: + _mode = None + +testrunner.run(demo, __file__, mode=_mode) +testrunner.devicereset() diff --git a/tests/api/basic.py.exp b/tests/api/basic.py.exp new file mode 100644 index 000000000..c0de908b1 --- /dev/null +++ b/tests/api/basic.py.exp @@ -0,0 +1,34 @@ + +FRAME: 0 (0, 0, 240, 32, 23040) +d5c5d09cff879bb12cb926dc44bf10161cded58d2057806e7cbde536540b1421 + +FRAME: 1 (0, 32, 240, 32, 23040) +f281e1fce42dc013342ad8a4573d74874238d995e6dff46dc29a1d68b780f920 + +FRAME: 2 (0, 64, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 3 (0, 96, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 4 (0, 128, 240, 32, 23040) +424125778438a53da017c2dca09964ec2cec5ad4e2689038dd0e830125112fd8 + +FRAME: 5 (0, 160, 240, 32, 23040) +9abb7f9219bb7ccc8784119c784b1bf41c451f9957989fd2a9fc12a15606b1d0 + +FRAME: 6 (0, 192, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 7 (0, 224, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 8 (0, 256, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 9 (0, 288, 240, 32, 23040) +f546d8ae7340f5fb71e30358ef0d6f33a4f2d72946d9b312444b07fa9d659396 +EVENT TEST: +RED PRESSED +GREEN PRESSED +BLUE PRESSED diff --git a/tests/api/basic_indev.py b/tests/api/basic_indev.py new file mode 100644 index 000000000..38ac0ed88 --- /dev/null +++ b/tests/api/basic_indev.py @@ -0,0 +1,77 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + _btn = lv.button(scr) + _btn.set_size(lv.pct(25), lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), 0) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name): + print(f"{name} PRESSED") + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name: button_cb(event, button_name), + lv.EVENT.CLICKED, + None, + ) + + await asyncio.sleep_ms(500) # await so the frame can be rendered + print("EVENT TEST:") + for _btn, name in _all_btns: + _btn.send_event(lv.EVENT.CLICKED, None) + await asyncio.sleep_ms(200) + + # simulate touch events + if display: + print("INDEV TEST:") + await display.touch(100, 100) + + await asyncio.sleep_ms(500) + + print("INDEV + BUTTONS TEST:") + # display.debug_indev(press=False, release=False) + display.debug_display(False) + for _btn, name in _all_btns: + pos = _btn.get_x(), _btn.get_y() + await display.touch(*pos) + await asyncio.sleep_ms(1000) + + await asyncio.sleep_ms(500) + + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode +except Exception: + _mode = None + +testrunner.run(demo, __file__, mode=_mode) +testrunner.devicereset() diff --git a/tests/api/basic_indev.py.exp b/tests/api/basic_indev.py.exp new file mode 100644 index 000000000..d8cbf7b85 --- /dev/null +++ b/tests/api/basic_indev.py.exp @@ -0,0 +1,47 @@ + +FRAME: 0 (0, 0, 240, 32, 23040) +d5c5d09cff879bb12cb926dc44bf10161cded58d2057806e7cbde536540b1421 + +FRAME: 1 (0, 32, 240, 32, 23040) +f281e1fce42dc013342ad8a4573d74874238d995e6dff46dc29a1d68b780f920 + +FRAME: 2 (0, 64, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 3 (0, 96, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 4 (0, 128, 240, 32, 23040) +424125778438a53da017c2dca09964ec2cec5ad4e2689038dd0e830125112fd8 + +FRAME: 5 (0, 160, 240, 32, 23040) +9abb7f9219bb7ccc8784119c784b1bf41c451f9957989fd2a9fc12a15606b1d0 + +FRAME: 6 (0, 192, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 7 (0, 224, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 8 (0, 256, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 9 (0, 288, 240, 32, 23040) +f546d8ae7340f5fb71e30358ef0d6f33a4f2d72946d9b312444b07fa9d659396 +EVENT TEST: +RED PRESSED +GREEN PRESSED +BLUE PRESSED +INDEV TEST: +[PRESSED]: (100,100) +[RELEASED]: (100,100) +INDEV + BUTTONS TEST: +[PRESSED]: (90,0) +RED PRESSED +[RELEASED]: (90,0) +[PRESSED]: (90,288) +GREEN PRESSED +[RELEASED]: (90,288) +[PRESSED]: (90,144) +BLUE PRESSED +[RELEASED]: (90,144) diff --git a/tests/api/basic_slider.py b/tests/api/basic_slider.py new file mode 100644 index 000000000..f226c5368 --- /dev/null +++ b/tests/api/basic_slider.py @@ -0,0 +1,77 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner # noqa + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + scr.set_style_pad_all(10, 0) + _btn = lv.slider(scr) + _btn.set_width(lv.pct(75)) + _btn.set_height(lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.set_style_text_color(lv.color_white(), 0) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.INDICATOR) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.MAIN) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.KNOB) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name, slider): + if slider.get_value() == 100: + print(f"{name} VALUE: {slider.get_value()}") + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name, slider=btn: button_cb( + event, button_name, slider + ), + lv.EVENT.VALUE_CHANGED, + None, + ) + + await asyncio.sleep_ms(500) # await so the frame can be rendered + # simulate touch events + if display: + print("INDEV + SLIDER TEST:") + display.debug_indev(press=False) + display.debug_display(False) + for _btn, name in _all_btns: + pos = _btn.get_x(), _btn.get_y() + pos2 = _btn.get_x2(), _btn.get_y2() + x1, y1 = pos + x2, y2 = pos2 + y_mid = y2 - ((y2 - y1) // 2) + await display.swipe(x1 + 5, y_mid, x2 + (y2 - y1), y_mid, ms=500) + await asyncio.sleep_ms(100) + + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode +except Exception: + _mode = None + +testrunner.run(demo, __file__, mode=_mode) +testrunner.devicereset() diff --git a/tests/api/basic_slider.py.exp b/tests/api/basic_slider.py.exp new file mode 100644 index 000000000..c6359c382 --- /dev/null +++ b/tests/api/basic_slider.py.exp @@ -0,0 +1,37 @@ + +FRAME: 0 (0, 0, 240, 32, 23040) +6e5737038637abc5ea724930a5113dd9193a3e708b13c3be75d2e5164ccfc57a + +FRAME: 1 (0, 32, 240, 32, 23040) +5c139099d39acc6aa2081459fb397f698035937288fd088b60325f11d8d839a9 + +FRAME: 2 (0, 64, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 3 (0, 96, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 4 (0, 128, 240, 32, 23040) +85b39d4c5e001bd4aa82e2c0efd390d2f1c20517426ebc07a63a64c8315dcdc4 + +FRAME: 5 (0, 160, 240, 32, 23040) +798e52f4dd160d6e592d0d4d075ef4779eeffed0a4f2339aa9b524e2fe008eae + +FRAME: 6 (0, 192, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 7 (0, 224, 240, 32, 23040) +46e2096b907947368d310929303a04005b39c4a278e3a7de2225c355b4522694 + +FRAME: 8 (0, 256, 240, 32, 23040) +a6b9cdacc2013dbb3ce95198217d24ce42333d0178da7fddd15ff353ab012891 + +FRAME: 9 (0, 288, 240, 32, 23040) +08943f10a0eeb2c8a3b8ec437fd2d0725b5a764d29375282553eaafd51ff704a +INDEV + SLIDER TEST: +RED VALUE: 100 +[RELEASED]: (218,15) +GREEN VALUE: 100 +[RELEASED]: (218,285) +BLUE VALUE: 100 +[RELEASED]: (218,150) diff --git a/tests/display/basic.py b/tests/display/basic.py new file mode 100644 index 000000000..703249234 --- /dev/null +++ b/tests/display/basic.py @@ -0,0 +1,60 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + _btn = lv.button(scr) + _btn.set_size(lv.pct(25), lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), 0) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name): + print(f"{name} PRESSED") + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name: button_cb(event, button_name), + lv.EVENT.CLICKED, + None, + ) + + await asyncio.sleep_ms(500) # await so the frame can be rendered + print("EVENT TEST:") + for _btn, name in _all_btns: + _btn.send_event(lv.EVENT.CLICKED, None) + await asyncio.sleep_ms(200) + + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode +except Exception: + _mode = None + +testrunner.run(demo, __file__, mode=_mode) +testrunner.devicereset() diff --git a/tests/display/basic.py.exp b/tests/display/basic.py.exp new file mode 100644 index 000000000..bdb7f65e8 --- /dev/null +++ b/tests/display/basic.py.exp @@ -0,0 +1,5 @@ +DISPLAY_MODE: INTERACTIVE +EVENT TEST: +RED PRESSED +GREEN PRESSED +BLUE PRESSED diff --git a/tests/display/basic_indev.py b/tests/display/basic_indev.py new file mode 100644 index 000000000..6dde2bc63 --- /dev/null +++ b/tests/display/basic_indev.py @@ -0,0 +1,77 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + _btn = lv.button(scr) + _btn.set_size(lv.pct(25), lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), 0) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name): + print(f"{name} PRESSED") + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name: button_cb(event, button_name), + lv.EVENT.CLICKED, + None, + ) + + await asyncio.sleep_ms(500) # await so the frame can be rendered + print("EVENT TEST:") + for _btn, name in _all_btns: + _btn.send_event(lv.EVENT.CLICKED, None) + await asyncio.sleep_ms(200) + + # simulate touch events + if display: + print("INDEV TEST:") + await display.touch(100, 100) + + await asyncio.sleep_ms(500) + + print("INDEV + BUTTONS TEST:") + # display.debug_indev(press=True, release=True) + display.debug_display(False) + for _btn, name in _all_btns: + pos = _btn.get_x(), _btn.get_y() + await display.touch(*pos) + await asyncio.sleep_ms(1000) + + await asyncio.sleep_ms(500) + + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode +except Exception: + _mode = None + +testrunner.run(demo, __file__, mode=_mode) +testrunner.devicereset() diff --git a/tests/display/basic_indev.py.exp b/tests/display/basic_indev.py.exp new file mode 100644 index 000000000..25f341f5e --- /dev/null +++ b/tests/display/basic_indev.py.exp @@ -0,0 +1,18 @@ +DISPLAY_MODE: INTERACTIVE +EVENT TEST: +RED PRESSED +GREEN PRESSED +BLUE PRESSED +INDEV TEST: +[PRESSED]: (100,100) +[RELEASED]: (100,100) +INDEV + BUTTONS TEST: +[PRESSED]: (90,0) +RED PRESSED +[RELEASED]: (90,0) +[PRESSED]: (90,288) +GREEN PRESSED +[RELEASED]: (90,288) +[PRESSED]: (90,144) +BLUE PRESSED +[RELEASED]: (90,144) diff --git a/tests/display/basic_slider.py b/tests/display/basic_slider.py new file mode 100644 index 000000000..f226c5368 --- /dev/null +++ b/tests/display/basic_slider.py @@ -0,0 +1,77 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner # noqa + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + scr.set_style_pad_all(10, 0) + _btn = lv.slider(scr) + _btn.set_width(lv.pct(75)) + _btn.set_height(lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.set_style_text_color(lv.color_white(), 0) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.INDICATOR) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.MAIN) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.KNOB) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name, slider): + if slider.get_value() == 100: + print(f"{name} VALUE: {slider.get_value()}") + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name, slider=btn: button_cb( + event, button_name, slider + ), + lv.EVENT.VALUE_CHANGED, + None, + ) + + await asyncio.sleep_ms(500) # await so the frame can be rendered + # simulate touch events + if display: + print("INDEV + SLIDER TEST:") + display.debug_indev(press=False) + display.debug_display(False) + for _btn, name in _all_btns: + pos = _btn.get_x(), _btn.get_y() + pos2 = _btn.get_x2(), _btn.get_y2() + x1, y1 = pos + x2, y2 = pos2 + y_mid = y2 - ((y2 - y1) // 2) + await display.swipe(x1 + 5, y_mid, x2 + (y2 - y1), y_mid, ms=500) + await asyncio.sleep_ms(100) + + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode +except Exception: + _mode = None + +testrunner.run(demo, __file__, mode=_mode) +testrunner.devicereset() diff --git a/tests/display/basic_slider.py.exp b/tests/display/basic_slider.py.exp new file mode 100644 index 000000000..6dc1b3cc5 --- /dev/null +++ b/tests/display/basic_slider.py.exp @@ -0,0 +1,8 @@ +DISPLAY_MODE: INTERACTIVE +INDEV + SLIDER TEST: +RED VALUE: 100 +[RELEASED]: (218,15) +GREEN VALUE: 100 +[RELEASED]: (218,285) +BLUE VALUE: 100 +[RELEASED]: (218,150) diff --git a/tests/display/display_mode.py b/tests/display/display_mode.py new file mode 100644 index 000000000..c6af2743b --- /dev/null +++ b/tests/display/display_mode.py @@ -0,0 +1 @@ +MODE = "interactive" diff --git a/tests/imageconvert.py b/tests/imageconvert.py new file mode 100755 index 000000000..4157cfe3d --- /dev/null +++ b/tests/imageconvert.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +from PIL import Image +import sys +import argparse + +parser = argparse.ArgumentParser(description="RGB888 to PNG converter") +parser.add_argument("file", help="file/s name", nargs="*") + +args = parser.parse_args() + +for file in args.file: + with open(file, "rb") as ff: + w, h, cs = ff.readline().split(b":") + width, height = int(w.decode()), int(h.decode()) + frame = ff.read() + assert len(frame) == width * height * int(cs) + + image = Image.new("RGB", (width, height)) + image.frombytes(frame) + image.save(file.replace(".bin", ".png"), "PNG") + image.close() diff --git a/tests/indev/basic.py b/tests/indev/basic.py new file mode 100644 index 000000000..1a96ad735 --- /dev/null +++ b/tests/indev/basic.py @@ -0,0 +1,69 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +class TestButton(lv.button): + def __init__(self, parent): + super().__init__(parent) + self.event_press = asyncio.Event() + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + _btn = TestButton(scr) + _btn.set_size(lv.pct(25), lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), 0) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name, button): + print(f"{name} PRESSED") + button.event_press.set() + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name, button=btn: button_cb( + event, button_name, button + ), + lv.EVENT.CLICKED, + None, + ) + + await asyncio.sleep_ms(500) # await so the frame can be rendered + print("PRESS EVENT TEST:") + for _btn, name in _all_btns: + await _btn.event_press.wait() + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode + from display_mode import POINTER as _pointer +except Exception: + _mode = "test" + _pointer = "sim" + +testrunner.run(demo, __file__, mode=_mode, pointer=_pointer) +testrunner.devicereset() diff --git a/tests/indev/basic.py.exp b/tests/indev/basic.py.exp new file mode 100644 index 000000000..83badc00a --- /dev/null +++ b/tests/indev/basic.py.exp @@ -0,0 +1,5 @@ +DISPLAY_MODE: INTERACTIVE +PRESS EVENT TEST: +RED PRESSED +BLUE PRESSED +GREEN PRESSED diff --git a/tests/indev/basic_slider.py b/tests/indev/basic_slider.py new file mode 100644 index 000000000..481ddb453 --- /dev/null +++ b/tests/indev/basic_slider.py @@ -0,0 +1,76 @@ +import lvgl as lv +import sys +import asyncio +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) +import testrunner # noqa + +# This is a basic test to test buttons, labels, +# RGB colors, layout aligment and events. + + +class TestSlider(lv.slider): + def __init__(self, parent): + super().__init__(parent) + self.event_completed = asyncio.Event() + + +async def demo(scr, display=None): + def get_button(scr, text, align, color): + scr.set_style_pad_all(10, 0) + _btn = TestSlider(scr) + _btn.set_width(lv.pct(75)) + _btn.set_height(lv.pct(10)) + _lab = lv.label(_btn) + _lab.set_text(text) + _lab.set_style_text_color(lv.color_white(), 0) + _lab.center() + _btn.set_style_align(align, 0) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.INDICATOR) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.MAIN) + _btn.set_style_bg_color(lv.color_make(*color), lv.PART.KNOB) + return _btn, text + + buttons = [ + ("RED", lv.ALIGN.TOP_MID, (255, 0, 0)), + ("GREEN", lv.ALIGN.BOTTOM_MID, (0, 255, 0)), + ("BLUE", lv.ALIGN.CENTER, (0, 0, 255)), + ] + + def button_cb(event, name, slider): + if slider.get_value() == 100: + if not slider.event_completed.is_set(): + print(f"{name} VALUE: {slider.get_value()}") + slider.event_completed.set() + + _all_btns = [get_button(scr, *btn) for btn in buttons] + + for btn, name in _all_btns: + btn.add_event_cb( + lambda event, button_name=name, slider=btn: button_cb( + event, button_name, slider + ), + lv.EVENT.VALUE_CHANGED, + None, + ) + + print("INDEV + SLIDER TEST:") + display.debug_indev(press=False) + display.debug_display(False) + for _btn, name in _all_btns: + await _btn.event_completed.wait() + return _all_btns + + +__file__ = globals().get("__file__", "test") + +try: + from display_mode import MODE as _mode + from display_mode import POINTER as _pointer +except Exception: + _mode = None + +testrunner.run(demo, __file__, mode=_mode, pointer=_pointer) +testrunner.devicereset() diff --git a/tests/indev/basic_slider.py.exp b/tests/indev/basic_slider.py.exp new file mode 100644 index 000000000..fe869d3ed --- /dev/null +++ b/tests/indev/basic_slider.py.exp @@ -0,0 +1,5 @@ +DISPLAY_MODE: INTERACTIVE +INDEV + SLIDER TEST: +RED VALUE: 100 +BLUE VALUE: 100 +GREEN VALUE: 100 diff --git a/tests/indev/display_mode.py b/tests/indev/display_mode.py new file mode 100644 index 000000000..54dfb8c14 --- /dev/null +++ b/tests/indev/display_mode.py @@ -0,0 +1,2 @@ +MODE = "interactive" +POINTER = False diff --git a/tests/testdisplay.py b/tests/testdisplay.py new file mode 100644 index 000000000..1f7e71433 --- /dev/null +++ b/tests/testdisplay.py @@ -0,0 +1,247 @@ +# adapted from https://github.com/bdbarnett/mpdisplay +# utils/lv_mpdisplay.py + +import lvgl as lv +import hashlib +from binascii import hexlify +import lv_utils +import sys +import asyncio + + +class TestDisplayDriver: + def __init__( + self, + display_drv, + frame_buffer1, + frame_buffer2, + color_format=lv.COLOR_FORMAT.RGB565, + mode="test", + pointer="sim", + fps=25, + ): + self.display_drv = display_drv + self._color_size = lv.color_format_get_size(color_format) + self._frame_buffer1 = frame_buffer1 + self._frame_buffer2 = frame_buffer2 + self._x = 0 + self._y = 0 + self._dstate = None + self._press_event = False + self._release_event = False + self._debug_press = True + self._debug_release = True + self.mode = mode + + render_mode = lv.DISPLAY_RENDER_MODE.PARTIAL + + if not lv_utils.event_loop.is_running(): + self.event_loop = lv_utils.event_loop(freq=fps, asynchronous=True) + + else: + self.event_loop = lv_utils.event_loop.current_instance() + self.event_loop.scheduled = 0 + + if not lv.is_initialized(): + lv.init() + + if mode == "test" or ( + mode == "interactive" and not isinstance(display_drv, DummyDisplay) + ): + if hasattr(display_drv, "blit"): + self.lv_display = lv.display_create( + self.display_drv.width, self.display_drv.height + ) + self.lv_display.set_color_format(color_format) + self.lv_display.set_flush_cb(self._flush_cb) + self.lv_display.set_buffers( + self._frame_buffer1, + self._frame_buffer2, + len(self._frame_buffer1), + render_mode, + ) + self.indev_test = lv.indev_create() + self.indev_test.set_display(lv.display_get_default()) + self.indev_test.set_group(lv.group_get_default()) + # TODO: test other types of indev + self.indev_test.set_type(lv.INDEV_TYPE.POINTER) + if hasattr(display_drv, "read_cb") and pointer != "sim": + self.indev_test.set_read_cb(display_drv.read_cb) + + else: + self.indev_test.set_read_cb(self._read_cb) + + else: # interactive + DummyDisplay -> SDL + self.group = lv.group_create() + self.group.set_default() + self.lv_display_int = lv.sdl_window_create( + display_drv.width, display_drv.height + ) + lv.sdl_window_set_title(self.lv_display_int, "MicroPython-LVGL") + self.mouse = lv.sdl_mouse_create() + self.keyboard = lv.sdl_keyboard_create() + self.keyboard.set_group(self.group) + if pointer == "sim": + self.indev = lv.indev_create() + self.indev.set_display(self.lv_display_int) + self.indev.set_group(self.group) + self.indev.set_type(lv.INDEV_TYPE.POINTER) + # NOTE: only one indev pointer allowed, use the keyboard + # for interactive control + self.indev.set_read_cb(self._read_cb) + + def set_test_name(self, name): + self.display_drv.test_name = name + + def debug_indev(self, press=None, release=None): + self._debug_press = press if press is not None else self._debug_press + self._debug_release = release if release is not None else self._debug_release + + def debug_display(self, debug=True): + if hasattr(self.display_drv, "debug"): + self.display_drv.debug = debug + + async def touch(self, x, y, ms=100): + self._x = x + self._y = y + self._dstate = lv.INDEV_STATE.PRESSED + self._press_event = True + await asyncio.sleep_ms(ms) + + self._dstate = lv.INDEV_STATE.RELEASED + self._release_event = True + await asyncio.sleep_ms(25) + + async def swipe(self, x1, y1, x2, y2, steps=5, ms=100): + self._dstate = lv.INDEV_STATE.PRESSED + if y1 == y2: # HORIZONTAL + self._y = y1 + self._x = x1 + if x2 < x1: + steps = -steps # RIGHT-LEFT + for xi in range(x1, x2, steps): + self._x = xi + self._press_event = True + await asyncio.sleep_ms(ms // steps) + elif x1 == x2: + self._y = y1 + self._x = x1 + if y2 < y1: + steps = -steps # BOTTOM-UP + for yi in range(y1, y2, steps): + self._y = yi + self._press_event = True + await asyncio.sleep_ms(ms // steps) + + self._dstate = lv.INDEV_STATE.RELEASED + self._release_event = True + await asyncio.sleep_ms(25) + + def _flush_cb(self, disp_drv, area, color_p): + width = area.x2 - area.x1 + 1 + height = area.y2 - area.y1 + 1 + + self.display_drv.blit( + area.x1, + area.y1, + width, + height, + color_p.__dereference__(width * height * self._color_size), + ) + self.lv_display.flush_ready() + + def _read_cb(self, indev, data): + if self._press_event: + self._press_event = False + if self._debug_press: + print(f"[PRESSED]: ({self._x},{self._y})") + data.point = lv.point_t({"x": self._x, "y": self._y}) + data.state = self._dstate + elif self._release_event: + self._release_event = False + if self._debug_release: + print(f"[RELEASED]: ({self._x},{self._y})") + data.state = self._dstate + + +class DummyDisplay: + def __init__(self, width=240, height=320, color_format=lv.COLOR_FORMAT.RGB565): + self.width = width + self.height = height + self.color_depth = lv.color_format_get_bpp(color_format) + self.color_size = lv.color_format_get_size(color_format) + self.n = 0 + self.test_name = "testframe" + self._header_set = False + self._save_frame = sys.platform in ["darwin", "linux"] + self._debug = True + + @property + def debug(self): + return self._debug + + @debug.setter + def debug(self, x): + self._debug = x + self._save_frame = x + + def save_frame(self, data): + if not self._header_set: + self._header_set = True + with open(f"{self.test_name}.bin", "wb") as fr: + fr.write(f"{self.width}:{self.height}:{self.color_size}\n".encode()) + + with open(f"{self.test_name}.bin", "ab") as fr: + fr.write(data) + + def _shasum_frame(self, data): + _hash = hashlib.sha256() + _hash.update(data) + _result = _hash.digest() + result = hexlify(_result).decode() + return result + + def blit(self, x1, y1, w, h, buff): + if self.debug: + print(f"\nFRAME: {self.n} {(x1, y1, w, h, len(buff[:]))}") + print(self._shasum_frame(bytes(buff[:]))) + if self._save_frame: + self.save_frame(buff[:]) + self.n += 1 + + +tdisp = DummyDisplay(color_format=lv.COLOR_FORMAT.RGB888) + + +alloc_buffer = lambda buffersize: memoryview(bytearray(buffer_size)) + +factor = 10 ### Must be 1 if using an RGBBus +double_buf = True ### Must be False if using an RGBBus + +buffer_size = tdisp.width * tdisp.height * (tdisp.color_depth // 8) // factor + +fbuf1 = alloc_buffer(buffer_size) +fbuf2 = alloc_buffer(buffer_size) if double_buf else None + + +def get_display( + width=240, + height=320, + disp=tdisp, + color_format=lv.COLOR_FORMAT.RGB888, + mode="test", + pointer="sim", +): + if mode == "test": + disp = tdisp + elif mode == "interactive": + print("DISPLAY_MODE: INTERACTIVE") + try: + from hwdisplay import display as disp + except Exception as e: + if sys.platform not in ["darwin", "linux"]: + sys.print_exception(e) + if hasattr(disp, "width") and hasattr(disp, "height"): + disp.width = width + disp.height = height + return TestDisplayDriver(disp, fbuf1, fbuf2, color_format, mode, pointer) diff --git a/tests/testrunner.py b/tests/testrunner.py new file mode 100644 index 000000000..71176c09f --- /dev/null +++ b/tests/testrunner.py @@ -0,0 +1,65 @@ +import sys +import os + +sys.path.append("..") +sys.path.append(os.getcwd()) + +from testdisplay import get_display # noqa + +_int = sys.argv.pop() if sys.platform in ["darwin", "linux"] else "" +_mode = "test" +if _int in ("-id", "-d"): + _mode = "interactive" + + +async def run_test(func, display=None): + import lvgl as lv # noqa + + lv.init() + + scr = lv.obj() + scr.set_style_bg_color(lv.color_black(), 0) + lv.screen_load(scr) + + resp = await func(scr, display) + return scr, resp + + +def run(func, filename, w=240, h=320, mode=None, **kwargs): + import asyncio + + # import micropython # noqa + + # micropython.mem_info() + + async def _run(func, w, h, mode=None, **kwargs): + display = get_display( + w, + h, + mode=mode if mode is not None else _mode, + pointer=kwargs.get("pointer", "sim"), + ) + + if display.mode == "test": + display.set_test_name(f"{filename.replace('.py', '')}.{func.__name__}") + await run_test(func, display) + await asyncio.sleep_ms(100) + elif display.mode == "interactive": + await run_test(func, display) + if _int == "-id": + while True: + try: + await asyncio.sleep_ms(1000) + except KeyboardInterrupt: + sys.exit() + except Exception as e: + sys.print_exception(e) + + asyncio.run(_run(func, w, h, mode, **kwargs)) + + +def devicereset(): + import lvgl as lv + + if lv.is_initialized(): + lv.deinit()