# # 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) #