From 9dc24280bae617aba7c3521a43851b1c014c520d Mon Sep 17 00:00:00 2001 From: Quasi Kili Date: Thu, 5 Feb 2026 15:02:13 +0100 Subject: [PATCH 01/33] a first vibe coded test implementation --- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 152 ++++++++++++++++++ internal_filesystem/lib/mpos/main.py | 85 +++++++++- scripts/build_all.sh | 17 ++ scripts/build_mpos.sh | 7 +- 4 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py new file mode 100644 index 00000000..c4acccd8 --- /dev/null +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -0,0 +1,152 @@ + +print("matouch_esp32_s3_2_8.py initialization") +# Hardware initialization for Makerfabs MaTouch ESP32-S3 SPI 2.8" with Camera +# Manufacturer's website: https://www.makerfabs.com/matouch-esp32-s3.html +# Hardware Specifications: +# - MCU: ESP32-S3 with 16MB Flash, 8MB Octal PSRAM +# - Display: 2.8" IPS LCD, 320x240 resolution, ST7789 driver, SPI interface +# - Touch: GT911 capacitive touch controller (5-point), I2C interface +# - Camera: OV3660 (3MP, up to 2048x1536) +# - No IMU sensor (unlike Fri3d and Waveshare boards) +# - No NeoPixel LEDs +# - No buzzer or I2S audio + +from micropython import const +import st7789 +import lcd_bus +import machine +import gt911 +import i2c + +import lvgl as lv +import task_handler + +import mpos.ui + +# Pin configuration for Display (SPI) +# Correct pins from hardware schematic +SPI_BUS = 1 +SPI_FREQ = 40000000 +LCD_SCLK = 14 +LCD_MOSI = 13 +LCD_MISO = 12 +LCD_DC = 21 +LCD_CS = 15 +LCD_RST = -1 +LCD_BL = 48 + +# Pin configuration for Touch (I2C) +# Correct pins from hardware schematic: +# TOUCH_SDA = 39, TOUCH_SCL = 38, TOUCH_INT = 40, TOUCH_RST = 1 +I2C_BUS = 0 +I2C_FREQ = 400000 +TP_SDA = 39 +TP_SCL = 38 +TP_INT = 40 +TP_RST = 1 + +# Display resolution +TFT_HOR_RES = 320 +TFT_VER_RES = 240 + +# Initialize SPI bus for display +spi_bus = machine.SPI.Bus( + host=SPI_BUS, + mosi=LCD_MOSI, + miso=LCD_MISO, + sck=LCD_SCLK +) + +display_bus = lcd_bus.SPIBus( + spi_bus=spi_bus, + freq=SPI_FREQ, + dc=LCD_DC, + cs=LCD_CS, +) + +# Allocate frame buffers +# Buffer size calculation: 2 bytes per pixel (RGB565) * width * height / divisor +# Using 28800 bytes (same as Waveshare and Fri3d) for good performance +_BUFFER_SIZE = const(28800) +fb1 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA) +fb2 = display_bus.allocate_framebuffer(_BUFFER_SIZE, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA) + +# Initialize ST7789 display +mpos.ui.main_display = st7789.ST7789( + data_bus=display_bus, + frame_buffer1=fb1, + frame_buffer2=fb2, + display_width=TFT_HOR_RES, + display_height=TFT_VER_RES, + color_space=lv.COLOR_FORMAT.RGB565, + color_byte_order=st7789.BYTE_ORDER_BGR, + rgb565_byte_swap=True, + reset_pin=LCD_RST, + backlight_pin=LCD_BL, + backlight_on_state=st7789.STATE_PWM, +) + +mpos.ui.main_display.init() +mpos.ui.main_display.set_power(True) +mpos.ui.main_display.set_backlight(100) + +# Touch handling: +i2c_bus = i2c.I2C.Bus(host=I2C_BUS, scl=TP_SCL, sda=TP_SDA, freq=I2C_FREQ, use_locks=False) +touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) +indev = gt911.GT911(touch_dev) + +# Initialize LVGL +lv.init() + +# Set display rotation if needed (adjust based on physical orientation) +# mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._0) + +# === BATTERY VOLTAGE MONITORING === +# Note: MaTouch ESP32-S3 battery monitoring configuration may vary +# This is a placeholder - adjust ADC pin and conversion formula based on actual hardware +from mpos import BatteryManager + +def adc_to_voltage(adc_value): + """ + Convert raw ADC value to battery voltage. + Currently uses simple linear scaling: voltage = adc * 0.00262 + + This should be calibrated with actual battery voltages and ADC readings. + To calibrate: measure actual battery voltages and corresponding ADC readings, + then fit a linear or polynomial function. + """ + return adc_value * 0.00262 + +# Note: Adjust ADC pin number based on actual hardware schematic +# BatteryManager.init_adc(5, adc_to_voltage) + +# === AUDIO HARDWARE === +# Note: MaTouch ESP32-S3 has no buzzer or I2S audio hardware +# AudioManager will not be initialized + +# === LED HARDWARE === +# Note: MaTouch ESP32-S3 has no NeoPixel LEDs +# LightsManager will not be initialized (functions will return False) + +# === SENSOR HARDWARE === +# Note: MaTouch ESP32-S3 has no IMU sensor +# SensorManager will not be initialized + +# === CAMERA HARDWARE === +from mpos import CameraManager + +# MaTouch ESP32-S3 has OV3660 camera (3MP, up to 2048x1536) +# Camera pins are available but initialization is handled by the camera driver +CameraManager.add_camera(CameraManager.Camera( + lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK, + name="OV3660", + vendor="OmniVision" +)) + +print("matouch_esp32_s3_2_8.py finished") +print("Board capabilities:") +print(" - Display: 320x240 ST7789 with GT911 touch") +print(" - Camera: OV3660 (3MP)") +print(" - No IMU sensor") +print(" - No LEDs") +print(" - No audio hardware") diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index 360a7423..532423ff 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -31,16 +31,89 @@ def detect_board(): if sys.platform == "linux" or sys.platform == "darwin": # linux and macOS return "linux" elif sys.platform == "esp32": + # Force MaTouch for ESP32-S3 with 8MB PSRAM + # This bypasses unreliable GT911 touch controller detection at boot + try: + import esp32 + if hasattr(esp32, 'spiram_size'): + psram_size = esp32.spiram_size() + print(f"Detected ESP32-S3 with PSRAM size: {psram_size} bytes ({psram_size // 1048576}MB)") + if psram_size == 8388608: # 8MB PSRAM + board_name = "matouch_esp32_s3_2_8" + print(f"Forcing board selection: {board_name}") + return board_name + except Exception as e: + print(f"PSRAM detection failed: {e}") + from machine import Pin, I2C - i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) - if {0x15, 0x6B} <= set(i2c0.scan()): # touch screen and IMU (at least, possibly more) - return "waveshare_esp32_s3_touch_lcd_2" - else: - i2c0 = I2C(0, sda=Pin(9), scl=Pin(18)) - if {0x6B} <= set(i2c0.scan()): # IMU (plus possibly the Communicator's LANA TNY at 0x38) + import time + + # Check for MaTouch ESP32-S3 (GT911 touch on I2C0) + # Correct pins from schematic: SDA=39, SCL=38, INT=40, RST=1 + # GT911 requires specific reset sequence with INT pin control for address selection + try: + # GT911 address selection via INT pin during reset: + # INT=LOW during reset -> address 0x5D + # INT=HIGH during reset -> address 0x14 + + # Try address 0x5D first (INT=LOW during reset) + tp_rst = Pin(1, Pin.OUT) + tp_int = Pin(40, Pin.OUT) + + # Reset sequence for address 0x5D + tp_int.value(0) # INT LOW for address 0x5D + tp_rst.value(0) + time.sleep_ms(10) + tp_rst.value(1) + time.sleep_ms(10) + tp_int.init(Pin.IN) # Release INT pin + time.sleep_ms(100) # Wait for GT911 to initialize + + # Now try I2C communication with correct pins from schematic + i2c0 = I2C(0, sda=Pin(39), scl=Pin(38), freq=400000) + devices = set(i2c0.scan()) + print(f"MaTouch I2C scan (SDA=39, SCL=38): {[hex(d) for d in devices]}") + + # GT911 touch controller uses addresses 0x5D or 0x14 + if 0x5D in devices or 0x14 in devices: + print("Detected MaTouch ESP32-S3 (GT911 found)") + return "matouch_esp32_s3_2_8" + + # Clean up pins + tp_rst.init(Pin.IN) + tp_int.init(Pin.IN) + except Exception as e: + print(f"MaTouch detection failed: {e}") + import sys + sys.print_exception(e) + + # Check for Waveshare ESP32-S3-Touch-LCD-2 + try: + i2c0 = I2C(0, sda=Pin(48), scl=Pin(47), freq=400000) + devices = set(i2c0.scan()) + print(f"Waveshare I2C scan (SDA=48, SCL=47): {[hex(d) for d in devices]}") + # Filter out invalid addresses (valid I2C range is 0x08-0x77) + valid_devices = {d for d in devices if 0x08 <= d <= 0x77} + if {0x15, 0x6B} <= valid_devices: # touch screen and IMU (at least, possibly more) + print("Detected Waveshare ESP32-S3-Touch-LCD-2") + return "waveshare_esp32_s3_touch_lcd_2" + except Exception as e: + print(f"Waveshare detection failed: {e}") + + # Check for Fri3d 2024 + try: + i2c0 = I2C(0, sda=Pin(9), scl=Pin(18), freq=400000) + devices = set(i2c0.scan()) + print(f"Fri3d I2C scan (SDA=9, SCL=18): {[hex(d) for d in devices]}") + if {0x6B} <= devices: # IMU (plus possibly the Communicator's LANA TNY at 0x38) + print("Detected Fri3d 2024") return "fri3d_2024" else: # if {0x6A} <= set(i2c0.scan()): # IMU (plus a few others, to be added later, but this should work) + print("Detected Fri3d 2026") return "fri3d_2026" + except Exception as e: + print(f"Fri3d detection failed: {e}") + return "fri3d_2026" # Default fallback board = detect_board() diff --git a/scripts/build_all.sh b/scripts/build_all.sh index c7a5ea8c..e5150418 100755 --- a/scripts/build_all.sh +++ b/scripts/build_all.sh @@ -47,6 +47,23 @@ if [ $result -ne 0 ]; then fi cp "$buildfile" "$outdir"/MicroPythonOS_waveshare-esp32-s3-touch-lcd-2_dev_"$version".bin +./scripts/build_lvgl_micropython.sh esp32 prod matouch-esp32-s3-2-8 +result=$? +if [ $result -ne 0 ]; then + echo "build_lvgl_micropython.sh esp32 prod matouch-esp32-s3-2-8 got error: $result" + exit 1 +fi +cp "$buildfile" "$outdir"/MicroPythonOS_matouch-esp32-s3-2-8_prod_"$version".bin +cp "$updatefile" "$updatesdir"/MicroPythonOS_matouch-esp32-s3-2-8_prod_"$version".ota + +./scripts/build_lvgl_micropython.sh esp32 dev matouch-esp32-s3-2-8 +result=$? +if [ $result -ne 0 ]; then + echo "build_lvgl_micropython.sh esp32 dev matouch-esp32-s3-2-8 got error: $result" + exit 1 +fi +cp "$buildfile" "$outdir"/MicroPythonOS_matouch-esp32-s3-2-8_dev_"$version".bin + ./scripts/build_lvgl_micropython.sh unix dev cp "$builddir"/lvgl_micropy_unix "$outdir"/MicroPythonOS_amd64_linux_"$version".elf result=$? diff --git a/scripts/build_mpos.sh b/scripts/build_mpos.sh index 797c592b..03254d34 100755 --- a/scripts/build_mpos.sh +++ b/scripts/build_mpos.sh @@ -14,6 +14,10 @@ if [ -z "$target" ]; then echo "Example: $0 unix" echo "Example: $0 macOS" echo "Example: $0 esp32" + echo "" + echo "Note: For TOML-based builds (e.g., MaTouch ESP32-S3), use:" + echo " cd lvgl_micropython && python3 make.py esp32 --toml=display_configs/MaTouch-ESP32-S3-SPI-2.8.toml" + echo " Or use build_lvgl_micropython.sh wrapper script" echo exit 1 fi @@ -112,7 +116,8 @@ if [ "$target" == "esp32" ]; then # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y pushd "$codebasedir"/lvgl_micropython/ rm -rf lib/micropython/ports/esp32/build-ESP32_GENERIC_S3-SPIRAM_OCT/ - python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=cst816s USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" + python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=gt911 USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" + # python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=gt911 USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" popd echo "Grepping..." pwd From ac3fe1ee3a6a879cb05bf2e2403ebc9e47326027 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 6 Feb 2026 19:14:20 +0100 Subject: [PATCH 02/33] Fix it --- internal_filesystem/lib/mpos/main.py | 91 ++++------------------------ scripts/build_mpos.sh | 8 +-- 2 files changed, 12 insertions(+), 87 deletions(-) diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index 532423ff..683b49a6 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -31,89 +31,20 @@ def detect_board(): if sys.platform == "linux" or sys.platform == "darwin": # linux and macOS return "linux" elif sys.platform == "esp32": - # Force MaTouch for ESP32-S3 with 8MB PSRAM - # This bypasses unreliable GT911 touch controller detection at boot - try: - import esp32 - if hasattr(esp32, 'spiram_size'): - psram_size = esp32.spiram_size() - print(f"Detected ESP32-S3 with PSRAM size: {psram_size} bytes ({psram_size // 1048576}MB)") - if psram_size == 8388608: # 8MB PSRAM - board_name = "matouch_esp32_s3_2_8" - print(f"Forcing board selection: {board_name}") - return board_name - except Exception as e: - print(f"PSRAM detection failed: {e}") - from machine import Pin, I2C - import time - - # Check for MaTouch ESP32-S3 (GT911 touch on I2C0) - # Correct pins from schematic: SDA=39, SCL=38, INT=40, RST=1 - # GT911 requires specific reset sequence with INT pin control for address selection - try: - # GT911 address selection via INT pin during reset: - # INT=LOW during reset -> address 0x5D - # INT=HIGH during reset -> address 0x14 - - # Try address 0x5D first (INT=LOW during reset) - tp_rst = Pin(1, Pin.OUT) - tp_int = Pin(40, Pin.OUT) - - # Reset sequence for address 0x5D - tp_int.value(0) # INT LOW for address 0x5D - tp_rst.value(0) - time.sleep_ms(10) - tp_rst.value(1) - time.sleep_ms(10) - tp_int.init(Pin.IN) # Release INT pin - time.sleep_ms(100) # Wait for GT911 to initialize - - # Now try I2C communication with correct pins from schematic + i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) + if {0x15, 0x6B} <= set(i2c0.scan()): # touch screen and IMU (at least, possibly more) + return "waveshare_esp32_s3_touch_lcd_2" + else: i2c0 = I2C(0, sda=Pin(39), scl=Pin(38), freq=400000) - devices = set(i2c0.scan()) - print(f"MaTouch I2C scan (SDA=39, SCL=38): {[hex(d) for d in devices]}") - - # GT911 touch controller uses addresses 0x5D or 0x14 - if 0x5D in devices or 0x14 in devices: - print("Detected MaTouch ESP32-S3 (GT911 found)") + if {0x14} <= set(i2c0.scan()): # GT911 touch return "matouch_esp32_s3_2_8" - - # Clean up pins - tp_rst.init(Pin.IN) - tp_int.init(Pin.IN) - except Exception as e: - print(f"MaTouch detection failed: {e}") - import sys - sys.print_exception(e) - - # Check for Waveshare ESP32-S3-Touch-LCD-2 - try: - i2c0 = I2C(0, sda=Pin(48), scl=Pin(47), freq=400000) - devices = set(i2c0.scan()) - print(f"Waveshare I2C scan (SDA=48, SCL=47): {[hex(d) for d in devices]}") - # Filter out invalid addresses (valid I2C range is 0x08-0x77) - valid_devices = {d for d in devices if 0x08 <= d <= 0x77} - if {0x15, 0x6B} <= valid_devices: # touch screen and IMU (at least, possibly more) - print("Detected Waveshare ESP32-S3-Touch-LCD-2") - return "waveshare_esp32_s3_touch_lcd_2" - except Exception as e: - print(f"Waveshare detection failed: {e}") - - # Check for Fri3d 2024 - try: - i2c0 = I2C(0, sda=Pin(9), scl=Pin(18), freq=400000) - devices = set(i2c0.scan()) - print(f"Fri3d I2C scan (SDA=9, SCL=18): {[hex(d) for d in devices]}") - if {0x6B} <= devices: # IMU (plus possibly the Communicator's LANA TNY at 0x38) - print("Detected Fri3d 2024") - return "fri3d_2024" - else: # if {0x6A} <= set(i2c0.scan()): # IMU (plus a few others, to be added later, but this should work) - print("Detected Fri3d 2026") - return "fri3d_2026" - except Exception as e: - print(f"Fri3d detection failed: {e}") - return "fri3d_2026" # Default fallback + else: + i2c0 = I2C(0, sda=Pin(9), scl=Pin(18)) + if {0x6B} <= set(i2c0.scan()): # IMU (plus possibly the Communicator's LANA TNY at 0x38) + return "fri3d_2024" + else: # if {0x6A} <= set(i2c0.scan()): # IMU (plus a few others, to be added later, but this should work) + return "fri3d_2026" board = detect_board() diff --git a/scripts/build_mpos.sh b/scripts/build_mpos.sh index 03254d34..776006ec 100755 --- a/scripts/build_mpos.sh +++ b/scripts/build_mpos.sh @@ -14,11 +14,6 @@ if [ -z "$target" ]; then echo "Example: $0 unix" echo "Example: $0 macOS" echo "Example: $0 esp32" - echo "" - echo "Note: For TOML-based builds (e.g., MaTouch ESP32-S3), use:" - echo " cd lvgl_micropython && python3 make.py esp32 --toml=display_configs/MaTouch-ESP32-S3-SPI-2.8.toml" - echo " Or use build_lvgl_micropython.sh wrapper script" - echo exit 1 fi @@ -116,8 +111,7 @@ if [ "$target" == "esp32" ]; then # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y pushd "$codebasedir"/lvgl_micropython/ rm -rf lib/micropython/ports/esp32/build-ESP32_GENERIC_S3-SPIRAM_OCT/ - python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=gt911 USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" - # python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=gt911 USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" + python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=gt911 USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" popd echo "Grepping..." pwd From 6ddcee3a0dcefa63d7c1c71dfe54b61a9686fd50 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 6 Feb 2026 19:22:30 +0100 Subject: [PATCH 03/33] Fix matouch detection --- internal_filesystem/lib/mpos/main.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index 683b49a6..ef13cbda 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -32,19 +32,21 @@ def detect_board(): return "linux" elif sys.platform == "esp32": from machine import Pin, I2C - i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) + + i2c0 = I2C(0, sda=Pin(39), scl=Pin(38), freq=400000) + if {0x14} <= set(i2c0.scan()): # GT911 touch + return "matouch_esp32_s3_2_8" + + i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # on the Matouch, this "finds" devices at all addresses 8-119 so do this one before if {0x15, 0x6B} <= set(i2c0.scan()): # touch screen and IMU (at least, possibly more) return "waveshare_esp32_s3_touch_lcd_2" - else: - i2c0 = I2C(0, sda=Pin(39), scl=Pin(38), freq=400000) - if {0x14} <= set(i2c0.scan()): # GT911 touch - return "matouch_esp32_s3_2_8" - else: - i2c0 = I2C(0, sda=Pin(9), scl=Pin(18)) - if {0x6B} <= set(i2c0.scan()): # IMU (plus possibly the Communicator's LANA TNY at 0x38) - return "fri3d_2024" - else: # if {0x6A} <= set(i2c0.scan()): # IMU (plus a few others, to be added later, but this should work) - return "fri3d_2026" + + i2c0 = I2C(0, sda=Pin(9), scl=Pin(18)) + if {0x6B} <= set(i2c0.scan()): # IMU (plus possibly the Communicator's LANA TNY at 0x38) + return "fri3d_2024" + + # default: if {0x6A} <= set(i2c0.scan()): # IMU (plus a few others, to be added later, but this should work) + return "fri3d_2026" board = detect_board() From b072a97c9a5078f44802265f0d5e994fa0fd6f51 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 6 Feb 2026 21:49:14 +0100 Subject: [PATCH 04/33] matouch_esp32_s3_2_8: fix display and touch screen --- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 43 +++++++++---------- internal_filesystem/lib/mpos/main.py | 2 +- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index c4acccd8..90b54715 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -15,8 +15,6 @@ from micropython import const import st7789 import lcd_bus import machine -import gt911 -import i2c import lvgl as lv import task_handler @@ -32,19 +30,8 @@ LCD_MOSI = 13 LCD_MISO = 12 LCD_DC = 21 LCD_CS = 15 -LCD_RST = -1 LCD_BL = 48 -# Pin configuration for Touch (I2C) -# Correct pins from hardware schematic: -# TOUCH_SDA = 39, TOUCH_SCL = 38, TOUCH_INT = 40, TOUCH_RST = 1 -I2C_BUS = 0 -I2C_FREQ = 400000 -TP_SDA = 39 -TP_SCL = 38 -TP_INT = 40 -TP_RST = 1 - # Display resolution TFT_HOR_RES = 320 TFT_VER_RES = 240 @@ -76,12 +63,11 @@ mpos.ui.main_display = st7789.ST7789( data_bus=display_bus, frame_buffer1=fb1, frame_buffer2=fb2, - display_width=TFT_HOR_RES, - display_height=TFT_VER_RES, + display_width=TFT_VER_RES, + display_height=TFT_HOR_RES, color_space=lv.COLOR_FORMAT.RGB565, color_byte_order=st7789.BYTE_ORDER_BGR, rgb565_byte_swap=True, - reset_pin=LCD_RST, backlight_pin=LCD_BL, backlight_on_state=st7789.STATE_PWM, ) @@ -90,16 +76,27 @@ mpos.ui.main_display.init() mpos.ui.main_display.set_power(True) mpos.ui.main_display.set_backlight(100) -# Touch handling: -i2c_bus = i2c.I2C.Bus(host=I2C_BUS, scl=TP_SCL, sda=TP_SDA, freq=I2C_FREQ, use_locks=False) -touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) -indev = gt911.GT911(touch_dev) +# Touch handling +# Often times, a "ghost" device seems to show up on the I2C bus at 0x14. +# Initializing it, although it fails, seems to bring up the "proper" GT911 at address 0x5D (gt911.I2C_ADDR). +try: + import i2c + import gt911 + i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39, freq=400000, use_locks=False) + touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=0x14, reg_bits=gt911.BITS) + indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) +except Exception as e: + print(f"Touch init phase 1 got exception: {e}") +try: + import pointer_framework + touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) + indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, startup_rotation=pointer_framework.lv.DISPLAY_ROTATION._180, debug=True) +except Exception as e: + print(f"Touch init phase 2 got exception: {e}") # Initialize LVGL lv.init() - -# Set display rotation if needed (adjust based on physical orientation) -# mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._0) +mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._90) # must be done after initializing display and creating the touch drivers, to ensure proper handling # === BATTERY VOLTAGE MONITORING === # Note: MaTouch ESP32-S3 battery monitoring configuration may vary diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index ef13cbda..472a93bc 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -34,7 +34,7 @@ def detect_board(): from machine import Pin, I2C i2c0 = I2C(0, sda=Pin(39), scl=Pin(38), freq=400000) - if {0x14} <= set(i2c0.scan()): # GT911 touch + if {0x14} <= set(i2c0.scan()): # GT911 touch initial "ghost" device return "matouch_esp32_s3_2_8" i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # on the Matouch, this "finds" devices at all addresses 8-119 so do this one before From 4e16096207e389128ab55117016ebf05cf6bc291 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Fri, 6 Feb 2026 22:03:29 +0100 Subject: [PATCH 05/33] Comments --- internal_filesystem/lib/mpos/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index 472a93bc..b54bda0c 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -37,7 +37,7 @@ def detect_board(): if {0x14} <= set(i2c0.scan()): # GT911 touch initial "ghost" device return "matouch_esp32_s3_2_8" - i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # on the Matouch, this "finds" devices at all addresses 8-119 so do this one before + i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # on matouch_esp32_s3_2_8, this "finds" devices at all addresses 8-119 so only do this after matouch_esp32_s3_2_8 if {0x15, 0x6B} <= set(i2c0.scan()): # touch screen and IMU (at least, possibly more) return "waveshare_esp32_s3_touch_lcd_2" From e8de45312fa556b6a256743b3a7c155ff1d312b2 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sat, 7 Feb 2026 12:54:14 +0100 Subject: [PATCH 06/33] matouch_esp32_s3_2_8_spi work - Natural orientation: camera on top, USB ports on bottom - OV3660 camera works (but colors are off and it breaks the touch input) - Improve board detection --- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 42 ++--- internal_filesystem/lib/mpos/indev/gt911.py | 178 ++++++++++++++++++ .../lib/mpos/indev/gt911_extension.py | 157 +++++++++++++++ .../lib/mpos/indev/gt911_settings_gui.py | 4 + internal_filesystem/lib/mpos/main.py | 4 +- .../lib/mpos/ui/camera_activity.py | 14 +- scripts/build_mpos.sh | 2 +- 7 files changed, 361 insertions(+), 40 deletions(-) create mode 100644 internal_filesystem/lib/mpos/indev/gt911.py create mode 100644 internal_filesystem/lib/mpos/indev/gt911_extension.py create mode 100644 internal_filesystem/lib/mpos/indev/gt911_settings_gui.py diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index 90b54715..679356ce 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -1,7 +1,7 @@ print("matouch_esp32_s3_2_8.py initialization") # Hardware initialization for Makerfabs MaTouch ESP32-S3 SPI 2.8" with Camera -# Manufacturer's website: https://www.makerfabs.com/matouch-esp32-s3.html +# Manufacturer's website: https://www.makerfabs.com/matouch-esp32-s3-spi-ips-2-8-with-camera-ov3660.html # Hardware Specifications: # - MCU: ESP32-S3 with 16MB Flash, 8MB Octal PSRAM # - Display: 2.8" IPS LCD, 320x240 resolution, ST7789 driver, SPI interface @@ -77,45 +77,25 @@ mpos.ui.main_display.set_power(True) mpos.ui.main_display.set_backlight(100) # Touch handling -# Often times, a "ghost" device seems to show up on the I2C bus at 0x14. -# Initializing it, although it fails, seems to bring up the "proper" GT911 at address 0x5D (gt911.I2C_ADDR). try: import i2c - import gt911 - i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39, freq=400000, use_locks=False) - touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=0x14, reg_bits=gt911.BITS) - indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) -except Exception as e: - print(f"Touch init phase 1 got exception: {e}") -try: - import pointer_framework + i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39) + import mpos.indev.gt911 as gt911 touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) - indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, startup_rotation=pointer_framework.lv.DISPLAY_ROTATION._180, debug=True) + indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) # remove debug because it's slower except Exception as e: - print(f"Touch init phase 2 got exception: {e}") + print(f"Touch init got exception: {e}") # Initialize LVGL lv.init() -mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._90) # must be done after initializing display and creating the touch drivers, to ensure proper handling -# === BATTERY VOLTAGE MONITORING === -# Note: MaTouch ESP32-S3 battery monitoring configuration may vary -# This is a placeholder - adjust ADC pin and conversion formula based on actual hardware -from mpos import BatteryManager +# TODO: initialize SDIO (instead of SPI) SD card with: +# CMD = 2 +# SCLK = 42 +# D0 = 41 +#import mpos.sdcard +#mpos.sdcard.init(spi_bus, cs_pin=14) -def adc_to_voltage(adc_value): - """ - Convert raw ADC value to battery voltage. - Currently uses simple linear scaling: voltage = adc * 0.00262 - - This should be calibrated with actual battery voltages and ADC readings. - To calibrate: measure actual battery voltages and corresponding ADC readings, - then fit a linear or polynomial function. - """ - return adc_value * 0.00262 - -# Note: Adjust ADC pin number based on actual hardware schematic -# BatteryManager.init_adc(5, adc_to_voltage) # === AUDIO HARDWARE === # Note: MaTouch ESP32-S3 has no buzzer or I2S audio hardware diff --git a/internal_filesystem/lib/mpos/indev/gt911.py b/internal_filesystem/lib/mpos/indev/gt911.py new file mode 100644 index 00000000..2b6ab191 --- /dev/null +++ b/internal_filesystem/lib/mpos/indev/gt911.py @@ -0,0 +1,178 @@ +# Copyright (c) 2024 - 2025 Kevin G. Schlosser + +# this driver uses a special i2c bus implimentation I have written. +# This implimentation takes into consideration the ESP32 and it having +# threading available. It also has some convience methods built into it +# that figure out what is wanting to be done automatically. +# read more about it's use in the stub files. + +from micropython import const # NOQA +import pointer_framework +import machine # NOQA +import time + + +_CMD_REG = const(0x8040) +_CMD_CHECK_REG = const(0x8046) +_CMD_READ_DATA = const(0x01) + +_ESD_CHECK_REG = const(0x8041) + +_STATUS_REG = const(0x814E) +_POINT_1_REG = const(0x8150) + +_PRODUCT_ID_REG = const(0x8140) +_FIRMWARE_VERSION_REG = const(0x8144) +_VENDOR_ID_REG = const(0x814A) + +_X_CORD_RES_REG = const(0x8146) +_Y_CORD_RES_REG = const(0x8148) + +I2C_ADDR = 0x5D +BITS = 16 + +_ADDR2 = const(0x14) + + +class GT911(pointer_framework.PointerDriver): + + def _read_reg(self, reg, num_bytes=None, buf=None): + self._tx_buf[0] = reg >> 8 + self._tx_buf[1] = reg & 0xFF + try: + if num_bytes is not None: + self._device.write_readinto(self._tx_mv[:2], self._rx_mv[:num_bytes]) + else: + self._device.write_readinto(self._tx_mv[:2], buf) + except Exception as e: + print(f"GT911 _read_reg got exception: {e}") + + def _write_reg(self, reg, value=None, buf=None): + try: + if value is not None: + self._tx_buf[0] = value + self._device.write_mem(reg, self._tx_mv[:1]) + elif buf is not None: + self._device.write_mem(reg, buf) + except Exception as e: + print(f"GT911 _write_reg got exception: {e}") + + def __init__( + self, + device, + reset_pin=None, + interrupt_pin=None, + touch_cal=None, + startup_rotation=pointer_framework.lv.DISPLAY_ROTATION._0, # NOQA + debug=False + ): + self._tx_buf = bytearray(3) + self._tx_mv = memoryview(self._tx_buf) + self._rx_buf = bytearray(6) + self._rx_mv = memoryview(self._rx_buf) + + self._device = device + + self.__x = 0 + self.__y = 0 + self.__last_state = self.RELEASED + + if isinstance(reset_pin, int): + reset_pin = machine.Pin(reset_pin, machine.Pin.OUT) + + if isinstance(interrupt_pin, int): + interrupt_pin = machine.Pin(interrupt_pin, machine.Pin.OUT) + + self._reset_pin = reset_pin + self._interrupt_pin = interrupt_pin + + self.hw_reset() + super().__init__( + touch_cal=touch_cal, startup_rotation=startup_rotation, debug=debug + ) + + def hw_reset(self): + if self._interrupt_pin and self._reset_pin: + self._interrupt_pin.init(self._interrupt_pin.OUT) + self._interrupt_pin(0) + self._reset_pin(0) + time.sleep_ms(10) # NOQA + self._interrupt_pin(0) + time.sleep_ms(1) # NOQA + self._reset_pin(1) + time.sleep_ms(5) # NOQA + self._interrupt_pin(0) + time.sleep_ms(50) # NOQA + self._interrupt_pin.init(self._interrupt_pin.IN) + time.sleep_ms(50) # NOQA + + self._write_reg(_ESD_CHECK_REG, 0x00) + self._write_reg(_CMD_CHECK_REG, _CMD_READ_DATA) + self._write_reg(_CMD_REG, _CMD_READ_DATA) + + self._read_reg(_PRODUCT_ID_REG, 4) + + product_id = '' + for item in self._rx_buf[:4]: + try: + product_id += chr(item) + except: # NOQA + break + + print('Touch Product id:', product_id) + + self._read_reg(_FIRMWARE_VERSION_REG, 2) + print( + 'Touch Firmware version:', + hex(self._rx_buf[0] + (self._rx_buf[1] << 8)) + ) + + self._read_reg(_VENDOR_ID_REG, 1) + print(f'Touch Vendor id: 0x{hex(self._rx_buf[0])[2:].upper()}') + x, y = self.hw_size + print(f'Touch resolution: width={x}, height={y}') + + @property + def hw_size(self): + self._read_reg(_X_CORD_RES_REG, 2) + x = self._rx_buf[0] + (self._rx_buf[1] << 8) + + self._read_reg(_Y_CORD_RES_REG, 2) + y = self._rx_buf[0] + (self._rx_buf[1] << 8) + + return x, y + + @property + def firmware_config(self): + try: + import gt911_extension + except ImportError: + raise ImportError( + 'you need to upload the gt911_extension.py file to the MCU' + ) + return gt911_extension.GT911Extension(self, self._device) + + def _get_coords(self): + self._read_reg(_STATUS_REG, 1) + touch_cnt = self._rx_buf[0] & 0x0F + status = self._rx_buf[0] & 0x80 + + if status: + if touch_cnt == 1: + self._read_reg(_POINT_1_REG, 6) + + x = self._rx_buf[0] + (self._rx_buf[1] << 8) + y = self._rx_buf[2] + (self._rx_buf[3] << 8) + + self._write_reg(_STATUS_REG, 0x00) + + self.__x = x + self.__y = y + self.__last_state = self.PRESSED + + elif touch_cnt == 0: + self.__last_state = self.RELEASED + + self._write_reg(_STATUS_REG, 0x00) + + return self.__last_state, self.__x, self.__y diff --git a/internal_filesystem/lib/mpos/indev/gt911_extension.py b/internal_filesystem/lib/mpos/indev/gt911_extension.py new file mode 100644 index 00000000..3d504c4d --- /dev/null +++ b/internal_filesystem/lib/mpos/indev/gt911_extension.py @@ -0,0 +1,157 @@ +# Copyright (c) 2024 - 2025 Kevin G. Schlosser + +from micropython import const # NOQA + + +_CONFIG_START_REG = const(0x8047) +_CONFIG_VERSION_POS = const(0x00) +_X_OUTPUT_MAX_LOW_POS = const(0x01) +_X_OUTPUT_MAX_HIGH_POS = const(0x02) +_Y_OUTPUT_MAX_LOW_POS = const(0x03) +_Y_OUTPUT_MAX_HIGH_POS = const(0x04) +# Touch Number 0x05 +# Module_Switch1 0x06 +# Module_Switch2 0x07 +# Shake_Count 0x08 +# Filter 0x09 +# Large_Touch 0x0A +_NOISE_REDUCTION_POS = const(0x0B) +_TOUCH_PRESS_LEVEL_POS = const(0x0C) +_TOUCH_LEAVE_LEVEL_POS = const(0x0D) +# Low_Power_Control 0x0E +# Refresh_Rate 0x0F +# x_threshold 0x10 +# y_threshold 0x11 +# X_Speed_Limit 0x12 +# y_Speed_Limit 0x13 +_VER_SPACE_POS = const(0x14) # const(0x805B) # low 4 bits are bottom and hight is top +_HOR_SPACE_POS = const(0x15) # const(0x805C) # low 4 bits is right and high is left + +_CONFIG_CHKSUM_REG = const(0x80FF) +_CONFIG_FRESH_REG = const(0x8100) +# 0-15 * 32 + + +class GT911Extension(object): + + def _read_reg(self, reg, num_bytes=None, buf=None): + self._tx_buf[0] = reg >> 8 + self._tx_buf[1] = reg & 0xFF + if num_bytes is not None: + self._i2c.write_readinto(self._tx_mv[:2], self._rx_mv[:num_bytes]) + else: + self._i2c.write_readinto(self._tx_mv[:2], buf) + + def _write_reg(self, reg, value=None, buf=None): + if value is not None: + self._tx_buf[0] = value + self._i2c.write_mem(reg, self._tx_mv[:1]) + elif buf is not None: + self._i2c.write_mem(reg, buf) + + def __init__(self, indev, i2c): + self._indev = indev + self._i2c = i2c + + self._tx_buf = bytearray(3) + self._tx_mv = memoryview(self._tx_buf) + self._rx_buf = bytearray(6) + self._rx_mv = memoryview(self._rx_buf) + + self._config_data = bytearray(_CONFIG_FRESH_REG - _CONFIG_START_REG + 1) + self._config_mv = memoryview(self._config_data) + + self._read_reg(_CONFIG_START_REG, buf=self._config_mv[:-2]) + + @property + def width(self): + return ( + (self._config_data[_X_OUTPUT_MAX_HIGH_POS] << 8) | + self._config_data[_X_OUTPUT_MAX_LOW_POS] + ) + + @width.setter + def width(self, value): + self._config_data[_X_OUTPUT_MAX_LOW_POS] = value & 0xFF + self._config_data[_X_OUTPUT_MAX_HIGH_POS] = (value >> 8) & 0xFF + + @property + def height(self): + return ( + (self._config_data[_Y_OUTPUT_MAX_HIGH_POS] << 8) | + self._config_data[_Y_OUTPUT_MAX_LOW_POS] + ) + + @height.setter + def height(self, value): + self._config_data[_Y_OUTPUT_MAX_LOW_POS] = value & 0xFF + self._config_data[_Y_OUTPUT_MAX_HIGH_POS] = (value >> 8) & 0xFF + + @property + def noise_reduction(self): + return self._config_data[_NOISE_REDUCTION_POS] & 0x0F + + @noise_reduction.setter + def noise_reduction(self, value): + upper_val = self._config_data[_NOISE_REDUCTION_POS] >> 4 + self._config_data[_NOISE_REDUCTION_POS + 2] = (upper_val << 4) | (value & 0x0F) + + @property + def touch_press_level(self): + return self._config_data[_TOUCH_PRESS_LEVEL_POS] + + @touch_press_level.setter + def touch_press_level(self, value): + self._config_data[_TOUCH_PRESS_LEVEL_POS] = value & 0xFF + + @property + def touch_leave_level(self): + return self._config_data[_TOUCH_LEAVE_LEVEL_POS] + + @touch_leave_level.setter + def touch_leave_level(self, value): + self._config_data[_TOUCH_LEAVE_LEVEL_POS] = value & 0xFF + + @property + def pad_left(self): + return self._config_data[_HOR_SPACE_POS] >> 4 + + @pad_left.setter + def pad_left(self, value): + self._config_data[_HOR_SPACE_POS] = (value << 4) | self.pad_right + + @property + def pad_right(self): + return self._config_data[_HOR_SPACE_POS] & 0xF + + @pad_right.setter + def pad_right(self, value): + self._config_data[_HOR_SPACE_POS] = (self.pad_left << 4) | (value & 0xF) + + @property + def pad_top(self): + return self._config_data[_VER_SPACE_POS] >> 4 + + @pad_top.setter + def pad_top(self, value): + self._config_data[_VER_SPACE_POS] = (value << 4) | self.pad_bottom + + @property + def pad_bottom(self): + return self._config_data[_VER_SPACE_POS] & 0xF + + @pad_bottom.setter + def pad_bottom(self, value): + self._config_data[_VER_SPACE_POS] = (self.pad_top << 4) | (value & 0xF) + + def save(self): + # calculate the checksum + self._config_data[-2] = ((~sum(self._config_data[:-2])) + 1) & 0xFF + + # set the flag to save the data the data + self._config_data[-1] = 0x01 # _CONFIG_FRESH_REG + + # write all config data to the touch IC + self._write_reg(_CONFIG_START_REG, buf=self._config_mv) + + self._indev.hw_reset() diff --git a/internal_filesystem/lib/mpos/indev/gt911_settings_gui.py b/internal_filesystem/lib/mpos/indev/gt911_settings_gui.py new file mode 100644 index 00000000..57f0eff7 --- /dev/null +++ b/internal_filesystem/lib/mpos/indev/gt911_settings_gui.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024 - 2025 Kevin G. Schlosser + +import lvgl as lv # NOQA + diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index b54bda0c..d75d155f 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -32,9 +32,11 @@ def detect_board(): return "linux" elif sys.platform == "esp32": from machine import Pin, I2C + return "matouch_esp32_s3_2_8" # i2c scan confuses the camera so hard-code for now i2c0 = I2C(0, sda=Pin(39), scl=Pin(38), freq=400000) - if {0x14} <= set(i2c0.scan()): # GT911 touch initial "ghost" device + devices = set(i2c0.scan()) # causes a "ghost" device to appear on 0x20 and breaks the camera + if {0x14} <= devices or {0x5D} <= devices: # "ghost" device or real GT911 return "matouch_esp32_s3_2_8" i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # on matouch_esp32_s3_2_8, this "finds" devices at all addresses 8-119 so only do this after matouch_esp32_s3_2_8 diff --git a/internal_filesystem/lib/mpos/ui/camera_activity.py b/internal_filesystem/lib/mpos/ui/camera_activity.py index 90c642a6..a457a283 100644 --- a/internal_filesystem/lib/mpos/ui/camera_activity.py +++ b/internal_filesystem/lib/mpos/ui/camera_activity.py @@ -414,13 +414,13 @@ class CameraActivity(Activity): for attempt in range(max_attempts): try: cam = Camera( - data_pins=[12,13,15,11,14,10,7,2], - vsync_pin=6, - href_pin=4, - sda_pin=21, - scl_pin=16, - pclk_pin=9, - xclk_pin=8, + data_pins=[7,5,4,6,16,8,3,46], + vsync_pin=11, + href_pin=10, + sda_pin=39, + scl_pin=38, + pclk_pin=17, + xclk_pin=9, xclk_freq=20000000, powerdown_pin=-1, reset_pin=-1, diff --git a/scripts/build_mpos.sh b/scripts/build_mpos.sh index 776006ec..6323443c 100755 --- a/scripts/build_mpos.sh +++ b/scripts/build_mpos.sh @@ -111,7 +111,7 @@ if [ "$target" == "esp32" ]; then # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y pushd "$codebasedir"/lvgl_micropython/ rm -rf lib/micropython/ports/esp32/build-ESP32_GENERIC_S3-SPIRAM_OCT/ - python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=gt911 USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" + python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=cst816s USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" popd echo "Grepping..." pwd From d453e778bd9911f16505530e146bb02858ecb423 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 8 Feb 2026 18:20:48 +0100 Subject: [PATCH 07/33] Fix board detection by not using i2c scan i2c scanning all the devices can confuse some of them, such as the OV* camera's. Better to selectively scan just the ones we want to know about, plus, it's also faster. --- internal_filesystem/lib/mpos/main.py | 43 +++++++++++++++++++++------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index d75d155f..78b10704 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -26,31 +26,52 @@ def init_rootscreen(): img.set_blend_mode(lv.BLEND_MODE.DIFFERENCE) img.center() +def single_address_i2c_scan(i2c_bus, address): + """ + Scan a specific I2C address to check if a device is present. + + Args: + i2c_bus: An I2C bus object (machine.I2C instance) + address: Integer address to scan (0-127) + + Returns: + True if a device responds at the specified address, False otherwise + """ + try: + # Attempt to write a single byte to the address + # This will raise an exception if no device responds + i2c_bus.writeto(address, b'') + return True + except OSError: + # No device at this address + return False + except Exception as e: + # Handle any other exceptions gracefully + print(f"single_address_i2c_scan: error scanning address 0x{address:02x}: {e}") + return False + def detect_board(): import sys if sys.platform == "linux" or sys.platform == "darwin": # linux and macOS return "linux" elif sys.platform == "esp32": from machine import Pin, I2C - return "matouch_esp32_s3_2_8" # i2c scan confuses the camera so hard-code for now - i2c0 = I2C(0, sda=Pin(39), scl=Pin(38), freq=400000) - devices = set(i2c0.scan()) # causes a "ghost" device to appear on 0x20 and breaks the camera - if {0x14} <= devices or {0x5D} <= devices: # "ghost" device or real GT911 - return "matouch_esp32_s3_2_8" - - i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # on matouch_esp32_s3_2_8, this "finds" devices at all addresses 8-119 so only do this after matouch_esp32_s3_2_8 - if {0x15, 0x6B} <= set(i2c0.scan()): # touch screen and IMU (at least, possibly more) + i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) + if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan(i2c0, 0x6B): # CST816S touch screen and IMU return "waveshare_esp32_s3_touch_lcd_2" + i2c0 = I2C(0, sda=Pin(39), scl=Pin(38)) + if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan(i2c0, 0x5D): # "ghost" or real GT911 touch screen + return "matouch_esp32_s3_2_8" + i2c0 = I2C(0, sda=Pin(9), scl=Pin(18)) - if {0x6B} <= set(i2c0.scan()): # IMU (plus possibly the Communicator's LANA TNY at 0x38) + if single_address_i2c_scan(i2c0, 0x6B): # IMU (plus possibly the Communicator's LANA TNY at 0x38) return "fri3d_2024" - # default: if {0x6A} <= set(i2c0.scan()): # IMU (plus a few others, to be added later, but this should work) + # default: if single_address_i2c_scan(i2c0, 0x6A): # IMU but currently not installed return "fri3d_2026" - board = detect_board() print(f"Initializing {board} hardware") DeviceInfo.set_hardware_id(board) From 0179800e8c53f610a349cc8e98fda554bf6ed195 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 8 Feb 2026 19:39:28 +0100 Subject: [PATCH 08/33] Fix board detect order --- internal_filesystem/lib/mpos/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index 78b10704..df169035 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -57,14 +57,14 @@ def detect_board(): elif sys.platform == "esp32": from machine import Pin, I2C - i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) - if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan(i2c0, 0x6B): # CST816S touch screen and IMU - return "waveshare_esp32_s3_touch_lcd_2" - i2c0 = I2C(0, sda=Pin(39), scl=Pin(38)) if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan(i2c0, 0x5D): # "ghost" or real GT911 touch screen return "matouch_esp32_s3_2_8" + i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # IO48 is floating on matouch and therefore, using that for I2C will find many devices, so do this after matouch_esp32_s3_2_8 + if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan(i2c0, 0x6B): # CST816S touch screen and IMU + return "waveshare_esp32_s3_touch_lcd_2" + i2c0 = I2C(0, sda=Pin(9), scl=Pin(18)) if single_address_i2c_scan(i2c0, 0x6B): # IMU (plus possibly the Communicator's LANA TNY at 0x38) return "fri3d_2024" From af38a612afcfe3e6f9032c73195483ea80a86adc Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 8 Feb 2026 19:42:43 +0100 Subject: [PATCH 09/33] Update lvgl_micropython --- lvgl_micropython | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lvgl_micropython b/lvgl_micropython index 3edda407..ec3ecd41 160000 --- a/lvgl_micropython +++ b/lvgl_micropython @@ -1 +1 @@ -Subproject commit 3edda407ad57c16ab42f6705556725991df4b6d6 +Subproject commit ec3ecd4150e81f73c9eaa0575d62437318b4dee8 From 1a9aefee9f259542e31d5d9a6769f86048abee63 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Sun, 8 Feb 2026 19:49:59 +0100 Subject: [PATCH 10/33] Remove rlottie stuff --- scripts/build_mpos.sh | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/scripts/build_mpos.sh b/scripts/build_mpos.sh index 6323443c..938878a7 100755 --- a/scripts/build_mpos.sh +++ b/scripts/build_mpos.sh @@ -41,19 +41,6 @@ fi echo "Resulting file:" cat "$idfile" -# Adding it doesn't hurt - it won't be used anyway as RLOTTIE is disabled in lv_conf.h -echo "Check need to add esp_rlottie" -#if ! grep rlottie "$idfile"; then -if false; then - echo "Adding esp_rlottie to $idfile" - echo " esp_rlottie: - git: https://github.com/MicroPythonOS/esp_rlottie" >> "$idfile" - echo "Resulting file:" - cat "$idfile" -else - echo "No need to add esp_rlottie to $idfile" -fi - echo "Check need to add lvgl_micropython manifest to micropython-camera-API's manifest..." camani="$codebasedir"/micropython-camera-API/src/manifest.py rellvglmani=lvgl_micropython/build/manifest.py @@ -113,9 +100,6 @@ if [ "$target" == "esp32" ]; then rm -rf lib/micropython/ports/esp32/build-ESP32_GENERIC_S3-SPIRAM_OCT/ python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=cst816s USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest" popd - echo "Grepping..." - pwd - grep FrameSize.R480X480 -nril . elif [ "$target" == "unix" -o "$target" == "macOS" ]; then manifest=$(readlink -f "$codebasedir"/manifests/manifest.py) frozenmanifest="FROZEN_MANIFEST=$manifest" From b0412d502aec6e79a217581493efe2bb719e6296 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 9 Feb 2026 16:39:33 +0100 Subject: [PATCH 11/33] GT911: use 0x14 address, add interrupt handler --- internal_filesystem/lib/mpos/indev/gt911.py | 41 +++++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/internal_filesystem/lib/mpos/indev/gt911.py b/internal_filesystem/lib/mpos/indev/gt911.py index 2b6ab191..bad05773 100644 --- a/internal_filesystem/lib/mpos/indev/gt911.py +++ b/internal_filesystem/lib/mpos/indev/gt911.py @@ -18,6 +18,12 @@ _CMD_READ_DATA = const(0x01) _ESD_CHECK_REG = const(0x8041) +_MODULE_SWITCH_1 = const(0x804D) +_CMD_INT_RISING_EDGE = const(0x00) +_CMD_INT_FALLING_EDGE = const(0x01) +_CMD_INT_LOW_LEVEL = const(0x02) +_CMD_INT_HIGH_LEVEL = const(0x03) + _STATUS_REG = const(0x814E) _POINT_1_REG = const(0x8150) @@ -28,7 +34,8 @@ _VENDOR_ID_REG = const(0x814A) _X_CORD_RES_REG = const(0x8146) _Y_CORD_RES_REG = const(0x8148) -I2C_ADDR = 0x5D +#I2C_ADDR = 0x5D +I2C_ADDR = 0x14 BITS = 16 _ADDR2 = const(0x14) @@ -76,12 +83,13 @@ class GT911(pointer_framework.PointerDriver): self.__x = 0 self.__y = 0 self.__last_state = self.RELEASED + self._interrupt_flag = False if isinstance(reset_pin, int): reset_pin = machine.Pin(reset_pin, machine.Pin.OUT) if isinstance(interrupt_pin, int): - interrupt_pin = machine.Pin(interrupt_pin, machine.Pin.OUT) + interrupt_pin = machine.Pin(interrupt_pin, machine.Pin.IN) self._reset_pin = reset_pin self._interrupt_pin = interrupt_pin @@ -91,19 +99,24 @@ class GT911(pointer_framework.PointerDriver): touch_cal=touch_cal, startup_rotation=startup_rotation, debug=debug ) + def _interrupt_handler(self, pin): + """Interrupt handler called when touch event occurs""" + self._interrupt_flag = True + def hw_reset(self): if self._interrupt_pin and self._reset_pin: self._interrupt_pin.init(self._interrupt_pin.OUT) self._interrupt_pin(0) self._reset_pin(0) time.sleep_ms(10) # NOQA - self._interrupt_pin(0) + self._interrupt_pin(1) # only for 0x14 address + #self._interrupt_pin(0) time.sleep_ms(1) # NOQA self._reset_pin(1) time.sleep_ms(5) # NOQA self._interrupt_pin(0) time.sleep_ms(50) # NOQA - self._interrupt_pin.init(self._interrupt_pin.IN) + self._interrupt_pin.init(mode=self._interrupt_pin.IN) time.sleep_ms(50) # NOQA self._write_reg(_ESD_CHECK_REG, 0x00) @@ -132,6 +145,16 @@ class GT911(pointer_framework.PointerDriver): x, y = self.hw_size print(f'Touch resolution: width={x}, height={y}') + # Set up interrupt handler if interrupt pin is available + if self._interrupt_pin: + self._interrupt_pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=self._interrupt_handler) + # Setting _MODULE_SWITCH_1 will "hang" the touch input after a second or 2 of initial swipe + #self._write_reg(_MODULE_SWITCH_1, _CMD_INT_FALLING_EDGE) # stops working + #self._write_reg(_MODULE_SWITCH_1, _CMD_INT_RISING_EDGE) # stops working + # Unknown IRQ_LOW_LEVEL: + #self._interrupt_pin.irq(trigger=machine.Pin.IRQ_LOW_LEVEL, handler=self._interrupt_handler) + #self._write_reg(_MODULE_SWITCH_1, _CMD_INT_LOW_LEVEL) + @property def hw_size(self): self._read_reg(_X_CORD_RES_REG, 2) @@ -153,6 +176,16 @@ class GT911(pointer_framework.PointerDriver): return gt911_extension.GT911Extension(self, self._device) def _get_coords(self): + # If interrupt pin is available, only fetch data when interrupt flag is set + if self._interrupt_pin and not self._interrupt_flag: + return self.__last_state, self.__x, self.__y + + # Clear interrupt flag before reading + if self._interrupt_pin: + self._interrupt_flag = False + #self._write_reg(_MODULE_SWITCH_1, _CMD_INT_FALLING_EDGE) + #print("[GT911] Interrupt-triggered read") + self._read_reg(_STATUS_REG, 1) touch_cnt = self._rx_buf[0] & 0x0F status = self._rx_buf[0] & 0x80 From c4beb0b3daac059d6edb317364f33c8bcd8cb664 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 9 Feb 2026 16:40:03 +0100 Subject: [PATCH 12/33] InputManager: add unregister_indev function --- internal_filesystem/lib/mpos/ui/input_manager.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal_filesystem/lib/mpos/ui/input_manager.py b/internal_filesystem/lib/mpos/ui/input_manager.py index b8c9251d..00e1b372 100644 --- a/internal_filesystem/lib/mpos/ui/input_manager.py +++ b/internal_filesystem/lib/mpos/ui/input_manager.py @@ -29,6 +29,18 @@ class InputManager: if indev and indev not in cls._registered_indevs: cls._registered_indevs.append(indev) + @classmethod + def unregister_indev(cls, indev): + """ + Unregister an input device. + + Parameters: + - indev: LVGL input device object to remove + """ + if indev in cls._registered_indevs: + indev.enable(False) + cls._registered_indevs.remove(indev) + @classmethod def list_indevs(cls): """ From 12e99228d18c43d4b037ef698259ddcf2914894f Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 9 Feb 2026 16:40:30 +0100 Subject: [PATCH 13/33] matouch_esp32_s3_2_8_spi: boot button for back action --- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index 679356ce..d9f37c5a 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -83,9 +83,20 @@ try: import mpos.indev.gt911 as gt911 touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) # remove debug because it's slower + from mpos import InputManager + InputManager.register_indev(indev) except Exception as e: print(f"Touch init got exception: {e}") +# IO0 Button interrupt handler +def io0_interrupt_handler(pin): + print("IO0 button pressed!") + from mpos import back_screen + back_screen() + +io0_pin = machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP) +io0_pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=io0_interrupt_handler) + # Initialize LVGL lv.init() From 23e94145c91234debdec9b27097287dc6bb7afc1 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 9 Feb 2026 16:40:54 +0100 Subject: [PATCH 14/33] Camera: disable GT911 touch screen to avoid conflict --- .../lib/mpos/ui/camera_activity.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/internal_filesystem/lib/mpos/ui/camera_activity.py b/internal_filesystem/lib/mpos/ui/camera_activity.py index a457a283..3b50ec90 100644 --- a/internal_filesystem/lib/mpos/ui/camera_activity.py +++ b/internal_filesystem/lib/mpos/ui/camera_activity.py @@ -138,7 +138,15 @@ class CameraActivity(Activity): # Init camera: self.cam = self.init_internal_cam(self.width, self.height) if self.cam: - self.image.set_rotation(900) # internal camera is rotated 90 degrees + try: + from mpos import InputManager + indev = InputManager.list_indevs()[0] + indev.enable(False) + InputManager.unregister_indev(indev) + print("input disabled") + except Exception as e: + print(f"disabling indev got exception: {e}") + #self.image.set_rotation(900) # internal camera is rotated 90 degrees # Apply saved camera settings, only for internal camera for now: self.apply_camera_settings(self.scanqr_prefs if self.scanqr_mode else self.prefs, self.cam, self.use_webcam) # needs to be done AFTER the camera is initialized else: @@ -166,7 +174,7 @@ class CameraActivity(Activity): # Power off, otherwise it keeps using a lot of current try: from machine import Pin, I2C - i2c = I2C(1, scl=Pin(16), sda=Pin(21)) # Adjust pins and frequency + i2c = I2C(1, scl=Pin(38), sda=Pin(39)) # Adjust pins and frequency #devices = i2c.scan() #print([hex(addr) for addr in devices]) # finds it on 60 = 0x3C after init camera_addr = 0x3C # for OV5640 @@ -177,6 +185,28 @@ class CameraActivity(Activity): i2c.writeto(camera_addr, bytes([reg_high, reg_low, power_off_command])) except Exception as e: print(f"Warning: powering off camera got exception: {e}") + import time + time.sleep_ms(100) + try: + # hardware reset might work too, but doesn't seem to: + #from mpos import InputManager + #indev = InputManager.list_indevs()[0] + #indev.hw_reset() + #indev.enable(True) + #print("input enabled") + #time.sleep(1) + import i2c + i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39) + import mpos.indev.gt911 as gt911 + touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) + indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) # remove debug because it's slower + print("new indev created") + from mpos import InputManager + InputManager.register_indev(indev) + print("new indev registered") + except Exception as e: + print(f"Indev enable got exception: {e}") + self.cam = None if self.image_dsc: # it's important to delete the image when stopping the camera, otherwise LVGL might try to display it and crash print("emptying self.current_cam_buffer...") From b9590d681d5cfe3c0f5b413b6a8d06fd1bb2ef76 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 9 Feb 2026 20:41:27 +0100 Subject: [PATCH 15/33] Launcher: reduce horizontal spacing To fit at least 3 icons on a 240px wide display. --- .../builtin/apps/com.micropythonos.launcher/assets/launcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py b/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py index 9b1a0f54..6b0f63b6 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py @@ -87,7 +87,7 @@ class Launcher(Activity): # Grid parameters icon_size = 64 label_height = 24 - iconcont_width = icon_size + label_height + iconcont_width = int(icon_size * 1.1) iconcont_height = icon_size + label_height for app in AppManager.get_app_list(): From 6e1b3023daa5424f565314fefbedc2bd7663ed18 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 9 Feb 2026 20:42:47 +0100 Subject: [PATCH 16/33] Scale boot logo down if necessary --- internal_filesystem/lib/mpos/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index df169035..da987d36 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -23,6 +23,8 @@ def init_rootscreen(): # Show logo img = lv.image(screen) img.set_src("M:builtin/res/mipmap-mdpi/MicroPythonOS-logo-white-long-w296.png") # from the MPOS-logo repo + if width < 296: + img.set_scale(int(256 * width/296)) img.set_blend_mode(lv.BLEND_MODE.DIFFERENCE) img.center() From c9041130561964a231b7b3812ec0f4b9eebba8f5 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Mon, 9 Feb 2026 20:45:07 +0100 Subject: [PATCH 17/33] Update CHANGELOG --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82166f29..2b4a99e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ===== Builtin Apps: -- OSUpdate app: replace "force update" checkbox with improved button labels +- Launcher: fit at least 3 apps per row on a 240px display +- OSUpdate: replace "force update" checkbox with improved button labels + +OS: +- Scale MicroPythonOS boot logo down if necessary +- Additional board support: MaTouch ESP32-S3 SPI IPS 2.8" with Camera OV3660 0.7.1 ===== From 437fc7cfdabbda2c2c64ffdfc1ca12fd4041a7b1 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 08:53:13 +0100 Subject: [PATCH 18/33] install.sh: script quasiboats for now --- scripts/install.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/install.sh b/scripts/install.sh index 17af080a..4ae00aee 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -70,6 +70,10 @@ $mpremote fs cp ../internal_filesystem_excluded/data/com.micropythonos.system.wi $mpremote fs mkdir :/apps $mpremote fs cp -r apps/com.micropythonos.* :/apps/ find apps/ -maxdepth 1 -type l | while read symlink; do + if echo $symlink | grep quasiboats; then + echo "Skipping $symlink because it's needlessly big..." + continue + fi echo "Handling symlink $symlink" $mpremote fs mkdir :/"$symlink" $mpremote fs cp -r "$symlink"/* :/"$symlink"/ From b50c0d09d215d1e552c06ffa8b450b24c1509993 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 09:54:41 +0100 Subject: [PATCH 19/33] SDCard framework: add support for SDIO/SD/MMC mode --- internal_filesystem/lib/mpos/sdcard.py | 135 +++++++++++++++++++++++-- 1 file changed, 125 insertions(+), 10 deletions(-) diff --git a/internal_filesystem/lib/mpos/sdcard.py b/internal_filesystem/lib/mpos/sdcard.py index 0f7c93bb..8b696e57 100644 --- a/internal_filesystem/lib/mpos/sdcard.py +++ b/internal_filesystem/lib/mpos/sdcard.py @@ -3,17 +3,97 @@ import machine import vfs class SDCardManager: - def __init__(self, spi_bus, cs_pin): + def __init__(self, mode='spi', spi_bus=None, cs_pin=None, cmd_pin=None, clk_pin=None, + d0_pin=None, d1_pin=None, d2_pin=None, d3_pin=None, slot=1, width=4, freq=20000000): self._sdcard = None + self._mode = None + + # Auto-detect mode: if SDIO pins provided, use SDIO; otherwise use SPI + if cmd_pin is not None or clk_pin is not None or d0_pin is not None: + self._mode = 'sdio' + else: + self._mode = 'spi' + + # Allow explicit mode override + if mode in ('spi', 'sdio'): + self._mode = mode + + print(f"SD card mode: {self._mode.upper()}") + + if self._mode == 'spi': + self._init_spi(spi_bus, cs_pin) + elif self._mode == 'sdio': + self._init_sdio(cmd_pin, clk_pin, d0_pin, d1_pin, d2_pin, d3_pin, slot, width, freq) + + def _init_spi(self, spi_bus, cs_pin): + """Initialize SD card in SPI mode.""" + if spi_bus is None or cs_pin is None: + print("ERROR: SPI mode requires spi_bus and cs_pin parameters") + print(" - Provide: init(spi_bus=machine.SPI(...), cs_pin=pin_number)") + return + try: self._sdcard = machine.SDCard(spi_bus=spi_bus, cs=cs_pin) self._sdcard.info() - print("SD card initialized successfully") + print("SD card initialized successfully in SPI mode") except Exception as e: - print(f"ERROR: Failed to initialize SD card: {e}") + print(f"ERROR: Failed to initialize SD card in SPI mode: {e}") print(" - Possible causes: Invalid SPI configuration, SD card not inserted, faulty wiring, or firmware issue") print(f" - Check: SPI pins for the SPI bus, card insertion, VCC (3.3V/5V), GND") print(" - Try: Hard reset ESP32, test with known-good SD card") + + def _init_sdio(self, cmd_pin, clk_pin, d0_pin, d1_pin=None, d2_pin=None, d3_pin=None, + slot=1, width=4, freq=20000000): + """Initialize SD card in SDIO mode.""" + # Validate required SDIO parameters + if cmd_pin is None or clk_pin is None or d0_pin is None: + print("ERROR: SDIO mode requires cmd_pin, clk_pin, and d0_pin parameters") + print(" - Provide: init(mode='sdio', cmd_pin=X, clk_pin=Y, d0_pin=Z, ...)") + return + + # Validate width parameter + if width not in (1, 4): + print(f"ERROR: SDIO width must be 1 or 4, got {width}") + return + + # Validate slot parameter + if slot not in (0, 1): + print(f"ERROR: SDIO slot must be 0 or 1, got {slot}") + return + + try: + # For 4-bit mode, all data pins are required + if width == 4: + if d1_pin is None or d2_pin is None or d3_pin is None: + print("ERROR: SDIO 4-bit mode requires d0_pin, d1_pin, d2_pin, and d3_pin") + print(" - Provide all four data pins for 4-bit mode") + return + + self._sdcard = machine.SDCard( + slot=slot, + cmd=cmd_pin, + clk=clk_pin, + data_pins=(d0_pin,d1_pin,d2_pin,d3_pin,), + width=width, + freq=freq + ) + else: # 1-bit mode + self._sdcard = machine.SDCard( + slot=slot, + cmd=cmd_pin, + clk=clk_pin, + data_pins=(d0_pin,), + width=width, + freq=freq + ) + + self._sdcard.info() + print(f"SD card initialized successfully in SDIO mode (slot={slot}, width={width}-bit, freq={freq}Hz)") + except Exception as e: + print(f"ERROR: Failed to initialize SD card in SDIO mode: {e}") + print(" - Possible causes: Invalid SDIO pin configuration, SD card not inserted, faulty wiring, or firmware issue") + print(f" - Check: SDIO pins (CMD, CLK, D0-D3), card insertion, VCC (3.3V), GND") + print(" - Try: Hard reset ESP32, verify pin assignments, test with known-good SD card") def _try_mount(self, mount_point): try: @@ -119,11 +199,36 @@ class SDCardManager: # --- Singleton pattern --- _manager = None -def init(spi_bus, cs_pin): - """Initialize the global SD card manager.""" +def init(mode='spi', spi_bus=None, cs_pin=None, cmd_pin=None, clk_pin=None, + d0_pin=None, d1_pin=None, d2_pin=None, d3_pin=None, slot=1, width=4, freq=20000000): + """ + Initialize the global SD card manager. + + SPI mode (default): + init(spi_bus=machine.SPI(...), cs_pin=pin_number) + + SDIO mode: + init(mode='sdio', cmd_pin=X, clk_pin=Y, d0_pin=Z, d1_pin=A, d2_pin=B, d3_pin=C, slot=1, width=4, freq=20000000) + + Auto-detection: + If SDIO pins are provided, SDIO mode is used automatically. + """ global _manager if _manager is None: - _manager = SDCardManager(spi_bus, cs_pin) + _manager = SDCardManager( + mode=mode, + spi_bus=spi_bus, + cs_pin=cs_pin, + cmd_pin=cmd_pin, + clk_pin=clk_pin, + d0_pin=d0_pin, + d1_pin=d1_pin, + d2_pin=d2_pin, + d3_pin=d3_pin, + slot=slot, + width=width, + freq=freq + ) else: print("WARNING: SDCardManager already initialized") print(" - Use existing instance via get()") @@ -133,22 +238,32 @@ def get(): """Get the global SD card manager instance.""" if _manager is None: print("ERROR: SDCardManager not initialized") - print(" - Call init(spi_bus, cs_pin) first in lib/mpos/board/*.py") + print(" - Call init() with appropriate parameters first in lib/mpos/board/*.py") + print(" - SPI mode: init(spi_bus=machine.SPI(...), cs_pin=pin_number)") + print(" - SDIO mode: init(mode='sdio', cmd_pin=X, clk_pin=Y, d0_pin=Z, ...)") return _manager +def get_mode(): + """Get the current SD card mode ('spi' or 'sdio').""" + mgr = get() + if mgr is None: + print("ERROR: Cannot get mode - SDCardManager not initialized") + return None + return mgr._mode + def mount(mount_point): mgr = get() if mgr is None: print("ERROR: Cannot mount - SDCardManager not initialized") - print(" - Call init(spi_bus, cs_pin) first") + print(" - Call init() with appropriate parameters first") return False - return mgr.mount(mount_point) + return mgr.mount_with_optional_format(mount_point) def mount_with_optional_format(mount_point): mgr = get() if mgr is None: print("ERROR: Cannot mount with format - SDCardManager not initialized") - print(" - Call init(spi_bus, cs_pin) first") + print(" - Call init() with appropriate parameters first") return False success = mgr.mount_with_optional_format(mount_point) if not success: From 67f01c3af207e6812fda10b6d2dcb9735b9fafeb Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 11:25:45 +0100 Subject: [PATCH 20/33] SDCard framework: add support for SDIO/SD/MMC mode --- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 10 +-- internal_filesystem/lib/mpos/sdcard.py | 72 +++++++++++++++---- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index d9f37c5a..870eb2a4 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -100,13 +100,9 @@ io0_pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=io0_interrupt_handler) # Initialize LVGL lv.init() -# TODO: initialize SDIO (instead of SPI) SD card with: -# CMD = 2 -# SCLK = 42 -# D0 = 41 -#import mpos.sdcard -#mpos.sdcard.init(spi_bus, cs_pin=14) - +# Initialize SD card in SDIO mode +from mpos import sdcard +sdcard.init(cmd_pin=2,clk_pin=42,d0_pin=41) # === AUDIO HARDWARE === # Note: MaTouch ESP32-S3 has no buzzer or I2S audio hardware diff --git a/internal_filesystem/lib/mpos/sdcard.py b/internal_filesystem/lib/mpos/sdcard.py index 8b696e57..28731d3e 100644 --- a/internal_filesystem/lib/mpos/sdcard.py +++ b/internal_filesystem/lib/mpos/sdcard.py @@ -3,8 +3,8 @@ import machine import vfs class SDCardManager: - def __init__(self, mode='spi', spi_bus=None, cs_pin=None, cmd_pin=None, clk_pin=None, - d0_pin=None, d1_pin=None, d2_pin=None, d3_pin=None, slot=1, width=4, freq=20000000): + def __init__(self, mode=None, spi_bus=None, cs_pin=None, cmd_pin=None, clk_pin=None, + d0_pin=None, d1_pin=None, d2_pin=None, d3_pin=None, slot=1, width=None, freq=20000000): self._sdcard = None self._mode = None @@ -14,8 +14,8 @@ class SDCardManager: else: self._mode = 'spi' - # Allow explicit mode override - if mode in ('spi', 'sdio'): + # Allow explicit mode override only if explicitly provided (not default) + if mode is not None and mode in ('spi', 'sdio'): self._mode = mode print(f"SD card mode: {self._mode.upper()}") @@ -43,7 +43,7 @@ class SDCardManager: print(" - Try: Hard reset ESP32, test with known-good SD card") def _init_sdio(self, cmd_pin, clk_pin, d0_pin, d1_pin=None, d2_pin=None, d3_pin=None, - slot=1, width=4, freq=20000000): + slot=1, width=None, freq=20000000): """Initialize SD card in SDIO mode.""" # Validate required SDIO parameters if cmd_pin is None or clk_pin is None or d0_pin is None: @@ -51,6 +51,33 @@ class SDCardManager: print(" - Provide: init(mode='sdio', cmd_pin=X, clk_pin=Y, d0_pin=Z, ...)") return + # Auto-detect SDIO width based on provided data pins + # This happens BEFORE explicit width validation to allow user override + if width is None: + # Count how many data pins are provided + data_pins_provided = sum([ + d0_pin is not None, + d1_pin is not None, + d2_pin is not None, + d3_pin is not None + ]) + + if data_pins_provided == 1: + # Only d0_pin provided: use 1-bit mode + width = 1 + print("INFO: Auto-detected SDIO width=1 (only d0_pin provided)") + elif data_pins_provided == 4: + # All four data pins provided: use 4-bit mode + width = 4 + print("INFO: Auto-detected SDIO width=4 (all four data pins provided)") + else: + # Partial pins provided: this is an error + print(f"ERROR: Invalid SDIO pin configuration - {data_pins_provided} data pins provided") + print(" - For 1-bit mode: provide only d0_pin") + print(" - For 4-bit mode: provide all four pins (d0_pin, d1_pin, d2_pin, d3_pin)") + print(" - Or explicitly specify width parameter to override auto-detection") + return + # Validate width parameter if width not in (1, 4): print(f"ERROR: SDIO width must be 1 or 4, got {width}") @@ -61,14 +88,23 @@ class SDCardManager: print(f"ERROR: SDIO slot must be 0 or 1, got {slot}") return + # Validate that provided pins match the requested width + if width == 4: + if d1_pin is None or d2_pin is None or d3_pin is None: + print("ERROR: SDIO 4-bit mode requires all four data pins (d0_pin, d1_pin, d2_pin, d3_pin)") + print(" - Provide all four data pins for 4-bit mode") + print(" - Or use 1-bit mode with only d0_pin") + return + elif width == 1: + if d1_pin is not None or d2_pin is not None or d3_pin is not None: + print("ERROR: SDIO 1-bit mode should only have d0_pin, but extra pins were provided") + print(" - For 1-bit mode: provide only d0_pin") + print(" - For 4-bit mode: provide all four pins (d0_pin, d1_pin, d2_pin, d3_pin)") + return + try: # For 4-bit mode, all data pins are required if width == 4: - if d1_pin is None or d2_pin is None or d3_pin is None: - print("ERROR: SDIO 4-bit mode requires d0_pin, d1_pin, d2_pin, and d3_pin") - print(" - Provide all four data pins for 4-bit mode") - return - self._sdcard = machine.SDCard( slot=slot, cmd=cmd_pin, @@ -199,18 +235,24 @@ class SDCardManager: # --- Singleton pattern --- _manager = None -def init(mode='spi', spi_bus=None, cs_pin=None, cmd_pin=None, clk_pin=None, - d0_pin=None, d1_pin=None, d2_pin=None, d3_pin=None, slot=1, width=4, freq=20000000): +def init(mode=None, spi_bus=None, cs_pin=None, cmd_pin=None, clk_pin=None, + d0_pin=None, d1_pin=None, d2_pin=None, d3_pin=None, slot=1, width=None, freq=20000000): """ Initialize the global SD card manager. SPI mode (default): init(spi_bus=machine.SPI(...), cs_pin=pin_number) - SDIO mode: - init(mode='sdio', cmd_pin=X, clk_pin=Y, d0_pin=Z, d1_pin=A, d2_pin=B, d3_pin=C, slot=1, width=4, freq=20000000) + SDIO mode with auto-detection: + init(mode='sdio', cmd_pin=X, clk_pin=Y, d0_pin=Z, d1_pin=A, d2_pin=B, d3_pin=C, slot=1, freq=20000000) - Auto-detection: + SDIO width auto-detection: + - If only d0_pin is provided: width is auto-set to 1 (1-bit mode) + - If all four data pins (d0, d1, d2, d3) are provided: width is auto-set to 4 (4-bit mode) + - If width parameter is explicitly provided: that value is used (overrides auto-detection) + - If partial data pins are provided (e.g., only d0 and d1): raises an error + + Auto-detection of mode: If SDIO pins are provided, SDIO mode is used automatically. """ global _manager From 6c4f4c0fdbf561ac900b76da451faecdf3c8e2c6 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 11:27:26 +0100 Subject: [PATCH 21/33] CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b4a99e7..44d0a492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Builtin Apps: - Launcher: fit at least 3 apps per row on a 240px display - OSUpdate: replace "force update" checkbox with improved button labels +Frameworks: +- SDCard framework: add support for SDIO/SD/MMC mode + OS: - Scale MicroPythonOS boot logo down if necessary - Additional board support: MaTouch ESP32-S3 SPI IPS 2.8" with Camera OV3660 From 1973d22b8a5b161b55d33a94c1fef059e3695c87 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 12:56:04 +0100 Subject: [PATCH 22/33] Cleanup --- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index 870eb2a4..208512e0 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -104,18 +104,10 @@ lv.init() from mpos import sdcard sdcard.init(cmd_pin=2,clk_pin=42,d0_pin=41) -# === AUDIO HARDWARE === -# Note: MaTouch ESP32-S3 has no buzzer or I2S audio hardware -# AudioManager will not be initialized - # === LED HARDWARE === # Note: MaTouch ESP32-S3 has no NeoPixel LEDs # LightsManager will not be initialized (functions will return False) -# === SENSOR HARDWARE === -# Note: MaTouch ESP32-S3 has no IMU sensor -# SensorManager will not be initialized - # === CAMERA HARDWARE === from mpos import CameraManager From 451a72a14e6a4bc145c9f132aefe0cbe34067b6c Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 12:58:47 +0100 Subject: [PATCH 23/33] CameraManager: add resolution_to_framesize(width, height) --- internal_filesystem/lib/mpos/board/linux.py | 4 +-- .../lib/mpos/ui/camera_activity.py | 34 ++----------------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/linux.py b/internal_filesystem/lib/mpos/board/linux.py index dd00f307..42298992 100644 --- a/internal_filesystem/lib/mpos/board/linux.py +++ b/internal_filesystem/lib/mpos/board/linux.py @@ -10,8 +10,8 @@ import mpos.ui.focus_direction from mpos import InputManager # Same as Waveshare ESP32-S3-Touch-LCD-2 and Fri3d Camp 2026 Badge -TFT_HOR_RES=320 -TFT_VER_RES=240 +TFT_HOR_RES=240 +TFT_VER_RES=320 # Fri3d Camp 2024 Badge: #TFT_HOR_RES=296 diff --git a/internal_filesystem/lib/mpos/ui/camera_activity.py b/internal_filesystem/lib/mpos/ui/camera_activity.py index 3b50ec90..2cf1e0dc 100644 --- a/internal_filesystem/lib/mpos/ui/camera_activity.py +++ b/internal_filesystem/lib/mpos/ui/camera_activity.py @@ -8,6 +8,7 @@ except Exception as e: from ..time import epoch_seconds from .camera_settings import CameraSettingsActivity +from .camera_manager import CameraManager from .. import ui as mpos_ui from ..app.activity import Activity @@ -406,37 +407,8 @@ class CameraActivity(Activity): try: from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling - # Map resolution to FrameSize enum - # Format: (width, height): FrameSize - resolution_map = { - (96, 96): FrameSize.R96X96, - (160, 120): FrameSize.QQVGA, - (128, 128): FrameSize.R128X128, - (176, 144): FrameSize.QCIF, - (240, 176): FrameSize.HQVGA, - (240, 240): FrameSize.R240X240, - (320, 240): FrameSize.QVGA, - (320, 320): FrameSize.R320X320, - (400, 296): FrameSize.CIF, - (480, 320): FrameSize.HVGA, - (480, 480): FrameSize.R480X480, - (640, 480): FrameSize.VGA, - (640, 640): FrameSize.R640X640, - (720, 720): FrameSize.R720X720, - (800, 600): FrameSize.SVGA, - (800, 800): FrameSize.R800X800, - (1024, 768): FrameSize.XGA, - (960, 960): FrameSize.R960X960, - (1280, 720): FrameSize.HD, - (1024, 1024): FrameSize.R1024X1024, - # These are disabled in camera_settings.py because they use a lot of RAM: - (1280, 1024): FrameSize.SXGA, - (1280, 1280): FrameSize.R1280X1280, - (1600, 1200): FrameSize.UXGA, - (1920, 1080): FrameSize.FHD, - } - - frame_size = resolution_map.get((width, height), FrameSize.R240X240) + # Map resolution to FrameSize enum using CameraManager + frame_size = CameraManager.resolution_to_framesize(width, height) print(f"init_internal_cam: Using FrameSize {frame_size} for {width}x{height}") # Try to initialize, with one retry for I2C poweroff issue From 52d7fed8ded83a6f48214809ab5db18c13b2aabf Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 15:38:13 +0100 Subject: [PATCH 24/33] CameraManager: add init and deinit callbacks Rather than hardcoding all the pins, they are set a board init time, and then camera_activity.py calls them when needed. --- CHANGELOG.md | 3 +- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 100 +++++++++++++++++- .../lib/mpos/camera_manager.py | 59 ++++++++++- .../lib/mpos/ui/camera_activity.py | 98 +---------------- 4 files changed, 160 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d0a492..305f6752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ Builtin Apps: - OSUpdate: replace "force update" checkbox with improved button labels Frameworks: -- SDCard framework: add support for SDIO/SD/MMC mode +- SDCard: add support for SDIO/SD/MMC mode +- CameraManager: add init and deinit callbacks OS: - Scale MicroPythonOS boot logo down if necessary diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index 208512e0..1ffc8a86 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -109,6 +109,100 @@ sdcard.init(cmd_pin=2,clk_pin=42,d0_pin=41) # LightsManager will not be initialized (functions will return False) # === CAMERA HARDWARE === +def init_cam(width, height, colormode): + try: + from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling + + # Map resolution to FrameSize enum using CameraManager + frame_size = CameraManager.resolution_to_framesize(width, height) + print(f"init_internal_cam: Using FrameSize {frame_size} for {width}x{height}") + + # Try to initialize, with one retry for I2C poweroff issue + max_attempts = 3 + toreturn = None + for attempt in range(max_attempts): + try: + cam = Camera( + data_pins=[7,5,4,6,16,8,3,46], + vsync_pin=11, + href_pin=10, + sda_pin=39, + scl_pin=38, + pclk_pin=17, + xclk_pin=9, + xclk_freq=20000000, + powerdown_pin=-1, + reset_pin=-1, + pixel_format=PixelFormat.RGB565 if colormode else PixelFormat.GRAYSCALE, + frame_size=frame_size, + #grab_mode=GrabMode.WHEN_EMPTY, + grab_mode=GrabMode.LATEST, + fb_count=1 + ) + cam.set_vflip(True) + + toreturn=cam + break + except Exception as e: + if attempt < max_attempts-1: + print(f"init_cam attempt {attempt} failed: {e}, retrying...") + else: + print(f"init_cam final exception: {e}") + break + + if toreturn: + try: + from mpos import InputManager + indev = InputManager.list_indevs()[0] + indev.enable(False) + InputManager.unregister_indev(indev) + print("input disabled") + except Exception as e: + print(f"disabling indev got exception: {e}") + + return toreturn + except Exception as e: + print(f"init_cam exception: {e}") + return None + +def deinit_cam(cam): + cam.deinit() + # Power off, otherwise it keeps using a lot of current + try: + from machine import Pin, I2C + i2c = I2C(1, scl=Pin(38), sda=Pin(39)) # Adjust pins and frequency + #devices = i2c.scan() + #print([hex(addr) for addr in devices]) # finds it on 60 = 0x3C after init + camera_addr = 0x3C # for OV5640 + reg_addr = 0x3008 + reg_high = (reg_addr >> 8) & 0xFF # 0x30 + reg_low = reg_addr & 0xFF # 0x08 + power_off_command = 0x42 # Power off command + i2c.writeto(camera_addr, bytes([reg_high, reg_low, power_off_command])) + except Exception as e: + print(f"Warning: powering off camera got exception: {e}") + import time + time.sleep_ms(100) + try: + # hardware reset might work too, but doesn't seem to: + #from mpos import InputManager + #indev = InputManager.list_indevs()[0] + #indev.hw_reset() + #indev.enable(True) + #print("input enabled") + #time.sleep(1) + import i2c + i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39) + import mpos.indev.gt911 as gt911 + touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) + indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) # remove debug because it's slower + print("new indev created") + from mpos import InputManager + InputManager.register_indev(indev) + print("new indev registered") + except Exception as e: + print(f"Indev enable got exception: {e}") + from mpos import CameraManager # MaTouch ESP32-S3 has OV3660 camera (3MP, up to 2048x1536) @@ -116,13 +210,13 @@ from mpos import CameraManager CameraManager.add_camera(CameraManager.Camera( lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK, name="OV3660", - vendor="OmniVision" + vendor="OmniVision", + init=init_cam, + deinit=deinit_cam, )) print("matouch_esp32_s3_2_8.py finished") print("Board capabilities:") print(" - Display: 320x240 ST7789 with GT911 touch") print(" - Camera: OV3660 (3MP)") -print(" - No IMU sensor") print(" - No LEDs") -print(" - No audio hardware") diff --git a/internal_filesystem/lib/mpos/camera_manager.py b/internal_filesystem/lib/mpos/camera_manager.py index 990aee68..cb505cbf 100644 --- a/internal_filesystem/lib/mpos/camera_manager.py +++ b/internal_filesystem/lib/mpos/camera_manager.py @@ -37,7 +37,7 @@ class Camera: Represents a camera device with its characteristics. """ - def __init__(self, lens_facing, name=None, vendor=None, version=None): + def __init__(self, lens_facing, name=None, vendor=None, version=None, init=None, deinit=None): """Initialize camera metadata. Args: @@ -50,6 +50,8 @@ class Camera: self.name = name or "Camera" self.vendor = vendor or "Unknown" self.version = version or 1 + self.init_function = init + self.deinit_function = deinit def __repr__(self): facing_names = { @@ -60,6 +62,13 @@ class Camera: facing_str = facing_names.get(self.lens_facing, f"UNKNOWN({self.lens_facing})") return f"Camera({self.name}, facing={facing_str})" + def init(self, width, height, colormode): + if self.init_function: + return self.init_function(width, height, colormode) + + def deinit(self, cam_obj=None): + if self.deinit_function: + return self.deinit_function(cam_obj) class CameraManager: """ @@ -180,6 +189,54 @@ class CameraManager: """ return len(CameraManager._cameras) + @staticmethod + def resolution_to_framesize(width, height): + """Map resolution (width, height) to FrameSize enum. + + Args: + width: Image width in pixels + height: Image height in pixels + + Returns: + FrameSize enum value corresponding to the resolution, or R240X240 as default + """ + try: + from camera import FrameSize + except ImportError: + print("Warning: camera module not available") + return None + + # Format: (width, height): FrameSize + resolution_map = { + (96, 96): FrameSize.R96X96, + (160, 120): FrameSize.QQVGA, + (128, 128): FrameSize.R128X128, + (176, 144): FrameSize.QCIF, + (240, 176): FrameSize.HQVGA, + (240, 240): FrameSize.R240X240, + (320, 240): FrameSize.QVGA, + (320, 320): FrameSize.R320X320, + (400, 296): FrameSize.CIF, + (480, 320): FrameSize.HVGA, + (480, 480): FrameSize.R480X480, + (640, 480): FrameSize.VGA, + (640, 640): FrameSize.R640X640, + (720, 720): FrameSize.R720X720, + (800, 600): FrameSize.SVGA, + (800, 800): FrameSize.R800X800, + (1024, 768): FrameSize.XGA, + (960, 960): FrameSize.R960X960, + (1280, 720): FrameSize.HD, + (1024, 1024): FrameSize.R1024X1024, + # These are disabled in camera_settings.py because they use a lot of RAM: + (1280, 1024): FrameSize.SXGA, + (1280, 1280): FrameSize.R1280X1280, + (1600, 1200): FrameSize.UXGA, + (1920, 1080): FrameSize.FHD, + } + + return resolution_map.get((width, height), FrameSize.R240X240) + # ============================================================================ # Class method delegation (at module level) diff --git a/internal_filesystem/lib/mpos/ui/camera_activity.py b/internal_filesystem/lib/mpos/ui/camera_activity.py index 2cf1e0dc..b260439c 100644 --- a/internal_filesystem/lib/mpos/ui/camera_activity.py +++ b/internal_filesystem/lib/mpos/ui/camera_activity.py @@ -8,7 +8,7 @@ except Exception as e: from ..time import epoch_seconds from .camera_settings import CameraSettingsActivity -from .camera_manager import CameraManager +from ..camera_manager import CameraManager from .. import ui as mpos_ui from ..app.activity import Activity @@ -137,17 +137,8 @@ class CameraActivity(Activity): def start_cam(self): # Init camera: - self.cam = self.init_internal_cam(self.width, self.height) + self.cam = CameraManager.get_cameras()[0].init(self.width, self.height, self.colormode) if self.cam: - try: - from mpos import InputManager - indev = InputManager.list_indevs()[0] - indev.enable(False) - InputManager.unregister_indev(indev) - print("input disabled") - except Exception as e: - print(f"disabling indev got exception: {e}") - #self.image.set_rotation(900) # internal camera is rotated 90 degrees # Apply saved camera settings, only for internal camera for now: self.apply_camera_settings(self.scanqr_prefs if self.scanqr_mode else self.prefs, self.cam, self.use_webcam) # needs to be done AFTER the camera is initialized else: @@ -171,43 +162,7 @@ class CameraActivity(Activity): if self.use_webcam: webcam.deinit(self.cam) elif self.cam: - self.cam.deinit() - # Power off, otherwise it keeps using a lot of current - try: - from machine import Pin, I2C - i2c = I2C(1, scl=Pin(38), sda=Pin(39)) # Adjust pins and frequency - #devices = i2c.scan() - #print([hex(addr) for addr in devices]) # finds it on 60 = 0x3C after init - camera_addr = 0x3C # for OV5640 - reg_addr = 0x3008 - reg_high = (reg_addr >> 8) & 0xFF # 0x30 - reg_low = reg_addr & 0xFF # 0x08 - power_off_command = 0x42 # Power off command - i2c.writeto(camera_addr, bytes([reg_high, reg_low, power_off_command])) - except Exception as e: - print(f"Warning: powering off camera got exception: {e}") - import time - time.sleep_ms(100) - try: - # hardware reset might work too, but doesn't seem to: - #from mpos import InputManager - #indev = InputManager.list_indevs()[0] - #indev.hw_reset() - #indev.enable(True) - #print("input enabled") - #time.sleep(1) - import i2c - i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39) - import mpos.indev.gt911 as gt911 - touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) - indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) # remove debug because it's slower - print("new indev created") - from mpos import InputManager - InputManager.register_indev(indev) - print("new indev registered") - except Exception as e: - print(f"Indev enable got exception: {e}") - + CameraManager.get_cameras()[0].deinit(self.cam) self.cam = None if self.image_dsc: # it's important to delete the image when stopping the camera, otherwise LVGL might try to display it and crash print("emptying self.current_cam_buffer...") @@ -399,51 +354,6 @@ class CameraActivity(Activity): if not self.use_webcam and self.cam: self.cam.free_buffer() # After QR decoding, free the old buffer, otherwise the camera doesn't provide a new one - def init_internal_cam(self, width, height): - """Initialize internal camera with specified resolution. - - Automatically retries once if initialization fails (to handle I2C poweroff issue). - """ - try: - from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling - - # Map resolution to FrameSize enum using CameraManager - frame_size = CameraManager.resolution_to_framesize(width, height) - print(f"init_internal_cam: Using FrameSize {frame_size} for {width}x{height}") - - # Try to initialize, with one retry for I2C poweroff issue - max_attempts = 3 - for attempt in range(max_attempts): - try: - cam = Camera( - data_pins=[7,5,4,6,16,8,3,46], - vsync_pin=11, - href_pin=10, - sda_pin=39, - scl_pin=38, - pclk_pin=17, - xclk_pin=9, - xclk_freq=20000000, - powerdown_pin=-1, - reset_pin=-1, - pixel_format=PixelFormat.RGB565 if self.colormode else PixelFormat.GRAYSCALE, - frame_size=frame_size, - #grab_mode=GrabMode.WHEN_EMPTY, - grab_mode=GrabMode.LATEST, - fb_count=1 - ) - cam.set_vflip(True) - return cam - except Exception as e: - if attempt < max_attempts-1: - print(f"init_cam attempt {attempt} failed: {e}, retrying...") - else: - print(f"init_cam final exception: {e}") - return None - except Exception as e: - print(f"init_cam exception: {e}") - return None - def print_qr_buffer(self, buffer): try: # Try to decode buffer as a UTF-8 string @@ -583,8 +493,6 @@ class CameraActivity(Activity): except Exception as e: print(f"Error applying camera settings: {e}") - - """ From bfd38041184caf3502eff5f9ad5c6a71d172dfbd Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 19:17:02 +0100 Subject: [PATCH 25/33] CameraActivity: make camera-independent --- internal_filesystem/lib/mpos/board/linux.py | 42 +++-- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 9 +- .../lib/mpos/camera_manager.py | 123 +++++++++++++- .../lib/mpos/ui/camera_activity.py | 150 ++---------------- 4 files changed, 168 insertions(+), 156 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/linux.py b/internal_filesystem/lib/mpos/board/linux.py index 42298992..2e519259 100644 --- a/internal_filesystem/lib/mpos/board/linux.py +++ b/internal_filesystem/lib/mpos/board/linux.py @@ -126,20 +126,36 @@ SensorManager.init(None) # === CAMERA HARDWARE === -try: - # Try to initialize webcam to verify it's available +def init_cam(width, height, colormode): + try: + # Try to initialize webcam to verify it's available + import webcam + return webcam.init("/dev/video0", width=width, height=height) + except Exception as e: + print(f"Info: webcam initialization failed, camera will not be available: {e}") + +def deinit_cam(cam_obj): import webcam - test_cam = webcam.init("/dev/video0", width=320, height=240) - if test_cam: - webcam.deinit(test_cam) - from mpos import CameraManager - CameraManager.add_camera(CameraManager.Camera( - lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT, - name="Video4Linux2 Camera", - vendor="ACME" - )) -except Exception as e: - print(f"Info: webcam initialization failed, camera will not be available: {e}") + webcam.deinit(cam_obj) + +def capture_cam(cam_obj, colormode): + import webcam + return webcam.capture_frame(cam_obj, "rgb565" if colormode else "grayscale") + +def apply_cam_settings(cam_obj, prefs): + print("V4L Camera doesn't support settings for now, skipping...") + +from mpos import CameraManager +CameraManager.add_camera(CameraManager.Camera( + lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT, + name="Video4Linux2 Camera", + vendor="ACME", + init=init_cam, + deinit=deinit_cam, + capture=capture_cam, + apply_settings=apply_cam_settings +)) + print("linux.py finished") diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index 1ffc8a86..fd6eaebb 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -109,6 +109,8 @@ sdcard.init(cmd_pin=2,clk_pin=42,d0_pin=41) # LightsManager will not be initialized (functions will return False) # === CAMERA HARDWARE === +from mpos import CameraManager + def init_cam(width, height, colormode): try: from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling @@ -203,7 +205,11 @@ def deinit_cam(cam): except Exception as e: print(f"Indev enable got exception: {e}") -from mpos import CameraManager +def capture_cam(cam_obj, colormode): + return cam_obj.capture() + +def apply_cam_settings(cam_obj, prefs): + return CameraManager.ov_apply_camera_settings(cam_obj, prefs) # MaTouch ESP32-S3 has OV3660 camera (3MP, up to 2048x1536) # Camera pins are available but initialization is handled by the camera driver @@ -213,6 +219,7 @@ CameraManager.add_camera(CameraManager.Camera( vendor="OmniVision", init=init_cam, deinit=deinit_cam, + capture=capture_cam )) print("matouch_esp32_s3_2_8.py finished") diff --git a/internal_filesystem/lib/mpos/camera_manager.py b/internal_filesystem/lib/mpos/camera_manager.py index cb505cbf..90cc37a3 100644 --- a/internal_filesystem/lib/mpos/camera_manager.py +++ b/internal_filesystem/lib/mpos/camera_manager.py @@ -37,7 +37,7 @@ class Camera: Represents a camera device with its characteristics. """ - def __init__(self, lens_facing, name=None, vendor=None, version=None, init=None, deinit=None): + def __init__(self, lens_facing, name=None, vendor=None, version=None, init=None, deinit=None, capture=None, apply_settings=None): """Initialize camera metadata. Args: @@ -52,6 +52,8 @@ class Camera: self.version = version or 1 self.init_function = init self.deinit_function = deinit + self.capture_function = capture + self.apply_settings_function = apply_settings def __repr__(self): facing_names = { @@ -70,6 +72,14 @@ class Camera: if self.deinit_function: return self.deinit_function(cam_obj) + def capture(self, cam_obj, colormode=None): + if self.capture_function: + return self.capture_function(cam_obj, colormode) + + def apply_settings(self, cam_obj, prefs): + if self.apply_settings_function: + return self.apply_settings_function(cam_obj, prefs) + class CameraManager: """ Centralized camera device management service. @@ -237,6 +247,117 @@ class CameraManager: return resolution_map.get((width, height), FrameSize.R240X240) + @staticmethod + def ov_apply_camera_settings(self, cam, prefs): + if not cam or not prefs: + print("ov_apply_camera_settings: Skipping because invalid prefs or cam object") + return + + try: + # Basic image adjustments + brightness = prefs.get_int("brightness") + cam.set_brightness(brightness) + + contrast = prefs.get_int("contrast") + cam.set_contrast(contrast) + + saturation = prefs.get_int("saturation") + cam.set_saturation(saturation) + + # Orientation + hmirror = prefs.get_bool("hmirror") + cam.set_hmirror(hmirror) + + vflip = prefs.get_bool("vflip") + cam.set_vflip(vflip) + + # Special effect + special_effect = prefs.get_int("special_effect") + cam.set_special_effect(special_effect) + + # Exposure control (apply master switch first, then manual value) + exposure_ctrl = prefs.get_bool("exposure_ctrl") + cam.set_exposure_ctrl(exposure_ctrl) + + if not exposure_ctrl: + aec_value = prefs.get_int("aec_value") + cam.set_aec_value(aec_value) + + # Mode-specific default comes from constructor + ae_level = prefs.get_int("ae_level") + cam.set_ae_level(ae_level) + + aec2 = prefs.get_bool("aec2") + cam.set_aec2(aec2) + + # Gain control (apply master switch first, then manual value) + gain_ctrl = prefs.get_bool("gain_ctrl") + cam.set_gain_ctrl(gain_ctrl) + + if not gain_ctrl: + agc_gain = prefs.get_int("agc_gain") + cam.set_agc_gain(agc_gain) + + gainceiling = prefs.get_int("gainceiling") + cam.set_gainceiling(gainceiling) + + # White balance (apply master switch first, then mode) + whitebal = prefs.get_bool("whitebal") + cam.set_whitebal(whitebal) + + if not whitebal: + wb_mode = prefs.get_int("wb_mode") + cam.set_wb_mode(wb_mode) + + awb_gain = prefs.get_bool("awb_gain") + cam.set_awb_gain(awb_gain) + + # Sensor-specific settings (try/except for unsupported sensors) + try: + sharpness = prefs.get_int("sharpness") + cam.set_sharpness(sharpness) + except: + pass # Not supported on OV2640? + + try: + denoise = prefs.get_int("denoise") + cam.set_denoise(denoise) + except: + pass # Not supported on OV2640? + + # Advanced corrections + colorbar = prefs.get_bool("colorbar") + cam.set_colorbar(colorbar) + + dcw = prefs.get_bool("dcw") + cam.set_dcw(dcw) + + bpc = prefs.get_bool("bpc") + cam.set_bpc(bpc) + + wpc = prefs.get_bool("wpc") + cam.set_wpc(wpc) + + # Mode-specific default comes from constructor + raw_gma = prefs.get_bool("raw_gma") + print(f"applying raw_gma: {raw_gma}") + cam.set_raw_gma(raw_gma) + + lenc = prefs.get_bool("lenc") + cam.set_lenc(lenc) + + # JPEG quality (only relevant for JPEG format) + #try: + # quality = prefs.get_int("quality", 85) + # cam.set_quality(quality) + #except: + # pass # Not in JPEG mode + + print("Camera settings applied successfully") + + except Exception as e: + print(f"Error applying camera settings: {e}") + # ============================================================================ # Class method delegation (at module level) diff --git a/internal_filesystem/lib/mpos/ui/camera_activity.py b/internal_filesystem/lib/mpos/ui/camera_activity.py index b260439c..70e6d301 100644 --- a/internal_filesystem/lib/mpos/ui/camera_activity.py +++ b/internal_filesystem/lib/mpos/ui/camera_activity.py @@ -140,18 +140,8 @@ class CameraActivity(Activity): self.cam = CameraManager.get_cameras()[0].init(self.width, self.height, self.colormode) if self.cam: # Apply saved camera settings, only for internal camera for now: - self.apply_camera_settings(self.scanqr_prefs if self.scanqr_mode else self.prefs, self.cam, self.use_webcam) # needs to be done AFTER the camera is initialized - else: - print("camera app: no internal camera found, trying webcam on /dev/video0") - try: - # Initialize webcam with desired resolution directly - print(f"Initializing webcam at {self.width}x{self.height}") - self.cam = webcam.init("/dev/video0", width=self.width, height=self.height) - self.use_webcam = True - except Exception as e: - print(f"camera app: webcam exception: {e}") - # Start refreshing: - if self.cam: + CameraManager.get_cameras()[0].apply_settings(self.cam, self.scanqr_prefs if self.scanqr_mode else self.prefs) # needs to be done AFTER the camera is initialized + # Start refreshing: print("Camera app initialized, continuing...") self.update_preview_image() self.capture_timer = lv.timer_create(self.try_capture, 100, None) @@ -159,9 +149,7 @@ class CameraActivity(Activity): def stop_cam(self): if self.capture_timer: self.capture_timer.delete() - if self.use_webcam: - webcam.deinit(self.cam) - elif self.cam: + if self.cam: CameraManager.get_cameras()[0].deinit(self.cam) self.cam = None if self.image_dsc: # it's important to delete the image when stopping the camera, otherwise LVGL might try to display it and crash @@ -337,11 +325,10 @@ class CameraActivity(Activity): self.startActivity(intent) def try_capture(self, event): + if not self.cam: + return try: - if self.use_webcam and self.cam: - self.current_cam_buffer = webcam.capture_frame(self.cam, "rgb565" if self.colormode else "grayscale") - elif self.cam and self.cam.frame_available(): - self.current_cam_buffer = self.cam.capture() + self.current_cam_buffer = CameraManager.get_cameras()[0].capture(self.cam, self.colormode) except Exception as e: print(f"Camera capture exception: {e}") return @@ -351,8 +338,10 @@ class CameraActivity(Activity): self.image.set_src(self.image_dsc) if self.scanqr_mode: self.qrdecode_one() - if not self.use_webcam and self.cam: + try: self.cam.free_buffer() # After QR decoding, free the old buffer, otherwise the camera doesn't provide a new one + except Exception as e: + pass # some camera API's don't have this def print_qr_buffer(self, buffer): try: @@ -373,127 +362,6 @@ class CameraActivity(Activity): if buffer.startswith(bom): return buffer[3:] return buffer - - - def apply_camera_settings(self, prefs, cam, use_webcam): - """Apply all saved camera settings to the camera. - - Only applies settings when use_webcam is False (ESP32 camera). - Settings are applied in dependency order (master switches before dependent values). - - Args: - cam: Camera object - use_webcam: Boolean indicating if using webcam - """ - if not cam or use_webcam: - print("apply_camera_settings: Skipping (no camera or webcam mode)") - return - - try: - # Basic image adjustments - brightness = prefs.get_int("brightness") - cam.set_brightness(brightness) - - contrast = prefs.get_int("contrast") - cam.set_contrast(contrast) - - saturation = prefs.get_int("saturation") - cam.set_saturation(saturation) - - # Orientation - hmirror = prefs.get_bool("hmirror") - cam.set_hmirror(hmirror) - - vflip = prefs.get_bool("vflip") - cam.set_vflip(vflip) - - # Special effect - special_effect = prefs.get_int("special_effect") - cam.set_special_effect(special_effect) - - # Exposure control (apply master switch first, then manual value) - exposure_ctrl = prefs.get_bool("exposure_ctrl") - cam.set_exposure_ctrl(exposure_ctrl) - - if not exposure_ctrl: - aec_value = prefs.get_int("aec_value") - cam.set_aec_value(aec_value) - - # Mode-specific default comes from constructor - ae_level = prefs.get_int("ae_level") - cam.set_ae_level(ae_level) - - aec2 = prefs.get_bool("aec2") - cam.set_aec2(aec2) - - # Gain control (apply master switch first, then manual value) - gain_ctrl = prefs.get_bool("gain_ctrl") - cam.set_gain_ctrl(gain_ctrl) - - if not gain_ctrl: - agc_gain = prefs.get_int("agc_gain") - cam.set_agc_gain(agc_gain) - - gainceiling = prefs.get_int("gainceiling") - cam.set_gainceiling(gainceiling) - - # White balance (apply master switch first, then mode) - whitebal = prefs.get_bool("whitebal") - cam.set_whitebal(whitebal) - - if not whitebal: - wb_mode = prefs.get_int("wb_mode") - cam.set_wb_mode(wb_mode) - - awb_gain = prefs.get_bool("awb_gain") - cam.set_awb_gain(awb_gain) - - # Sensor-specific settings (try/except for unsupported sensors) - try: - sharpness = prefs.get_int("sharpness") - cam.set_sharpness(sharpness) - except: - pass # Not supported on OV2640? - - try: - denoise = prefs.get_int("denoise") - cam.set_denoise(denoise) - except: - pass # Not supported on OV2640? - - # Advanced corrections - colorbar = prefs.get_bool("colorbar") - cam.set_colorbar(colorbar) - - dcw = prefs.get_bool("dcw") - cam.set_dcw(dcw) - - bpc = prefs.get_bool("bpc") - cam.set_bpc(bpc) - - wpc = prefs.get_bool("wpc") - cam.set_wpc(wpc) - - # Mode-specific default comes from constructor - raw_gma = prefs.get_bool("raw_gma") - print(f"applying raw_gma: {raw_gma}") - cam.set_raw_gma(raw_gma) - - lenc = prefs.get_bool("lenc") - cam.set_lenc(lenc) - - # JPEG quality (only relevant for JPEG format) - #try: - # quality = prefs.get_int("quality", 85) - # cam.set_quality(quality) - #except: - # pass # Not in JPEG mode - - print("Camera settings applied successfully") - - except Exception as e: - print(f"Error applying camera settings: {e}") - """ def zoom_button_click_unused(self, e): From d7b98f2f3822f83b1580b76158201b5fcd88cd89 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 20:03:53 +0100 Subject: [PATCH 26/33] waveshare_esp32_s3_touch_lcd_2: use generic CameraManager --- CHANGELOG.md | 2 +- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 15 ++-- .../board/waveshare_esp32_s3_touch_lcd_2.py | 79 +++++++++++++++++-- 3 files changed, 80 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 305f6752..64228d9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Builtin Apps: Frameworks: - SDCard: add support for SDIO/SD/MMC mode -- CameraManager: add init and deinit callbacks +- CameraManager and CameraActivity: work fully camera-agnostic OS: - Scale MicroPythonOS boot logo down if necessary diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index fd6eaebb..ff5038ba 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -112,6 +112,7 @@ sdcard.init(cmd_pin=2,clk_pin=42,d0_pin=41) from mpos import CameraManager def init_cam(width, height, colormode): + toreturn = None try: from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling @@ -121,7 +122,6 @@ def init_cam(width, height, colormode): # Try to initialize, with one retry for I2C poweroff issue max_attempts = 3 - toreturn = None for attempt in range(max_attempts): try: cam = Camera( @@ -160,12 +160,12 @@ def init_cam(width, height, colormode): InputManager.unregister_indev(indev) print("input disabled") except Exception as e: - print(f"disabling indev got exception: {e}") + print(f"init_cam: disabling indev got exception: {e}") - return toreturn except Exception as e: print(f"init_cam exception: {e}") - return None + + return toreturn def deinit_cam(cam): cam.deinit() @@ -173,9 +173,7 @@ def deinit_cam(cam): try: from machine import Pin, I2C i2c = I2C(1, scl=Pin(38), sda=Pin(39)) # Adjust pins and frequency - #devices = i2c.scan() - #print([hex(addr) for addr in devices]) # finds it on 60 = 0x3C after init - camera_addr = 0x3C # for OV5640 + camera_addr = 0x3C # for OV3660 reg_addr = 0x3008 reg_high = (reg_addr >> 8) & 0xFF # 0x30 reg_low = reg_addr & 0xFF # 0x08 @@ -219,7 +217,8 @@ CameraManager.add_camera(CameraManager.Camera( vendor="OmniVision", init=init_cam, deinit=deinit_cam, - capture=capture_cam + capture=capture_cam, + apply_settings=apply_cam_settings )) print("matouch_esp32_s3_2_8.py finished") diff --git a/internal_filesystem/lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py b/internal_filesystem/lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py index ee8b8e10..43c975be 100644 --- a/internal_filesystem/lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py +++ b/internal_filesystem/lib/mpos/board/waveshare_esp32_s3_touch_lcd_2.py @@ -112,12 +112,6 @@ try: except Exception as e: print(f"Warning: powering off camera got exception: {e}") -# === AUDIO HARDWARE: Waveshare board has no buzzer or I2S audio so no need to initialize. - -# === LED HARDWARE === -# Note: Waveshare board has no NeoPixel LEDs -# LightsManager will not be initialized (functions will return False) - # === SENSOR HARDWARE === from mpos import SensorManager @@ -128,11 +122,82 @@ SensorManager.init(i2c_bus, address=0x6B, mounted_position=SensorManager.FACING_ # === CAMERA HARDWARE === from mpos import CameraManager +def init_cam(width, height, colormode): + toreturn = None + try: + from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling + + # Map resolution to FrameSize enum using CameraManager + frame_size = CameraManager.resolution_to_framesize(width, height) + print(f"init_internal_cam: Using FrameSize {frame_size} for {width}x{height}") + + # Try to initialize, with one retry for I2C poweroff issue + max_attempts = 3 + for attempt in range(max_attempts): + try: + cam = Camera( + data_pins=[12,13,15,11,14,10,7,2], + vsync_pin=6, + href_pin=4, + sda_pin=21, + scl_pin=16, + pclk_pin=9, + xclk_pin=8, + xclk_freq=20000000, + powerdown_pin=-1, + reset_pin=-1, + pixel_format=PixelFormat.RGB565 if self.colormode else PixelFormat.GRAYSCALE, + frame_size=frame_size, + #grab_mode=GrabMode.WHEN_EMPTY, + grab_mode=GrabMode.LATEST, + fb_count=1 + ) + cam.set_vflip(True) + toreturn=cam + break + except Exception as e: + if attempt < max_attempts-1: + print(f"init_cam attempt {attempt} failed: {e}, retrying...") + else: + print(f"init_cam final exception: {e}") + break + except Exception as e: + print(f"init_cam exception: {e}") + + return toreturn + +def deinit_cam(cam): + cam.deinit() + # Power off, otherwise it keeps using a lot of current + try: + from machine import Pin, I2C + i2c = I2C(1, scl=Pin(16), sda=Pin(21)) # Adjust pins and frequency + camera_addr = 0x3C # for OV5640 + reg_addr = 0x3008 + reg_high = (reg_addr >> 8) & 0xFF # 0x30 + reg_low = reg_addr & 0xFF # 0x08 + power_off_command = 0x42 # Power off command + i2c.writeto(camera_addr, bytes([reg_high, reg_low, power_off_command])) + except Exception as e: + print(f"Warning: powering off camera got exception: {e}") + import time + time.sleep_ms(100) + +def capture_cam(cam_obj, colormode): + return cam_obj.capture() + +def apply_cam_settings(cam_obj, prefs): + return CameraManager.ov_apply_camera_settings(cam_obj, prefs) + # Waveshare ESP32-S3-Touch-LCD-2 has OV5640 camera CameraManager.add_camera(CameraManager.Camera( lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK, name="OV5640", - vendor="OmniVision" + vendor="OmniVision", + init=init_cam, + deinit=deinit_cam, + capture=capture_cam, + apply_settings=apply_cam_settings )) print("waveshare_esp32_s3_touch_lcd_2.py finished") From 38af82a1e2aaba23472f46b7a441962c0a433021 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 20:06:02 +0100 Subject: [PATCH 27/33] Camera Settings: save button on the right Like in the other "Settings" activities. --- .../lib/mpos/ui/camera_settings.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/internal_filesystem/lib/mpos/ui/camera_settings.py b/internal_filesystem/lib/mpos/ui/camera_settings.py index 2c14a44d..72ac5806 100644 --- a/internal_filesystem/lib/mpos/ui/camera_settings.py +++ b/internal_filesystem/lib/mpos/ui/camera_settings.py @@ -244,16 +244,13 @@ class CameraSettingsActivity(Activity): button_cont.align(lv.ALIGN.BOTTOM_MID, 0, 0) button_cont.set_style_border_width(0, lv.PART.MAIN) - save_button = lv.button(button_cont) - save_button.set_size(lv.SIZE_CONTENT, lv.SIZE_CONTENT) - save_button.align(lv.ALIGN.BOTTOM_LEFT, 0, 0) - save_button.add_event_cb(lambda e: self.save_and_close(), lv.EVENT.CLICKED, None) - save_label = lv.label(save_button) - savetext = "Save" - if self.scanqr_mode: - savetext += " QR tweaks" - save_label.set_text(savetext) - save_label.center() + erase_button = lv.button(button_cont) + erase_button.set_size(DisplayMetrics.pct_of_width(20), lv.SIZE_CONTENT) + erase_button.align(lv.ALIGN.BOTTOM_LEFT, 0, 0) + erase_button.add_event_cb(lambda e: self.erase_and_close(), lv.EVENT.CLICKED, None) + erase_label = lv.label(erase_button) + erase_label.set_text("Erase") + erase_label.center() cancel_button = lv.button(button_cont) cancel_button.set_size(DisplayMetrics.pct_of_width(25), lv.SIZE_CONTENT) @@ -267,13 +264,17 @@ class CameraSettingsActivity(Activity): cancel_label.set_text("Cancel") cancel_label.center() - erase_button = lv.button(button_cont) - erase_button.set_size(DisplayMetrics.pct_of_width(20), lv.SIZE_CONTENT) - erase_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) - erase_button.add_event_cb(lambda e: self.erase_and_close(), lv.EVENT.CLICKED, None) - erase_label = lv.label(erase_button) - erase_label.set_text("Erase") - erase_label.center() + save_button = lv.button(button_cont) + save_button.set_size(lv.SIZE_CONTENT, lv.SIZE_CONTENT) + save_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) + save_button.add_event_cb(lambda e: self.save_and_close(), lv.EVENT.CLICKED, None) + save_label = lv.label(save_button) + savetext = "Save" + if self.scanqr_mode: + savetext += " QR tweaks" + save_label.set_text(savetext) + save_label.center() + def create_basic_tab(self, tab, prefs): From 1ad6656e2d7a628facdde7eda717eb85ce9aefb0 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 20:09:27 +0100 Subject: [PATCH 28/33] Camera: make it webcam-agnostic --- .../lib/mpos/ui/camera_activity.py | 30 +------------------ .../lib/mpos/ui/camera_settings.py | 18 +++++------ 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/internal_filesystem/lib/mpos/ui/camera_activity.py b/internal_filesystem/lib/mpos/ui/camera_activity.py index 70e6d301..47ab3834 100644 --- a/internal_filesystem/lib/mpos/ui/camera_activity.py +++ b/internal_filesystem/lib/mpos/ui/camera_activity.py @@ -1,11 +1,6 @@ import lvgl as lv import time -try: - import webcam -except Exception as e: - print(f"Info: could not import webcam module: {e}") - from ..time import epoch_seconds from .camera_settings import CameraSettingsActivity from ..camera_manager import CameraManager @@ -34,7 +29,6 @@ class CameraActivity(Activity): image_dsc = None scanqr_mode = False scanqr_intent = False - use_webcam = False capture_timer = None prefs = None # regular prefs @@ -321,7 +315,7 @@ class CameraActivity(Activity): def open_settings(self): from ..content.intent import Intent - intent = Intent(activity_class=CameraSettingsActivity, extras={"prefs": self.prefs if not self.scanqr_mode else self.scanqr_prefs, "use_webcam": self.use_webcam, "scanqr_mode": self.scanqr_mode}) + intent = Intent(activity_class=CameraSettingsActivity, extras={"prefs": self.prefs if not self.scanqr_mode else self.scanqr_prefs, "scanqr_mode": self.scanqr_mode}) self.startActivity(intent) def try_capture(self, event): @@ -363,25 +357,3 @@ class CameraActivity(Activity): return buffer[3:] return buffer -""" - def zoom_button_click_unused(self, e): - print("zooming...") - if self.use_webcam: - print("zoom_button_click is not supported for webcam") - return - if self.cam: - startX = self.prefs.get_int("startX", CameraSettingsActivity.startX_default) - startY = self.prefs.get_int("startX", CameraSettingsActivity.startY_default) - endX = self.prefs.get_int("startX", CameraSettingsActivity.endX_default) - endY = self.prefs.get_int("startX", CameraSettingsActivity.endY_default) - offsetX = self.prefs.get_int("startX", CameraSettingsActivity.offsetX_default) - offsetY = self.prefs.get_int("startX", CameraSettingsActivity.offsetY_default) - totalX = self.prefs.get_int("startX", CameraSettingsActivity.totalX_default) - totalY = self.prefs.get_int("startX", CameraSettingsActivity.totalY_default) - outputX = self.prefs.get_int("startX", CameraSettingsActivity.outputX_default) - outputY = self.prefs.get_int("startX", CameraSettingsActivity.outputY_default) - scale = self.prefs.get_bool("scale", CameraSettingsActivity.scale_default) - binning = self.prefs.get_bool("binning", CameraSettingsActivity.binning_default) - result = self.cam.set_res_raw(startX,startY,endX,endY,offsetX,offsetY,totalX,totalY,outputX,outputY,scale,binning) - print(f"self.cam.set_res_raw returned {result}") -""" diff --git a/internal_filesystem/lib/mpos/ui/camera_settings.py b/internal_filesystem/lib/mpos/ui/camera_settings.py index 72ac5806..7e9c713c 100644 --- a/internal_filesystem/lib/mpos/ui/camera_settings.py +++ b/internal_filesystem/lib/mpos/ui/camera_settings.py @@ -74,8 +74,7 @@ class CameraSettingsActivity(Activity): "raw_gma": False, # Disable raw gamma for better contrast } - # Resolution options for both ESP32 and webcam - # Webcam supports all ESP32 resolutions via automatic cropping/padding + # Resolution options are the same for all cameras for now (can be split later) RESOLUTIONS = [ ("96x96", "96x96"), ("160x120", "160x120"), @@ -114,7 +113,6 @@ class CameraSettingsActivity(Activity): self.dependent_controls = {} def onCreate(self): - self.use_webcam = self.getIntent().extras.get("use_webcam") self.prefs = self.getIntent().extras.get("prefs") self.scanqr_mode = self.getIntent().extras.get("scanqr_mode") @@ -132,16 +130,14 @@ class CameraSettingsActivity(Activity): basic_tab = tabview.add_tab("Basic") self.create_basic_tab(basic_tab, self.prefs) - # Create Advanced and Expert tabs only for ESP32 camera - if not self.use_webcam or True: # for now, show all tabs - advanced_tab = tabview.add_tab("Advanced") - self.create_advanced_tab(advanced_tab, self.prefs) + advanced_tab = tabview.add_tab("Advanced") + self.create_advanced_tab(advanced_tab, self.prefs) - expert_tab = tabview.add_tab("Expert") - self.create_expert_tab(expert_tab, self.prefs) + expert_tab = tabview.add_tab("Expert") + self.create_expert_tab(expert_tab, self.prefs) - #raw_tab = tabview.add_tab("Raw") - #self.create_raw_tab(raw_tab, self.prefs) + #raw_tab = tabview.add_tab("Raw") + #self.create_raw_tab(raw_tab, self.prefs) self.setContentView(screen) From 420588ce7de990006f64634dda526c5bd00849b5 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 20:38:29 +0100 Subject: [PATCH 29/33] gt911: try to run without interrupt option --- internal_filesystem/lib/mpos/indev/gt911.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal_filesystem/lib/mpos/indev/gt911.py b/internal_filesystem/lib/mpos/indev/gt911.py index bad05773..5a7d63d0 100644 --- a/internal_filesystem/lib/mpos/indev/gt911.py +++ b/internal_filesystem/lib/mpos/indev/gt911.py @@ -104,19 +104,23 @@ class GT911(pointer_framework.PointerDriver): self._interrupt_flag = True def hw_reset(self): - if self._interrupt_pin and self._reset_pin: - self._interrupt_pin.init(self._interrupt_pin.OUT) - self._interrupt_pin(0) + if self._reset_pin: + if self._interrupt_pin: + self._interrupt_pin.init(self._interrupt_pin.OUT) + self._interrupt_pin(0) self._reset_pin(0) time.sleep_ms(10) # NOQA - self._interrupt_pin(1) # only for 0x14 address - #self._interrupt_pin(0) + if self._interrupt_pin: + self._interrupt_pin(1) # only for 0x14 address + #self._interrupt_pin(0) time.sleep_ms(1) # NOQA self._reset_pin(1) time.sleep_ms(5) # NOQA - self._interrupt_pin(0) + if self._interrupt_pin: + self._interrupt_pin(0) time.sleep_ms(50) # NOQA - self._interrupt_pin.init(mode=self._interrupt_pin.IN) + if self._interrupt_pin: + self._interrupt_pin.init(mode=self._interrupt_pin.IN) time.sleep_ms(50) # NOQA self._write_reg(_ESD_CHECK_REG, 0x00) From 7f70b678c22e0718d480845cacb1b094e5e7d220 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 21:06:13 +0100 Subject: [PATCH 30/33] gt911: add interrupt config mode --- internal_filesystem/lib/mpos/indev/gt911.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/internal_filesystem/lib/mpos/indev/gt911.py b/internal_filesystem/lib/mpos/indev/gt911.py index 5a7d63d0..6be8a85a 100644 --- a/internal_filesystem/lib/mpos/indev/gt911.py +++ b/internal_filesystem/lib/mpos/indev/gt911.py @@ -40,6 +40,7 @@ BITS = 16 _ADDR2 = const(0x14) +_USE_INTERRUPTS = False # Interrupt handler based? Or just polling? class GT911(pointer_framework.PointerDriver): @@ -88,8 +89,10 @@ class GT911(pointer_framework.PointerDriver): if isinstance(reset_pin, int): reset_pin = machine.Pin(reset_pin, machine.Pin.OUT) - if isinstance(interrupt_pin, int): + if isinstance(interrupt_pin, int) and _USE_INTERRUPTS: interrupt_pin = machine.Pin(interrupt_pin, machine.Pin.IN) + else: + interrupt_pin = machine.Pin(interrupt_pin, machine.Pin.OUT) self._reset_pin = reset_pin self._interrupt_pin = interrupt_pin @@ -119,7 +122,7 @@ class GT911(pointer_framework.PointerDriver): if self._interrupt_pin: self._interrupt_pin(0) time.sleep_ms(50) # NOQA - if self._interrupt_pin: + if self._interrupt_pin and _USE_INTERRUPTS: self._interrupt_pin.init(mode=self._interrupt_pin.IN) time.sleep_ms(50) # NOQA @@ -150,7 +153,7 @@ class GT911(pointer_framework.PointerDriver): print(f'Touch resolution: width={x}, height={y}') # Set up interrupt handler if interrupt pin is available - if self._interrupt_pin: + if self._interrupt_pin and _USE_INTERRUPTS: self._interrupt_pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=self._interrupt_handler) # Setting _MODULE_SWITCH_1 will "hang" the touch input after a second or 2 of initial swipe #self._write_reg(_MODULE_SWITCH_1, _CMD_INT_FALLING_EDGE) # stops working @@ -181,11 +184,11 @@ class GT911(pointer_framework.PointerDriver): def _get_coords(self): # If interrupt pin is available, only fetch data when interrupt flag is set - if self._interrupt_pin and not self._interrupt_flag: + if self._interrupt_pin and not self._interrupt_flag and _USE_INTERRUPTS: return self.__last_state, self.__x, self.__y # Clear interrupt flag before reading - if self._interrupt_pin: + if self._interrupt_pin and _USE_INTERRUPTS: self._interrupt_flag = False #self._write_reg(_MODULE_SWITCH_1, _CMD_INT_FALLING_EDGE) #print("[GT911] Interrupt-triggered read") From bc62096fbd1372dc489193763dd5e5eadc0f0f0b Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 21:08:32 +0100 Subject: [PATCH 31/33] BatteryManageR: add has_battery() --- CHANGELOG.md | 6 +++--- internal_filesystem/lib/mpos/battery_manager.py | 10 ++++++++++ .../lib/mpos/board/matouch_esp32_s3_2_8.py | 2 +- internal_filesystem/lib/mpos/ui/camera_activity.py | 2 -- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64228d9e..f07bd449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -0.7.2 +0.8.0 ===== Builtin Apps: - Launcher: fit at least 3 apps per row on a 240px display -- OSUpdate: replace "force update" checkbox with improved button labels +- OSUpdate: replace 'force update' checkbox with improved button labels Frameworks: - SDCard: add support for SDIO/SD/MMC mode @@ -11,7 +11,7 @@ Frameworks: OS: - Scale MicroPythonOS boot logo down if necessary -- Additional board support: MaTouch ESP32-S3 SPI IPS 2.8" with Camera OV3660 +- Add board support: MaTouch ESP32-S3 SPI IPS 2.8' with Camera OV3660 0.7.1 ===== diff --git a/internal_filesystem/lib/mpos/battery_manager.py b/internal_filesystem/lib/mpos/battery_manager.py index 4849252d..a430f53c 100644 --- a/internal_filesystem/lib/mpos/battery_manager.py +++ b/internal_filesystem/lib/mpos/battery_manager.py @@ -67,6 +67,16 @@ class BatteryManager: initial_adc_value = BatteryManager.read_raw_adc() print(f"Reading ADC at init to fill cache: {initial_adc_value} => {BatteryManager.read_battery_voltage(raw_adc_value=initial_adc_value)}V => {BatteryManager.get_battery_percentage(raw_adc_value=initial_adc_value)}%") + @staticmethod + def has_battery(): + """ + Check if battery monitoring is initialized. + + Returns: + bool: True if init_adc() was called, False otherwise + """ + return _adc_pin is not None + @staticmethod def read_raw_adc(force_refresh=False): """ diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index ff5038ba..d5ac1d94 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -82,7 +82,7 @@ try: i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39) import mpos.indev.gt911 as gt911 touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) - indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) # remove debug because it's slower + indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=False) # debug makes it slower from mpos import InputManager InputManager.register_indev(indev) except Exception as e: diff --git a/internal_filesystem/lib/mpos/ui/camera_activity.py b/internal_filesystem/lib/mpos/ui/camera_activity.py index 47ab3834..51c394ee 100644 --- a/internal_filesystem/lib/mpos/ui/camera_activity.py +++ b/internal_filesystem/lib/mpos/ui/camera_activity.py @@ -92,7 +92,6 @@ class CameraActivity(Activity): snap_label.set_text(lv.SYMBOL.OK) snap_label.center() - self.status_label_cont = lv.obj(self.main_screen) width = mpos_ui.DisplayMetrics.pct_of_width(70) height = mpos_ui.DisplayMetrics.pct_of_width(60) @@ -356,4 +355,3 @@ class CameraActivity(Activity): if buffer.startswith(bom): return buffer[3:] return buffer - From 1971e1c1ecc9b1365dc89426e78e1859002b7e8e Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 21:34:44 +0100 Subject: [PATCH 32/33] UI: Don't show battery icon if not present --- .../lib/mpos/board/matouch_esp32_s3_2_8.py | 2 +- internal_filesystem/lib/mpos/ui/topmenu.py | 74 ++++++++++--------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index d5ac1d94..4ee25325 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -212,7 +212,7 @@ def apply_cam_settings(cam_obj, prefs): # MaTouch ESP32-S3 has OV3660 camera (3MP, up to 2048x1536) # Camera pins are available but initialization is handled by the camera driver CameraManager.add_camera(CameraManager.Camera( - lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_BACK, + lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT, name="OV3660", vendor="OmniVision", init=init_cam, diff --git a/internal_filesystem/lib/mpos/ui/topmenu.py b/internal_filesystem/lib/mpos/ui/topmenu.py index d3ad8157..422314f6 100644 --- a/internal_filesystem/lib/mpos/ui/topmenu.py +++ b/internal_filesystem/lib/mpos/ui/topmenu.py @@ -106,22 +106,49 @@ def create_notification_bar(): #notif_icon = lv.label(notification_bar) #notif_icon.set_text(lv.SYMBOL.BELL) #notif_icon.align_to(time_label, lv.ALIGN.OUT_RIGHT_MID, PADDING_TINY, 0) - # Battery percentage - #battery_label = lv.label(notification_bar) - #battery_label.set_text("100%") - #battery_label.align(lv.ALIGN.RIGHT_MID, 0, 0) - #battery_label.add_flag(lv.obj.FLAG.HIDDEN) - # Battery icon - battery_icon = lv.label(notification_bar) - battery_icon.set_text(lv.SYMBOL.BATTERY_FULL) - #battery_icon.align_to(battery_label, lv.ALIGN.OUT_LEFT_MID, 0, 0) - battery_icon.align(lv.ALIGN.RIGHT_MID, -DisplayMetrics.pct_of_width(10), 0) - battery_icon.add_flag(lv.obj.FLAG.HIDDEN) # keep it hidden until it has a correct value + # WiFi icon wifi_icon = lv.label(notification_bar) wifi_icon.set_text(lv.SYMBOL.WIFI) - wifi_icon.align_to(battery_icon, lv.ALIGN.OUT_LEFT_MID, -DisplayMetrics.pct_of_width(1), 0) wifi_icon.add_flag(lv.obj.FLAG.HIDDEN) + wifi_icon.align(lv.ALIGN.RIGHT_MID, -DisplayMetrics.pct_of_width(10), 0) + + # Battery percentage + if BatteryManager.has_battery(): + #battery_label = lv.label(notification_bar) + #battery_label.set_text("100%") + #battery_label.align(lv.ALIGN.RIGHT_MID, 0, 0) + #battery_label.add_flag(lv.obj.FLAG.HIDDEN) + # Battery icon + battery_icon = lv.label(notification_bar) + battery_icon.set_text(lv.SYMBOL.BATTERY_FULL) + #battery_icon.align_to(battery_label, lv.ALIGN.OUT_LEFT_MID, 0, 0) + battery_icon.align(lv.ALIGN.RIGHT_MID, -DisplayMetrics.pct_of_width(10), 0) + wifi_icon.align_to(battery_icon, lv.ALIGN.OUT_LEFT_MID, -DisplayMetrics.pct_of_width(1), 0) + battery_icon.add_flag(lv.obj.FLAG.HIDDEN) # keep it hidden until it has a correct value + def update_battery_icon(timer=None): + try: + percent = BatteryManager.get_battery_percentage() + except Exception as e: + print(f"BatteryManager.get_battery_percentage got exception, not updating battery_icon: {e}") + return + if percent > 80: + battery_icon.set_text(lv.SYMBOL.BATTERY_FULL) + elif percent > 60: + battery_icon.set_text(lv.SYMBOL.BATTERY_3) + elif percent > 40: + battery_icon.set_text(lv.SYMBOL.BATTERY_2) + elif percent > 20: + battery_icon.set_text(lv.SYMBOL.BATTERY_1) + else: + battery_icon.set_text(lv.SYMBOL.BATTERY_EMPTY) + battery_icon.remove_flag(lv.obj.FLAG.HIDDEN) + # Percentage is not shown for now: + #battery_label.set_text(f"{round(percent)}%") + #battery_label.remove_flag(lv.obj.FLAG.HIDDEN) + update_battery_icon() # run it immediately instead of waiting for the timer + lv.timer_create(update_battery_icon, BATTERY_ICON_UPDATE_INTERVAL, None) + # Update time def update_time(timer): hours = mpos.time.localtime()[3] @@ -136,28 +163,6 @@ def create_notification_bar(): except Exception as e: print("Warning: could not check WLAN status:", str(e)) - def update_battery_icon(timer=None): - try: - percent = BatteryManager.get_battery_percentage() - except Exception as e: - print(f"BatteryManager.get_battery_percentage got exception, not updating battery_icon: {e}") - return - if percent > 80: - battery_icon.set_text(lv.SYMBOL.BATTERY_FULL) - elif percent > 60: - battery_icon.set_text(lv.SYMBOL.BATTERY_3) - elif percent > 40: - battery_icon.set_text(lv.SYMBOL.BATTERY_2) - elif percent > 20: - battery_icon.set_text(lv.SYMBOL.BATTERY_1) - else: - battery_icon.set_text(lv.SYMBOL.BATTERY_EMPTY) - battery_icon.remove_flag(lv.obj.FLAG.HIDDEN) - # Percentage is not shown for now: - #battery_label.set_text(f"{round(percent)}%") - #battery_label.remove_flag(lv.obj.FLAG.HIDDEN) - update_battery_icon() # run it immediately instead of waiting for the timer - def update_wifi_icon(timer): from mpos import WifiService if WifiService.is_connected(): @@ -197,7 +202,6 @@ def create_notification_bar(): lv.timer_create(update_temperature, TEMPERATURE_UPDATE_INTERVAL, None) #lv.timer_create(update_memfree, MEMFREE_UPDATE_INTERVAL, None) lv.timer_create(update_wifi_icon, WIFI_ICON_UPDATE_INTERVAL, None) - lv.timer_create(update_battery_icon, BATTERY_ICON_UPDATE_INTERVAL, None) # hide bar animation global hide_bar_animation From 2f9a3735b20e2d3fbab4091cd32670a9e2943eae Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Tue, 10 Feb 2026 22:02:56 +0100 Subject: [PATCH 33/33] matouch_esp32_s3_2_8: fix touch during camera --- CHANGELOG.md | 1 + .../lib/mpos/board/matouch_esp32_s3_2_8.py | 19 ++- .../lib/mpos/camera_manager.py | 2 +- internal_filesystem/lib/mpos/indev/gt911.py | 4 +- .../lib/mpos/indev/gt911_extension.py | 157 ------------------ .../lib/mpos/indev/gt911_settings_gui.py | 4 - 6 files changed, 16 insertions(+), 171 deletions(-) delete mode 100644 internal_filesystem/lib/mpos/indev/gt911_extension.py delete mode 100644 internal_filesystem/lib/mpos/indev/gt911_settings_gui.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f07bd449..fbb9005a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Frameworks: OS: - Scale MicroPythonOS boot logo down if necessary - Add board support: MaTouch ESP32-S3 SPI IPS 2.8' with Camera OV3660 +- UI: Don't show battery icon if not present 0.7.1 ===== diff --git a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py index 4ee25325..78449298 100644 --- a/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py +++ b/internal_filesystem/lib/mpos/board/matouch_esp32_s3_2_8.py @@ -161,6 +161,18 @@ def init_cam(width, height, colormode): print("input disabled") except Exception as e: print(f"init_cam: disabling indev got exception: {e}") + try: + import i2c + i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39) + import mpos.indev.gt911 as gt911 + touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=gt911.I2C_ADDR, reg_bits=gt911.BITS) + indev = gt911.GT911(touch_dev, reset_pin=1, interrupt_pin=40, debug=True) # remove debug because it's slower + print("new indev created") + from mpos import InputManager + InputManager.register_indev(indev) + print("new indev registered") + except Exception as e: + print(f"Indev enable got exception: {e}") except Exception as e: print(f"init_cam exception: {e}") @@ -184,13 +196,6 @@ def deinit_cam(cam): import time time.sleep_ms(100) try: - # hardware reset might work too, but doesn't seem to: - #from mpos import InputManager - #indev = InputManager.list_indevs()[0] - #indev.hw_reset() - #indev.enable(True) - #print("input enabled") - #time.sleep(1) import i2c i2c_bus = i2c.I2C.Bus(host=0, scl=38, sda=39) import mpos.indev.gt911 as gt911 diff --git a/internal_filesystem/lib/mpos/camera_manager.py b/internal_filesystem/lib/mpos/camera_manager.py index 90cc37a3..8dbd6142 100644 --- a/internal_filesystem/lib/mpos/camera_manager.py +++ b/internal_filesystem/lib/mpos/camera_manager.py @@ -248,7 +248,7 @@ class CameraManager: return resolution_map.get((width, height), FrameSize.R240X240) @staticmethod - def ov_apply_camera_settings(self, cam, prefs): + def ov_apply_camera_settings(cam, prefs): if not cam or not prefs: print("ov_apply_camera_settings: Skipping because invalid prefs or cam object") return diff --git a/internal_filesystem/lib/mpos/indev/gt911.py b/internal_filesystem/lib/mpos/indev/gt911.py index 6be8a85a..ada0f428 100644 --- a/internal_filesystem/lib/mpos/indev/gt911.py +++ b/internal_filesystem/lib/mpos/indev/gt911.py @@ -114,8 +114,8 @@ class GT911(pointer_framework.PointerDriver): self._reset_pin(0) time.sleep_ms(10) # NOQA if self._interrupt_pin: - self._interrupt_pin(1) # only for 0x14 address - #self._interrupt_pin(0) + self._interrupt_pin(1) # causes it to stay on 0x14 address + #self._interrupt_pin(0) # causes it to go to 0x5D address time.sleep_ms(1) # NOQA self._reset_pin(1) time.sleep_ms(5) # NOQA diff --git a/internal_filesystem/lib/mpos/indev/gt911_extension.py b/internal_filesystem/lib/mpos/indev/gt911_extension.py deleted file mode 100644 index 3d504c4d..00000000 --- a/internal_filesystem/lib/mpos/indev/gt911_extension.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (c) 2024 - 2025 Kevin G. Schlosser - -from micropython import const # NOQA - - -_CONFIG_START_REG = const(0x8047) -_CONFIG_VERSION_POS = const(0x00) -_X_OUTPUT_MAX_LOW_POS = const(0x01) -_X_OUTPUT_MAX_HIGH_POS = const(0x02) -_Y_OUTPUT_MAX_LOW_POS = const(0x03) -_Y_OUTPUT_MAX_HIGH_POS = const(0x04) -# Touch Number 0x05 -# Module_Switch1 0x06 -# Module_Switch2 0x07 -# Shake_Count 0x08 -# Filter 0x09 -# Large_Touch 0x0A -_NOISE_REDUCTION_POS = const(0x0B) -_TOUCH_PRESS_LEVEL_POS = const(0x0C) -_TOUCH_LEAVE_LEVEL_POS = const(0x0D) -# Low_Power_Control 0x0E -# Refresh_Rate 0x0F -# x_threshold 0x10 -# y_threshold 0x11 -# X_Speed_Limit 0x12 -# y_Speed_Limit 0x13 -_VER_SPACE_POS = const(0x14) # const(0x805B) # low 4 bits are bottom and hight is top -_HOR_SPACE_POS = const(0x15) # const(0x805C) # low 4 bits is right and high is left - -_CONFIG_CHKSUM_REG = const(0x80FF) -_CONFIG_FRESH_REG = const(0x8100) -# 0-15 * 32 - - -class GT911Extension(object): - - def _read_reg(self, reg, num_bytes=None, buf=None): - self._tx_buf[0] = reg >> 8 - self._tx_buf[1] = reg & 0xFF - if num_bytes is not None: - self._i2c.write_readinto(self._tx_mv[:2], self._rx_mv[:num_bytes]) - else: - self._i2c.write_readinto(self._tx_mv[:2], buf) - - def _write_reg(self, reg, value=None, buf=None): - if value is not None: - self._tx_buf[0] = value - self._i2c.write_mem(reg, self._tx_mv[:1]) - elif buf is not None: - self._i2c.write_mem(reg, buf) - - def __init__(self, indev, i2c): - self._indev = indev - self._i2c = i2c - - self._tx_buf = bytearray(3) - self._tx_mv = memoryview(self._tx_buf) - self._rx_buf = bytearray(6) - self._rx_mv = memoryview(self._rx_buf) - - self._config_data = bytearray(_CONFIG_FRESH_REG - _CONFIG_START_REG + 1) - self._config_mv = memoryview(self._config_data) - - self._read_reg(_CONFIG_START_REG, buf=self._config_mv[:-2]) - - @property - def width(self): - return ( - (self._config_data[_X_OUTPUT_MAX_HIGH_POS] << 8) | - self._config_data[_X_OUTPUT_MAX_LOW_POS] - ) - - @width.setter - def width(self, value): - self._config_data[_X_OUTPUT_MAX_LOW_POS] = value & 0xFF - self._config_data[_X_OUTPUT_MAX_HIGH_POS] = (value >> 8) & 0xFF - - @property - def height(self): - return ( - (self._config_data[_Y_OUTPUT_MAX_HIGH_POS] << 8) | - self._config_data[_Y_OUTPUT_MAX_LOW_POS] - ) - - @height.setter - def height(self, value): - self._config_data[_Y_OUTPUT_MAX_LOW_POS] = value & 0xFF - self._config_data[_Y_OUTPUT_MAX_HIGH_POS] = (value >> 8) & 0xFF - - @property - def noise_reduction(self): - return self._config_data[_NOISE_REDUCTION_POS] & 0x0F - - @noise_reduction.setter - def noise_reduction(self, value): - upper_val = self._config_data[_NOISE_REDUCTION_POS] >> 4 - self._config_data[_NOISE_REDUCTION_POS + 2] = (upper_val << 4) | (value & 0x0F) - - @property - def touch_press_level(self): - return self._config_data[_TOUCH_PRESS_LEVEL_POS] - - @touch_press_level.setter - def touch_press_level(self, value): - self._config_data[_TOUCH_PRESS_LEVEL_POS] = value & 0xFF - - @property - def touch_leave_level(self): - return self._config_data[_TOUCH_LEAVE_LEVEL_POS] - - @touch_leave_level.setter - def touch_leave_level(self, value): - self._config_data[_TOUCH_LEAVE_LEVEL_POS] = value & 0xFF - - @property - def pad_left(self): - return self._config_data[_HOR_SPACE_POS] >> 4 - - @pad_left.setter - def pad_left(self, value): - self._config_data[_HOR_SPACE_POS] = (value << 4) | self.pad_right - - @property - def pad_right(self): - return self._config_data[_HOR_SPACE_POS] & 0xF - - @pad_right.setter - def pad_right(self, value): - self._config_data[_HOR_SPACE_POS] = (self.pad_left << 4) | (value & 0xF) - - @property - def pad_top(self): - return self._config_data[_VER_SPACE_POS] >> 4 - - @pad_top.setter - def pad_top(self, value): - self._config_data[_VER_SPACE_POS] = (value << 4) | self.pad_bottom - - @property - def pad_bottom(self): - return self._config_data[_VER_SPACE_POS] & 0xF - - @pad_bottom.setter - def pad_bottom(self, value): - self._config_data[_VER_SPACE_POS] = (self.pad_top << 4) | (value & 0xF) - - def save(self): - # calculate the checksum - self._config_data[-2] = ((~sum(self._config_data[:-2])) + 1) & 0xFF - - # set the flag to save the data the data - self._config_data[-1] = 0x01 # _CONFIG_FRESH_REG - - # write all config data to the touch IC - self._write_reg(_CONFIG_START_REG, buf=self._config_mv) - - self._indev.hw_reset() diff --git a/internal_filesystem/lib/mpos/indev/gt911_settings_gui.py b/internal_filesystem/lib/mpos/indev/gt911_settings_gui.py deleted file mode 100644 index 57f0eff7..00000000 --- a/internal_filesystem/lib/mpos/indev/gt911_settings_gui.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) 2024 - 2025 Kevin G. Schlosser - -import lvgl as lv # NOQA -