This commit is contained in:
Thomas Farstrike
2026-02-16 21:52:43 +01:00
4 changed files with 189 additions and 146 deletions
@@ -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:
@@ -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
+29 -11
View File
@@ -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
+14 -6
View File
@@ -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