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