From 9f61b54f157b4a5a0ef4cf545c27f981735e886f Mon Sep 17 00:00:00 2001 From: FreedTapestry21 Date: Mon, 22 Sep 2025 15:44:24 +0200 Subject: [PATCH 1/5] First version of the SYSTEM module --- main.py | 34 +++++++++++------ petcard.py | 4 -- system.py | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 16 deletions(-) delete mode 100644 petcard.py diff --git a/main.py b/main.py index 8290f58..232de12 100644 --- a/main.py +++ b/main.py @@ -2,9 +2,12 @@ # Petcard Bootloader - v1.0 # Copyright (c) 2025 FreedTapestry21 # +# Licensed under the MIT license +# from machine import Pin -import network, urequest, machine +import machine +#import network, urequest, machine # Define variables CONSOLE_WELCOME = """ @@ -37,12 +40,13 @@ def connect(ssid, psk): pass def boot(): - import system - init() + print("Loading system...") + with open("petcard.py") as app: + exec(app.read()) -# Check if button is pressed for recovery/update process -if __name__ == '__main__': +def main(): print(CONSOLE_WELCOME) + UPDATE = False if ok.value() == 1: UPDATE = True @@ -51,21 +55,29 @@ if __name__ == '__main__': 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...") + machine.soft_reset() 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) + #connect(ssid, psk) print('Connected to network "' + str(ssid) + '"') # Downloading system.py - download("system.py", "Petcard system") - download("petcard.py", "Petcard") + # Disabled due to being stuck with a Pico for now + #download("system.py", "Petcard system") + #download("petcard.py", "Petcard") print("System recovery complete, restarting system...") - machine.reset() + machine.soft_reset() else: print("'" + inp + "' is not recognized as an option! Rebooting...") - machine.reset() + machine.soft_reset() else: - boot() \ No newline at end of file + boot() + + + +# Check if button is pressed for recovery/update process +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/petcard.py b/petcard.py deleted file mode 100644 index 7ad7feb..0000000 --- a/petcard.py +++ /dev/null @@ -1,4 +0,0 @@ -# -# Petcard - v1.0 -# Copyright (c) 2025 FreedTapestry21 -# \ No newline at end of file diff --git a/system.py b/system.py index 9a5cc94..4f02430 100644 --- a/system.py +++ b/system.py @@ -1,4 +1,112 @@ # # Petcard System - v1.0 # Copyright (c) 2025 FreedTapestry21 -# \ No newline at end of file +# +# Licensed under the MIT license +# + +# Importing libraries +import sys, time, machine, framebuf, ssd1306, _thread + +# Start Petcard frontend on core 1 +with open("petcard.py") as app: + _thread.start_new_thread(exec(app.read()), ()) + +# Initialize global variables +CONFIG = json.loads(open("config.json", "r").read()) + +TEXT_HEIGHT = 8 +PADDING = 4 + +# Initialize classes +class Display: + def __init__(self, disp, x, y) -> None: + self.x = x + self.y = y + self.type = disp + self.pages = y // 8 + self.buffer = framebuf.FrameBuffer(bytearray(self.pages * x), x, y, framebuf.MONO_VLSB) + + def log(self, msg, IGN=False): # IGN => IGNore to report to console (Only log) + if CONFIG["debug"] == 1: + date = time.localtime() + stamp = str(date[0]) + "-" + str(date[1]) + "-" + str(date[2]) + " " + str(date[3]) + ":" + str(date[4]) + ":" + str(date[5]) + if IGN == False: + text = "[" + stamp + "] " + msg + print(text) + else: + if msg == "": + text = msg + else: + text = "[" + stamp + "] " + msg + if CONFIG["logging"] == 1: # Write log to log file + with open(CONFIG["logfile"], "a") as file: + file.write(text + '\n') + file.close() + + def init(self): + if self.type == "SSD1306": + self.i2c = machine.I2C(0, sda=machine.Pin(4), scl=machine.Pin(5)) + self.display = ssd1306.SSD1306_I2C(128, 64, self.i2c) + + def update(self): + self.display.blit(self.buffer, 0, 0) + self.display.show() + +class Controls: + def __init__(self): + pass + + def waitfor(self): + while True: + for x in CONFIG["controls"]: + if machine.Pin(int(CONFIG["controls"][x]), machine.Pin.IN, machine.Pin.PULL_DOWN).value() == 1: + for y in CONFIG["controls"]: + if CONFIG["controls"][y] == x: + return y + else: pass + +class UI: + def __init__(self): + self.selection = -1 + def menu(self, display, controls, entries): + if self.selection == -1: + self.selection = 0 + while True: + inp = controls.waitfor() + if inp == "down": + self.selection = self.selection + 1 + if inp == "up": + self.selection = self.selection - 1 + if self.selection >= len(entries): + self.selection = 0 + if self.selection < 0: + self.slection = len(entries) + print(str(self.selection)) + display.buffer.fill(0) + display.buffer.fill_rect(0, int(self.selection*TEXT_HEIGHT) + int(self.selection*PADDING) + 2, display.x, 10, 1) + i = PADDING + q = 0 + for x in entries: + if i < display.y: + if q == self.selection: + display.buffer.text(x, 0, i, 0) + else: + display.buffer.text(x, 0, i, 1) + i = i + TEXT_HEIGHT + PADDING + q = q + 1 + else: + pass + display.update() + time.sleep(0.2) + + display.update() + return display + +# Initialize functions + +def quit(msg=""): + if msg == '': + sys.exit() + else: + print("[" + NAME + "] " + msg) \ No newline at end of file From 1be6e12b54dcd1ecff7f8db44fbca475c2ef08a5 Mon Sep 17 00:00:00 2001 From: FreedTapestry21 Date: Tue, 23 Sep 2025 12:40:47 +0200 Subject: [PATCH 2/5] Finished the menu demo --- config.json | 13 ++ lib/ssd1306.py | 155 +++++++++++++++++++++++ main.py | 115 +++++++++-------- petcard.py | 75 +++++++++++ system.py | 336 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 643 insertions(+), 51 deletions(-) create mode 100644 config.json create mode 100644 lib/ssd1306.py diff --git a/config.json b/config.json new file mode 100644 index 0000000..2bfe55c --- /dev/null +++ b/config.json @@ -0,0 +1,13 @@ +{ + "debug": 1, + "logging": 1, + "loglevel": 4, + "logfile": "petcard.log", + "display": "SSD1306", + "controls": { + "up": "0", + "down": "3", + "select": "1", + "back": "2" + } +} \ No newline at end of file diff --git a/lib/ssd1306.py b/lib/ssd1306.py new file mode 100644 index 0000000..1bb6679 --- /dev/null +++ b/lib/ssd1306.py @@ -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) \ No newline at end of file diff --git a/main.py b/main.py index 8290f58..057c770 100644 --- a/main.py +++ b/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() \ No newline at end of file + 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) +# \ No newline at end of file diff --git a/petcard.py b/petcard.py index 7ad7feb..85e6852 100644 --- a/petcard.py +++ b/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) # \ No newline at end of file diff --git a/system.py b/system.py index 9a5cc94..bddc2c1 100644 --- a/system.py +++ b/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) # \ No newline at end of file From e62a61e56546a7d3d5687b56ef8e21c3dddcc060 Mon Sep 17 00:00:00 2001 From: FreedTapestry21 Date: Tue, 23 Sep 2025 13:44:35 +0200 Subject: [PATCH 3/5] Finalized the module names --- petcard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/petcard.py b/petcard.py index 85e6852..15b8919 100644 --- a/petcard.py +++ b/petcard.py @@ -1,5 +1,5 @@ # -# Petcard - v1.0 +# Petcard Application - v1.0 # Copyright (c) 2025 FreedTapestry21 # # Licensed under the MIT license From faf0b2973cee363f459bbd79f51aa5e8b286123c Mon Sep 17 00:00:00 2001 From: FreedTapestry21 Date: Tue, 23 Sep 2025 15:07:15 +0200 Subject: [PATCH 4/5] Made code more readable --- system.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/system.py b/system.py index bddc2c1..4290cbf 100644 --- a/system.py +++ b/system.py @@ -40,7 +40,7 @@ class ConfigurationManager: def _get_default_config(self): return { "debug": 1, - "logging": 1, + "logging": 0, "loglevel": 3, "logfile": "petcard.log", "display": "SSD1306", @@ -55,8 +55,8 @@ class ConfigurationManager: # 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_level = config.get("loglevel") + self.logging_enabled = config.get("logging") self.log_file = config.get("logfile", "system.log") def log(self, message, log_level, ignore_console=False): @@ -77,7 +77,7 @@ class Logger: if not ignore_console: print(formatted_message) - if self.logging_enabled: + if self.logging_enabled == 1: self._write_to_file(formatted_message) def _get_timestamp(self): From b2470eac1631e24ff9e0dff5da199fba76e67f7c Mon Sep 17 00:00:00 2001 From: FreedTapestry21 Date: Wed, 24 Sep 2025 13:25:21 +0200 Subject: [PATCH 5/5] Added Pet class and made a final demo --- main.py | 5 +++-- petcard.py | 29 ++++++++++++++++++--------- system.py | 58 ++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/main.py b/main.py index 057c770..2b23097 100644 --- a/main.py +++ b/main.py @@ -48,6 +48,7 @@ class Instance: def __init__(self): self.config_manager = system.ConfigurationManager() self.logger = system.Logger(self.config_manager.config) + self.pet = system.Pet(self.config_manager, self.logger) self.input_controller = system.InputController(self.config_manager.config["controls"], self.logger) # Select display driver @@ -59,7 +60,7 @@ class Instance: 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) + self.power = system.Power(self.config_manager, self.display, self.input_controller, self.logger) # # Entry point @@ -76,7 +77,7 @@ if __name__ == '__main__': 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 = petcard.Application(instance.config_manager, instance.logger, instance.pet, instance.display_driver, instance.display, instance.input_controller, instance.ui, instance.power) app.run() # diff --git a/petcard.py b/petcard.py index 15b8919..1baa96a 100644 --- a/petcard.py +++ b/petcard.py @@ -9,7 +9,7 @@ # Importing libraries # -import sys, json, machine +import sys, json, machine, _thread, time, random # Import SYSTEM module import system @@ -20,9 +20,10 @@ import system # Main system & application controller class Application: - def __init__(self, config_manager, logger, display_driver, display, input_controller, ui, power): + def __init__(self, config_manager, logger, pet, display_driver, display, input_controller, ui, power): self.config_manager = config_manager self.logger = logger + self.pet = pet self.display_driver = display_driver self.display = display self.input_controller = input_controller @@ -33,10 +34,12 @@ class Application: def initialize(self): self.logger.log("Initializing Petcard System...", 3) self.display.initialize() + self.logger.log("Initializing Petcard health manager...", 3) + _thread.start_new_thread(self._pet_health_manager, ()) self.logger.log("System initialization complete", 3) # Main system loop - def run(self): + def run(self): try: self.initialize() @@ -45,6 +48,7 @@ class Application: system.MenuItem("Food", self._action, "f"), system.MenuItem("Drinks", self._action, "d"), system.MenuItem("Play", self._action, "p"), + system.MenuItem("Save", self._save), system.MenuItem("Sleep", self.power.sleep) ] @@ -55,25 +59,32 @@ class Application: 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") + self.power.shutdown("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) + self.pet.nom("pancakes", 10) elif act == "d": - self.logger.log("Drink!", 3) + self.pet.slurp("water", 10) elif act == "p": - self.logger.log("Play!", 3) + self.pet.play("petted", 10) else: self.logger.log(f"Invalid action {act}", 1) return + def _save(self): + self.logger.log("Saving game progression...", 4) + self.config_manager.save_config() + self.logger.log("Game progression has been saved!", 4) + + def _pet_health_manager(self): + while True: + time.sleep(random.randint(10, 60)) + self.pet.wombicombi() # # End Of File (EOF) # \ No newline at end of file diff --git a/system.py b/system.py index 4290cbf..545bd11 100644 --- a/system.py +++ b/system.py @@ -9,13 +9,7 @@ # Importing libraries # -import sys -import time -import json -import machine -import framebuf -import ssd1306 -import _thread +import sys, time, json, machine, framebuf, ssd1306, random # # Classes @@ -36,6 +30,13 @@ class ConfigurationManager: 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 { @@ -49,6 +50,13 @@ class ConfigurationManager: "down": "3", "select": "1", "back": "2" + }, + "pet": { + "name": "Pet", + "level": 0, + "happiness": 100, + "hunger": 100, + "thirst": 100, } } @@ -57,7 +65,7 @@ class Logger: def __init__(self, config): self.log_level = config.get("loglevel") self.logging_enabled = config.get("logging") - self.log_file = config.get("logfile", "system.log") + self.log_file = config.get("logfile") def log(self, message, log_level, ignore_console=False): timestamp = self._get_timestamp() @@ -96,6 +104,33 @@ class Logger: # 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: @@ -311,7 +346,8 @@ class UserInterface: # Handles power management class Power: - def __init__(self, display, input_controller, logger): + def __init__(self, config, display, input_controller, logger): + self.config = config self.display = display self.input_controller = input_controller self.logger = logger @@ -328,11 +364,13 @@ class Power: self.logger.log("Returning from sleep mode", 4) return - def exit_system(self, message=""): + 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() #