Finished the menu demo
This commit is contained in:
parent
fc2af7031e
commit
1be6e12b54
5 changed files with 643 additions and 51 deletions
13
config.json
Normal file
13
config.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"debug": 1,
|
||||
"logging": 1,
|
||||
"loglevel": 4,
|
||||
"logfile": "petcard.log",
|
||||
"display": "SSD1306",
|
||||
"controls": {
|
||||
"up": "0",
|
||||
"down": "3",
|
||||
"select": "1",
|
||||
"back": "2"
|
||||
}
|
||||
}
|
155
lib/ssd1306.py
Normal file
155
lib/ssd1306.py
Normal file
|
@ -0,0 +1,155 @@
|
|||
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
|
||||
|
||||
from micropython import const
|
||||
import framebuf
|
||||
|
||||
|
||||
# register definitions
|
||||
SET_CONTRAST = const(0x81)
|
||||
SET_ENTIRE_ON = const(0xA4)
|
||||
SET_NORM_INV = const(0xA6)
|
||||
SET_DISP = const(0xAE)
|
||||
SET_MEM_ADDR = const(0x20)
|
||||
SET_COL_ADDR = const(0x21)
|
||||
SET_PAGE_ADDR = const(0x22)
|
||||
SET_DISP_START_LINE = const(0x40)
|
||||
SET_SEG_REMAP = const(0xA0)
|
||||
SET_MUX_RATIO = const(0xA8)
|
||||
SET_COM_OUT_DIR = const(0xC0)
|
||||
SET_DISP_OFFSET = const(0xD3)
|
||||
SET_COM_PIN_CFG = const(0xDA)
|
||||
SET_DISP_CLK_DIV = const(0xD5)
|
||||
SET_PRECHARGE = const(0xD9)
|
||||
SET_VCOM_DESEL = const(0xDB)
|
||||
SET_CHARGE_PUMP = const(0x8D)
|
||||
|
||||
# Subclassing FrameBuffer provides support for graphics primitives
|
||||
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
|
||||
class SSD1306(framebuf.FrameBuffer):
|
||||
def __init__(self, width, height, external_vcc):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.external_vcc = external_vcc
|
||||
self.pages = self.height // 8
|
||||
self.buffer = bytearray(self.pages * self.width)
|
||||
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
|
||||
self.init_display()
|
||||
|
||||
def init_display(self):
|
||||
for cmd in (
|
||||
SET_DISP | 0x00, # off
|
||||
# address setting
|
||||
SET_MEM_ADDR,
|
||||
0x00, # horizontal
|
||||
# resolution and layout
|
||||
SET_DISP_START_LINE | 0x00,
|
||||
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
|
||||
SET_MUX_RATIO,
|
||||
self.height - 1,
|
||||
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
|
||||
SET_DISP_OFFSET,
|
||||
0x00,
|
||||
SET_COM_PIN_CFG,
|
||||
0x02 if self.width > 2 * self.height else 0x12,
|
||||
# timing and driving scheme
|
||||
SET_DISP_CLK_DIV,
|
||||
0x80,
|
||||
SET_PRECHARGE,
|
||||
0x22 if self.external_vcc else 0xF1,
|
||||
SET_VCOM_DESEL,
|
||||
0x30, # 0.83*Vcc
|
||||
# display
|
||||
SET_CONTRAST,
|
||||
0xFF, # maximum
|
||||
SET_ENTIRE_ON, # output follows RAM contents
|
||||
SET_NORM_INV, # not inverted
|
||||
# charge pump
|
||||
SET_CHARGE_PUMP,
|
||||
0x10 if self.external_vcc else 0x14,
|
||||
SET_DISP | 0x01,
|
||||
): # on
|
||||
self.write_cmd(cmd)
|
||||
self.fill(0)
|
||||
self.show()
|
||||
|
||||
def poweroff(self):
|
||||
self.write_cmd(SET_DISP | 0x00)
|
||||
|
||||
def poweron(self):
|
||||
self.write_cmd(SET_DISP | 0x01)
|
||||
|
||||
def contrast(self, contrast):
|
||||
self.write_cmd(SET_CONTRAST)
|
||||
self.write_cmd(contrast)
|
||||
|
||||
def invert(self, invert):
|
||||
self.write_cmd(SET_NORM_INV | (invert & 1))
|
||||
|
||||
def show(self):
|
||||
x0 = 0
|
||||
x1 = self.width - 1
|
||||
if self.width == 64:
|
||||
# displays with width of 64 pixels are shifted by 32
|
||||
x0 += 32
|
||||
x1 += 32
|
||||
self.write_cmd(SET_COL_ADDR)
|
||||
self.write_cmd(x0)
|
||||
self.write_cmd(x1)
|
||||
self.write_cmd(SET_PAGE_ADDR)
|
||||
self.write_cmd(0)
|
||||
self.write_cmd(self.pages - 1)
|
||||
self.write_data(self.buffer)
|
||||
|
||||
|
||||
class SSD1306_I2C(SSD1306):
|
||||
def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
|
||||
self.i2c = i2c
|
||||
self.addr = addr
|
||||
self.temp = bytearray(2)
|
||||
self.write_list = [b"\x40", None] # Co=0, D/C#=1
|
||||
super().__init__(width, height, external_vcc)
|
||||
|
||||
def write_cmd(self, cmd):
|
||||
self.temp[0] = 0x80 # Co=1, D/C#=0
|
||||
self.temp[1] = cmd
|
||||
self.i2c.writeto(self.addr, self.temp)
|
||||
|
||||
def write_data(self, buf):
|
||||
self.write_list[1] = buf
|
||||
self.i2c.writevto(self.addr, self.write_list)
|
||||
|
||||
|
||||
class SSD1306_SPI(SSD1306):
|
||||
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
|
||||
self.rate = 10 * 1024 * 1024
|
||||
dc.init(dc.OUT, value=0)
|
||||
res.init(res.OUT, value=0)
|
||||
cs.init(cs.OUT, value=1)
|
||||
self.spi = spi
|
||||
self.dc = dc
|
||||
self.res = res
|
||||
self.cs = cs
|
||||
import time
|
||||
|
||||
self.res(1)
|
||||
time.sleep_ms(1)
|
||||
self.res(0)
|
||||
time.sleep_ms(10)
|
||||
self.res(1)
|
||||
super().__init__(width, height, external_vcc)
|
||||
|
||||
def write_cmd(self, cmd):
|
||||
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||
self.cs(1)
|
||||
self.dc(0)
|
||||
self.cs(0)
|
||||
self.spi.write(bytearray([cmd]))
|
||||
self.cs(1)
|
||||
|
||||
def write_data(self, buf):
|
||||
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||
self.cs(1)
|
||||
self.dc(1)
|
||||
self.cs(0)
|
||||
self.spi.write(buf)
|
||||
self.cs(1)
|
115
main.py
115
main.py
|
@ -2,70 +2,83 @@
|
|||
# Petcard Bootloader - v1.0
|
||||
# Copyright (c) 2025 FreedTapestry21
|
||||
#
|
||||
# Licensed under the MIT license
|
||||
#
|
||||
|
||||
#
|
||||
# Importing libraries
|
||||
#
|
||||
|
||||
from machine import Pin
|
||||
import network, urequest, machine
|
||||
import time
|
||||
|
||||
#
|
||||
# Define global variables
|
||||
#
|
||||
|
||||
# Define variables
|
||||
CONSOLE_WELCOME = """
|
||||
Petcard Bootloader - v1.0
|
||||
Copyright (c) 2025 FreedTapestry21
|
||||
|
||||
Licensed under the MIT license
|
||||
"""
|
||||
UPDATE = False
|
||||
MIRROR = "https://mirror.pixelated.sh/dev/petcard"
|
||||
|
||||
# Initialize OK button (with pull down resistor)
|
||||
ok = Pin(0, Pin.IN, Pin.PULL_DOWN)
|
||||
ok = Pin(1, Pin.IN, Pin.PULL_DOWN)
|
||||
|
||||
# Define functions
|
||||
def download(file, name):
|
||||
print("Downloading " + name + "...")
|
||||
f = open(file)
|
||||
r = urequests.get(MIRROR + "/" + file)
|
||||
f.write(r.text)
|
||||
f.close()
|
||||
r.close
|
||||
print("Downloaded " + name + "!")
|
||||
#
|
||||
# Function(s)
|
||||
#
|
||||
|
||||
def connect(ssid, psk):
|
||||
nic = network.WLAN(network.WLAN.IF_STA)
|
||||
nic.active(True)
|
||||
nic.connect(ssid, psk)
|
||||
while not sta_if.isconnected():
|
||||
pass
|
||||
|
||||
def boot():
|
||||
import system
|
||||
init()
|
||||
|
||||
# Check if button is pressed for recovery/update process
|
||||
if __name__ == '__main__':
|
||||
print(CONSOLE_WELCOME)
|
||||
def main():
|
||||
# Check if a boot interuption was triggered (you can trigger this by holding down the OK button) and wait for 10 seconds
|
||||
# In later revisions I'd like this to trigger some kind of update screen to easily update the Petcard
|
||||
INTERUPT = False
|
||||
if ok.value() == 1:
|
||||
UPDATE = True
|
||||
INTERUPT = True
|
||||
|
||||
if UPDATE == True:
|
||||
print("Petcard bootloader has been interupted by the OK button")
|
||||
inp = input("Are you sure you want to enter the Petcard recovery process? (y/N)")
|
||||
if inp == "" or inp == "n" or inp == "N":
|
||||
print("Rebooting to resume boot process...")
|
||||
if inp == "y" or inp == "Y":
|
||||
print("Entering Petcard recovery process...")
|
||||
print("Please enter your Wi-Fi credentials")
|
||||
ssid = input("SSID: ")
|
||||
psk = input("Password: ")
|
||||
connect(ssid, psk)
|
||||
print('Connected to network "' + str(ssid) + '"')
|
||||
|
||||
# Downloading system.py
|
||||
download("system.py", "Petcard system")
|
||||
download("petcard.py", "Petcard")
|
||||
print("System recovery complete, restarting system...")
|
||||
machine.reset()
|
||||
if INTERUPT == True: return 1
|
||||
else: return 0
|
||||
|
||||
#
|
||||
# Classes
|
||||
#
|
||||
|
||||
class Instance:
|
||||
def __init__(self):
|
||||
self.config_manager = system.ConfigurationManager()
|
||||
self.logger = system.Logger(self.config_manager.config)
|
||||
self.input_controller = system.InputController(self.config_manager.config["controls"], self.logger)
|
||||
|
||||
# Select display driver
|
||||
if self.config_manager.config["display"] == "SSD1306":
|
||||
self.display_driver = system.SSD1306Driver()
|
||||
else:
|
||||
print("'" + inp + "' is not recognized as an option! Rebooting...")
|
||||
machine.reset()
|
||||
else:
|
||||
boot()
|
||||
self.logger.log(f"No available driver was found for display {self.config_manager.config['display']}!", 0)
|
||||
self.display_driver = system.DisplayDriver() # DO NOT REMOVE
|
||||
|
||||
self.display = system.Display(self.display_driver, 128, 64, self.logger)
|
||||
self.ui = system.UserInterface(self.display, self.input_controller, self.logger)
|
||||
self.power = system.Power(self.display, self.input_controller, self.logger)
|
||||
|
||||
#
|
||||
# Entry point
|
||||
#
|
||||
|
||||
if __name__ == '__main__':
|
||||
if main() == 1:
|
||||
time.sleep(10)
|
||||
machine.soft_reset()
|
||||
print(CONSOLE_WELCOME)
|
||||
print("Importing SYSTEM module...")
|
||||
import system
|
||||
print("Importing APP module...")
|
||||
import petcard
|
||||
print("Starting Petcard")
|
||||
instance = Instance()
|
||||
app = petcard.Application(instance.config_manager, instance.logger, instance.display_driver, instance.display, instance.input_controller, instance.ui, instance.power)
|
||||
app.run()
|
||||
|
||||
#
|
||||
# End Of File (EOF)
|
||||
#
|
75
petcard.py
75
petcard.py
|
@ -1,4 +1,79 @@
|
|||
#
|
||||
# Petcard - v1.0
|
||||
# Copyright (c) 2025 FreedTapestry21
|
||||
#
|
||||
# Licensed under the MIT license
|
||||
#
|
||||
|
||||
#
|
||||
# Importing libraries
|
||||
#
|
||||
|
||||
import sys, json, machine
|
||||
|
||||
# Import SYSTEM module
|
||||
import system
|
||||
|
||||
#
|
||||
# Classes
|
||||
#
|
||||
|
||||
# Main system & application controller
|
||||
class Application:
|
||||
def __init__(self, config_manager, logger, display_driver, display, input_controller, ui, power):
|
||||
self.config_manager = config_manager
|
||||
self.logger = logger
|
||||
self.display_driver = display_driver
|
||||
self.display = display
|
||||
self.input_controller = input_controller
|
||||
self.ui = ui
|
||||
self.power = power
|
||||
|
||||
# Initializes all system components
|
||||
def initialize(self):
|
||||
self.logger.log("Initializing Petcard System...", 3)
|
||||
self.display.initialize()
|
||||
self.logger.log("System initialization complete", 3)
|
||||
|
||||
# Main system loop
|
||||
def run(self):
|
||||
try:
|
||||
self.initialize()
|
||||
|
||||
# Create main menu
|
||||
main_menu_items = [
|
||||
system.MenuItem("Food", self._action, "f"),
|
||||
system.MenuItem("Drinks", self._action, "d"),
|
||||
system.MenuItem("Play", self._action, "p"),
|
||||
system.MenuItem("Sleep", self.power.sleep)
|
||||
]
|
||||
|
||||
main_menu = system.Menu(main_menu_items, self.logger)
|
||||
|
||||
# Main menu loop
|
||||
while True:
|
||||
selected_item = self.ui.show_menu(main_menu)
|
||||
if selected_item:
|
||||
result = selected_item.execute()
|
||||
if result == "exit":
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"System error: {e}", 0)
|
||||
self.power.exit_system("System encountered an error")
|
||||
|
||||
# Defines what to do when a certain action is triggered
|
||||
def _action(self, act):
|
||||
if act == "f":
|
||||
self.logger.log("Food!", 3)
|
||||
elif act == "d":
|
||||
self.logger.log("Drink!", 3)
|
||||
elif act == "p":
|
||||
self.logger.log("Play!", 3)
|
||||
else: self.logger.log(f"Invalid action {act}", 1)
|
||||
|
||||
return
|
||||
|
||||
#
|
||||
# End Of File (EOF)
|
||||
#
|
336
system.py
336
system.py
|
@ -1,4 +1,340 @@
|
|||
#
|
||||
# Petcard System - v1.0
|
||||
# Copyright (c) 2025 FreedTapestry21
|
||||
#
|
||||
# Licensed under the MIT license
|
||||
#
|
||||
|
||||
#
|
||||
# Importing libraries
|
||||
#
|
||||
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import machine
|
||||
import framebuf
|
||||
import ssd1306
|
||||
import _thread
|
||||
|
||||
#
|
||||
# Classes
|
||||
#
|
||||
|
||||
# Manages system configuration
|
||||
class ConfigurationManager:
|
||||
def __init__(self, config_file="config.json"):
|
||||
self.config_file = config_file
|
||||
self.config = self._load_config()
|
||||
|
||||
# Load configuration from file
|
||||
def _load_config(self):
|
||||
try:
|
||||
with open(self.config_file, "r") as file:
|
||||
return json.loads(file.read())
|
||||
except Exception as e:
|
||||
print(f"Failed to load config: {e}")
|
||||
return self._get_default_config()
|
||||
|
||||
# Return default configuration
|
||||
def _get_default_config(self):
|
||||
return {
|
||||
"debug": 1,
|
||||
"logging": 1,
|
||||
"loglevel": 3,
|
||||
"logfile": "petcard.log",
|
||||
"display": "SSD1306",
|
||||
"controls": {
|
||||
"up": "0",
|
||||
"down": "3",
|
||||
"select": "1",
|
||||
"back": "2"
|
||||
}
|
||||
}
|
||||
|
||||
# Handles logging functionality with timestamp formatting
|
||||
class Logger:
|
||||
def __init__(self, config):
|
||||
self.log_level = config.get("loglevel", 0) == 3
|
||||
self.logging_enabled = config.get("logging", 0) == 1
|
||||
self.log_file = config.get("logfile", "system.log")
|
||||
|
||||
def log(self, message, log_level, ignore_console=False):
|
||||
timestamp = self._get_timestamp()
|
||||
|
||||
if log_level == 0:
|
||||
formatted_message = f"[CRITICAL] [{timestamp}] {message}"
|
||||
elif log_level == 1:
|
||||
formatted_message = f"[ERROR] [{timestamp}] {message}"
|
||||
elif log_level == 2:
|
||||
formatted_message = f"[WARNING] [{timestamp}] {message}"
|
||||
elif log_level == 3:
|
||||
formatted_message = f"[INFO] [{timestamp}] {message}"
|
||||
elif log_level == 4:
|
||||
formatted_message = f"[DEBUG] [{timestamp}] {message}"
|
||||
|
||||
if self.log_level <= log_level:
|
||||
if not ignore_console:
|
||||
print(formatted_message)
|
||||
|
||||
if self.logging_enabled:
|
||||
self._write_to_file(formatted_message)
|
||||
|
||||
def _get_timestamp(self):
|
||||
# Generate formatted timestamp
|
||||
date = time.localtime()
|
||||
return f"{date[0]}-{date[1]}-{date[2]} {date[3]}:{date[4]}:{date[5]}"
|
||||
|
||||
def _write_to_file(self, message):
|
||||
# Write message to log file
|
||||
try:
|
||||
with open(self.log_file, "a") as file:
|
||||
file.write(message + '\n')
|
||||
except Exception as e:
|
||||
print(f"Failed to write to log file: {e}")
|
||||
|
||||
# Get configuration value
|
||||
def get(self, key, default=None):
|
||||
return self.config.get(key, default)
|
||||
|
||||
# Handles input from buttons/controls
|
||||
class InputController:
|
||||
def __init__(self, control_config, logger):
|
||||
self.controls = control_config
|
||||
self.logger = logger
|
||||
self.pins = {}
|
||||
self._initialize_pins()
|
||||
|
||||
# Initialize pin objects for all controls
|
||||
def _initialize_pins(self):
|
||||
for control_name, pin_number in self.controls.items():
|
||||
try:
|
||||
self.pins[control_name] = machine.Pin(
|
||||
int(pin_number),
|
||||
machine.Pin.IN,
|
||||
machine.Pin.PULL_DOWN
|
||||
)
|
||||
if control_name == "select":
|
||||
self.pins["select"].irq(trigger=machine.Pin.IRQ_RISING, handler=lambda p:None)
|
||||
self.logger.log(f"Set interupt pin to {control_name}", 4)
|
||||
except Exception as e:
|
||||
self.logger.log(f"Failed to initialize pin for {control_name}: {e}", 1)
|
||||
|
||||
# Wait for and return the first button press detected
|
||||
def wait_for_input(self):
|
||||
while True:
|
||||
for control_name, pin in self.pins.items():
|
||||
try:
|
||||
if pin.value() == 1:
|
||||
time.sleep(0.05) # Debounce delay
|
||||
if pin.value() == 1: # Confirm press
|
||||
return control_name
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error reading pin {control_name}: {e}", 1)
|
||||
time.sleep(0.01) # Small delay to prevent excessive polling
|
||||
|
||||
# Base class for display drivers - implement all methods in subclasses
|
||||
class DisplayDriver:
|
||||
def initialize(self):
|
||||
raise NotImplementedError("DisplayDriver subclass must implement initialize()")
|
||||
|
||||
def show(self):
|
||||
raise NotImplementedError("DisplayDriver subclass must implement show()")
|
||||
|
||||
def clear(self):
|
||||
raise NotImplementedError("DisplayDriver subclass must implement clear()")
|
||||
|
||||
def blit(self, buffer, x=0, y=0):
|
||||
raise NotImplementedError("DisplayDriver subclass must implement blit()")
|
||||
|
||||
# SSD1306 OLED display driver implementation
|
||||
class SSD1306Driver(DisplayDriver):
|
||||
def __init__(self, width=128, height=64, sda_pin=4, scl_pin=5):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.sda_pin = sda_pin
|
||||
self.scl_pin = scl_pin
|
||||
self.display = None
|
||||
self.i2c = None
|
||||
|
||||
# Initializes the SSD1306 display
|
||||
def initialize(self):
|
||||
self.i2c = machine.I2C(0, sda=machine.Pin(self.sda_pin), scl=machine.Pin(self.scl_pin))
|
||||
self.display = ssd1306.SSD1306_I2C(self.width, self.height, self.i2c)
|
||||
|
||||
# Updates the physical display
|
||||
def show(self):
|
||||
if self.display:
|
||||
self.display.show()
|
||||
|
||||
# Clears the display
|
||||
def clear(self):
|
||||
if self.display:
|
||||
self.display.fill(0)
|
||||
|
||||
# Blit buffer to display
|
||||
def blit(self, buffer, x=0, y=0):
|
||||
if self.display:
|
||||
self.display.blit(buffer, x, y)
|
||||
|
||||
|
||||
# Main display controller with frame buffer management
|
||||
class Display:
|
||||
def __init__(self, driver, width, height, logger):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.driver = driver
|
||||
self.logger = logger
|
||||
self.pages = height // 8
|
||||
self.buffer = framebuf.FrameBuffer(
|
||||
bytearray(self.pages * width),
|
||||
width,
|
||||
height,
|
||||
framebuf.MONO_VLSB
|
||||
)
|
||||
|
||||
# Initializes the display driver
|
||||
def initialize(self):
|
||||
try:
|
||||
self.driver.initialize()
|
||||
self.logger.log("Display initialized successfully", 4)
|
||||
except Exception as e:
|
||||
self.logger.log(f"Display initialization failed: {e}", 0)
|
||||
|
||||
# Update the physical display with buffer contents
|
||||
def update(self):
|
||||
try:
|
||||
self.driver.blit(self.buffer, 0, 0)
|
||||
self.driver.show()
|
||||
except Exception as e:
|
||||
self.logger.log(f"Display update failed: {e}", 1)
|
||||
|
||||
# Clear the display buffer
|
||||
def clear(self):
|
||||
self.buffer.fill(0)
|
||||
|
||||
# Draw text on the buffer
|
||||
def draw_text(self, text, x, y, color=1):
|
||||
self.buffer.text(text, x, y, color)
|
||||
|
||||
# Draw filled rectangle on the buffer
|
||||
def draw_filled_rect(self, x, y, width, height, color=1):
|
||||
self.buffer.fill_rect(x, y, width, height, color)
|
||||
|
||||
# Object for a single menu item, can be passed through to the Menu class
|
||||
class MenuItem:
|
||||
def __init__(self, text, action=None, args=None):
|
||||
self.text = text
|
||||
self.action = action
|
||||
self.args = args
|
||||
|
||||
# Execute the menu item's action
|
||||
def execute(self):
|
||||
if callable(self.action) == True:
|
||||
if self.args: return self.action(self.args)
|
||||
else: return self.action()
|
||||
return self.action
|
||||
|
||||
# Menu system with navigation and rendering
|
||||
class Menu:
|
||||
|
||||
TEXT_HEIGHT = 8
|
||||
PADDING = 4
|
||||
|
||||
def __init__(self, items, logger):
|
||||
self.items = [item if isinstance(item, MenuItem) else MenuItem(str(item)) for item in items]
|
||||
self.logger = logger
|
||||
self.selection = 0
|
||||
|
||||
# Handles menu navigation selection
|
||||
def navigate(self, direction):
|
||||
if direction == "up":
|
||||
self.selection = (self.selection - 1) % len(self.items) # The percentage ensures the result wraps around within the valid range of the list
|
||||
elif direction == "down":
|
||||
self.selection = (self.selection + 1) % len(self.items)
|
||||
|
||||
# Gets the currently selected menu item
|
||||
def get_selected_item(self):
|
||||
if 0 <= self.selection < len(self.items):
|
||||
return self.items[self.selection]
|
||||
return None
|
||||
|
||||
def render(self, display):
|
||||
# Render menu to display
|
||||
display.clear()
|
||||
|
||||
# Draw selection highlight
|
||||
highlight_y = self.selection * (self.TEXT_HEIGHT + self.PADDING) + 2
|
||||
display.draw_filled_rect(0, highlight_y, display.width, 10, 1)
|
||||
|
||||
# Draw menu items
|
||||
y_pos = self.PADDING
|
||||
for i, item in enumerate(self.items):
|
||||
if y_pos < display.height:
|
||||
if i == self.selection: color = 0 # Normal color for non-selected item
|
||||
else: color = 1 # Inverted color for selected item
|
||||
display.draw_text(item.text, 0, y_pos, color)
|
||||
y_pos += self.TEXT_HEIGHT + self.PADDING
|
||||
|
||||
display.update()
|
||||
|
||||
# Main UI controller managing menus and interactions
|
||||
class UserInterface:
|
||||
def __init__(self, display, input_controller, logger):
|
||||
self.display = display
|
||||
self.input_controller = input_controller
|
||||
self.logger = logger
|
||||
|
||||
# Displays the menu and handles navigation until selection is made
|
||||
def show_menu(self, menu):
|
||||
menu.render(self.display)
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_input = self.input_controller.wait_for_input()
|
||||
self.logger.log(f"Menu input received: {user_input}", 4)
|
||||
|
||||
if user_input in ["up", "down"]:
|
||||
menu.navigate(user_input)
|
||||
menu.render(self.display)
|
||||
elif user_input == "select":
|
||||
selected_item = menu.get_selected_item()
|
||||
if selected_item:
|
||||
return selected_item
|
||||
elif user_input == "back":
|
||||
return None
|
||||
|
||||
time.sleep(0.2) # Prevent input spam
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"Error in menu navigation: {e}", 1)
|
||||
|
||||
# Handles power management
|
||||
class Power:
|
||||
def __init__(self, display, input_controller, logger):
|
||||
self.display = display
|
||||
self.input_controller = input_controller
|
||||
self.logger = logger
|
||||
|
||||
def sleep(self):
|
||||
self.logger.log("Entering sleep mode", 3)
|
||||
self.logger.log("Turning off display", 4)
|
||||
self.display.clear()
|
||||
self.display.update()
|
||||
self.logger.log("Waiting for select to be pressed...", 4)
|
||||
while True:
|
||||
if self.input_controller.wait_for_input() == "select":
|
||||
break
|
||||
self.logger.log("Returning from sleep mode", 4)
|
||||
return
|
||||
|
||||
def exit_system(self, message=""):
|
||||
# Exit the system with optional message
|
||||
if message:
|
||||
self.logger.log(message, 3)
|
||||
self.logger.log("System shutting down...", 3)
|
||||
machine.deepsleep()
|
||||
|
||||
#
|
||||
# End Of File (EOF)
|
||||
#
|
Loading…
Add table
Add a link
Reference in a new issue