From ed0b94a44a1d639607712cc7b36808de854be4ae Mon Sep 17 00:00:00 2001 From: Jens Diemer Date: Mon, 16 Feb 2026 21:28:30 +0100 Subject: [PATCH 1/3] Enhance ShowBattery App (#39) Make the Layout nicer. Make the graph bigger. Display battery icon. --- .../assets/show_battery.py | 181 +++++++++--------- 1 file changed, 88 insertions(+), 93 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.showbattery/assets/show_battery.py b/internal_filesystem/apps/com.micropythonos.showbattery/assets/show_battery.py index e2bff8d5..08b525f9 100644 --- a/internal_filesystem/apps/com.micropythonos.showbattery/assets/show_battery.py +++ b/internal_filesystem/apps/com.micropythonos.showbattery/assets/show_battery.py @@ -42,6 +42,7 @@ I want application that will show big time (hour, minutes), with smaller seconds import lvgl as lv import mpos.time from mpos import Activity, BatteryManager +from mpos.battery_manager import MAX_VOLTAGE, MIN_VOLTAGE HISTORY_LEN = 60 @@ -52,71 +53,60 @@ class ShowBattery(Activity): refresh_timer = None - # Widgets - lbl_time = None - lbl_sec = None - lbl_text = None - - bat_outline = None - bat_fill = None - - clear_cache_checkbox = None # Add reference to checkbox - history_v = [] history_p = [] def onCreate(self): - scr = lv.obj() + main_content = lv.obj() + main_content.set_flex_flow(lv.FLEX_FLOW.COLUMN) + main_content.set_style_pad_all(0, 0) + main_content.set_size(lv.pct(100), lv.pct(100)) - # --- TIME --- - self.lbl_time = lv.label(scr) - self.lbl_time.set_style_text_font(lv.font_montserrat_40, 0) - self.lbl_time.align(lv.ALIGN.TOP_LEFT, 5, 5) + # --- TOP FLEX BOX: INFORMATION --- - self.lbl_sec = lv.label(scr) - self.lbl_sec.set_style_text_font(lv.font_montserrat_24, 0) - self.lbl_sec.align_to(self.lbl_time, lv.ALIGN.OUT_RIGHT_BOTTOM, 24, -4) + info_column = lv.obj(main_content) + info_column.set_flex_flow(lv.FLEX_FLOW.COLUMN) + info_column.set_style_pad_all(1, 1) + info_column.set_size(lv.pct(100), lv.SIZE_CONTENT) - # --- CHECKBOX --- - self.clear_cache_checkbox = lv.checkbox(scr) + self.lbl_datetime = lv.label(info_column) + self.lbl_datetime.set_style_text_font(lv.font_montserrat_16, 0) + + self.lbl_battery = lv.label(info_column) + self.lbl_battery.set_style_text_font(lv.font_montserrat_24, 0) + + self.lbl_battery_raw = lv.label(info_column) + self.lbl_battery_raw.set_style_text_font(lv.font_montserrat_14, 0) + + self.clear_cache_checkbox = lv.checkbox(info_column) self.clear_cache_checkbox.set_text("Real-time values") - self.clear_cache_checkbox.align(lv.ALIGN.TOP_LEFT, 5, 50) - self.lbl_text = lv.label(scr) - self.lbl_text.set_style_text_font(lv.font_montserrat_16, 0) - self.lbl_text.align(lv.ALIGN.TOP_LEFT, 5, 80) + # --- BOTTOM FLEX BOX: GRAPH --- - # --- BATTERY ICON --- - self.bat_outline = lv.obj(scr) - self.bat_size = 225 - self.bat_outline.set_size(80, self.bat_size) - self.bat_outline.align(lv.ALIGN.TOP_RIGHT, -10, 10) - self.bat_outline.set_style_border_width(2, 0) - self.bat_outline.set_style_radius(4, 0) + self.canvas_width = main_content.get_width() + self.canvas_height = 100 - self.bat_fill = lv.obj(self.bat_outline) - self.bat_fill.align(lv.ALIGN.BOTTOM_MID, 0, -2) - self.bat_fill.set_width(52) - self.bat_fill.set_style_radius(2, 0) + canvas_column = lv.obj(main_content) + canvas_column.set_flex_flow(lv.FLEX_FLOW.COLUMN) + canvas_column.set_style_pad_all(0, 0) + canvas_column.set_size(self.canvas_width, self.canvas_height) + + self.canvas = lv.canvas(canvas_column) + self.canvas.set_size(self.canvas_width, self.canvas_height) + buffer = bytearray(self.canvas_width * self.canvas_height * 4) + self.canvas.set_buffer( + buffer, self.canvas_width, self.canvas_height, lv.COLOR_FORMAT.NATIVE + ) - # --- CANVAS --- - self.canvas = lv.canvas(scr) - self.canvas.set_size(220, 100) - self.canvas.align(lv.ALIGN.BOTTOM_LEFT, 5, -5) - self.canvas.set_style_border_width(1, 0) - self.canvas.set_style_bg_color(lv.color_white(), lv.PART.MAIN) - buffer = bytearray(220 * 100 * 4) - self.canvas.set_buffer(buffer, 220, 100, lv.COLOR_FORMAT.NATIVE) self.layer = lv.layer_t() self.canvas.init_layer(self.layer) - - self.setContentView(scr) + self.setContentView(main_content) def draw_line(self, color, x1, y1, x2, y2): dsc = lv.draw_line_dsc_t() lv.draw_line_dsc_t.init(dsc) dsc.color = color - dsc.width = 4 + dsc.width = 2 dsc.round_end = 1 dsc.round_start = 1 dsc.p1 = lv.point_precise_t() @@ -125,17 +115,47 @@ class ShowBattery(Activity): dsc.p2 = lv.point_precise_t() dsc.p2.x = x2 dsc.p2.y = y2 - lv.draw_line(self.layer,dsc) + lv.draw_line(self.layer, dsc) self.canvas.finish_layer(self.layer) + def draw_graph(self): + self.canvas.fill_bg(lv.color_white(), lv.OPA.COVER) + self.canvas.clean() + + w = self.canvas_width + h = self.canvas_height + + if len(self.history_v) < 2: + return + + v_range = max(MAX_VOLTAGE - MIN_VOLTAGE, 0.01) + + for i in range(1, len(self.history_v)): + x1 = int((i - 1) * w / HISTORY_LEN) + x2 = int(i * w / HISTORY_LEN) + + yv1 = h - int((self.history_v[i - 1] - MIN_VOLTAGE) / v_range * h) + yv2 = h - int((self.history_v[i] - MIN_VOLTAGE) / v_range * h) + + yp1 = h - int(self.history_p[i - 1] / 100 * h) + yp2 = h - int(self.history_p[i] / 100 * h) + + self.draw_line(DARKPINK, x1, yv1, x2, yv2) + self.draw_line(BLACK, x1, yp1, x2, yp2) + def onResume(self, screen): super().onResume(screen) def update(timer): + # --- DATE+TIME --- now = mpos.time.localtime() - + year, month, day = now[0], now[1], now[2] hour, minute, second = now[3], now[4], now[5] - date = f"{now[0]}-{now[1]:02}-{now[2]:02}" + self.lbl_datetime.set_text( + f"{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}" + ) + + # --- BATTERY VALUES --- if self.clear_cache_checkbox.get_state() & lv.STATE.CHECKED: # Get "real-time" values by clearing the cache before reading @@ -144,25 +164,27 @@ class ShowBattery(Activity): voltage = BatteryManager.read_battery_voltage() percent = BatteryManager.get_battery_percentage() - # --- TIME --- - self.lbl_time.set_text(f"{hour:02}:{minute:02}") - self.lbl_sec.set_text(f":{second:02}") - - # --- BATTERY VALUES --- - date += f"\n{voltage:.2f}V {percent:.0f}%" - date += f"\nRaw ADC: {BatteryManager.read_raw_adc()}" - self.lbl_text.set_text(date) - - # --- BATTERY ICON --- - fill_h = int((percent / 100) * (self.bat_size * 0.9)) - self.bat_fill.set_height(fill_h) - - if percent >= 30: - self.bat_fill.set_style_bg_color(lv.palette_main(lv.PALETTE.GREEN), 0) + if percent > 80: + symbol = lv.SYMBOL.BATTERY_FULL + elif percent > 60: + symbol = lv.SYMBOL.BATTERY_3 + elif percent > 40: + symbol = lv.SYMBOL.BATTERY_2 + elif percent > 20: + symbol = lv.SYMBOL.BATTERY_1 else: - self.bat_fill.set_style_bg_color(lv.palette_main(lv.PALETTE.RED), 0) + symbol = lv.SYMBOL.BATTERY_EMPTY - # --- HISTORY --- + self.lbl_battery.set_text(f"{symbol} {voltage:.2f}V {percent:.0f}%") + if percent >= 30: + bg_color = lv.PALETTE.GREEN + else: + bg_color = lv.PALETTE.RED + self.lbl_battery.set_style_text_color(lv.palette_main(bg_color), 0) + + self.lbl_battery_raw.set_text(f"Raw ADC: {BatteryManager.read_raw_adc()}") + + # --- HISTORY GRAPH --- self.history_v.append(voltage) self.history_p.append(percent) @@ -174,33 +196,6 @@ class ShowBattery(Activity): self.refresh_timer = lv.timer_create(update, 1000, None) - def draw_graph(self): - self.canvas.fill_bg(lv.color_white(), lv.OPA.COVER) - self.canvas.clean() - - w = self.canvas.get_width() - h = self.canvas.get_height() - - if len(self.history_v) < 2: - return - - v_min = 3.3 - v_max = 4.2 - v_range = max(v_max - v_min, 0.01) - - for i in range(1, len(self.history_v)): - x1 = int((i - 1) * w / HISTORY_LEN) - x2 = int(i * w / HISTORY_LEN) - - yv1 = h - int((self.history_v[i - 1] - v_min) / v_range * h) - yv2 = h - int((self.history_v[i] - v_min) / v_range * h) - - yp1 = h - int(self.history_p[i - 1] / 100 * h) - yp2 = h - int(self.history_p[i] / 100 * h) - - self.draw_line(DARKPINK, x1, yv1, x2, yv2) - self.draw_line(BLACK, x1, yp1, x2, yp2) - def onPause(self, screen): super().onPause(screen) if self.refresh_timer: From 0423e09522c0bfdb8882f86c6b1b6bb048ea54e4 Mon Sep 17 00:00:00 2001 From: Jens Diemer Date: Mon, 16 Feb 2026 21:42:14 +0100 Subject: [PATCH 2/3] Updates for ODROID-GO (#40) Setup the "Buzzer" and play intro and outro ;) Don't know if "I2S audio" is possible. Battery "settings": I tested to run ODROID-GO as long as it's possible. The min. raw ADC value on ODROID-GO i have seen is 210. So update the calculation. Fix the boot by moving ODROID-GO below `fri3d_2024` because the device will hard crash on `fail_save_i2c(sda=9, scl=18)` like: ``` MicroPythonOS 0.8.1 running lib/mpos/main.py matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660 ? Try to I2C initialized on sda=39 scl=38 OK Attempt to write a single byte to I2C bus address 0x14... No device at this address: [Errno 116] ETIMEDOUT Attempt to write a single byte to I2C bus address 0x5d... No device at this address: [Errno 116] ETIMEDOUT waveshare_esp32_s3_touch_lcd_2 ? Try to I2C initialized on sda=48 scl=47 Failed: invalid pin m5stack_fire ? Try to I2C initialized on sda=21 scl=22 OK Attempt to write a single byte to I2C bus address 0x68... No device at this address: [Errno 19] ENODEV fri3d_2024 ? Try to I2C initialized on sda=9 scl=18 OK A fatal error occurred. The crash dump printed below may be used to help determine what caused it. If you are not already running the most recent version of MicroPython, consider upgrading. New versions often fix bugs. To learn more about how to debug and/or report this crash visit the wiki page at: https://github.com/micropython/micropython/wiki/ESP32-debugging LVGL MicroPython IDF version : v5.4 Machine : Generic ESP32 module with SPIRAM with ESP32 Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled. Core 1 register dump: PC : 0x401b04dd PS : 0x00060830 A0 : 0x801b0944 A1 : 0x3ffdb390 A2 : 0x3f80d2b0 A3 : 0x00000054 A4 : 0x3f8105e8 A5 : 0x3f54b240 A6 : 0x00000001 A7 : 0xaaaaae2a A8 : 0x00000019 A9 : 0x3ffdb370 A10 : 0xaaaaae2a A11 : 0x00000063 A12 : 0x3ffc7ccc A13 : 0x00000000 A14 : 0x3f4464f4 A15 : 0x00000001 SAR : 0x00000020 EXCCAUSE: 0x0000001c EXCVADDR: 0xaaaaae37 LBEG : 0x401d2964 LEND : 0x401d296d LCOUNT : 0x00000000 Backtrace: 0x401b04da:0x3ffdb390 0x401b0941:0x3ffdb3b0 0x40086719:0x3ffdb3d0 0x401a90da:0x3ffdb460 0x401b07ba:0x3ffdb490 0x40085de9:0x3ffdb4b0 0x401a90da:0x3ffdb540 0x401b07ba:0x3ffdb5b0 0x40085de9:0x3ffdb5d0 0x401a90da:0x3ffdb660 0x401b07ba:0x3ffdb690 0x401b083a:0x3ffdb6b0 0x401d35c1:0x3ffdb6f0 0x401d3809:0x3ffdb730 0x401b0919:0x3ffdb830 0x40085b59:0x3ffdb870 0x401a90da:0x3ffdb900 0x401b07ba:0x3ffdb970 0x401b07e2:0x3ffdb990 0x401e8d02:0x3ffdb9b0 0x401e90c9:0x3ffdba40 0x401c5b2d:0x3ffdba70 ``` --- .../lib/mpos/board/odroid_go.py | 40 ++++++++++++++----- internal_filesystem/lib/mpos/main.py | 20 +++++++--- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/odroid_go.py b/internal_filesystem/lib/mpos/board/odroid_go.py index b32d0767..4aac438d 100644 --- a/internal_filesystem/lib/mpos/board/odroid_go.py +++ b/internal_filesystem/lib/mpos/board/odroid_go.py @@ -12,9 +12,9 @@ import lcd_bus import lvgl as lv import machine import mpos.ui -from machine import ADC, Pin +from machine import ADC, PWM, Pin from micropython import const -from mpos import InputManager +from mpos import AudioManager, BatteryManager, InputManager # Display settings: SPI_HOST = const(1) @@ -49,14 +49,26 @@ CROSSBAR_Y = const(35) # Misc settings: LED_BLUE = const(2) BATTERY_PIN = const(36) -SPEAKER_ENABLE_PIN = const(25) -SPEAKER_PIN = const(26) + +# Buzzer +BUZZER_PIN = const(26) +BUZZER_DAC_PIN = const(25) +BUZZER_TONE_CHANNEL = const(0) print("odroid_go.py turn on blue LED") blue_led = machine.Pin(LED_BLUE, machine.Pin.OUT) blue_led.on() +print("odroid_go.py init buzzer") +buzzer = PWM(Pin(BUZZER_PIN, Pin.OUT, value=1), duty=5) +dac_pin = Pin(BUZZER_DAC_PIN, Pin.OUT, value=1) +dac_pin.value(1) # Unmute +AudioManager(i2s_pins=None, buzzer_instance=buzzer) +AudioManager.set_volume(40) +AudioManager.play_rtttl("Star Trek:o=4,d=20,b=200:8f.,a#,4d#6.,8d6,a#.,g.,c6.,4f6") +while AudioManager.is_playing(): + time.sleep(0.1) print("odroid_go.py machine.SPI.Bus() initialization") try: @@ -102,24 +114,23 @@ lv.init() print("odroid_go.py Battery initialization...") -from mpos import BatteryManager def adc_to_voltage(raw_adc_value): """ The percentage calculation uses MIN_VOLTAGE = 3.15 and MAX_VOLTAGE = 4.15 - 0% at 3.15V -> raw_adc_value = 270 + 0% at 3.15V -> raw_adc_value = 210 100% at 4.15V -> raw_adc_value = 310 4.15 - 3.15 = 1V - 310 - 270 = 40 raw ADC steps + 310 - 210 = 100 raw ADC steps - So each raw ADC step is 1V / 40 = 0.025V + So each raw ADC step is 1V / 100 = 0.01V Offset calculation: - 270 * 0.025 = 6.75V. but we want it to be 3.15V - So the offset is 3.15V - 6.75V = -3.6V + 210 * 0.01 = 2.1V. but we want it to be 3.15V + So the offset is 3.15V - 2.1V = 1.05V """ - voltage = raw_adc_value * 0.025 - 3.6 + voltage = raw_adc_value * 0.01 + 1.05 return voltage @@ -198,6 +209,13 @@ def input_callback(indev, data): elif button_volume.value() == 0: print("Volume button pressed -> reset") blue_led.on() + AudioManager.play_rtttl( + "Outro:o=5,d=32,b=160,b=160:c6,b,a,g,f,e,d,c", + stream_type=AudioManager.STREAM_ALARM, + volume=40, + ) + while AudioManager.is_playing(): + time.sleep(0.1) machine.reset() elif button_select.value() == 0: current_key = lv.KEY.BACKSPACE diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index dc4270d0..16a73420 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -104,20 +104,28 @@ def detect_board(): if i2c0 := fail_save_i2c(sda=21, scl=22): if single_address_i2c_scan(i2c0, 0x68): # IMU (MPU6886) return "m5stack_fire" - + + import machine + unique_id_prefix = machine.unique_id()[0] + + print("odroid_go ?") + if unique_id_prefix == 0x30: + return "odroid_go" + print("fri3d_2024 ?") if i2c0 := fail_save_i2c(sda=9, scl=18): if single_address_i2c_scan(i2c0, 0x6B): # IMU (plus possibly the Communicator's LANA TNY at 0x38) return "fri3d_2024" - import machine - if machine.unique_id()[0] == 0xdc: # prototype board had: dc:b4:d9:0b:7d:80 + print("fri3d_2026 ?") + if unique_id_prefix == 0xDC: # prototype board had: dc:b4:d9:0b:7d:80 # or: if single_address_i2c_scan(i2c0, 0x6A): # IMU currently not installed on prototype board return "fri3d_2026" - print("odroid_go ?") - #if check_pins(0, 13, 27, 39): # not good because it matches other boards (like fri3d_2024 and fri3d_2026) - return "odroid_go" + raise Exception( + "Unknown ESP32-S3 board: couldn't detect known I2C devices or unique_id prefix" + ) + # EXECUTION STARTS HERE From e35c47ccddb8d1a7c0a1a6faff6bd1b1bb1e3874 Mon Sep 17 00:00:00 2001 From: Jens Diemer Date: Mon, 16 Feb 2026 21:45:56 +0100 Subject: [PATCH 3/3] Update board/m5stack_fire.py (#43) Init Buzzer and play a intro on startup. * Cleanup imports. * Use const() * Hard reset if `machine.SPI.Bus()` init not possible --- .../lib/mpos/board/m5stack_fire.py | 94 ++++++++++++------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/internal_filesystem/lib/mpos/board/m5stack_fire.py b/internal_filesystem/lib/mpos/board/m5stack_fire.py index 7da98c44..3f41a0f8 100644 --- a/internal_filesystem/lib/mpos/board/m5stack_fire.py +++ b/internal_filesystem/lib/mpos/board/m5stack_fire.py @@ -2,42 +2,66 @@ # Manufacturer's website at https://https://docs.m5stack.com/en/core/fire_v2.7 # Original author: https://github.com/ancebfer +import time + import drivers.display.ili9341 as ili9341 import lcd_bus -import machine - import lvgl as lv -import task_handler - +import machine import mpos.ui import mpos.ui.focus_direction -from mpos import InputManager +from machine import PWM, Pin +from micropython import const +from mpos import AudioManager, 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 +# Display settings: +SPI_BUS = const(1) # SPI2 +SPI_FREQ = const(40000000) -TFT_HOR_RES=320 -TFT_VER_RES=240 +LCD_SCLK = const(18) +LCD_MOSI = const(23) +LCD_DC = const(27) +LCD_CS = const(14) +LCD_BL = const(32) +LCD_RST = const(33) +LCD_TYPE = const(2) # ILI9341 type 2 + +TFT_HOR_RES = const(320) +TFT_VER_RES = const(240) + +# Button settings: +BUTTON_A = const(39) # A +BUTTON_B = const(38) # B +BUTTON_C = const(37) # C + +# Misc settings: +BATTERY_PIN = const(35) + +# Buzzer +BUZZER_PIN = const(25) + + +print("m5stack_fire.py init buzzer") +buzzer = PWM(Pin(BUZZER_PIN, Pin.OUT, value=1), duty=5) +AudioManager(i2s_pins=None, buzzer_instance=buzzer) +AudioManager.set_volume(40) +AudioManager.play_rtttl("Star Trek:o=4,d=20,b=200:8f.,a#,4d#6.,8d6,a#.,g.,c6.,4f6") +while AudioManager.is_playing(): + time.sleep(0.1) + + +print("m5stack_fire.py machine.SPI.Bus() initialization") +try: + spi_bus = machine.SPI.Bus(host=SPI_BUS, 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() + + +display_bus = lcd_bus.SPIBus(spi_bus=spi_bus, freq=SPI_FREQ, dc=LCD_DC, cs=LCD_CS) -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): @@ -45,9 +69,10 @@ class ILI9341(ili9341.ILI9341): 0x00, 0x40 | 0x20, # _MADCTL_MX | _MADCTL_MV 0x80 | 0x40, # _MADCTL_MY | _MADCTL_MX - 0x80 | 0x20 # _MADCTL_MY | _MADCTL_MV + 0x80 | 0x20, # _MADCTL_MY | _MADCTL_MV ) + mpos.ui.main_display = ILI9341( data_bus=display_bus, display_width=TFT_HOR_RES, @@ -58,7 +83,7 @@ mpos.ui.main_display = ILI9341( reset_pin=LCD_RST, reset_state=ili9341.STATE_LOW, backlight_pin=LCD_BL, - backlight_on_state=ili9341.STATE_PWM + backlight_on_state=ili9341.STATE_PWM, ) mpos.ui.main_display.init(LCD_TYPE) mpos.ui.main_display.set_power(True) @@ -68,12 +93,9 @@ 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 +btn_a = Pin(BUTTON_A, Pin.IN, Pin.PULL_UP) # A +btn_b = Pin(BUTTON_B, Pin.IN, Pin.PULL_UP) # B +btn_c = Pin(BUTTON_C, 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