[{"element_type":"image_url","content":"https://ataary.com/wp-content/uploads/2025/06/IMG_7267-scaled.jpeg","metadata":{"alt":"Hands holding the Herman Entertainment System","caption":"HES Raspi Zero 2w Handheld"}},{"element_type":"image_url","content":"https://ataary.com/wp-content/uploads/2025/06/IMG_6897-ezgif.com-video-to-gif-converter.gif","metadata":{"alt":"Gif of HES main screen and opening the snake game.","caption":"HES Title Screen and Snake Game"}},{"element_type":"text","content":"\n        \u003cp\u003e\u003cspan\u003eThis is the Herman Entertainment System, a handheld gaming system with a\u0026nbsp;\u003c/span\u003e\u003ca href=\"https://www.adafruit.com/product/6008\"\u003e\u003cem\u003eRaspberry\u003c/em\u003e \u003cem\u003ePi Zero 2 W\u003c/em\u003e\u003c/a\u003e\u003cem\u003e and the \u003ca href=\"https://www.adafruit.com/product/4506\" target=\"_blank\"\u003eAdafruit 1.3″ TFT Bonnet\u003c/a\u003e.\u003c/em\u003e\u003cspan\u003e Why Herman? I don’t know, that is what\u0026nbsp;\u003c/span\u003ethe kids \u003cspan\u003ewanted to call it, apparently Herman needs entertained. As for why we needed this instead of an existing emulator, well this particular setup struggles to run systems like \u003c/span\u003e\u003ca href=\"https://retropie.org.uk/\"\u003eRetroPie\u003c/a\u003e\u003cspan\u003e due to a display issue, so we created a small setup that uses \u003c/span\u003e\u003ca href=\"https://www.python.org/\"\u003ePython\u003c/a\u003e\u003cspan\u003e and creates a home screen that lists all the games on the device and you simply choose which to play. It automatically updates with all the games you have added, in our example we have a Snake game, but you can add or make nearly anything. Print files for the enclosure are available \u003ca href=\"https://www.printables.com/model/1342935-herman-entertainment-system-a-raspi-based-handheld\" target=\"_blank\"\u003ehere via Printables\u003c/a\u003e, and additional details and instructions are \u003ca title=\"ataary HES\" href=\"https://ataary.com/tag/herman/\" target=\"_blank\"\u003eavailable here\u003c/a\u003e.\u003cbr\u003e\u003cbr\u003e\u003c/span\u003e\u003c/p\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 1: Prepare the SD Card (Headless Setup)\u003c/h2\u003e\n\u003ch4 class=\"wp-block-heading\"\u003eFlash Pi OS Lite\u003c/h4\u003e\n\u003col class=\"wp-block-list\"\u003e\n\u003cli\u003eDownload \u003cstrong\u003eRaspberry Pi Imager\u003c/strong\u003e from raspberrypi.org\u003c/li\u003e\n\u003cli\u003eInsert your SD card into your computer\u003c/li\u003e\n\u003cli\u003eOpen Raspberry Pi Imager\u003c/li\u003e\n\u003cli\u003eChoose \u003cstrong\u003e“Raspberry Pi OS Lite (32-bit)”\u003c/strong\u003e – no desktop needed\u003c/li\u003e\n\u003cli\u003eClick the \u003cstrong\u003egear icon\u003c/strong\u003e (Advanced options) and configure:\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003e\n\u003cstrong\u003eEnable SSH\u003c/strong\u003e ✓\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eSet username\u003c/strong\u003e: \u003ccode\u003eherman\u003c/code\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eSet password\u003c/strong\u003e: (your choice)\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eConfigure WiFi\u003c/strong\u003e: Enter your network name and password\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eSet locale settings\u003c/strong\u003e: Your country/timezone\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eFlash the image to SD card\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 2: First Boot and SSH Connection\u003c/h2\u003e\n\u003col class=\"wp-block-list\"\u003e\n\u003cli\u003e\n\u003cstrong\u003eInsert SD card\u003c/strong\u003e into your Pi Zero 2 W\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003ePower on\u003c/strong\u003e the Pi (green LED should flash, then stay solid)\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eWait 2-3 minutes\u003c/strong\u003e for first boot to complete\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eFind the Pi’s IP address\u003c/strong\u003e:\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003eCheck your router’s admin page for connected devices\u003c/li\u003e\n\u003cli\u003eOr use an IP scanner like Advanced IP Scanner\u003c/li\u003e\n\u003cli\u003eLook for device named “raspberrypi”\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch4 class=\"wp-block-heading\"\u003eConnect via SSH\u003c/h4\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003essh herman@192.168.1.XXX\n# Replace XXX with your Pi's actual IP address\n\u003c/pre\u003e\n\u003cp\u003eEnter your password when prompted.\u003c/p\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 3: System Updates and Basic Setup\u003c/h2\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003e# Update the system\nsudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y\n\n# Install essential packages\nsudo apt install python3-pip python3-pil git -y\n\n# Enable SPI (required for TFT display)\nsudo raspi-config\n\u003c/pre\u003e\n\u003cp\u003eIn raspi-config:\u003c/p\u003e\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003eGo to \u003cstrong\u003eInterface Options\u003c/strong\u003e → \u003cstrong\u003eSPI\u003c/strong\u003e → \u003cstrong\u003eEnable\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eFinish\u003c/strong\u003e and \u003cstrong\u003ereboot\u003c/strong\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eReconnect via SSH after reboot.\u003c/p\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 4: Install TFT Display Libraries\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eNote:\u003c/strong\u003e Newer Pi OS versions require \u003ccode\u003e--break-system-packages\u003c/code\u003e flag\u003c/p\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003e# Install compatible Adafruit libraries\nsudo pip3 install \"adafruit-circuitpython-rgb-display==3.10.0\" --break-system-packages\nsudo pip3 install \"adafruit-circuitpython-busdevice==5.2.0\" --break-system-packages\nsudo pip3 install \"adafruit-circuitpython-typing==1.10.1\" --break-system-packages\n\n# Install additional required libraries for SPI configuration\nsudo pip3 install click --break-system-packages\nsudo pip3 install adafruit-python-shell --break-system-packages\n\n# Install system packages\nsudo apt install python3-numpy -y\u003c/pre\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 5: Configure TFT Display Hardware\u003c/h2\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003e# Download and run the SPI configuration script\nwget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/main/raspi-spi-reassign.py\nsudo python3 raspi-spi-reassign.py\n\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003eWhen prompted:\u003c/strong\u003e\u003c/p\u003e\n\u003col class=\"wp-block-list\"\u003e\n\u003cli\u003eSelect \u003cstrong\u003e1 Reassign SPI Chip Enable Pins\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli\u003eFor CE0 selection, choose \u003cstrong\u003e22 Disabled\u003c/strong\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eReboot when prompted\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 6: Create the Gaming System Files\u003c/h2\u003e\n\u003cp\u003eReconnect via SSH and create your gaming system:\u003c/p\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003e# Create directories\nmkdir -p /home/herman/games\nmkdir -p /home/herman/system\n\u003c/pre\u003e\n\u003ch4 class=\"wp-block-heading\"\u003eCreate the main entertainment system:\u003c/h4\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003enano /home/herman/herman_entertainment_system.py\u003cbr\u003e\u003cbr\u003eThen copy the following code and paste it into the new file.\u003cbr\u003e\u003cbr\u003e\u003cbr\u003e#!/usr/bin/env python3\u003cbr\u003eimport time\u003cbr\u003eimport os\u003cbr\u003eimport subprocess\u003cbr\u003eimport digitalio\u003cbr\u003eimport board\u003cbr\u003eimport importlib.util\u003cbr\u003eimport sys\u003cbr\u003efrom adafruit_rgb_display.rgb import color565\u003cbr\u003efrom adafruit_rgb_display import st7789\u003cbr\u003eimport RPi.GPIO as GPIO\u003cbr\u003efrom PIL import Image, ImageDraw, ImageFont\u003cbr\u003e\u003cbr\u003eclass HermanEntertainmentSystem:\u003cbr\u003e    def __init__(self):\u003cbr\u003e        # Initialize display and GPIO in the class\u003cbr\u003e        self.setup_hardware()\u003cbr\u003e        \u003cbr\u003e        # Load fonts\u003cbr\u003e        try:\u003cbr\u003e            self.font_huge = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 36)\u003cbr\u003e            self.font_large = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 24)\u003cbr\u003e            self.font_medium = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 18)\u003cbr\u003e            self.font_small = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 14)\u003cbr\u003e        except:\u003cbr\u003e            self.font_huge = ImageFont.load_default()\u003cbr\u003e            self.font_large = ImageFont.load_default()\u003cbr\u003e            self.font_medium = ImageFont.load_default()\u003cbr\u003e            self.font_small = ImageFont.load_default()\u003cbr\u003e        \u003cbr\u003e        # Button debouncing\u003cbr\u003e        self.last_button_time = 0\u003cbr\u003e        self.button_debounce = 0.2\u003cbr\u003e        \u003cbr\u003e        # Game list\u003cbr\u003e        self.games = []\u003cbr\u003e        self.selected_game = 0\u003cbr\u003e        self.games_directory = \"/home/herman/games\"\u003cbr\u003e        \u003cbr\u003e        # Create games directory if it doesn't exist\u003cbr\u003e        if not os.path.exists(self.games_directory):\u003cbr\u003e            os.makedirs(self.games_directory)\u003cbr\u003e        \u003cbr\u003e        self.scan_for_games()\u003cbr\u003e    \u003cbr\u003e    def setup_hardware(self):\u003cbr\u003e        \"\"\"Initialize display and GPIO\"\"\"\u003cbr\u003e        # TFT Display Setup\u003cbr\u003e        self.cs_pin = digitalio.DigitalInOut(board.CE0)\u003cbr\u003e        self.dc_pin = digitalio.DigitalInOut(board.D25)\u003cbr\u003e        reset_pin = None\u003cbr\u003e        BAUDRATE = 40000000\u003cbr\u003e\u003cbr\u003e        self.display = st7789.ST7789(\u003cbr\u003e            board.SPI(),\u003cbr\u003e            cs=self.cs_pin,\u003cbr\u003e            dc=self.dc_pin,\u003cbr\u003e            rst=reset_pin,\u003cbr\u003e            baudrate=BAUDRATE,\u003cbr\u003e            width=240,\u003cbr\u003e            height=240,\u003cbr\u003e            x_offset=0,\u003cbr\u003e            y_offset=80,\u003cbr\u003e            rotation=180,  # Fixed rotation for proper orientation\u003cbr\u003e        )\u003cbr\u003e\u003cbr\u003e        self.backlight = digitalio.DigitalInOut(board.D26)\u003cbr\u003e        self.backlight.switch_to_output()\u003cbr\u003e        self.backlight.value = True\u003cbr\u003e\u003cbr\u003e        # GPIO Setup - Correct pins for Pi Zero 2 W + TFT Bonnet\u003cbr\u003e        GPIO.setmode(GPIO.BCM)\u003cbr\u003e        GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)  # UP\u003cbr\u003e        GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)  # DOWN\u003cbr\u003e        GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP)  # LEFT\u003cbr\u003e        GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)  # RIGHT\u003cbr\u003e        GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_UP)   # Button A\u003cbr\u003e        GPIO.setup(6, GPIO.IN, pull_up_down=GPIO.PUD_UP)   # Button B\u003cbr\u003e        GPIO.setup(4, GPIO.IN, pull_up_down=GPIO.PUD_UP)   # Button C\u003cbr\u003e\u003cbr\u003e    \u003cbr\u003e    def get_text_size(self, draw, text, font):\u003cbr\u003e        \"\"\"Compatible text size function for older PIL versions\"\"\"\u003cbr\u003e        try:\u003cbr\u003e            bbox = draw.textbbox((0, 0), text, font=font)\u003cbr\u003e            return bbox[2] - bbox[0], bbox[3] - bbox[1]\u003cbr\u003e        except AttributeError:\u003cbr\u003e            try:\u003cbr\u003e                return draw.textsize(text, font=font)\u003cbr\u003e            except:\u003cbr\u003e                return len(text) * 8, 16\u003cbr\u003e    \u003cbr\u003e    def draw_centered_text(self, draw, text, y_pos, font, color):\u003cbr\u003e        \"\"\"Draw text centered horizontally\"\"\"\u003cbr\u003e        text_width, text_height = self.get_text_size(draw, text, font)\u003cbr\u003e        x_pos = (240 - text_width) // 2\u003cbr\u003e        draw.text((x_pos, y_pos), text, font=font, fill=color)\u003cbr\u003e        return text_height\u003cbr\u003e    \u003cbr\u003e    def read_controls_debounced(self):\u003cbr\u003e        \"\"\"Read controls with debouncing\"\"\"\u003cbr\u003e        current_time = time.time()\u003cbr\u003e        \u003cbr\u003e        raw_controls = {\u003cbr\u003e            'up': not GPIO.input(17),\u003cbr\u003e            'down': not GPIO.input(22),\u003cbr\u003e            'left': not GPIO.input(27),\u003cbr\u003e            'right': not GPIO.input(23),\u003cbr\u003e            'button_a': not GPIO.input(5),\u003cbr\u003e            'button_b': not GPIO.input(6),\u003cbr\u003e            'button_c': not GPIO.input(4)\u003cbr\u003e        }\u003cbr\u003e        \u003cbr\u003e        # Apply debouncing\u003cbr\u003e        if current_time - self.last_button_time \u0026gt; self.button_debounce:\u003cbr\u003e            if any(raw_controls.values()):\u003cbr\u003e                self.last_button_time = current_time\u003cbr\u003e                return raw_controls\u003cbr\u003e        \u003cbr\u003e        return {key: False for key in raw_controls}\u003cbr\u003e    \u003cbr\u003e    def scan_for_games(self):\u003cbr\u003e        \"\"\"Scan for available game files\"\"\"\u003cbr\u003e        self.games = []\u003cbr\u003e        \u003cbr\u003e        # Add built-in games\u003cbr\u003e        builtin_games = [\u003cbr\u003e            {\"name\": \"Snake Game\", \"file\": \"snake_game_module.py\", \"type\": \"builtin\"},\u003cbr\u003e        ]\u003cbr\u003e        \u003cbr\u003e        for game in builtin_games:\u003cbr\u003e            if os.path.exists(f\"/home/herman/{game['file']}\"):\u003cbr\u003e                self.games.append(game)\u003cbr\u003e        \u003cbr\u003e        # Scan games directory for additional games\u003cbr\u003e        if os.path.exists(self.games_directory):\u003cbr\u003e            for filename in os.listdir(self.games_directory):\u003cbr\u003e                if filename.endswith('.py'):\u003cbr\u003e                    game_name = filename.replace('.py', '').replace('_', ' ').title()\u003cbr\u003e                    self.games.append({\u003cbr\u003e                        \"name\": game_name,\u003cbr\u003e                        \"file\": filename,\u003cbr\u003e                        \"type\": \"custom\"\u003cbr\u003e                    })\u003cbr\u003e        \u003cbr\u003e        # Add system options\u003cbr\u003e        self.games.extend([\u003cbr\u003e            {\"name\": \"System Info\", \"file\": None, \"type\": \"system\"},\u003cbr\u003e            {\"name\": \"Shutdown\", \"file\": None, \"type\": \"system\"}\u003cbr\u003e        ])\u003cbr\u003e        \u003cbr\u003e        # Reset selection if out of bounds\u003cbr\u003e        if self.selected_game \u0026gt;= len(self.games):\u003cbr\u003e            self.selected_game = 0\u003cbr\u003e    \u003cbr\u003e    def show_welcome_screen(self):\u003cbr\u003e        \"\"\"Display welcome screen\"\"\"\u003cbr\u003e        image = Image.new('RGB', (240, 240), (0, 0, 50))  # Dark blue background\u003cbr\u003e        draw = ImageDraw.Draw(image)\u003cbr\u003e        \u003cbr\u003e        # Title with shadow effect\u003cbr\u003e        self.draw_centered_text(draw, \"Herman\", 31, self.font_huge, (0, 0, 0))  # Shadow\u003cbr\u003e        self.draw_centered_text(draw, \"Herman\", 30, self.font_huge, (0, 255, 100))  # Main\u003cbr\u003e        \u003cbr\u003e        self.draw_centered_text(draw, \"Entertainment\", 71, self.font_large, (0, 0, 0))  # Shadow\u003cbr\u003e        self.draw_centered_text(draw, \"Entertainment\", 70, self.font_large, (255, 255, 255))  # Main\u003cbr\u003e        \u003cbr\u003e        self.draw_centered_text(draw, \"System\", 101, self.font_large, (0, 0, 0))  # Shadow\u003cbr\u003e        self.draw_centered_text(draw, \"System\", 100, self.font_large, (255, 255, 255))  # Main\u003cbr\u003e        \u003cbr\u003e        # Animated dots\u003cbr\u003e        dots = \"...\" if int(time.time() * 2) % 2 else \"\"\u003cbr\u003e        self.draw_centered_text(draw, f\"Loading{dots}\", 140, self.font_medium, (100, 100, 255))\u003cbr\u003e        \u003cbr\u003e        # Instructions\u003cbr\u003e        self.draw_centered_text(draw, \"Press A to Continue\", 180, self.font_medium, (255, 255, 0))\u003cbr\u003e        self.draw_centered_text(draw, \"Press B to Shutdown\", 210, self.font_small, (255, 100, 100))\u003cbr\u003e        \u003cbr\u003e        self.display.image(image)\u003cbr\u003e    \u003cbr\u003e    def show_game_list(self):\u003cbr\u003e        \"\"\"Display scrollable game list\"\"\"\u003cbr\u003e        image = Image.new('RGB', (240, 240), (0, 0, 0))\u003cbr\u003e        draw = ImageDraw.Draw(image)\u003cbr\u003e        \u003cbr\u003e        # Header\u003cbr\u003e        self.draw_centered_text(draw, \"Select Game\", 10, self.font_large, (0, 255, 0))\u003cbr\u003e        \u003cbr\u003e        # Draw game list (show 5 games at a time)\u003cbr\u003e        start_index = max(0, self.selected_game - 2)\u003cbr\u003e        end_index = min(len(self.games), start_index + 5)\u003cbr\u003e        \u003cbr\u003e        y_pos = 50\u003cbr\u003e        for i in range(start_index, end_index):\u003cbr\u003e            game = self.games[i]\u003cbr\u003e            \u003cbr\u003e            # Highlight selected game\u003cbr\u003e            if i == self.selected_game:\u003cbr\u003e                # Selection background\u003cbr\u003e                draw.rectangle([10, y_pos - 2, 230, y_pos + 25], fill=(0, 100, 0))\u003cbr\u003e                text_color = (255, 255, 255)\u003cbr\u003e                # Selection indicator\u003cbr\u003e                draw.text((15, y_pos + 2), \"\u0026gt;\", font=self.font_medium, fill=(255, 255, 0))\u003cbr\u003e            else:\u003cbr\u003e                text_color = (200, 200, 200)\u003cbr\u003e            \u003cbr\u003e            # Game name\u003cbr\u003e            draw.text((35, y_pos + 2), game[\"name\"], font=self.font_medium, fill=text_color)\u003cbr\u003e            \u003cbr\u003e            # Game type indicator\u003cbr\u003e            if game[\"type\"] == \"builtin\":\u003cbr\u003e                draw.rectangle([220, y_pos + 5, 235, y_pos + 15], fill=(0, 255, 0))\u003cbr\u003e            elif game[\"type\"] == \"custom\":\u003cbr\u003e                draw.rectangle([220, y_pos + 5, 235, y_pos + 15], fill=(0, 0, 255))\u003cbr\u003e            elif game[\"type\"] == \"system\":\u003cbr\u003e                draw.rectangle([220, y_pos + 5, 235, y_pos + 15], fill=(255, 0, 0))\u003cbr\u003e            \u003cbr\u003e            y_pos += 30\u003cbr\u003e        \u003cbr\u003e        # Instructions\u003cbr\u003e        self.draw_centered_text(draw, \"Up/Down = Navigate\", 190, self.font_small, (100, 100, 100))\u003cbr\u003e        self.draw_centered_text(draw, \"A = Select  B = Back\", 210, self.font_small, (100, 100, 100))\u003cbr\u003e        \u003cbr\u003e        # Scroll indicators\u003cbr\u003e        if start_index \u0026gt; 0:\u003cbr\u003e            draw.text((115, 35), \"▲\", font=self.font_small, fill=(255, 255, 0))\u003cbr\u003e        if end_index \u0026lt; len(self.games):\u003cbr\u003e            draw.text((115, 175), \"▼\", font=self.font_small, fill=(255, 255, 0))\u003cbr\u003e        \u003cbr\u003e        self.display.image(image)\u003cbr\u003e    \u003cbr\u003e    def show_system_info(self):\u003cbr\u003e        \"\"\"Display system information\"\"\"\u003cbr\u003e        image = Image.new('RGB', (240, 240), (0, 0, 0))\u003cbr\u003e        draw = ImageDraw.Draw(image)\u003cbr\u003e        \u003cbr\u003e        self.draw_centered_text(draw, \"System Info\", 20, self.font_large, (0, 255, 0))\u003cbr\u003e        \u003cbr\u003e        # Get system info\u003cbr\u003e        try:\u003cbr\u003e            with open('/proc/cpuinfo', 'r') as f:\u003cbr\u003e                for line in f:\u003cbr\u003e                    if 'Model' in line:\u003cbr\u003e                        model = line.split(':')[1].strip()\u003cbr\u003e                        break\u003cbr\u003e                else:\u003cbr\u003e                    model = \"Raspberry Pi\"\u003cbr\u003e        except:\u003cbr\u003e            model = \"Unknown\"\u003cbr\u003e        \u003cbr\u003e        info_lines = [\u003cbr\u003e            f\"Device: {model[:20]}\",\u003cbr\u003e            f\"Games: {len([g for g in self.games if g['type'] != 'system'])}\",\u003cbr\u003e            f\"Python: 3.11\",\u003cbr\u003e            f\"Display: 240x240 ST7789\"\u003cbr\u003e        ]\u003cbr\u003e        \u003cbr\u003e        y_pos = 70\u003cbr\u003e        for line in info_lines:\u003cbr\u003e            self.draw_centered_text(draw, line, y_pos, self.font_small, (255, 255, 255))\u003cbr\u003e            y_pos += 25\u003cbr\u003e        \u003cbr\u003e        self.draw_centered_text(draw, \"Press B to return\", 200, self.font_small, (255, 255, 0))\u003cbr\u003e        \u003cbr\u003e        self.display.image(image)\u003cbr\u003e    \u003cbr\u003e    def launch_game(self, game):\u003cbr\u003e        \"\"\"Launch selected game using dynamic import for custom games\"\"\"\u003cbr\u003e        if game[\"type\"] == \"system\":\u003cbr\u003e            if game[\"name\"] == \"System Info\":\u003cbr\u003e                return \"system_info\"\u003cbr\u003e            elif game[\"name\"] == \"Shutdown\":\u003cbr\u003e                return \"shutdown\"\u003cbr\u003e        \u003cbr\u003e        try:\u003cbr\u003e            # Show loading screen for both built-in and custom games\u003cbr\u003e            image = Image.new('RGB', (240, 240), (0, 0, 0))\u003cbr\u003e            draw = ImageDraw.Draw(image)\u003cbr\u003e            self.draw_centered_text(draw, \"Loading Game...\", 120, self.font_medium, (255, 255, 0))\u003cbr\u003e            self.display.image(image)\u003cbr\u003e            \u003cbr\u003e            if game[\"type\"] == \"builtin\":\u003cbr\u003e                # Built-in game handling (existing snake game)\u003cbr\u003e                if game['file'] == 'snake_game_module.py':\u003cbr\u003e                    from snake_game_module import SnakeGame\u003cbr\u003e                    game_instance = SnakeGame(self.display, GPIO, self.backlight)\u003cbr\u003e                    game_instance.run()\u003cbr\u003e            \u003cbr\u003e            elif game[\"type\"] == \"custom\":\u003cbr\u003e                # Dynamic import for custom games\u003cbr\u003e                game_path = os.path.join(self.games_directory, game[\"file\"])\u003cbr\u003e                module_name = os.path.splitext(game[\"file\"])[0]\u003cbr\u003e                \u003cbr\u003e                # Import the module\u003cbr\u003e                spec = importlib.util.spec_from_file_location(module_name, game_path)\u003cbr\u003e                game_module = importlib.util.module_from_spec(spec)\u003cbr\u003e                sys.modules[module_name] = game_module\u003cbr\u003e                spec.loader.exec_module(game_module)\u003cbr\u003e                \u003cbr\u003e                # Convert filename to CamelCase class name\u003cbr\u003e                class_name = ''.join(\u003cbr\u003e                    part.capitalize() \u003cbr\u003e                    for part in module_name.split('_')\u003cbr\u003e                )\u003cbr\u003e                \u003cbr\u003e                # Get the game class\u003cbr\u003e                GameClass = getattr(game_module, class_name)\u003cbr\u003e                \u003cbr\u003e                # Instantiate and run the game\u003cbr\u003e                game_instance = GameClass(self.display, GPIO, self.backlight)\u003cbr\u003e                game_instance.run()\u003cbr\u003e\u003cbr\u003e            return \"game_list\"\u003cbr\u003e            \u003cbr\u003e        except Exception as e:\u003cbr\u003e            # Unified error handling\u003cbr\u003e            image = Image.new('RGB', (240, 240), (0, 0, 0))\u003cbr\u003e            draw = ImageDraw.Draw(image)\u003cbr\u003e            self.draw_centered_text(draw, \"Game Error!\", 80, self.font_medium, (255, 0, 0))\u003cbr\u003e            \u003cbr\u003e            error_msg = str(e)\u003cbr\u003e            if len(error_msg) \u0026gt; 30:\u003cbr\u003e                error_msg = error_msg[:30] + \"...\"\u003cbr\u003e            self.draw_centered_text(draw, error_msg, 110, self.font_small, (255, 255, 255))\u003cbr\u003e            \u003cbr\u003e            self.draw_centered_text(draw, \"Press A to continue\", 160, self.font_small, (255, 255, 255))\u003cbr\u003e            self.display.image(image)\u003cbr\u003e            \u003cbr\u003e            # Wait for button press\u003cbr\u003e            while True:\u003cbr\u003e                controls = self.read_controls_debounced()\u003cbr\u003e                if controls['button_a']:\u003cbr\u003e                    break\u003cbr\u003e                time.sleep(0.1)\u003cbr\u003e            \u003cbr\u003e            return \"game_list\"\u003cbr\u003e    \u003cbr\u003e    def run(self):\u003cbr\u003e        \"\"\"Main system loop\"\"\"\u003cbr\u003e        state = \"welcome\"\u003cbr\u003e        \u003cbr\u003e        try:\u003cbr\u003e            while True:\u003cbr\u003e                controls = self.read_controls_debounced()\u003cbr\u003e                \u003cbr\u003e                if state == \"welcome\":\u003cbr\u003e                    self.show_welcome_screen()\u003cbr\u003e                    \u003cbr\u003e                    if controls['button_a']:\u003cbr\u003e                        state = \"game_list\"\u003cbr\u003e                        self.scan_for_games()  # Refresh game list\u003cbr\u003e                    elif controls['button_b']:\u003cbr\u003e                        # Shutdown\u003cbr\u003e                        image = Image.new('RGB', (240, 240), (0, 0, 0))\u003cbr\u003e                        draw = ImageDraw.Draw(image)\u003cbr\u003e                        self.draw_centered_text(draw, \"Shutting Down...\", 120, self.font_medium, (255, 0, 0))\u003cbr\u003e                        self.display.image(image)\u003cbr\u003e                        time.sleep(2)\u003cbr\u003e                        subprocess.run(['sudo', 'shutdown', '-h', 'now'])\u003cbr\u003e                        break\u003cbr\u003e                \u003cbr\u003e                elif state == \"game_list\":\u003cbr\u003e                    self.show_game_list()\u003cbr\u003e                    \u003cbr\u003e                    if controls['up']:\u003cbr\u003e                        self.selected_game = (self.selected_game - 1) % len(self.games)\u003cbr\u003e                    elif controls['down']:\u003cbr\u003e                        self.selected_game = (self.selected_game + 1) % len(self.games)\u003cbr\u003e                    elif controls['button_a']:\u003cbr\u003e                        if self.games:\u003cbr\u003e                            result = self.launch_game(self.games[self.selected_game])\u003cbr\u003e                            if result == \"system_info\":\u003cbr\u003e                                state = \"system_info\"\u003cbr\u003e                            elif result == \"shutdown\":\u003cbr\u003e                                subprocess.run(['sudo', 'shutdown', '-h', 'now'])\u003cbr\u003e                                break\u003cbr\u003e                            # Game returned, stay in game_list state\u003cbr\u003e                    elif controls['button_b']:\u003cbr\u003e                        state = \"welcome\"\u003cbr\u003e                \u003cbr\u003e                elif state == \"system_info\":\u003cbr\u003e                    self.show_system_info()\u003cbr\u003e                    \u003cbr\u003e                    if controls['button_b']:\u003cbr\u003e                        state = \"game_list\"\u003cbr\u003e                \u003cbr\u003e                time.sleep(0.1)\u003cbr\u003e                \u003cbr\u003e        except KeyboardInterrupt:\u003cbr\u003e            pass\u003cbr\u003e        finally:\u003cbr\u003e            try:\u003cbr\u003e                GPIO.cleanup()\u003cbr\u003e            except:\u003cbr\u003e                pass\u003cbr\u003e            try:\u003cbr\u003e                self.backlight.value = False\u003cbr\u003e            except:\u003cbr\u003e                pass\u003cbr\u003e\u003cbr\u003e# Main execution\u003cbr\u003eif __name__ == \"__main__\":\u003cbr\u003e    system = HermanEntertainmentSystem()\u003cbr\u003e    system.run()\u003cbr\u003e\u003cbr\u003eControl + x to exit, then Y to save, and Enter to confirm the name. This will be the same process for the Snake game.\u003c/pre\u003e\n\u003ch4 class=\"wp-block-heading\"\u003eCreate the Snake game:\u003c/h4\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003enano /home/herman/snake_game_module.py\n\u003c/pre\u003e\n\u003cpre class=\"wp-block-code\"\u003e\u003ccode\u003e#!/usr/bin/env python3\nimport time\nimport random\nimport RPi.GPIO as GPIO\nfrom PIL import Image, ImageDraw, ImageFont\n\nclass SnakeGame:\n    def __init__(self, display_obj, gpio_obj, backlight_obj):\n        # Verify objects are not None\n        if display_obj is None or gpio_obj is None or backlight_obj is None:\n            raise ValueError(\"Display, GPIO, or backlight object is None\")\n            \n        self.display = display_obj\n        self.GPIO = gpio_obj\n        self.backlight = backlight_obj\n        \n        self.grid_size = 12\n        self.grid_width = 240 // self.grid_size\n        self.grid_height = 240 // self.grid_size\n        self.reset_game()\n        \n        # Load fonts\n        try:\n            self.font_huge = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 48)\n            self.font_large = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf\", 32)\n            self.font_medium = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 20)\n            self.font_small = ImageFont.truetype(\"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf\", 16)\n        except:\n            self.font_huge = ImageFont.load_default()\n            self.font_large = ImageFont.load_default()\n            self.font_medium = ImageFont.load_default()\n            self.font_small = ImageFont.load_default()\n        \n        # Button debouncing\n        self.last_button_time = 0\n        self.button_debounce = 0.1\n        self.last_direction_time = 0\n        self.direction_debounce = 0.1\n        \n        # Display optimization\n        self.last_display_update = 0\n        self.display_interval = 0.05\n        \n    def reset_game(self):\n        center_x = self.grid_width // 2\n        center_y = self.grid_height // 2\n        self.snake = [\n            [center_x, center_y],\n            [center_x - 1, center_y],\n            [center_x - 2, center_y]\n        ]\n        self.direction = [1, 0]\n        self.food = self.generate_food()\n        self.score = 0\n        self.game_over = False\n        self.needs_redraw = True\n        \n    def generate_food(self):\n        while True:\n            food_x = random.randint(0, self.grid_width - 1)\n            food_y = random.randint(0, self.grid_height - 1)\n            if [food_x, food_y] not in self.snake:\n                return [food_x, food_y]\n    \n    def read_controls_debounced(self):\n        current_time = time.time()\n        \n        raw_controls = {\n            'up': not self.GPIO.input(17),\n            'down': not self.GPIO.input(22),\n            'left': not self.GPIO.input(27),\n            'right': not self.GPIO.input(23),\n            'button_a': not self.GPIO.input(5),\n            'button_b': not self.GPIO.input(6)\n        }\n        \n        debounced_controls = {\n            'up': False, 'down': False, 'left': False, 'right': False,\n            'button_a': False, 'button_b': False\n        }\n        \n        if current_time - self.last_direction_time \u0026gt; self.direction_debounce:\n            if any([raw_controls['up'], raw_controls['down'], \n                   raw_controls['left'], raw_controls['right']]):\n                debounced_controls.update({\n                    'up': raw_controls['up'],\n                    'down': raw_controls['down'],\n                    'left': raw_controls['left'],\n                    'right': raw_controls['right']\n                })\n                self.last_direction_time = current_time\n        \n        if current_time - self.last_button_time \u0026gt; self.button_debounce:\n            if raw_controls['button_a'] or raw_controls['button_b']:\n                debounced_controls.update({\n                    'button_a': raw_controls['button_a'],\n                    'button_b': raw_controls['button_b']\n                })\n                self.last_button_time = current_time\n        \n        return debounced_controls\n    \n    def update_direction(self, controls):\n        old_direction = self.direction.copy()\n        \n        if controls['up'] and self.direction[1] != 1:\n            self.direction = [0, -1]\n        elif controls['down'] and self.direction[1] != -1:\n            self.direction = [0, 1]\n        elif controls['left'] and self.direction[0] != 1:\n            self.direction = [-1, 0]\n        elif controls['right'] and self.direction[0] != -1:\n            self.direction = [1, 0]\n        \n        if old_direction != self.direction:\n            self.needs_redraw = True\n    \n    def move_snake(self):\n        old_snake = self.snake.copy()\n        old_food = self.food.copy()\n        old_score = self.score\n        \n        head = self.snake[0].copy()\n        head[0] += self.direction[0]\n        head[1] += self.direction[1]\n        \n        if (head[0] \u0026lt; 0 or head[0] \u0026gt;= self.grid_width or \n            head[1] \u0026lt; 0 or head[1] \u0026gt;= self.grid_height or\n            head in self.snake):\n            self.game_over = True\n            self.needs_redraw = True\n            return\n        \n        self.snake.insert(0, head)\n        \n        if head == self.food:\n            self.score += 1\n            self.food = self.generate_food()\n        else:\n            self.snake.pop()\n        \n        if (self.snake != old_snake or self.food != old_food or \n            self.score != old_score):\n            self.needs_redraw = True\n    \n    def get_text_size(self, draw, text, font):\n        try:\n            bbox = draw.textbbox((0, 0), text, font=font)\n            return bbox[2] - bbox[0], bbox[3] - bbox[1]\n        except AttributeError:\n            try:\n                return draw.textsize(text, font=font)\n            except:\n                return len(text) * 8, 16\n    \n    def draw_centered_text(self, draw, text, y_pos, font, color):\n        text_width, text_height = self.get_text_size(draw, text, font)\n        x_pos = (240 - text_width) // 2\n        draw.text((x_pos, y_pos), text, font=font, fill=color)\n        return text_height\n    \n    def draw_game(self):\n        current_time = time.time()\n        \n        if (not self.needs_redraw or \n            current_time - self.last_display_update \u0026lt; self.display_interval):\n            return\n        \n        image = Image.new('RGB', (240, 240), (0, 0, 0))\n        draw = ImageDraw.Draw(image)\n        \n        # Draw game area border\n        draw.rectangle([0, 25, 239, 239], outline=(100, 100, 100), width=2)\n        \n        # Draw score at top\n        score_text = f\"Score: {self.score}\"\n        draw.text((5, 2), score_text, font=self.font_medium, fill=(255, 255, 255))\n        \n        # Draw snake\n        for i, segment in enumerate(self.snake):\n            x = segment[0] * self.grid_size\n            y = 25 + segment[1] * self.grid_size\n            \n            if i == 0:  # Head\n                draw.rectangle([x, y, x + self.grid_size - 1, y + self.grid_size - 1], \n                             fill=(0, 255, 0))\n                # Eyes\n                draw.rectangle([x + 2, y + 2, x + 4, y + 4], fill=(255, 255, 255))\n                draw.rectangle([x + 7, y + 2, x + 9, y + 4], fill=(255, 255, 255))\n            else:  # Body\n                draw.rectangle([x, y, x + self.grid_size - 1, y + self.grid_size - 1], \n                             fill=(0, 150, 0))\n                draw.rectangle([x + 2, y + 2, x + self.grid_size - 3, y + self.grid_size - 3], \n                             fill=(0, 200, 0))\n        \n        # Draw food\n        food_x = self.food[0] * self.grid_size\n        food_y = 25 + self.food[1] * self.grid_size\n        draw.ellipse([food_x, food_y, food_x + self.grid_size - 1, food_y + self.grid_size - 1], \n                    fill=(255, 0, 0))\n        draw.rectangle([food_x + 5, food_y - 2, food_x + 7, food_y + 2], fill=(139, 69, 19))\n        \n        self.display.image(image)\n        self.last_display_update = current_time\n        self.needs_redraw = False\n    \n    def draw_start_screen(self):\n        image = Image.new('RGB', (240, 240), (0, 0, 0))\n        draw = ImageDraw.Draw(image)\n        \n        self.draw_centered_text(draw, \"SNAKE\", 25, self.font_huge, (0, 255, 0))\n        \n        instructions = [\n            (\"Joystick = Move\", 90, (255, 255, 255)),\n            (\"Eat Red Apples\", 120, (255, 100, 100)),\n            (\"Don't Hit Walls!\", 150, (255, 255, 100)),\n            (\"Press A = Start\", 190, (100, 255, 100)),\n            (\"Press B = Main Menu\", 220, (100, 100, 255))\n        ]\n        \n        for text, y_pos, color in instructions:\n            self.draw_centered_text(draw, text, y_pos, self.font_medium, color)\n        \n        self.display.image(image)\n    \n    def draw_game_over_screen(self):\n        image = Image.new('RGB', (240, 240), (0, 0, 0))\n        draw = ImageDraw.Draw(image)\n        \n        self.draw_centered_text(draw, \"GAME\", 20, self.font_large, (255, 0, 0))\n        self.draw_centered_text(draw, \"OVER\", 60, self.font_large, (255, 0, 0))\n        \n        score_text = f\"Score: {self.score}\"\n        self.draw_centered_text(draw, score_text, 110, self.font_large, (255, 255, 0))\n        \n        if self.score \u0026gt;= 20:\n            rating, color = \"MASTER!\", (255, 215, 0)\n        elif self.score \u0026gt;= 10:\n            rating, color = \"Great!\", (192, 192, 192)\n        elif self.score \u0026gt;= 5:\n            rating, color = \"Good!\", (205, 127, 50)\n        else:\n            rating, color = \"Try Again!\", (255, 255, 255)\n        \n        self.draw_centered_text(draw, rating, 150, self.font_medium, color)\n        \n        self.draw_centered_text(draw, \"A = Play Again\", 190, self.font_medium, (0, 255, 0))\n        self.draw_centered_text(draw, \"B = Main Menu\", 220, self.font_medium, (0, 255, 0))\n        \n        self.display.image(image)\n    \n    def run(self):\n        game_state = \"start\"\n        last_state = None\n        \n        while True:\n            controls = self.read_controls_debounced()\n            \n            if game_state != last_state:\n                if game_state == \"start\":\n                    self.draw_start_screen()\n                elif game_state == \"game_over\":\n                    self.draw_game_over_screen()\n                last_state = game_state\n            \n            if game_state == \"start\":\n                if controls['button_a']:\n                    game_state = \"playing\"\n                    self.reset_game()\n                elif controls['button_b']:\n                    return  # Return to main menu\n                    \n            elif game_state == \"playing\":\n                self.update_direction(controls)\n                \n                if controls['button_b']:\n                    return  # Return to main menu\n                \n                self.move_snake()\n                \n                if self.game_over:\n                    game_state = \"game_over\"\n                \n                self.draw_game()\n                \n                speed = max(0.1, 0.3 - (self.score * 0.01))\n                time.sleep(speed)\n                \n            elif game_state == \"game_over\":\n                if controls['button_a']:\n                    game_state = \"playing\"\n                    self.reset_game()\n                elif controls['button_b']:\n                    return  # Return to main menu\n            \n            time.sleep(0.02)\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch4 class=\"wp-block-heading\"\u003eMake files executable:\u003c/h4\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003echmod +x /home/herman/*.py\n\u003c/pre\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 7: Test the System Manually\u003c/h2\u003e\n\u003cp\u003eBefore setting up auto-start, test everything works:\u003c/p\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003epython3 /home/herman/herman_entertainment_system.py\n\u003c/pre\u003e\n\u003cp\u003eYou should see:\u003c/p\u003e\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003e\n\u003cstrong\u003eHerman Entertainment System\u003c/strong\u003e welcome screen\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eWorking joystick controls\u003c/strong\u003e (GPIO 17=UP, 22=DOWN, 27=LEFT, 23=RIGHT)\u003c/li\u003e\n\u003cli\u003e\n\u003cstrong\u003eDisplay rotation=180\u003c/strong\u003e (correct orientation)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSnake game launches and returns to menu properly\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 8: Set Up Auto-Start Service\u003c/h2\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003esudo nano /etc/systemd/system/gaming-os.service\n\u003c/pre\u003e\n\u003cp\u003eAdd this content:\u003c/p\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003e[Unit]\nDescription=Herman Entertainment System\nAfter=multi-user.target\n\n[Service]\nType=simple\nUser=herman\nGroup=herman\nWorkingDirectory=/home/herman\nEnvironment=PATH=/usr/bin:/usr/local/bin\nEnvironment=PYTHONPATH=/usr/local/lib/python3.11/dist-packages\nExecStart=/usr/bin/python3 /home/herman/herman_entertainment_system.py\nRestart=always\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target\n\u003c/pre\u003e\n\u003cp class=\"wp-block-preformatted\"\u003eThen run:\u003c/p\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003e# Set correct permissions and enable the service\u003cbr\u003esudo chmod 644 /etc/systemd/system/gaming-os.service\u003cbr\u003esudo systemctl daemon-reload\u003cbr\u003esudo systemctl enable gaming-os.service\u003cbr\u003esudo systemctl start gaming-os.service\u003cbr\u003e\u003cbr\u003e# Check if it's working\u003cbr\u003esudo systemctl status gaming-os.service\u003c/pre\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 9: Final Reboot and Testing\u003c/h2\u003e\n\u003cpre class=\"wp-block-preformatted\"\u003esudo reboot\n\u003c/pre\u003e\n\u003cp\u003eAfter reboot, your Pi should automatically:\u003c/p\u003e\n\u003col class=\"wp-block-list\"\u003e\n\u003cli\u003e\u003cstrong\u003eBoot to the Herman Entertainment System welcome screen\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDisplay “Press A to Continue”\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eShow the game menu when you press Button A\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLaunch Snake game and return to menu properly\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eStep 10: Adding More Games\u003c/h2\u003e\n\u003cp\u003eTo add new games later:\u003c/p\u003e\n\u003col class=\"wp-block-list\"\u003e\n\u003cli\u003eSSH into your Pi\u003c/li\u003e\n\u003cli\u003eCopy Python game files to \u003ccode\u003e/home/herman/games/\u003c/code\u003e\n\u003c/li\u003e\n\u003cli\u003eThe system will automatically detect and list them\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThere is a full write up here to \u003ca href=\"https://ataary.com/herman-entertainment-system-adding-games/\" target=\"_blank\"\u003eadd games to the HES\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 class=\"wp-block-heading\"\u003eTroubleshooting Tips\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eIf display doesn’t work:\u003c/strong\u003e\u003c/p\u003e\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003eCheck TFT Bonnet is firmly seated on all 40 GPIO pins\u003c/li\u003e\n\u003cli\u003eVerify SPI is enabled: \u003ccode\u003elsmod | grep spi\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eIf controls are wrong:\u003c/strong\u003e\u003c/p\u003e\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003eThe code includes the correct GPIO mappings: 17=UP, 22=DOWN, 27=LEFT, 23=RIGHT\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eIf service won’t start:\u003c/strong\u003e\u003c/p\u003e\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003eCheck logs: \u003ccode\u003esudo journalctl -u gaming-os.service -f\u003c/code\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eIf WiFi doesn’t connect:\u003c/strong\u003e\u003c/p\u003e\n\u003cul class=\"wp-block-list\"\u003e\n\u003cli\u003eCheck: \u003ccode\u003esudo nmcli radio wifi\u003c/code\u003e (should show “enabled”)\u003c/li\u003e\n\u003cli\u003eReconfigure: \u003ccode\u003esudo raspi-config\u003c/code\u003e → Network Options → WiFi\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u0026nbsp;\u003c/p\u003e\n      ","metadata":{}},{"element_type":"image_url","content":"https://ataary.com/wp-content/uploads/2025/06/IMG_7258-scaled.jpeg","metadata":{"alt":"A low angle picture of Herman Entertainment system showing its port openings.","caption":"HES Raspi Zero 2w Handheld Port Cutouts"}},{"element_type":"image_url","content":"https://ataary.com/wp-content/uploads/2025/06/IMG_7012-scaled.jpg","metadata":{"alt":"A Raspi Zero 2w sandwiched between an Adafruit TFT Bonnet and a Waveshare Battery","caption":"A Raspi Zero 2w sandwiched between an Adafruit TFT Bonnet and a Waveshare Battery"}},{"element_type":"product","content":"https://www.adafruit.com/product/6008","metadata":{}},{"element_type":"product","content":"https://www.adafruit.com/product/4506","metadata":{}},{"element_type":"parts","content":"\n  \u003cdiv class=\"parts-details\"\u003e\n    \u003cdiv\u003e\n      \u003cspan class=\"parts-quantity\"\u003e1\u003c/span\u003e\u003cspan\u003e x \u003c/span\u003e\n        \u003ca href=\"https://www.waveshare.com/ups-hat-c.htm\" class=\"parts-name\" target=\"_blank\"\u003eWaveshare UPS HAT (C)\u003c/a\u003e\n      \u003cdiv class=\"parts-description\"\u003eWaveshare UPS HAT (C) for battery and power management.\u003c/div\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n  \u003cdiv class=\"parts-action\"\u003e\n  \u003cdiv\u003e\n      \u003ca href=\"https://www.waveshare.com/ups-hat-c.htm\" class=\"parts-url btn-primary\" target=\"_blank\"\u003eBuy Now\u003c/a\u003e\n\n  \u003c/div\u003e\n\u003c/div\u003e\n\n  \u003cdiv class=\"clearfix\"\u003e\u003c/div\u003e\n","metadata":{"name":"Waveshare UPS HAT (C)","url":"https://www.waveshare.com/ups-hat-c.htm","description":"Waveshare UPS HAT (C) for battery and power management.","quantity":"1"}},{"element_type":"button","content":"https://ataary.com/tag/herman/","metadata":{"class":"btn btn-large btn-block btn-primary","button_type":"primary","target":"_self","zip_folder":"false"}}]