From 2ce973ff6cdf189b1a89cf0ea0800d45dae70b73 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Tue, 3 Mar 2026 16:31:18 +0100 Subject: [PATCH] Add support for unPhone 9 https://unphone.net/ What worked: - hx8357d Display and XPT2046 touch screen works - Turn display backlight on/off via TCA9555 chip - Buttons TODOs: - Use LEDs - LoRa - IR `.../lib/drivers/display/hx8357d/` is a not modified copy from https://github.com/lvgl-micropython/lvgl_micropython/tree/main/api_drivers/common_api_drivers/display/hx8357d `.../lib/drivers/indev/xpt2046.py` based on https://github.com/lvgl-micropython/lvgl_micropython/blob/main/api_drivers/common_api_drivers/indev/xpt2046.py but is modified: Because of the shared SPI bus for SPI for hx8357d display and xpt2046 touch controller. For this i add the management of `CS` pins for reading the touch controller. Let's discuss how to add this to upstream in https://github.com/lvgl-micropython/lvgl_micropython/issues/536 --- .../lib/drivers/display/hx8357d/__init__.py | 26 + .../drivers/display/hx8357d/_hx8357d_init.py | 93 +++ .../lib/drivers/display/hx8357d/hx8357d.py | 15 + .../lib/drivers/indev/xpt2046.py | 127 +++++ internal_filesystem/lib/mpos/board/unphone.py | 536 ++++++++++++++++++ internal_filesystem/lib/mpos/main.py | 6 +- scripts/build_mpos.sh | 16 +- 7 files changed, 814 insertions(+), 5 deletions(-) create mode 100644 internal_filesystem/lib/drivers/display/hx8357d/__init__.py create mode 100644 internal_filesystem/lib/drivers/display/hx8357d/_hx8357d_init.py create mode 100644 internal_filesystem/lib/drivers/display/hx8357d/hx8357d.py create mode 100644 internal_filesystem/lib/drivers/indev/xpt2046.py create mode 100644 internal_filesystem/lib/mpos/board/unphone.py diff --git a/internal_filesystem/lib/drivers/display/hx8357d/__init__.py b/internal_filesystem/lib/drivers/display/hx8357d/__init__.py new file mode 100644 index 00000000..481c92ce --- /dev/null +++ b/internal_filesystem/lib/drivers/display/hx8357d/__init__.py @@ -0,0 +1,26 @@ +import sys +from . import hx8357d +from . import _hx8357d_init + +# Register _hx8357d_init in sys.modules so __import__('_hx8357d_init') can find it +# This is needed because display_driver_framework.py uses __import__('_hx8357d_init') +# expecting a top-level module, but _hx8357d_init is in the hx8357d package subdirectory +sys.modules['_hx8357d_init'] = _hx8357d_init + +# Explicitly define __all__ and re-export public symbols from hx8357d module +__all__ = [ + 'HX8357D', + 'STATE_HIGH', + 'STATE_LOW', + 'STATE_PWM', + 'BYTE_ORDER_RGB', + 'BYTE_ORDER_BGR', +] + +# Re-export the public symbols +HX8357D = hx8357d.HX8357D +STATE_HIGH = hx8357d.STATE_HIGH +STATE_LOW = hx8357d.STATE_LOW +STATE_PWM = hx8357d.STATE_PWM +BYTE_ORDER_RGB = hx8357d.BYTE_ORDER_RGB +BYTE_ORDER_BGR = hx8357d.BYTE_ORDER_BGR diff --git a/internal_filesystem/lib/drivers/display/hx8357d/_hx8357d_init.py b/internal_filesystem/lib/drivers/display/hx8357d/_hx8357d_init.py new file mode 100644 index 00000000..8ade855c --- /dev/null +++ b/internal_filesystem/lib/drivers/display/hx8357d/_hx8357d_init.py @@ -0,0 +1,93 @@ +# Copyright (c) 2024 - 2025 Kevin G. Schlosser + +import time +from micropython import const # NOQA + +import lvgl as lv # NOQA +import lcd_bus # NOQA + + +_SWRESET = const(0x01) +_SLPOUT = const(0x11) +_DISPON = const(0x29) +_COLMOD = const(0x3A) +_MADCTL = const(0x36) +_TEON = const(0x35) +_TEARLINE = const(0x44) +_SETOSC = const(0xB0) +_SETPWR1 = const(0xB1) +_SETRGB = const(0xB3) +_SETCOM = const(0xB6) +_SETCYC = const(0xB4) +_SETC = const(0xB9) +_SETSTBA = const(0xC0) +_SETPANEL = const(0xCC) +_SETGAMMA = const(0xE0) + + +def init(self): + param_buf = bytearray(34) + param_mv = memoryview(param_buf) + + time.sleep_ms(300) # NOQA + param_buf[:3] = bytearray([0xFF, 0x83, 0x57]) + self.set_params(_SETC, param_mv[:3]) + + param_buf[0] = 0x80 + self.set_params(_SETRGB, param_mv[:1]) + + param_buf[:4] = bytearray([0x00, 0x06, 0x06, 0x25]) + self.set_params(_SETCOM, param_mv[:4]) + + param_buf[0] = 0x68 + self.set_params(_SETOSC, param_mv[:1]) + + param_buf[0] = 0x05 + self.set_params(_SETPANEL, param_mv[:1]) + + param_buf[:6] = bytearray([0x00, 0x15, 0x1C, 0x1C, 0x83, 0xAA]) + self.set_params(_SETPWR1, param_mv[:6]) + + param_buf[:6] = bytearray([0x50, 0x50, 0x01, 0x3C, 0x1E, 0x08]) + self.set_params(_SETSTBA, param_mv[:6]) + + param_buf[:7] = bytearray([0x02, 0x40, 0x00, 0x2A, 0x2A, 0x0D, 0x78]) + self.set_params(_SETCYC, param_mv[:7]) + + param_buf[:34] = bytearray([ + 0x02, 0x0A, 0x11, 0x1d, 0x23, 0x35, 0x41, 0x4b, 0x4b, 0x42, 0x3A, + 0x27, 0x1B, 0x08, 0x09, 0x03, 0x02, 0x0A, 0x11, 0x1d, 0x23, 0x35, + 0x41, 0x4b, 0x4b, 0x42, 0x3A, 0x27, 0x1B, 0x08, 0x09, 0x03, 0x00, 0x01]) + self.set_params(_SETGAMMA, param_mv[:34]) + + param_buf[0] = ( + self._madctl( + self._color_byte_order, + self._ORIENTATION_TABLE # NOQA + ) + ) + self.set_params(_MADCTL, param_mv[:1]) + + color_size = lv.color_format_get_size(self._color_space) + if color_size == 2: # NOQA + pixel_format = 0x55 + else: + raise RuntimeError( + f'{self.__class__.__name__} IC only supports ' + 'lv.COLOR_FORMAT.RGB565' + ) + + param_buf[0] = pixel_format + self.set_params(_COLMOD, param_mv[:1]) + + param_buf[0] = 0x00 + self.set_params(_TEON, param_mv[:1]) + + param_buf[:2] = bytearray([0x00, 0x02]) + self.set_params(_TEARLINE, param_mv[:2]) + + time.sleep_ms(150) # NOQA + self.set_params(_SLPOUT) + + time.sleep_ms(50) # NOQA + self.set_params(_DISPON) diff --git a/internal_filesystem/lib/drivers/display/hx8357d/hx8357d.py b/internal_filesystem/lib/drivers/display/hx8357d/hx8357d.py new file mode 100644 index 00000000..90b91306 --- /dev/null +++ b/internal_filesystem/lib/drivers/display/hx8357d/hx8357d.py @@ -0,0 +1,15 @@ +# Copyright (c) 2024 - 2025 Kevin G. Schlosser + +import display_driver_framework + + +STATE_HIGH = display_driver_framework.STATE_HIGH +STATE_LOW = display_driver_framework.STATE_LOW +STATE_PWM = display_driver_framework.STATE_PWM + +BYTE_ORDER_RGB = display_driver_framework.BYTE_ORDER_RGB +BYTE_ORDER_BGR = display_driver_framework.BYTE_ORDER_BGR + + +class HX8357D(display_driver_framework.DisplayDriver): + pass diff --git a/internal_filesystem/lib/drivers/indev/xpt2046.py b/internal_filesystem/lib/drivers/indev/xpt2046.py new file mode 100644 index 00000000..dde01c70 --- /dev/null +++ b/internal_filesystem/lib/drivers/indev/xpt2046.py @@ -0,0 +1,127 @@ +# Copyright (c) 2024 - 2025 Kevin G. Schlosser + +import lvgl as lv # NOQA +from micropython import const # NOQA +import micropython # NOQA +import machine # NOQA +import pointer_framework +import time + + +_CMD_X_READ = const(0xD0) # 12 bit resolution +_CMD_Y_READ = const(0x90) # 12 bit resolution +_CMD_Z1_READ = const(0xB0) +_CMD_Z2_READ = const(0xC0) +_MIN_RAW_COORD = const(10) +_MAX_RAW_COORD = const(4090) + + +class XPT2046(pointer_framework.PointerDriver): + touch_threshold = 400 + confidence = 5 + margin = 50 + + def __init__( + self, + device: machine.SPI.Bus, + display_width: int, + display_height: int, + lcd_cs: int, + touch_cs: int, + touch_cal=None, + startup_rotation=lv.DISPLAY_ROTATION._0, + debug=False, + ): + self._device = device # machine.SPI.Bus() instance, shared with display + self._debug = debug + + self.lcd_cs = machine.Pin(lcd_cs, machine.Pin.OUT, value=0) + self.touch_cs = machine.Pin(touch_cs, machine.Pin.OUT, value=1) + + self._width = display_width + self._height = display_height + + self._tx_buf = bytearray(3) + self._tx_mv = memoryview(self._tx_buf) + + self._rx_buf = bytearray(3) + self._rx_mv = memoryview(self._rx_buf) + + self.__confidence = max(min(self.confidence, 25), 3) + self.__points = [[0, 0] for _ in range(self.__confidence)] + + margin = max(min(self.margin, 100), 1) + self.__margin = margin * margin + + super().__init__( + touch_cal=touch_cal, startup_rotation=startup_rotation, debug=debug + ) + + def _read_reg(self, reg, num_bytes): + self._tx_buf[0] = reg + self._device.write_readinto(self._tx_mv[:num_bytes], self._rx_mv[:num_bytes]) + return ((self._rx_buf[1] << 8) | self._rx_buf[2]) >> 3 + + def _get_coords(self): + try: + self.lcd_cs.value(1) # deselect LCD to avoid conflicts + self.touch_cs.value(0) # select touch chip + + z1 = self._read_reg(_CMD_Z1_READ, 3) + z2 = self._read_reg(_CMD_Z2_READ, 3) + z = z1 + ((_MAX_RAW_COORD + 6) - z2) + if z < self.touch_threshold: + return None # Not touched + + points = self.__points + count = 0 + end_time = time.ticks_us() + 5000 + while time.ticks_us() < end_time: + if count == self.__confidence: + break + + raw_x = self._read_reg(_CMD_X_READ, 3) + if raw_x < _MIN_RAW_COORD: + continue + + raw_y = self._read_reg(_CMD_Y_READ, 3) + if raw_y > _MAX_RAW_COORD: + continue + + # put in buff + points[count][0] = raw_x + points[count][1] = raw_y + count += 1 + + finally: + self.touch_cs.value(1) # deselect touch chip + self.lcd_cs.value(0) # select LCD + + if not count: + return None # Not touched + + meanx = sum([points[i][0] for i in range(count)]) // count + meany = sum([points[i][1] for i in range(count)]) // count + dev = ( + sum( + [ + (points[i][0] - meanx) ** 2 + (points[i][1] - meany) ** 2 + for i in range(count) + ] + ) + / count + ) + if dev >= self.__margin: + return None # Not touched + + x = pointer_framework.remap( + meanx, _MIN_RAW_COORD, _MAX_RAW_COORD, 0, self._orig_width + ) + y = pointer_framework.remap( + meany, _MIN_RAW_COORD, _MAX_RAW_COORD, 0, self._orig_height + ) + if self._debug: + print( + f"{self.__class__.__name__}_TP_DATA({count=} {meanx=} {meany=} {z1=} {z2=} {z=})" + ) # NOQA + return self.PRESSED, x, y diff --git a/internal_filesystem/lib/mpos/board/unphone.py b/internal_filesystem/lib/mpos/board/unphone.py new file mode 100644 index 00000000..299afda0 --- /dev/null +++ b/internal_filesystem/lib/mpos/board/unphone.py @@ -0,0 +1,536 @@ +print("unphone.py initialization") +""" +Hardware initialization for the unPhone 9 +https://unphone.net/ + +Based on C++ implementation (unPhone.h, unPhone.cpp) from: +https://gitlab.com/hamishcunningham/unphonelibrary/ + +other references: +https://gitlab.com/hamishcunningham/unphone/-/blob/master/examples/circuitpython/LCD.py +https://www.espboards.dev/esp32/unphone9/ +https://github.com/espressif/arduino-esp32/blob/master/variants/unphone9/pins_arduino.h +https://github.com/meshtastic/device-ui/blob/master/include/graphics/LGFX/LGFX_UNPHONE.h + +Original author: https://github.com/jedie +""" + +import struct +import sys +import time + +import esp32 +import i2c +import lcd_bus +import lvgl as lv +import machine +import mpos.ui +from drivers.display.hx8357d import hx8357d +from drivers.indev.xpt2046 import XPT2046 +from machine import Pin +from micropython import const +from mpos import InputManager + +SDA = const(3) +SCL = const(4) +SCK = const(39) +MOSI = const(40) +MISO = const(41) + +SPI_HOST = const(1) # Shared SPI for hx8357d display and xpt2046 touch controller + +# 27Mhz used in extras/port-lvgl/lib9/TFT_eSPI_files/Setup15_HX8357D.h +SPI_LCD_FREQ = const(27_000_000) +# SPI_LCD_FREQ = const(20_000_000) +# SPI_LCD_FREQ = const(10_000_000) +# SPI_LCD_FREQ = const(1_000_000) + +I2C_BUS = const(0) +I2C_FREQ = const(100_000) # rates > 100k used to trigger an unPhoneTCA bug...? + +LCD_CS = const(48) # Chip select control pin +LCD_DC = const(47) # Data Command control pin +LCD_RESET = const(46) + +# FIXME: Two backlights? One on the TCA9555 expander, one directly controlled by the ESP32? +LCD_BACKLIGHT = const(2) # 0x02 +BACKLIGHT = const(0x42) + +TFT_WIDTH = const(320) +TFT_HEIGHT = const(480) + +TOUCH_I2C_ADDR = const(106) # 0x6a - Touchscreen controller +TOUCH_REGBITS = const(8) +TOUCH_CS = const(38) # Chip select pin (T_CS) of touch screen + +# 2,5Mhz used in extras/port-lvgl/lib9/TFT_eSPI_files/Setup15_HX8357D.h +SPI_TOUCH_FREQ = const(2_500_000) +# SPI_TOUCH_FREQ = const(500_000) +# SPI_TOUCH_FREQ = const(100_000) + +EXPANDER_POWER = const(0x40) +LED_GREEN = const(0x49) +LED_BLUE = const(0x4D) # 13 | 0x40 +LED_RED = const(13) + +# Power management (known variously as PMU, BMU or just BM): +BM_I2C_ADDR = const(107) # 0x6b + +LORA_CS = const(44) +LORA_RESET = const(42) +SD_CS = const(43) +VIBE = const(0x47) +IR_LEDS = const(12) +USB_VSENSE = const(78) # 14 | 0x40 + +POWER_SWITCH = const(18) +BUTTON_LEFT = const(45) +BUTTON_MIDDLE = const(0) +BUTTON_RIGHT = const(21) + + +print("unphone.py turn on red LED") +machine.Pin(LED_RED, machine.Pin.OUT).on() +time.sleep(1) +print("unphone.py init...") + + +class UnPhoneTCA: + """ + unPhone spin 9 - TCA9555 IO expansion chip + """ + + I2C_DEV_ID = const(38) # 0x26 - TI TCA9555's I²C addr + + # Register addresses + REG_INPUT = const(0x00) + REG_OUTPUT = const(0x02) + REG_CONFIG = const(0x06) + + def __init__(self, i2c_bus: i2c.I2C.Bus): + self.tca_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=self.I2C_DEV_ID) + self.directions = 0xFFFF # All inputs by default + self.output_states = 0x0000 # All low by default + + # Set IO expander initially as all inputs + self._write_word(0x06, self.directions) + + # Read current directions and states + self.directions = self._read_word(0x06) + self.output_states = self._read_word(0x02) + + def _write_word(self, reg, value): + print(f"Writing to TCA9555: reg={reg:#02x}, value={value:#04x}") + self.tca_dev.write(bytes([reg, value & 0xFF, (value >> 8) & 0xFF])) + + def _read_word(self, reg): + self.tca_dev.write(bytes([reg])) + data = self.tca_dev.read(2) + return struct.unpack("= self.STORE_SIZE: + # self.current_store_index = 0 + # self.nvs.set_i8("unPhoneStoreIdx", self.current_store_index) + # self.nvs.commit() + + def power_switch_is_on(self): + return bool(self.tca.digital_read(POWER_SWITCH)) + + def usb_power_connected(self): + status = self.i2c.readfrom_mem(self.BM_I2CADD, self.BM_STATUS, 1)[0] + connected = bool((status >> 2) & 1) # Bit 2 indicates USB connection + print(f"USB power connected: {connected}") + return connected + + def _wake_on_power_switch(self): + print("Configuring ESP32 wake on power switch...") + wake_pin = machine.Pin(POWER_SWITCH, machine.Pin.IN) + esp32.wake_on_ext0(pin=wake_pin, level=esp32.WAKEUP_ALL_LOW) + + def set_shipping(self, *, enable): + print(f"Setting shipping mode to: {enable=}") + wdt = self.i2c.readfrom_mem(self.BM_I2CADD, self.BM_WATCHDOG, 1)[0] + opcon = self.i2c.readfrom_mem(self.BM_I2CADD, self.BM_OPCON, 1)[0] + if enable: + print("Asks BM chip to powering down and shutting off USB power") + wdt = wdt & ~(1 << 5) & ~(1 << 4) # Clear bits 5 and 4 + opcon = opcon | (1 << 5) # Set bit 5 + else: + print("Asks BM chip to power up and enable USB power") + wdt = (wdt & ~(1 << 5)) | (1 << 4) # Clear 5, Set 4 + opcon = opcon & ~(1 << 5) # Clear bit 5 + self.i2c.writeto_mem(self.BM_I2CADD, self.BM_WATCHDOG, bytes([wdt])) + self.i2c.writeto_mem(self.BM_I2CADD, self.BM_OPCON, bytes([opcon])) + + def turn_peripherals_off(self): + print("Turning off peripherals...") + self.expander_power(on=False) + self.backlight(on=False) + self.ir(on=False) + self.rgb(0, 0, 0) + + def turn_off(self): + print("turning unPhone off...") + self.turn_peripherals_off() + if not self.usb_power_connected(): + print("switch is off, power is OFF: going to shipping mode") + self.set_shipping(enable=True) + else: + print("switch is off, but power is ON: going to deep sleep") + self._wake_on_power_switch() + machine.deepsleep(60000) # Deep sleep + + def check_power_switch(self): + if not self.power_switch_is_on(): + print("Power switch is OFF, initiating shutdown sequence...") + self.turn_off() + + def reset(self): + print("Resetting unPhone TCA9555 to default state...") + + # Setup pins: + self.tca.pin_mode(EXPANDER_POWER, machine.Pin.OUT) + self.tca.pin_mode(VIBE, machine.Pin.OUT) + self.tca.pin_mode(BUTTON_LEFT, machine.Pin.IN) + self.tca.pin_mode(BUTTON_MIDDLE, machine.Pin.IN) + self.tca.pin_mode(BUTTON_RIGHT, machine.Pin.IN) + self.tca.pin_mode(IR_LEDS, machine.Pin.OUT) + self.tca.pin_mode(LED_RED, machine.Pin.OUT) + self.tca.pin_mode(LED_GREEN, machine.Pin.OUT) + self.tca.pin_mode(LED_BLUE, machine.Pin.OUT) + + # Initialise unPhone hardware to default state: + self.backlight(on=True) + self.expander_power(on=True) + self.vibe(on=False) + self.ir(on=False) + + # Mute devices on the SPI bus by deselecting them: + for pin in [LCD_CS, TOUCH_CS, LORA_CS, SD_CS]: + machine.Pin(pin, machine.Pin.OUT, value=1) + + time.sleep_ms(200) # Short delay to help things settle + + # Turn RGB LED blue to indicate reset is done: + self.rgb(0, 0, 1) + + +def recover_i2c(): + """ + NOTE: only do this in setup **BEFORE** Wire.begin! + from: https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.cpp#L220 + """ + print("try to recover I2C bus in case it's locked up...") + scl = machine.Pin(SCL, machine.Pin.OUT) + sda = machine.Pin(SDA, machine.Pin.OUT) + sda.value(1) + + for _ in range(10): # 9th cycle acts as NACK + scl.value(1) + time.sleep_us(5) + scl.value(0) + time.sleep_us(5) + + # STOP signal (SDA from low to high while SCL is high) + sda.value(0) + time.sleep_us(5) + scl.value(1) + time.sleep_us(2) + sda.value(1) + time.sleep_us(2) + + # Short delay to help things settle + time.sleep_ms(200) + + +try: + recover_i2c() + print(f"unphone.py init i2c Bus with: scl={SCL}, sda={SDA}...") + i2c_bus = i2c.I2C.Bus( + host=I2C_BUS, scl=SCL, sda=SDA, freq=I2C_FREQ, use_locks=False + ) +except Exception as e: + sys.print_exception(e) + print("Attempting hard reset in 3sec...") + time.sleep(3) + machine.reset() +else: + print("Scanning I2C bus for devices...") + for dev in i2c_bus.scan(): + print(f"Found I2C device at address: {dev} (${dev:#02X})") + # Typical output here is: + # Found I2C device at address: 38 ($0x26) -> TCA9555 IO expansion chip + # Found I2C device at address: 106 ($0x6A) -> Touchscreen controller + # Found I2C device at address: 107 ($0x6B) -> Power management unit (PMU/BMU) + + unphone = UnPhone(i2c=i2c_bus) + + +# Manually set MISO pin to input with pull-up to avoid it floating and causing issues on the SPI bus, +# since it's shared between display and touch controller: +Pin(MISO, Pin.IN, Pin.PULL_UP) + + +print("unphone.py shared SPI bus initialization") +time.sleep_ms(200) # Short delay to help things settle +try: + spi_bus = machine.SPI.Bus(host=SPI_HOST, sck=SCK, mosi=MOSI, miso=MISO) +except Exception as e: + sys.print_exception(e) + print("Attempting hard reset in 3sec...") + time.sleep(3) + machine.reset() + + +print("unphone.py HX8357D() display initialization") +try: + display_bus = lcd_bus.SPIBus( + spi_bus=spi_bus, # Use **the same** SPI bus hx8357d display + freq=SPI_LCD_FREQ, + dc=LCD_DC, + cs=LCD_CS, + ) + mpos.ui.main_display = hx8357d.HX8357D( + data_bus=display_bus, + display_width=TFT_WIDTH, + display_height=TFT_HEIGHT, + color_space=lv.COLOR_FORMAT.RGB565, + color_byte_order=hx8357d.BYTE_ORDER_BGR, + rgb565_byte_swap=True, + reset_pin=LCD_RESET, + reset_state=hx8357d.STATE_LOW, + backlight_pin=LCD_BACKLIGHT, + backlight_on_state=hx8357d.STATE_PWM, + ) +except Exception as e: + sys.print_exception(e) + print("Attempting hard reset in 3sec...") + time.sleep(3) + machine.reset() + + +print("unphone.py display.init()") +mpos.ui.main_display.init() + +print("unphone.py XPT2046() touch controller initialization") +time.sleep_ms(200) # Short delay to help things settle +startup_rotation = lv.DISPLAY_ROTATION._0 +try: + touch_dev = machine.SPI.Device( + spi_bus=spi_bus, # Use **the same** SPI bus for xpt2046 touch + freq=SPI_TOUCH_FREQ, + cs=TOUCH_CS, + ) +except Exception as e: + sys.print_exception(e) + print("Attempting hard reset in 3sec...") + time.sleep(3) + machine.reset() +else: + print(f"unphone.py init touch...") + touch_input_dev = XPT2046( + device=touch_dev, + lcd_cs=LCD_CS, + touch_cs=TOUCH_CS, + display_width=TFT_WIDTH, + display_height=TFT_HEIGHT, + startup_rotation=startup_rotation, + # debug=True, + ) + print(f"{touch_input_dev.is_calibrated=}") + # FIXME: Persistent calibration data is not working yet? + # if touch_input_dev.is_calibrated: + # print('Touch input is already calibrated, skipping calibration step.') + # else: + # print("Starting touch calibration...") + # touch_input_dev.calibrate() + InputManager.register_indev(touch_input_dev) + + +print("unphone.py display.set_rotation() initialization") +mpos.ui.main_display.set_rotation( + startup_rotation +) # must be done after initializing display and creating the touch drivers, to ensure proper handling + + +print("unphone.py button initialization...") +button_left = Pin(BUTTON_LEFT, Pin.IN, Pin.PULL_UP) +button_middle = Pin(BUTTON_MIDDLE, Pin.IN, Pin.PULL_UP) +button_right = Pin(BUTTON_RIGHT, Pin.IN, Pin.PULL_UP) + + +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 +last_power_switch = None +next_check = time.time() + 1 + + +def input_callback(indev, data): + global next_repeat, last_power_switch, next_check + + current_key = None + + if button_left.value() == 0: + current_key = lv.KEY.ESC + elif button_middle.value() == 0: + current_key = lv.KEY.NEXT + elif button_right.value() == 0: + current_key = lv.KEY.ENTER + + else: + # No 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 + + if time.time() > next_check: + # Check power switch state and update backlight accordingly + unphone.check_power_switch() + next_check = time.time() + 1 # Check every second + + return + + # A key is currently pressed + + 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 + + +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) + +unphone.rgb(0, 1, 0) # Green to indicate init is done + +print("\nunphone.py init finished\n") diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index c6e065e7..2fc5e69c 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -16,7 +16,7 @@ def init_rootscreen(): # Initialize DisplayMetrics with actual display values DisplayMetrics.set_resolution(width, height) - DisplayMetrics.set_dpi(dpi) + DisplayMetrics.set_dpi(dpi) print(f"init_rootscreen set resolution to {width}x{height} at {dpi} DPI") # Show logo @@ -92,6 +92,10 @@ def detect_board(): import machine unique_id_prefixes = machine.unique_id()[0:3] + print("unPhone ?") + if unique_id_prefixes == b'00\xf9': # '30:30:F9' + return "unphone" + print("(emulated) lilygo_t_display_s3 ?") if unique_id_prefixes == b'\x10\x01\x00' or unique_id_prefixes == b'\xc0\x4e\x30': return "lilygo_t_display_s3" # display gets confused by the i2c stuff below diff --git a/scripts/build_mpos.sh b/scripts/build_mpos.sh index b8c6e158..4f916a76 100755 --- a/scripts/build_mpos.sh +++ b/scripts/build_mpos.sh @@ -14,6 +14,7 @@ if [ -z "$target" ]; then echo "Example: $0 macOS" echo "Example: $0 esp32" echo "Example: $0 esp32s3" + echo "Example: $0 unphone" exit 1 fi @@ -97,12 +98,18 @@ popd echo "Refreshing freezefs..." "$codebasedir"/scripts/freezefs_mount_builtin.sh -if [ "$target" == "esp32" -o "$target" == "esp32s3" ]; then +if [ "$target" == "esp32" -o "$target" == "esp32s3" -o "$target" == "unphone" ]; then + partition_size="4194304" + flash_size="16" extra_configs="" - if [ "$target" == "esp32" ]; then + if [ "$target" == "esp32" ]; then BOARD=ESP32_GENERIC BOARD_VARIANT=SPIRAM - else # esp32s3 + else # esp32s3 or unphone + if [ "$target" == "unphone" ]; then + partition_size="3900000" + flash_size="8" + fi BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT # These options disable hardware AES, SHA and MPI because they give warnings in QEMU: [AES] Error reading from GDMA buffer @@ -131,7 +138,8 @@ if [ "$target" == "esp32" -o "$target" == "esp32s3" ]; then # CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y # CONFIG_ADC_MIC_TASK_CORE=1 because with the default (-1) it hangs the CPU # CONFIG_SPIRAM_XIP_FROM_PSRAM: load entire firmware into RAM to reduce SD vs PSRAM contention (recommended at https://github.com/MicroPythonOS/MicroPythonOS/issues/17) - python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=$BOARD BOARD_VARIANT=$BOARD_VARIANT \ +# python3 make.py --ota --partition-size=$partition_size --flash-size=$flash_size esp32 BOARD=$BOARD BOARD_VARIANT=$BOARD_VARIANT \ + python3 make.py --optimize-size --partition-size=$partition_size --flash-size=$flash_size esp32 BOARD=$BOARD BOARD_VARIANT=$BOARD_VARIANT \ 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 \