Merge branch 'matouch_esp32_s3_2_8_spi'

This commit is contained in:
Thomas Farstrike
2026-02-10 22:03:58 +01:00
18 changed files with 1073 additions and 390 deletions
+12 -2
View File
@@ -1,8 +1,18 @@
0.7.2
0.8.0
=====
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
Frameworks:
- SDCard: add support for SDIO/SD/MMC mode
- CameraManager and CameraActivity: work fully camera-agnostic
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
=====
@@ -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():
@@ -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):
"""
+31 -15
View File
@@ -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
@@ -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")
@@ -0,0 +1,233 @@
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-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
# - 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 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_BL = 48
# 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_VER_RES,
display_height=TFT_HOR_RES,
color_space=lv.COLOR_FORMAT.RGB565,
color_byte_order=st7789.BYTE_ORDER_BGR,
rgb565_byte_swap=True,
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
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=False) # debug makes it 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()
# Initialize SD card in SDIO mode
from mpos import sdcard
sdcard.init(cmd_pin=2,clk_pin=42,d0_pin=41)
# === LED HARDWARE ===
# Note: MaTouch ESP32-S3 has no NeoPixel LEDs
# LightsManager will not be initialized (functions will return False)
# === 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=[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"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}")
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(38), sda=Pin(39)) # Adjust pins and frequency
camera_addr = 0x3C # for OV3660
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:
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}")
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
CameraManager.add_camera(CameraManager.Camera(
lens_facing=CameraManager.CameraCharacteristics.LENS_FACING_FRONT,
name="OV3660",
vendor="OmniVision",
init=init_cam,
deinit=deinit_cam,
capture=capture_cam,
apply_settings=apply_cam_settings
))
print("matouch_esp32_s3_2_8.py finished")
print("Board capabilities:")
print(" - Display: 320x240 ST7789 with GT911 touch")
print(" - Camera: OV3660 (3MP)")
print(" - No LEDs")
@@ -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")
+179 -1
View File
@@ -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, capture=None, apply_settings=None):
"""Initialize camera metadata.
Args:
@@ -50,6 +50,10 @@ 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
self.capture_function = capture
self.apply_settings_function = apply_settings
def __repr__(self):
facing_names = {
@@ -60,6 +64,21 @@ 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)
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:
"""
@@ -180,6 +199,165 @@ 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)
@staticmethod
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
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)
+218
View File
@@ -0,0 +1,218 @@
# 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)
_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)
_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
I2C_ADDR = 0x14
BITS = 16
_ADDR2 = const(0x14)
_USE_INTERRUPTS = False # Interrupt handler based? Or just polling?
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
self._interrupt_flag = False
if isinstance(reset_pin, int):
reset_pin = machine.Pin(reset_pin, machine.Pin.OUT)
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
self.hw_reset()
super().__init__(
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._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
if self._interrupt_pin:
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
if self._interrupt_pin:
self._interrupt_pin(0)
time.sleep_ms(50) # NOQA
if self._interrupt_pin and _USE_INTERRUPTS:
self._interrupt_pin.init(mode=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}')
# Set up interrupt handler if interrupt pin is available
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
#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)
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):
# If interrupt pin is available, only fetch data when interrupt flag is set
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 and _USE_INTERRUPTS:
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
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
+40 -9
View File
@@ -23,25 +23,56 @@ 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()
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
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)
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(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"
# default: if single_address_i2c_scan(i2c0, 0x6A): # IMU but currently not installed
return "fri3d_2026"
board = detect_board()
print(f"Initializing {board} hardware")
+167 -10
View File
@@ -3,17 +3,133 @@ import machine
import vfs
class SDCardManager:
def __init__(self, spi_bus, cs_pin):
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
# 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 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()}")
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=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:
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
# 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}")
return
# Validate slot parameter
if slot not in (0, 1):
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:
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 +235,42 @@ class SDCardManager:
# --- Singleton pattern ---
_manager = None
def init(spi_bus, cs_pin):
"""Initialize the global SD card manager."""
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 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)
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
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 +280,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:
@@ -1,13 +1,9 @@
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
from .. import ui as mpos_ui
from ..app.activity import Activity
@@ -33,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
@@ -97,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)
@@ -136,22 +130,11 @@ 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:
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:
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,24 +142,8 @@ 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:
self.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
#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}")
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
print("emptying self.current_cam_buffer...")
@@ -347,15 +314,14 @@ 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):
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
@@ -365,82 +331,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:
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
# 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)
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)
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
self.cam.free_buffer() # After QR decoding, free the old buffer, otherwise the camera doesn't provide a new one
except Exception as e:
print(f"init_cam exception: {e}")
return None
pass # some camera API's don't have this
def print_qr_buffer(self, buffer):
try:
@@ -461,149 +355,3 @@ 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):
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}")
"""
@@ -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)
@@ -244,16 +240,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 +260,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):
@@ -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):
"""
+39 -35
View File
@@ -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
+17
View File
@@ -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=$?
-17
View File
@@ -14,7 +14,6 @@ if [ -z "$target" ]; then
echo "Example: $0 unix"
echo "Example: $0 macOS"
echo "Example: $0 esp32"
echo
exit 1
fi
@@ -42,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
@@ -114,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"
+4
View File
@@ -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"/