378 lines
No EOL
14 KiB
Python
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)
|
|
# |