diff --git a/ports/esp32/boards/ESP32_GENERIC_S3/manifest.py b/ports/esp32/boards/ESP32_GENERIC_S3/manifest.py new file mode 100644 index 0000000000000..5b1cf9f7125e8 --- /dev/null +++ b/ports/esp32/boards/ESP32_GENERIC_S3/manifest.py @@ -0,0 +1,2 @@ +include("$(PORT_DIR)/boards/manifest.py") +include("$(MPY_DIR)/user_modules/lv_binding_micropython/ports/esp32") diff --git a/ports/esp32/boards/ESP32_GENERIC_S3/mpconfigboard.cmake b/ports/esp32/boards/ESP32_GENERIC_S3/mpconfigboard.cmake index 2bfdad21df495..6fe1545e719a5 100644 --- a/ports/esp32/boards/ESP32_GENERIC_S3/mpconfigboard.cmake +++ b/ports/esp32/boards/ESP32_GENERIC_S3/mpconfigboard.cmake @@ -8,3 +8,7 @@ set(SDKCONFIG_DEFAULTS boards/sdkconfig.spiram_sx boards/ESP32_GENERIC_S3/sdkconfig.board ) + +set(LV_CFLAGS -DLV_COLOR_DEPTH=16 -DLV_COLOR_16_SWAP=1) + +set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py) \ No newline at end of file diff --git a/ports/esp32/esp32_common.cmake b/ports/esp32/esp32_common.cmake index 750d8e5c65c6d..d4f07da7609b1 100644 --- a/ports/esp32/esp32_common.cmake +++ b/ports/esp32/esp32_common.cmake @@ -11,7 +11,7 @@ endif() # Include core source components. include(${MICROPY_DIR}/py/py.cmake) -set(USER_C_MODULES "${MICROPY_DIR}/user_modules/lv_binding_micropython/micropython.cmake") +list(APPEND USER_C_MODULES "${MICROPY_DIR}/user_modules/lv_binding_micropython/micropython.cmake") # CMAKE_BUILD_EARLY_EXPANSION is set during the component-discovery phase of # `idf.py build`, so none of the extmod/usermod (and in reality, most of the diff --git a/ports/webassembly/Makefile b/ports/webassembly/Makefile index 435636531cc26..d00efe1bef86e 100644 --- a/ports/webassembly/Makefile +++ b/ports/webassembly/Makefile @@ -46,6 +46,7 @@ INC += -I$(BUILD) INC += -I$(VARIANT_DIR) CFLAGS += -std=c99 -Wall -Werror -Wdouble-promotion -Wfloat-conversion +CFLAGS += -Wno-unused-function CFLAGS += -Os -DNDEBUG CFLAGS += $(INC) @@ -77,7 +78,8 @@ EXPORTED_RUNTIME_METHODS_EXTRA += ,\ getValue,\ lengthBytesUTF8,\ setValue,\ - stringToUTF8 + stringToUTF8, \ + setMainLoop JSFLAGS += -s EXPORTED_FUNCTIONS="\ _free,\ @@ -95,6 +97,7 @@ JSFLAGS += --js-library library.js JSFLAGS += -s SUPPORT_LONGJMP=emscripten JSFLAGS += -s MODULARIZE -s EXPORT_NAME=_createMicroPythonModule + ################################################################################ # Source files and libraries. @@ -130,7 +133,7 @@ OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) ################################################################################ # Main targets. -.PHONY: all repl min test test_min +.PHONY: all repl min test test_min all: $(BUILD)/micropython.mjs diff --git a/ports/webassembly/api.js b/ports/webassembly/api.js index 0718d13d6786f..183ed4409d631 100644 --- a/ports/webassembly/api.js +++ b/ports/webassembly/api.js @@ -35,12 +35,15 @@ // - stderr: same behaviour as stdout but for error output. // - linebuffer: whether to buffer line-by-line to stdout/stderr. export async function loadMicroPython(options) { - const { pystack, heapsize, url, stdin, stdout, stderr, linebuffer } = + const { pystack, heapsize, url, stdin, stdout, stderr, linebuffer, canvasRef } = Object.assign( { pystack: 2 * 1024, heapsize: 1024 * 1024, linebuffer: true }, options, ); let Module = {}; + if (canvasRef) { + Module.canvas = canvasRef; + } Module.locateFile = (path, scriptDirectory) => url || scriptDirectory + path; Module._textDecoder = new TextDecoder(); @@ -105,6 +108,7 @@ export async function loadMicroPython(options) { [pystack, heapsize], ); Module.ccall("proxy_c_init", "null", [], []); + return { _module: Module, PyProxy: PyProxy, diff --git a/ports/webassembly/index.html b/ports/webassembly/index.html new file mode 100644 index 0000000000000..25d597cbad325 --- /dev/null +++ b/ports/webassembly/index.html @@ -0,0 +1,84 @@ + + + + + µPy + term.js REPL + + + + + + +
+
+ +
+ + + + + + \ No newline at end of file diff --git a/ports/webassembly/variants/lvgl/manifest.py b/ports/webassembly/variants/lvgl/manifest.py new file mode 100644 index 0000000000000..3901e78dae782 --- /dev/null +++ b/ports/webassembly/variants/lvgl/manifest.py @@ -0,0 +1,2 @@ +include("$(PORT_DIR)/variants/pyscript/manifest.py") +freeze("$(PORT_DIR)/variants/lvgl/modules") \ No newline at end of file diff --git a/ports/webassembly/variants/lvgl/modules/display_driver.py b/ports/webassembly/variants/lvgl/modules/display_driver.py new file mode 100644 index 0000000000000..53f6201452f19 --- /dev/null +++ b/ports/webassembly/variants/lvgl/modules/display_driver.py @@ -0,0 +1,2 @@ +from display_driver_utils import driver +drv = driver() \ No newline at end of file diff --git a/ports/webassembly/variants/lvgl/modules/display_driver_utils.py b/ports/webassembly/variants/lvgl/modules/display_driver_utils.py new file mode 100644 index 0000000000000..018478adc700b --- /dev/null +++ b/ports/webassembly/variants/lvgl/modules/display_driver_utils.py @@ -0,0 +1,137 @@ +import usys as sys +sys.path.append('') # See: https://github.com/micropython/micropython/issues/6419 + +import lvgl as lv + +try: + import lv_utils + lv_utils_available = True +except: + lv_utils_available = False + +ORIENT_LANDSCAPE = False +ORIENT_PORTRAIT = True + +class driver: + + def __init__(self,width=420,height=320,orientation=ORIENT_PORTRAIT, asynchronous=False, exception_sink=None, defaultGroup=True): + + if not lv.is_initialized(): + lv.init() + + self.group = lv.group_create() + if defaultGroup: + self.group.set_default() + + self.width = width + self.height = height + self.orientation = orientation + self.asynchronous = asynchronous + self.exception_sink = exception_sink + self.disp = None + self.touch = None + self.type = None + if not (lv_utils_available and lv_utils.event_loop.is_running()): + self.init_gui() + + def init_gui_SDL(self): + if lv_utils_available: + self.event_loop = lv_utils.event_loop(asynchronous=self.asynchronous, exception_sink=self.exception_sink) + + lv.sdl_window_create(self.width, self.height) + self.mouse = lv.sdl_mouse_create() + self.keyboard = lv.sdl_keyboard_create() + self.keyboard.set_group(self.group) + self.type = "SDL" + print("Running the SDL lvgl version") + + def init_gui_ili9341(self): + + # Initialize ILI9341 display + + from ili9XXX import ili9341,LANDSCAPE + from xpt2046 import xpt2046 + #import espidf as esp + + if lv_utils_available: + self.event_loop = lv_utils.event_loop(asynchronous=self.asynchronous, exception_sink=self.exception_sink) + + if self.orientation == ORIENT_PORTRAIT: + print ("Running the ili9341 lvgl version in portrait mode") + + # Initialize ILI9341 display in prtrait mode + # the following are the settings for the Lolin tft 2.4 display + # self.disp = ili9341(miso=19,mosi=23,clk=18, cs=26, dc=5, rst=-1, power=-1, backlight=-1, spihost=esp.VSPI_HOST) + # self.touch = xpt2046(spihost=esp.VSPI_HOST,cal_x0=3751, cal_x1 = 210, cal_y0=3906, cal_y1 = 283, transpose=True) + + self.disp = ili9341() + self.touch = xpt2046() + + elif self.orientation == ORIENT_LANDSCAPE: + print ("Running the ili9341 lvgl version in landscape mode") + # Initialize ILI9341 display + # the following are the settings for the Lolin tft 2.4 display + # self.disp = ili9341(miso=19,mosi=23,clk=18, cs=26, dc=5, rst=-1, power=-1, backlight=-1, backlight_on=0, + # spihost=esp.VSPI_HOST, width=320, height=240, rot=LANDSCAPE) + # self.touch = xpt2046(spihost=esp.VSPI_HOST,cal_x0=3799, cal_x1 = 353, cal_y0=220, cal_y1 = 3719, transpose=False) + + # Register xpt2046 touch driver + self.disp = ili9341(width=320, height=240, rot=LANDSCAPE) + self.touch = xpt2046(cal_x0=3799, cal_x1 = 353, cal_y0=220, cal_y1 = 3719,transpose = False) + + else: + raise RuntimeError("Invalid orientation") + + self.type="ili9341" + + ''' + # Register raw resistive touch driver (remove xpt2046 initialization first) + import rtch + touch = rtch.touch(xp = 32, yp = 33, xm = 25, ym = 26, touch_rail = 27, touch_sense = 33) + touch.init() + self.indev_drv = lv.indev_create() + self.indev_drv.set_type(lv.INDEV_TYPE.POINTER) + self.indev_drv.set_read_cb(touch.read) + ''' + + def init_gui_twatch(self): + + import ttgo + from axp_constants import AXP202_VBUS_VOL_ADC1,AXP202_VBUS_CUR_ADC1,AXP202_BATT_CUR_ADC1,AXP202_BATT_VOL_ADC1 + + watch = ttgo.Watch() + tft = watch.tft + power = watch.pmu + power.adc1Enable(AXP202_VBUS_VOL_ADC1 + | AXP202_VBUS_CUR_ADC1 + | AXP202_BATT_CUR_ADC1 + | AXP202_BATT_VOL_ADC1, True) + watch.lvgl_begin() + watch.tft.backlight_fade(100) + + self.type="t-watch" + print("Running lvgl on the LilyGo t-watch 2020") + + def init_gui(self): + + # Identify platform and initialize it + + try: + self.init_gui_twatch() + return + except: + pass + + try: + self.init_gui_ili9341() + return + except ImportError: + pass + + try: + self.init_gui_SDL() + return + except ImportError: + pass + + raise RuntimeError("Could not find a suitable display driver!") diff --git a/ports/webassembly/variants/lvgl/modules/lv_utils.py b/ports/webassembly/variants/lvgl/modules/lv_utils.py new file mode 100644 index 0000000000000..fd925ae40a086 --- /dev/null +++ b/ports/webassembly/variants/lvgl/modules/lv_utils.py @@ -0,0 +1,196 @@ +############################################################################## +# Event Loop module: advancing tick count and scheduling lvgl task handler. +# Import after lvgl module. +# This should be imported and used by display driver. +# Display driver should first check if an event loop is already running. +# +# Usage example with SDL: +# +# SDL.init(auto_refresh=False) +# # Register SDL display driver. +# # Register SDL mouse driver +# event_loop = lv_utils.event_loop() +# +# +# asyncio example with SDL: +# +# SDL.init(auto_refresh=False) +# # Register SDL display driver. +# # Register SDL mouse driver +# event_loop = lv_utils.event_loop(asynchronous=True) +# asyncio.Loop.run_forever() +# +# asyncio example with ili9341: +# +# event_loop = lv_utils.event_loop(asynchronous=True) # Optional! +# self.disp = ili9341(asynchronous=True) +# asyncio.Loop.run_forever() +# +# MIT license; Copyright (c) 2021 Amir Gonnen +# +############################################################################## + +import lvgl as lv +import micropython +import sys + +# Try standard machine.Timer, or custom timer from lv_timer, if available + +try: + from machine import Timer +except: + try: + from lv_timer import Timer + except: + if sys.platform != "darwin": + raise RuntimeError("Missing machine.Timer implementation!") + Timer = False + +# Try to determine default timer id + +default_timer_id = 0 +if sys.platform == "pyboard": + # stm32 only supports SW timer -1 + default_timer_id = -1 + +if sys.platform == "rp2": + # rp2 only supports SW timer -1 + default_timer_id = -1 + +# Try importing asyncio, if available + +try: + import asyncio + + asyncio_available = True +except: + asyncio_available = False + +############################################################################## + + +class event_loop: + _current_instance = None + + def __init__( + self, + freq=25, + timer_id=default_timer_id, + max_scheduled=2, + refresh_cb=None, + asynchronous=False, + exception_sink=None, + ): + if self.is_running(): + raise RuntimeError("Event loop is already running!") + + if not lv.is_initialized(): + lv.init() + + event_loop._current_instance = self + + self.delay = 1000 // freq + self.refresh_cb = refresh_cb + self.exception_sink = ( + exception_sink if exception_sink else self.default_exception_sink + ) + + self.asynchronous = asynchronous + if self.asynchronous: + if not asyncio_available: + raise RuntimeError( + "Cannot run asynchronous event loop. asyncio is not available!" + ) + self.refresh_event = asyncio.Event() + self.refresh_task = asyncio.create_task(self.async_refresh()) + self.timer_task = asyncio.create_task(self.async_timer()) + else: + if Timer: + self.timer = Timer(timer_id) + self.timer.init( + mode=Timer.PERIODIC, period=self.delay, callback=self.timer_cb + ) + self.task_handler_ref = self.task_handler # Allocation occurs here + self.max_scheduled = max_scheduled + self.scheduled = 0 + + def init_async(self): + self.refresh_event = asyncio.Event() + self.refresh_task = asyncio.create_task(self.async_refresh()) + self.timer_task = asyncio.create_task(self.async_timer()) + + def deinit(self): + if self.asynchronous: + self.refresh_task.cancel() + self.timer_task.cancel() + else: + if Timer: + self.timer.deinit() + event_loop._current_instance = None + + def disable(self): + self.scheduled += self.max_scheduled + + def enable(self): + self.scheduled -= self.max_scheduled + + @staticmethod + def is_running(): + return event_loop._current_instance is not None + + @staticmethod + def current_instance(): + return event_loop._current_instance + + def task_handler(self, _): + try: + if lv._nesting.value == 0: + lv.task_handler() + if self.refresh_cb: + self.refresh_cb() + self.scheduled -= 1 + except Exception as e: + if self.exception_sink: + self.exception_sink(e) + + def tick(self): + self.timer_cb(None) + + def run(self): + if sys.platform == "darwin": + while True: + self.tick() + + def timer_cb(self, t): + # Can be called in Interrupt context + # Use task_handler_ref since passing self.task_handler would cause allocation. + lv.tick_inc(self.delay) + if self.scheduled < self.max_scheduled: + try: + micropython.schedule(self.task_handler_ref, 0) + self.scheduled += 1 + except: + pass + + async def async_refresh(self): + while True: + await self.refresh_event.wait() + if lv._nesting.value == 0: + self.refresh_event.clear() + try: + lv.task_handler() + except Exception as e: + if self.exception_sink: + self.exception_sink(e) + if self.refresh_cb: + self.refresh_cb() + + async def async_timer(self): + while True: + await asyncio.sleep_ms(self.delay) + lv.tick_inc(self.delay) + self.refresh_event.set() + + def default_exception_sink(self, e): + sys.print_exception(e) + # event_loop.current_instance().deinit() \ No newline at end of file diff --git a/ports/webassembly/variants/lvgl/mpconfigvariant.h b/ports/webassembly/variants/lvgl/mpconfigvariant.h new file mode 100644 index 0000000000000..ed8e812803533 --- /dev/null +++ b/ports/webassembly/variants/lvgl/mpconfigvariant.h @@ -0,0 +1,3 @@ +#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_FULL_FEATURES) +#define MICROPY_GC_SPLIT_HEAP (1) +#define MICROPY_GC_SPLIT_HEAP_AUTO (1) diff --git a/ports/webassembly/variants/lvgl/mpconfigvariant.mk b/ports/webassembly/variants/lvgl/mpconfigvariant.mk new file mode 100644 index 0000000000000..1ad2e049e5e23 --- /dev/null +++ b/ports/webassembly/variants/lvgl/mpconfigvariant.mk @@ -0,0 +1,8 @@ +JSFLAGS += -s ALLOW_MEMORY_GROWTH +JSFLAGS += -s USE_SDL=2 +JSFLAGS += -lidbfs.js + +CFLAGS_EXTMOD += -DMICROPY_SDL=1 +LV_CFLAGS += -DMICROPY_SDL=1 + +FROZEN_MANIFEST ?= variants/lvgl/manifest.py