Finished the menu demo
This commit is contained in:
parent
fc2af7031e
commit
1be6e12b54
5 changed files with 643 additions and 51 deletions
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