diff --git a/internal_filesystem/lib/mpos/board/m5stack_fire.py b/internal_filesystem/lib/mpos/board/m5stack_fire.py new file mode 100644 index 00000000..2d3f0094 --- /dev/null +++ b/internal_filesystem/lib/mpos/board/m5stack_fire.py @@ -0,0 +1,157 @@ +# Hardware initialization for ESP32 M5Stack-Fire board +# Manufacturer's website at https://https://docs.m5stack.com/en/core/fire_v2.7 +import ili9341 +import lcd_bus +import machine + +import lvgl as lv +import task_handler + +import mpos.ui +import mpos.ui.focus_direction +from mpos import InputManager + +# Pin configuration +SPI_BUS = 1 # SPI2 +SPI_FREQ = 40000000 +LCD_SCLK = 18 +LCD_MOSI = 23 +LCD_DC = 27 +LCD_CS = 14 +LCD_BL = 32 +LCD_RST = 33 +LCD_TYPE = 2 # ILI9341 type 2 + +TFT_HOR_RES=320 +TFT_VER_RES=240 + +spi_bus = machine.SPI.Bus( + host=SPI_BUS, + mosi=LCD_MOSI, + sck=LCD_SCLK +) +display_bus = lcd_bus.SPIBus( + spi_bus=spi_bus, + freq=SPI_FREQ, + dc=LCD_DC, + cs=LCD_CS +) + +# M5Stack-Fire ILI9342 uses ILI9341 type 2 with a modified orientation table. +class ILI9341(ili9341.ILI9341): + _ORIENTATION_TABLE = ( + 0x00, + 0x40 | 0x20, # _MADCTL_MX | _MADCTL_MV + 0x80 | 0x40, # _MADCTL_MY | _MADCTL_MX + 0x80 | 0x20 # _MADCTL_MY | _MADCTL_MV + ) + +mpos.ui.main_display = ILI9341( + data_bus=display_bus, + display_width=TFT_HOR_RES, + display_height=TFT_VER_RES, + color_space=lv.COLOR_FORMAT.RGB565, + color_byte_order=ili9341.BYTE_ORDER_BGR, + rgb565_byte_swap=True, + reset_pin=LCD_RST, + reset_state=ili9341.STATE_LOW, + backlight_pin=LCD_BL, + backlight_on_state=ili9341.STATE_PWM +) +mpos.ui.main_display.init(LCD_TYPE) +mpos.ui.main_display.set_power(True) +mpos.ui.main_display.set_color_inversion(True) +mpos.ui.main_display.set_backlight(25) + +lv.init() + +# Button handling code: +from machine import Pin +import time + +btn_a = Pin(39, Pin.IN, Pin.PULL_UP) # A +btn_b = Pin(38, Pin.IN, Pin.PULL_UP) # B +btn_c = Pin(37, Pin.IN, Pin.PULL_UP) # C + +# Key repeat configuration +# This whole debounce logic is only necessary because LVGL 9.2.2 seems to have an issue where +# the lv_keyboard widget doesn't handle PRESSING (long presses) properly, it loses focus. +REPEAT_INITIAL_DELAY_MS = 300 # Delay before first repeat +REPEAT_RATE_MS = 100 # Interval between repeats +last_key = None +last_state = lv.INDEV_STATE.RELEASED +key_press_start = 0 # Time when key was first pressed +last_repeat_time = 0 # Time of last repeat event + +# Read callback +# Warning: This gets called several times per second, and if it outputs continuous debugging on the serial line, +# that will break tools like mpremote from working properly to upload new files over the serial line, thus needing a reflash. +def keypad_read_cb(indev, data): + global last_key, last_state, key_press_start, last_repeat_time + since_last_repeat = 0 + + # Check buttons + current_key = None + current_time = time.ticks_ms() + if btn_a.value() == 0: + current_key = lv.KEY.PREV + elif btn_b.value() == 0: + current_key = lv.KEY.ENTER + elif btn_c.value() == 0: + current_key = lv.KEY.NEXT + + if (btn_a.value() == 0) and (btn_c.value() == 0): + current_key = lv.KEY.ESC + + # Key repeat logic + if current_key: + if current_key != last_key: + # New key press + data.key = current_key + data.state = lv.INDEV_STATE.PRESSED + last_key = current_key + last_state = lv.INDEV_STATE.PRESSED + key_press_start = current_time + last_repeat_time = current_time + else: # same key + # Key held: Check for repeat + elapsed = time.ticks_diff(current_time, key_press_start) + since_last_repeat = time.ticks_diff(current_time, last_repeat_time) + if elapsed >= REPEAT_INITIAL_DELAY_MS and since_last_repeat >= REPEAT_RATE_MS: + # Send a new PRESSED/RELEASED pair for repeat + data.key = current_key + data.state = lv.INDEV_STATE.PRESSED if last_state == lv.INDEV_STATE.RELEASED else lv.INDEV_STATE.RELEASED + last_state = data.state + last_repeat_time = current_time + else: + # No repeat yet, send RELEASED to avoid PRESSING + data.state = lv.INDEV_STATE.RELEASED + last_state = lv.INDEV_STATE.RELEASED + else: + # No key pressed + data.key = last_key if last_key else lv.KEY.ENTER + data.state = lv.INDEV_STATE.RELEASED + last_key = None + last_state = lv.INDEV_STATE.RELEASED + key_press_start = 0 + last_repeat_time = 0 + + # Handle ESC for back navigation (only on initial PRESSED) + if last_state == lv.INDEV_STATE.PRESSED: + if current_key == lv.KEY.ESC and since_last_repeat == 0: + mpos.ui.back_screen() + +group = lv.group_create() +group.set_default() + +# Create and set up the input device +indev = lv.indev_create() +indev.set_type(lv.INDEV_TYPE.KEYPAD) +indev.set_read_cb(keypad_read_cb) +indev.set_group(group) # is this needed? maybe better to move the default group creation to main.py so it's available everywhere... +disp = lv.display_get_default() # NOQA +indev.set_display(disp) # different from display +indev.enable(True) # NOQA +InputManager.register_indev(indev) + +print("m5stack_fire.py finished") diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index a8bdeb1f..c6fcf192 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -88,33 +88,30 @@ def detect_board(): if sys.platform == "linux" or sys.platform == "darwin": # linux and macOS return "linux" elif sys.platform == "esp32": - print("Detecting ESP32 board by scanning I2C addresses...") print("matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660 ?") if i2c0 := fail_save_i2c(sda=39, scl=38): - if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan( - i2c0, 0x5D - ): - # "ghost" or real GT911 touch screen + if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan(i2c0, 0x5D): # "ghost" or real GT911 touch screen return "matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660" print("waveshare_esp32_s3_touch_lcd_2 ?") if i2c0 := fail_save_i2c(sda=48, scl=47): - # IO48 is floating on matouch and therefore, using that for I2C will find many devices, so do this after matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660 - if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan( - i2c0, 0x6B - ): - # CST816S touch screen and IMU + # IO48 is floating on matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660 and therefore, using that for I2C will find many devices, so do this after matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660 + 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" + print("m5stack_fire ?") + if i2c0 := fail_save_i2c(sda=21, scl=22): + if single_address_i2c_scan(i2c0, 0x68): # IMU (MPU6886) + return "m5stack_fire" + print("odroid_go ?") if check_pins(0, 13, 27, 39): return "odroid_go" print("fri3d_2024 ?") if i2c0 := fail_save_i2c(sda=9, scl=18): - # IMU (plus possibly the Communicator's LANA TNY at 0x38) - if single_address_i2c_scan(i2c0, 0x6B): + if single_address_i2c_scan(i2c0, 0x6B): # IMU (plus possibly the Communicator's LANA TNY at 0x38) return "fri3d_2024" print("Fallback to fri3d_2026") diff --git a/tests/test_sensor_manager.py b/tests/test_sensor_manager.py index bb1052bb..d93caa87 100644 --- a/tests/test_sensor_manager.py +++ b/tests/test_sensor_manager.py @@ -159,8 +159,14 @@ mock_esp32 = type('module', (), { # Inject mocks into sys.modules sys.modules['machine'] = mock_machine -sys.modules['mpos.hardware.drivers.qmi8658'] = mock_qmi8658 -sys.modules['mpos.hardware.drivers.wsen_isds'] = mock_wsen_isds + +# Mock parent packages for driver imports +# These need to exist for the import path to work +sys.modules['drivers'] = type('module', (), {})() +sys.modules['drivers.imu_sensor'] = type('module', (), {})() + +sys.modules['drivers.imu_sensor.qmi8658'] = mock_qmi8658 +sys.modules['drivers.imu_sensor.wsen_isds'] = mock_wsen_isds sys.modules['esp32'] = mock_esp32 sys.modules['mpos.config'] = mock_config