Petcard/system.py

378 lines
No EOL
14 KiB
Python

#
# Petcard System - v1.0
# Copyright (c) 2025 FreedTapestry21
#
# Licensed under the MIT license
#
#
# Importing libraries
#
import sys, time, json, machine, framebuf, ssd1306, random
#
# 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()
def save_config(self):
try:
with open(self.config_file, "w") as file:
json.dump(self.config, file)
except Exception as e:
print(f"Failed to save config: {e}")
# Return default configuration
def _get_default_config(self):
return {
"debug": 1,
"logging": 0,
"loglevel": 3,
"logfile": "petcard.log",
"display": "SSD1306",
"controls": {
"up": "0",
"down": "3",
"select": "1",
"back": "2"
},
"pet": {
"name": "Pet",
"level": 0,
"happiness": 100,
"hunger": 100,
"thirst": 100,
}
}
# Handles logging functionality with timestamp formatting
class Logger:
def __init__(self, config):
self.log_level = config.get("loglevel")
self.logging_enabled = config.get("logging")
self.log_file = config.get("logfile")
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 == 1:
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)
class Pet:
def __init__(self, config, logger):
self.config = config
self.logger = logger
def play(self, name, amount):
self.config.config["pet"]["happiness"] = self.config.config["pet"]["happiness"] + (amount * ((self.config.config["pet"]["level"] / 100)+1))
self.logger.log(f"You {self.config.config['pet']['name']} {name} and gained {str(amount)} of happiness! Your happiness bar is now at {str(self.config.config['pet']['happiness'])}!", 3)
def nom(self, name, amount):
self.config.config["pet"]["hunger"] = self.config.config["pet"]["hunger"] + (amount * ((self.config.config["pet"]["level"] / 100)+1))
self.logger.log(f"You fed {self.config.config['pet']['name']} {name} and gained {str(amount)} of food! Your food bar is now at {str(self.config.config['pet']['hunger'])}!", 3)
def slurp(self, name, amount):
self.config.config["pet"]["thirst"] = self.config.config["pet"]["thirst"] + (amount * ((self.config.config["pet"]["level"] / 100)+1))
self.logger.log(f"You gave {self.config.config['pet']['name']} {name} and gained {str(amount)} of hydration! Your hydration bar is now at {str(self.config.config['pet']['thirst'])}!", 3)
def wombicombi(self):
self.logger.log("Health depletion triggered", 4)
self.config.config["pet"]["happiness"] = round(self.config.config["pet"]["happiness"] - ((100 - self.config.config["pet"]["level"]) * random.uniform(0.0, 0.2)))
self.config.config["pet"]["hunger"] = round(self.config.config["pet"]["hunger"] - ((100 - self.config.config["pet"]["level"]) * random.uniform(0.0, 0.2)))
self.config.config["pet"]["thirst"] = round(self.config.config["pet"]["thirst"] - ((100 - self.config.config["pet"]["level"]) * random.uniform(0.0, 0.2)))
self.logger.log(f"Health is now at the following values...", 4)
self.logger.log(f"Happiness: {self.config.config['pet']['happiness']}", 4)
self.logger.log(f"Food: {self.config.config['pet']['hunger']}", 4)
self.logger.log(f"Hydration: {self.config.config['pet']['thirst']}", 4)
# 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, config, display, input_controller, logger):
self.config = config
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 shutdown(self, message=""):
# Exit the system with optional message
if message:
self.logger.log(message, 3)
self.logger.log("System shutting down...", 3)
self.display.clear()
self.display.update()
machine.deepsleep()
#
# End Of File (EOF)
#