diff --git a/internal_filesystem/lib/mpos/board/odroid_go.py b/internal_filesystem/lib/mpos/board/odroid_go.py new file mode 100644 index 00000000..81bf92a2 --- /dev/null +++ b/internal_filesystem/lib/mpos/board/odroid_go.py @@ -0,0 +1,253 @@ +print("odroid_go.py initialization") + +# Hardware initialization for Hardkernel ODROID-Go +# https://github.com/hardkernel/ODROID-GO/ +# https://wiki.odroid.com/odroid_go/odroid_go + +import time + +import ili9341 +import lcd_bus +import lvgl as lv +import machine +import mpos.ui +from machine import ADC, Pin +from micropython import const +from mpos import InputManager + +# Display settings: +SPI_HOST = const(1) +SPI_FREQ = const(40000000) + +LCD_SCLK = const(18) +LCD_MOSI = const(23) +LCD_DC = const(21) +LCD_CS = const(5) +LCD_BL = const(32) +LCD_RST = const(33) +LCD_TYPE = const(2) # ILI9341 type 2 + +TFT_VER_RES = const(320) +TFT_HOR_RES = const(240) + + +# Button settings: +BUTTON_MENU = const(13) +BUTTON_VOLUME = const(0) +BUTTON_SELECT = const(27) +BUTTON_START = const(39) + +BUTTON_B = const(33) +BUTTON_A = const(32) + +# The crossbar pin numbers: +CROSSBAR_X = const(34) +CROSSBAR_Y = const(35) + + +# Misc settings: +LED_BLUE = const(2) +BATTERY_PIN = const(36) +BATTERY_RESISTANCE_NUM = const(2) +SPEAKER_ENABLE_PIN = const(25) +SPEAKER_PIN = const(26) + + +print("odroid_go.py turn on blue LED") +blue_led = machine.Pin(LED_BLUE, machine.Pin.OUT) +blue_led.on() + + +print("odroid_go.py machine.SPI.Bus() initialization") +try: + spi_bus = machine.SPI.Bus(host=SPI_HOST, mosi=LCD_MOSI, sck=LCD_SCLK) +except Exception as e: + print(f"Error initializing SPI bus: {e}") + print("Attempting hard reset in 3sec...") + time.sleep(3) + machine.reset() + +print("odroid_go.py lcd_bus.SPIBus() initialization") +display_bus = lcd_bus.SPIBus(spi_bus=spi_bus, freq=SPI_FREQ, dc=LCD_DC, cs=LCD_CS) + +print("odroid_go.py ili9341.ILI9341() initialization") +try: + mpos.ui.main_display = ili9341.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, + ) +except Exception as e: + print(f"Error initializing ILI9341: {e}") + print("Attempting hard reset in 3sec...") + time.sleep(3) + machine.reset() + +print("odroid_go.py display.init()") +mpos.ui.main_display.init(type=LCD_TYPE) +mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._270) +mpos.ui.main_display.set_power(True) +mpos.ui.main_display.set_color_inversion(False) +mpos.ui.main_display.set_backlight(25) + +print("odroid_go.py lv.init() initialization") +lv.init() + + +print("odroid_go.py Battery initialization...") +from mpos import BatteryManager + + +def adc_to_voltage(adc_value): + return adc_value * BATTERY_RESISTANCE_NUM + + +BatteryManager.init_adc(BATTERY_PIN, adc_to_voltage) + + +print("odroid_go.py button initialization...") + +button_menu = Pin(BUTTON_MENU, Pin.IN, Pin.PULL_UP) +button_volume = Pin(BUTTON_VOLUME, Pin.IN, Pin.PULL_UP) +button_select = Pin(BUTTON_SELECT, Pin.IN, Pin.PULL_UP) +button_start = Pin(BUTTON_START, Pin.IN, Pin.PULL_UP) # -> ENTER + +# PREV <- B | A -> NEXT +button_b = Pin(BUTTON_B, Pin.IN, Pin.PULL_UP) +button_a = Pin(BUTTON_A, Pin.IN, Pin.PULL_UP) + + +class CrossbarHandler: + # ADC values are around low: ~236 and high ~511 + # So the mid value is around (236+511)/2 = 373.5 + CROSSBAR_MIN_ADC_LOW = const(100) + CROSSBAR_MIN_ADC_MID = const(370) + + def __init__(self, pin, high_key, low_key): + self.adc = ADC(Pin(pin, mode=Pin.IN)) + self.adc.width(ADC.WIDTH_9BIT) + self.adc.atten(ADC.ATTN_11DB) + + self.high_key = high_key + self.low_key = low_key + + def poll(self): + value = self.adc.read() + if value > self.CROSSBAR_MIN_ADC_LOW: + if value > self.CROSSBAR_MIN_ADC_MID: + return self.high_key + elif value < self.CROSSBAR_MIN_ADC_MID: + return self.low_key + + +class Crossbar: + def __init__(self, *, up, down, left, right): + self.joy_x = CrossbarHandler(CROSSBAR_X, high_key=left, low_key=right) + self.joy_y = CrossbarHandler(CROSSBAR_Y, high_key=up, low_key=down) + + def poll(self): + crossbar_pressed = self.joy_x.poll() or self.joy_y.poll() + return crossbar_pressed + + +# see: internal_filesystem/lib/mpos/indev/mpos_sdl_keyboard.py +# lv.KEY.UP +# lv.KEY.LEFT - lv.KEY.RIGHT +# lv.KEY.DOWN +# +crossbar = Crossbar( + up=lv.KEY.UP, down=lv.KEY.DOWN, left=lv.KEY.LEFT, right=lv.KEY.RIGHT +) + +REPEAT_INITIAL_DELAY_MS = 300 # Delay before first repeat +REPEAT_RATE_MS = 100 # Interval between repeats +next_repeat = None # Used for auto-repeat key handling + + +def input_callback(indev, data): + global next_repeat + + current_key = None + + if crossbar_pressed := crossbar.poll(): + current_key = crossbar_pressed + + elif button_menu.value() == 0: + current_key = lv.KEY.ESC + elif button_volume.value() == 0: + print("Volume button pressed -> reset") + machine.reset() + elif button_select.value() == 0: + current_key = lv.KEY.BACKSPACE + elif button_start.value() == 0: + current_key = lv.KEY.ENTER + + elif button_b.value() == 0: + current_key = lv.KEY.PREV + elif button_a.value() == 0: + current_key = lv.KEY.NEXT + else: + # No crossbar/buttons pressed + if data.key: # A key was previously pressed and now released + # print(f"Key {data.key=} released") + data.key = 0 + data.state = lv.INDEV_STATE.RELEASED + next_repeat = None + blue_led.off() + return + + # A key is currently pressed + + blue_led.on() # Blink on key press and auto repeat for feedback + + current_time = time.ticks_ms() + repeat = current_time > next_repeat if next_repeat else False # Auto repeat? + if repeat or current_key != data.key: + print(f"Key {current_key} pressed {repeat=}") + + data.key = current_key + data.state = lv.INDEV_STATE.PRESSED + + if current_key == lv.KEY.ESC: # Handle ESC for back navigation + mpos.ui.back_screen() + elif current_key == lv.KEY.RIGHT: + mpos.ui.focus_direction.move_focus_direction(90) + elif current_key == lv.KEY.LEFT: + mpos.ui.focus_direction.move_focus_direction(270) + elif current_key == lv.KEY.UP: + mpos.ui.focus_direction.move_focus_direction(0) + elif current_key == lv.KEY.DOWN: + mpos.ui.focus_direction.move_focus_direction(180) + + if not repeat: + # Initial press: Delay before first repeat + next_repeat = current_time + REPEAT_INITIAL_DELAY_MS + else: + # Faster auto repeat after initial press + next_repeat = current_time + REPEAT_RATE_MS + blue_led.off() # Blink the LED, too + + +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(input_callback) +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("odroid_go.py finished") diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index af6f1c92..a8bdeb1f 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -39,38 +39,85 @@ def single_address_i2c_scan(i2c_bus, address): Returns: True if a device responds at the specified address, False otherwise """ + print(f"Attempt to write a single byte to I2C bus address 0x{address:02x}...") try: # Attempt to write a single byte to the address # This will raise an exception if no device responds - i2c_bus.writeto(address, b'') + i2c_bus.writeto(address, b"") + print("Write test successful") return True - except OSError: - # No device at this address + except OSError as e: + print(f"No device at this address: {e}") return False except Exception as e: # Handle any other exceptions gracefully - print(f"single_address_i2c_scan: error scanning address 0x{address:02x}: {e}") + print(f"scan error: {e}") return False + +def fail_save_i2c(sda, scl): + from machine import I2C, Pin + + print(f"Try to I2C initialized on {sda=} {scl=}") + try: + i2c0 = I2C(0, sda=Pin(sda), scl=Pin(scl)) + except Exception as e: + print(f"Failed: {e}") + return None + else: + print("OK") + return i2c0 + + +def check_pins(*pins): + from machine import Pin + + print(f"Test {pins=}...") + for pin in pins: + try: + Pin(pin) + except Exception as e: + print(f"Failed to initialize {pin=}: {e}") + return True + print("All pins initialized successfully") + return True + + 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(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_spi_ips_2_8_with_camera_ov3660" - - 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_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" - - 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" - + 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 + 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 + return "waveshare_esp32_s3_touch_lcd_2" + + 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): + return "fri3d_2024" + + print("Fallback to fri3d_2026") # default: if single_address_i2c_scan(i2c0, 0x6A): # IMU but currently not installed return "fri3d_2026" diff --git a/internal_filesystem/main.py b/internal_filesystem/main.py index 935d0476..1dfb64bf 100644 --- a/internal_filesystem/main.py +++ b/internal_filesystem/main.py @@ -2,8 +2,28 @@ # Make sure the storage partition's lib/ is first in the path, so whatever is placed there overrides frozen libraries. # This allows any build to be used for development as well, just by overriding the libraries in lib/ +import gc +import os import sys -sys.path.insert(0, 'lib') -print(f"Minimal main.py importing mpos.main with sys.path: {sys.path}") -import mpos.main +sys.path.insert(0, "lib") + +print(f"{sys.version=}") +print(f"{sys.implementation=}") + + +print("Check free space on root filesystem:") +stat = os.statvfs("/") +total_space = stat[0] * stat[2] +free_space = stat[0] * stat[3] +used_space = total_space - free_space +print(f"{total_space=} / {used_space=} / {free_space=} bytes") + + +gc.collect() +print( + f"RAM: {gc.mem_free()} free, {gc.mem_alloc()} allocated, {gc.mem_alloc() + gc.mem_free()} total" +) + +print("Passing execution over to mpos.main") +import mpos.main # noqa: F401 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..f1c43c0a --- /dev/null +++ b/ruff.toml @@ -0,0 +1,2 @@ +[format] +quote-style = "double" diff --git a/scripts/build_mpos.sh b/scripts/build_mpos.sh index 938878a7..b20ae9be 100755 --- a/scripts/build_mpos.sh +++ b/scripts/build_mpos.sh @@ -14,6 +14,7 @@ if [ -z "$target" ]; then echo "Example: $0 unix" echo "Example: $0 macOS" echo "Example: $0 esp32" + echo "Example: $0 odroid_go" exit 1 fi @@ -100,6 +101,23 @@ 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 +elif [ "$target" == "odroid_go" ]; then + manifest=$(readlink -f "$codebasedir"/manifests/manifest.py) + frozenmanifest="FROZEN_MANIFEST=$manifest" + echo "Note that you can also prevent the builtin filesystem from being mounted by umounting it and creating a builtin/ folder." + # Build for https://wiki.odroid.com/odroid_go/odroid_go + 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 BOARD_VARIANT=SPIRAM DISPLAY=ili9341 \ + 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 elif [ "$target" == "unix" -o "$target" == "macOS" ]; then manifest=$(readlink -f "$codebasedir"/manifests/manifest.py) frozenmanifest="FROZEN_MANIFEST=$manifest"