Technical Deep Dive - griffingilreath/Punch-Card-Project GitHub Wiki

Punch Card Project: Technical Deep Dive

Introduction

This document provides a comprehensive technical overview of the Punch Card Project, focusing on the core logic, module interactions, hardware integration, and GUI control. Explore how the system manages data flow, decision-making processes, and the rationale behind key architectural decisions.


Project Overview and Architecture

The Punch Card Project simulates an IBM 80-column punch card system, integrating modern hardware (LED matrices) and a graphical user interface (GUI). The architecture is modular, designed for clarity, maintainability, and educational value.

High-Level Architecture Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Punch Card Project                      β”‚
β”‚                                                           β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ GUI Layer │───│ Display Logic │───│ Hardware Control  β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚       β”‚                 β”‚                     β”‚           β”‚
β”‚       β”‚                 β”‚                     β”‚           β”‚
β”‚       β–Ό                 β–Ό                     β–Ό           β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ User Data │───│ Core Logic    │───│ LED State Manager β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • GUI Layer: User interaction and visualization.
  • Display Logic: Translates internal data into visual representations.
  • Hardware Control: Interfaces with physical or simulated hardware.
  • Core Logic: Central decision-making and data processing.
  • LED State Manager: Manages LED states and animations.

Core Logic Modules

The core logic is encapsulated primarily within the src/core/ directory:

1. punch_card.py

Purpose:
Handles the encoding and decoding of messages into punch card patterns.

Key Logic:

  • Converts text input into IBM 80 column punch card encoding.
  • Manages internal representation of punch card data.

Decision Rationale:
Using IBM 80 column encoding provides historical accuracy and educational value.

Example Logic (simplified):

def encode_message(message):
    encoded = []
    for char in message.upper():
        pattern = IBM_80_COLUMN_ENCODING.get(char, [])
        encoded.append(pattern)
    return encoded

2. message_generator.py

Purpose:
Generates messages for display, including integration with external APIs (e.g., OpenAI).

Key Logic:

  • Fetches or generates messages based on user input or external sources.
  • Ensures messages are formatted correctly for punch card encoding.

Decision Rationale:
Integration with OpenAI provides dynamic, educational content generation.


3. database.py

Purpose:
Manages persistent storage of messages, configurations, and historical data.

Key Logic:

  • CRUD operations for message history and user settings.
    # CRUD = Create, Read, Update, Delete
    # These are the four basic operations for persistent storage systems
    
    def create_message(message_text, encoding_type):
        # Store a new message in the database with metadata
        message_id = generate_unique_id()
        timestamp = get_current_timestamp()
        store_in_database(message_id, message_text, encoding_type, timestamp)
        return message_id
        
    def read_message(message_id):
        # Retrieve a stored message by its ID
        return query_database_by_id(message_id)
        
    def update_message(message_id, new_text=None, new_encoding=None):
        # Modify an existing message
        existing_message = read_message(message_id)
        if new_text:
            existing_message.text = new_text
        if new_encoding:
            existing_message.encoding = new_encoding
        existing_message.last_modified = get_current_timestamp()
        save_to_database(existing_message)
        
    def delete_message(message_id):
        # Remove a message from the database
        remove_from_database(message_id)
    
  • Secure storage and retrieval of sensitive data (e.g., API keys).

Decision Rationale:
Centralized data management simplifies data handling and enhances security.


Display and GUI Control

The GUI and display logic are managed within the src/display/ directory:

1. gui_display.py

Purpose:
Provides an interactive graphical interface for users.

Key Logic:

  • Renders punch card visualizations and LED states.

    # GUI rendering logic for punch card visualization
    class PunchCardGrid(QWidget):
        def __init__(self, rows=12, cols=80, parent=None):
            super().__init__(parent)
            # Define grid dimensions based on IBM 80 column standard
            self.rows = rows  # 12 rows in standard IBM punch card
            self.cols = cols  # 80 columns in standard IBM punch card
            self.led_states = [[False for _ in range(cols)] for _ in range(rows)]
            self.led_size = 12  # Size of each LED visualization
            self.padding = 2    # Spacing between LEDs
            self.setMinimumSize(
                (self.led_size + self.padding) * cols,
                (self.led_size + self.padding) * rows
            )
            
        def paintEvent(self, event):
            """
            This critical method handles the actual drawing of LEDs.
            It's called automatically by Qt when the widget needs to be redrawn.
            """
            painter = QPainter(self)
            painter.setRenderHint(QPainter.Antialiasing)
            
            # Draw each LED in the grid
            for row in range(self.rows):
                for col in range(self.cols):
                    x = col * (self.led_size + self.padding)
                    y = row * (self.led_size + self.padding)
                    
                    # Select color based on LED state
                    if self.led_states[row][col]:
                        # LED is on - use highlight color
                        painter.setBrush(QBrush(QColor(255, 100, 100)))  # Light red for "on" state
                    else:
                        # LED is off - use background color
                        painter.setBrush(QBrush(QColor(50, 50, 50)))  # Dark gray for "off" state
                        
                    # Draw the LED as a rounded rectangle
                    painter.drawRoundedRect(x, y, self.led_size, self.led_size, 2, 2)
                    
        def updateLED(self, row, col, state):
            """
            Updates a single LED state and triggers a repaint of that area.
            This is more efficient than repainting the entire grid.
            """
            if 0 <= row < self.rows and 0 <= col < self.cols:
                self.led_states[row][col] = state
                # Calculate the area to update
                x = col * (self.led_size + self.padding)
                y = row * (self.led_size + self.padding)
                # Request repaint of just this LED
                self.update(QRect(x, y, self.led_size, self.led_size))
                
        def clear(self):
            """Reset all LEDs to off state"""
            self.led_states = [[False for _ in range(self.cols)] for _ in range(self.rows)]
            self.update()  # Repaint entire widget
    
  • Handles user interactions (button clicks, menu selections).

    # User interaction handling with signals/slots architecture
    class PunchCardApp(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("IBM 80 Column Punch Card Simulator")
            
            # Create central widget and layout
            central_widget = QWidget()
            self.setCentralWidget(central_widget)
            layout = QVBoxLayout(central_widget)
            
            # Create punch card grid visualization
            self.punch_card_grid = PunchCardGrid()
            layout.addWidget(self.punch_card_grid)
            
            # Create controls
            controls_layout = QHBoxLayout()
            
            # Add message input
            self.message_input = QLineEdit()
            self.message_input.setPlaceholderText("Enter message to display...")
            controls_layout.addWidget(self.message_input)
            
            # Add display button 
            self.display_button = QPushButton("Display Message")
            self.display_button.clicked.connect(self.on_display_message)
            controls_layout.addWidget(self.display_button)
            
            # Add clear button
            self.clear_button = QPushButton("Clear Display")
            self.clear_button.clicked.connect(self.on_clear_display)
            controls_layout.addWidget(self.clear_button)
            
            layout.addLayout(controls_layout)
            
            # Connect to data model for updates
            self.data_model = PunchCardDataModel.get_instance()
            self.data_model.register_observer(self)
            
        def on_display_message(self):
            """
            This method is called when the user clicks the Display Message button.
            It retrieves the message from the input field and passes it to the 
            message processor.
            """
            message = self.message_input.text().strip()
            if message:
                # Process the message via core logic
                self.display_processor.process_message(message)
                
        def on_clear_display(self):
            """Clear the visualization"""
            self.punch_card_grid.clear()
            
        def update_display(self, row, col, state):
            """
            This method is called by the observer pattern when the data model changes.
            It ensures the GUI stays synchronized with the underlying data.
            """
            self.punch_card_grid.updateLED(row, col, state)
    

Decision Rationale:
A GUI enhances usability, making the project accessible to a broader audience.


2. terminal_display.py

Purpose:
Offers a terminal-based visualization alternative.

Key Logic:

  • Uses curses for interactive terminal UI.

    # Terminal-based visualization using the curses library
    class CursesTerminalDisplay:
        def __init__(self):
            self.stdscr = None
            self.led_grid = None
            self.debug_window = None
            self.rows = 12  # Standard IBM 80 column punch card has 12 rows
            self.cols = 80  # 80 columns
            self.char_set = {
                'on': 'β–ˆ',   # Filled block for active LED
                'off': 'Β·'   # Small dot for inactive LED
            }
        
        def initialize(self):
            """
            Initialize the curses interface with split windows for LED display
            and debug messages.
            """
            # Initialize curses
            self.stdscr = curses.initscr()
            curses.start_color()
            curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)  # For active LEDs
            curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_BLACK)  # For inactive LEDs
            curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK)  # For debug messages
            curses.noecho()
            curses.cbreak()
            self.stdscr.keypad(True)
            
            # Get terminal dimensions
            max_y, max_x = self.stdscr.getmaxyx()
            
            # Create LED display window (upper half)
            led_height = min(self.rows + 2, max_y // 2)
            self.led_grid = curses.newwin(led_height, max_x, 0, 0)
            self.led_grid.box()
            self.led_grid.addstr(0, 2, " LED Display ", curses.A_BOLD)
            
            # Create debug window (lower half)
            debug_height = max_y - led_height
            self.debug_window = curses.newwin(debug_height, max_x, led_height, 0)
            self.debug_window.box()
            self.debug_window.addstr(0, 2, " Debug Messages ", curses.A_BOLD)
            
            # Initial refresh
            self.led_grid.refresh()
            self.debug_window.refresh()
            self.stdscr.refresh()
        
        def display_led_state(self, led_states):
            """
            Display the current state of all LEDs in the grid window.
            led_states is a 2D array representing the state of each LED.
            """
            if not self.led_grid:
                return
            
            # Display each LED
            for row in range(min(self.rows, self.led_grid.getmaxyx()[0] - 3)):
                for col in range(min(self.cols, self.led_grid.getmaxyx()[1] - 3)):
                    # Determine character and color based on LED state
                    if led_states[row][col]:
                        char = self.char_set['on']
                        attr = curses.color_pair(1)  # Active LED color
                    else:
                        char = self.char_set['off']
                        attr = curses.color_pair(2)  # Inactive LED color
                    
                    # Display the LED at the correct position
                    # Add 1 to row/col for the window border
                    self.led_grid.addstr(row + 1, col + 1, char, attr)
            
            self.led_grid.refresh()
        
        def log_message(self, message):
            """
            Add a debug message to the debug window.
            Messages are scrolled if necessary.
            """
            if not self.debug_window:
                return
            
            max_y, max_x = self.debug_window.getmaxyx()
            
            # Move existing content up one line if at bottom
            if self.next_log_line >= max_y - 2:
                # Scroll contents
                self.debug_window.move(1, 0)
                self.debug_window.deleteln()
                self.next_log_line = max_y - 3
            
            # Add new message
            self.debug_window.addstr(self.next_log_line, 1, message, curses.color_pair(3))
            self.next_log_line += 1
            self.debug_window.refresh()
        
        def cleanup(self):
            """
            Safely close curses interface when program ends.
            This is essential to restore terminal to proper state.
            """
            if self.stdscr:
                curses.nocbreak()
                self.stdscr.keypad(False)
                curses.echo()
                curses.endwin()
    
  • Provides fallback ASCII visualization when curses is unavailable.

    # Fallback console mode that works in any terminal
    class FallbackConsoleDisplay:
        def __init__(self):
            self.rows = 12
            self.cols = 80
            self.char_set = {
                'on': '#',   # Hash for active LED
                'off': '.'   # Period for inactive LED
            }
        
        def display_led_state(self, led_states):
            """
            Simple ASCII representation of LED states with row/column markers.
            This works in any terminal without requiring curses.
            """
            # Print column headers (in chunks of 5 for readability)
            print("    ", end="")
            for i in range(0, min(self.cols, 80), 5):
                print(f"{i:<5}", end="")
            print("\n   +", end="")
            print("-" * (min(self.cols, 80) + 2), end="")
            print("+")
            
            # Print each row with row number
            for row in range(self.rows):
                print(f"{row:2d} | ", end="")
                for col in range(min(self.cols, 80)):
                    if led_states[row][col]:
                        print(self.char_set['on'], end=" ")
                    else:
                        print(self.char_set['off'], end=" ")
                print("|")
            
            # Print bottom border
            print("   +", end="")
            print("-" * (min(self.cols, 80) + 2), end="")
            print("+")
        
        def log_message(self, message):
            """
            Simple logging for debug messages in fallback mode.
            """
            print(f"[DEBUG] {message}")
    

Decision Rationale:
Ensures compatibility across diverse environments, including headless systems.


3. display_adapter.py

Purpose:
Acts as an intermediary between core logic and display modules.

Key Logic:

  • Translates internal punch card data into visual formats.

    # Display adapter connects the core punch card logic to display implementations
    class DisplayAdapter:
        def __init__(self, display_type="auto"):
            self.display = None
            self.display_type = display_type
            self.initialize_display()
            
        def initialize_display(self):
            """
            Select and initialize the appropriate display based on:
            1. User preference
            2. System capabilities
            3. Terminal size
            """
            if self.display_type == "gui" or (self.display_type == "auto" and self._can_use_gui()):
                # Use the GUI display if requested or if auto-detection permits
                try:
                    from gui_display import PunchCardApp
                    self.display = PunchCardApp()
                    return
                except ImportError:
                    print("GUI libraries not available, falling back to terminal mode")
                    
            # If GUI is not used, try curses for terminal UI
            if self.display_type == "terminal" or self.display_type == "auto":
                try:
                    from terminal_display import CursesTerminalDisplay
                    terminal_size = self._get_terminal_size()
                    
                    # Check if terminal is large enough for curses interface
                    if terminal_size and terminal_size[0] >= 12 and terminal_size[1] >= 40:
                        self.display = CursesTerminalDisplay()
                        self.display.initialize()
                        return
                except (ImportError, Exception) as e:
                    # Curses might not be available or initialization could fail
                    print(f"Curses terminal display not available: {e}")
            
            # Final fallback: simple console output that works everywhere
            from terminal_display import FallbackConsoleDisplay
            self.display = FallbackConsoleDisplay()
            print("Using fallback console display mode")
            
        def _can_use_gui(self):
            """Check if GUI libraries are available and display is possible"""
            try:
                # Try to import PyQt to check availability
                import PyQt6
                
                # Check if we have a display (not running in headless environment)
                import os
                return os.environ.get('DISPLAY') is not None or os.environ.get('WAYLAND_DISPLAY') is not None
            except ImportError:
                return False
                
        def _get_terminal_size(self):
            """Get terminal dimensions in rows, columns"""
            try:
                import shutil
                return shutil.get_terminal_size()
            except (ImportError, AttributeError):
                try:
                    # Fallback method using stty command
                    import subprocess
                    size = subprocess.check_output(['stty', 'size']).decode().split()
                    return (int(size[0]), int(size[1]))
                except (subprocess.SubprocessError, ValueError, IndexError):
                    return None
    
  • Manages synchronization between GUI and hardware states.

    class DisplaySynchronizer:
        """
        Ensures that all display outputs (GUI, terminal, hardware) 
        stay synchronized with the internal data model.
        """
        def __init__(self, data_model, displays=None):
            self.data_model = data_model
            self.displays = displays or []
            self.hardware_controller = None
            
            # Register as observer of data model to receive updates
            self.data_model.register_observer(self)
            
        def add_display(self, display):
            """Add a display to be synchronized"""
            if display not in self.displays:
                self.displays.append(display)
                
        def set_hardware_controller(self, controller):
            """Connect a hardware controller for physical LED updates"""
            self.hardware_controller = controller
            
        def update(self, row, col, state):
            """
            Called when data model changes - propagate changes to all displays
            and hardware controller.
            """
            # Update software displays
            for display in self.displays:
                if hasattr(display, 'updateLED'):
                    display.updateLED(row, col, state)
                elif hasattr(display, 'display_led_state'):
                    # Terminal displays need the entire grid
                    display.display_led_state(self.data_model.get_grid())
            
            # Update hardware if available
            if self.hardware_controller:
                self.hardware_controller.set_led(row, col, state)
                
        def display_message(self, message, duration=5.0):
            """
            Handle high-level message display with timing controls.
            This encapsulates the entire display sequence.
            """
            # Clear any previous message
            self.clear_display()
            
            # Encode message to punch card format
            from punch_card import encode_message
            encoded_message = encode_message(message)
            
            # Update data model with encoded message
            # This will trigger the observer pattern and update all displays
            for col_idx, char_pattern in enumerate(encoded_message):
                if col_idx >= self.data_model.cols:
                    break  # Respect column limit
                    
                for row_idx in char_pattern:
                    self.data_model.set_punch(row_idx, col_idx, True)
            
            # If requested, schedule clearing after duration
            if duration > 0:
                import threading
                threading.Timer(duration, self.clear_display).start()
                
        def clear_display(self):
            """Clear all displays and hardware"""
            self.data_model.clear()
            
            # Hardware may need direct clearing too
            if self.hardware_controller:
                self.hardware_controller.clear_all()
    

Decision Rationale:
Separating display logic from core logic improves modularity and maintainability.


Hardware Integration and LED Control

Hardware integration is managed through dedicated modules:

1. hardware_controller.py

Purpose:
Abstracts hardware interactions, supporting both simulated and physical hardware.

Key Logic:

  • Provides a unified interface for hardware operations.
  • Supports Raspberry Pi GPIO integration for physical LED matrices.

Decision Rationale:
Abstracting hardware interactions allows easy switching between simulated and real hardware, facilitating development and testing.


2. led_state_manager.py

Purpose:
Manages the state of LEDs, including animations and patterns.

Key Logic:

  • Maintains an internal representation of LED states.
  • Provides methods to update, animate, and reset LED states.

Decision Rationale:
Centralized LED state management simplifies synchronization between software logic and hardware display.

Example Logic (simplified):

def set_led_state(x, y, state):
    led_grid[x][y] = state
    hardware_controller.update_led(x, y, state)

Data Flow and Module Interaction

Data Flow Diagram

User Input (GUI/Terminal)
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Message Generator β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Punch Card      β”‚
β”‚   (Core Logic)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Display Adapter   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
   β–Ό              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ GUI     β”‚   β”‚ Hardware  β”‚
β”‚ Display β”‚   β”‚ Controllerβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚ LED State     β”‚
          β”‚ Manager       β”‚
          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Explanation:

  • User input triggers message generation.
  • Messages are encoded into punch card patterns.
  • Display adapter translates patterns for GUI and hardware.
  • GUI displays visual representation; hardware controller updates physical LEDs.
  • LED state manager maintains synchronization between software and hardware states.

Message Translation and LED Control

Understanding how messages are processed and displayed is central to the Punch Card Project. This section details the character-by-character processing flow and how the system translates text into visual representations across both GUI and hardware interfaces.

Character-to-LED Transformation Process

The translation of a message into LED states follows a precise sequence:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 β”‚   β”‚                β”‚   β”‚                  β”‚
β”‚  Input Message  │──►│  Encode Each   │──►│  Generate Punch  β”‚
β”‚  "HELLO WORLD"  β”‚   β”‚  Character     β”‚   β”‚  Card Pattern    β”‚
β”‚                 β”‚   β”‚                β”‚   β”‚                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                     β”‚
                                                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 β”‚   β”‚                β”‚   β”‚                  β”‚
β”‚  Update Display │◄──│  Map to Grid   │◄──│  Translate to    β”‚
β”‚  (GUI & LEDs)   β”‚   β”‚  Coordinates   β”‚   β”‚  LED Matrix      β”‚
β”‚                 β”‚   β”‚                β”‚   β”‚                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

1. Character Encoding (IBM 80 Column Punch Card Format)

Each character in the input message is individually processed through the IBM 80 column encoding system:

def process_message(message_text):
    encoded_message = []
    for char in message_text:
        # Convert each character to its punch card representation
        punch_pattern = encode_character(char)
        encoded_message.append(punch_pattern)
    return encoded_message

For example, the letter 'H' in IBM 80 column encoding is represented by punches in rows 12 and 8:

Column for 'H':
Row 12: β–  (punched)
Row 11: β–‘ (not punched)
Row 0:  β–‘ (not punched)
Row 1:  β–‘ (not punched)
Row 2:  β–‘ (not punched)
Row 3:  β–‘ (not punched)
Row 4:  β–‘ (not punched)
Row 5:  β–‘ (not punched)
Row 6:  β–‘ (not punched)
Row 7:  β–‘ (not punched)
Row 8:  β–  (punched)
Row 9:  β–‘ (not punched)

2. Matrix Mapping

After encoding, each character's punch pattern is mapped to LED grid coordinates:

def map_to_grid(encoded_characters):
    led_grid = initialize_empty_grid()
    for col_idx, character in enumerate(encoded_characters):
        if col_idx >= MAX_COLS:
            break  # Respect the 80-column limit
        
        for punch in character:
            row = punch  # The row number where the punch occurs
            led_grid[row][col_idx] = True  # Set LED to ON
    
    return led_grid

3. Shared Data Model

A critical design decision was to implement a shared data model for both the GUI and physical LED hardware. This approach ensures consistency between visual representations:

class PunchCardDataModel:
    def __init__(self):
        self.grid = [[False for _ in range(80)] for _ in range(12)]
        self.observers = []  # Both GUI and hardware controllers register here
    
    def set_punch(self, row, col, state):
        self.grid[row][col] = state
        self.notify_observers(row, col, state)
    
    def notify_observers(self, row, col, state):
        for observer in self.observers:
            observer.update_display(row, col, state)

Parallel Display Systems: GUI vs. Hardware LEDs

The project maintains two parallel display systems that share core data but differ in implementation:

GUI Display System

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              GUI Display               β”‚
β”‚                                        β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚ β”‚ PyQt Widgets β”‚    β”‚ Punch Card    β”‚  β”‚
β”‚ β”‚ (Buttons,    β”‚    β”‚ Visualization β”‚  β”‚
β”‚ β”‚  Menus)      β”‚    β”‚ (Grid of LEDs)β”‚  β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚          β”‚                 β–²           β”‚
β”‚          β”‚                 β”‚           β”‚
β”‚          β–Ό                 β”‚           β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚ β”‚     Shared Data Model Observer   β”‚   β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The GUI system:

  • Uses PyQt widgets to create a visual representation
  • Renders LED states as colored circles/squares in a grid
  • Updates in real-time when the shared data model changes
  • Provides visual feedback and interactive controls

Hardware LED System

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Hardware Display             β”‚
β”‚                                        β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚ β”‚ Hardware     β”‚    β”‚ LED Matrix    β”‚  β”‚
β”‚ β”‚ Abstraction  β”‚    β”‚ (Physical or  β”‚  β”‚
β”‚ β”‚ Layer        β”‚    β”‚  Simulated)   β”‚  β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚          β”‚                 β–²           β”‚
β”‚          β”‚                 β”‚           β”‚
β”‚          β–Ό                 β”‚           β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚ β”‚     Shared Data Model Observer   β”‚   β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The hardware system:

  • Interfaces with physical or simulated LED hardware
  • Translates logical LED states into hardware signals
  • Uses hardware-specific controllers (GPIO for Raspberry Pi)
  • Implements timing controls for hardware limitations

Synchronization Between Systems

The two display systems remain synchronized through the Observer pattern:

# When a message is processed in the core logic:
def display_message(message):
    encoded = encode_message(message)
    for col_idx, char_pattern in enumerate(encoded):
        for row_idx in char_pattern:
            # This updates both GUI and hardware simultaneously
            punch_card_data_model.set_punch(row_idx, col_idx, True)
    
    # Schedule clearing after display time
    schedule_clear(display_duration)

This approach provides several advantages:

  1. Consistency: Both displays always show identical information
  2. Modularity: Either display can be used independently
  3. Testability: The system can be tested with simulated hardware
  4. Flexibility: New display types can be added by implementing the observer interface

Implementation Differences

While sharing the same data model, the implementations differ in key ways:

GUI Implementation:

  • Uses PyQt's signal/slot mechanism for updates
  • Operates in the application's main thread
  • Updates occur immediately
  • Rendering is handled by the GUI toolkit

Hardware Implementation:

  • May use separate threads for hardware communication
  • Implements timing controls to account for hardware limitations
  • Includes hardware-specific error handling
  • Provides fallback mechanisms when hardware is unavailable

Decision Rationale

The shared data model with separate implementations was chosen for several reasons:

  1. Separation of Concerns: Display logic is separated from hardware control
  2. Maintainability: GUI can be updated without affecting hardware control
  3. Flexibility: Supports both simulated and physical hardware
  4. Educational Value: Clearly demonstrates how different interfaces can present the same data

Letter-by-Letter Processing Flow

To better understand how the system processes text, let's examine the journey of a single message from input to display across both digital and physical interfaces.

Message Processing Timeline

The system processes messages in distinct phases that happen in sequence:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Input Phase β”‚β†’ β”‚ Encode Phaseβ”‚β†’ β”‚ Buffer Phaseβ”‚β†’ β”‚Display Phaseβ”‚β†’ β”‚ Clear Phase β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     ~1ms             ~5ms            ~2ms          500-2000ms         ~10ms
  • Input Phase: The message is received from user input or API
  • Encode Phase: Each character is converted to its punch card pattern
  • Buffer Phase: The encoded message is prepared for visualization
  • Display Phase: LEDs are activated according to the pattern (longest phase)
  • Clear Phase: All LEDs are deactivated after the display period

Single Letter Journey

Let's follow the letter 'A' from input through the entire system:

                                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                 β”‚          INPUT: LETTER 'A'        β”‚
                                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                               β”‚
                                               β–Ό
                               β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                               β”‚         ENCODING LOOKUP           β”‚
                               β”‚                                   β”‚
                               β”‚  'A' β†’ IBM 80 Column Format       β”‚
                               β”‚     β†’ Punches in rows 12,1        β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                               β”‚
                                               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     GUI PROCESSING BRANCH    β”‚     β”‚        SHARED DATA MODEL          β”‚    β”‚  HARDWARE PROCESSING BRANCH  β”‚
β”‚                              β”‚     β”‚                                   β”‚    β”‚                              β”‚
β”‚ β€’ Convert to QRect           β”‚     β”‚ β€’ Update internal grid            β”‚    β”‚ β€’ Convert to physical pins   β”‚
β”‚ β€’ Calculate pixel positions  │◄───── β€’ Notify all observers            β”œβ”€β”€β”€β–Ίβ”‚ β€’ Apply hardware timing      β”‚
β”‚ β€’ Update Qt widget           β”‚     β”‚ β€’ Maintain grid state             β”‚    β”‚ β€’ Send signals to LED matrix β”‚
β”‚ β€’ Trigger repaint            β”‚     β”‚                                   β”‚    β”‚ β€’ Handle hardware errors     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚                                                                              β”‚
            β–Ό                                                                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      DISPLAY RENDERING       β”‚                                        β”‚      HARDWARE CONTROL        β”‚
β”‚                              β”‚                                        β”‚                              β”‚
β”‚ β€’ Draw rectangles for LEDs   β”‚                                        β”‚ β€’ GPIO pin activation        β”‚
β”‚ β€’ Highlight active LEDs      β”‚                                        β”‚ β€’ Power management           β”‚
β”‚ β€’ Show in GUI window         β”‚                                        β”‚ β€’ Physical LED illumination  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Detailed Character-to-Interface Translation

This diagram shows the exact transformation process of a character into both GUI elements and hardware signals:

Character 'A'
    β”‚
    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Row 12: β–  (on)  β”‚
β”‚ Row 11: β–‘ (off) β”‚
β”‚ Row 0:  β–‘ (off) β”‚
β”‚ Row 1:  β–  (on)  β”‚
β”‚ Row 2:  β–‘ (off) β”‚
β”‚ Row 3:  β–‘ (off) β”‚
β”‚ Row 4:  β–‘ (off) β”‚
β”‚ Row 5:  β–‘ (off) β”‚
β”‚ Row 6:  β–‘ (off) β”‚
β”‚ Row 7:  β–‘ (off) β”‚
β”‚ Row 8:  β–‘ (off) β”‚
β”‚ Row 9:  β–‘ (off) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”‚
    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β–Ό                β–Ό                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ GUI Display β”‚  β”‚Terminal Viewβ”‚  β”‚Hardware Pinsβ”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β–“β–“          β”‚  β”‚ β–ˆβ–ˆ          β”‚  β”‚ HIGH        β”‚  Row 12
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 11
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 0
β”‚ β–“β–“          β”‚  β”‚ β–ˆβ–ˆ          β”‚  β”‚ HIGH        β”‚  Row 1
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 2
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 3
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 4
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 5
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 6
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 7
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 8
β”‚             β”‚  β”‚             β”‚  β”‚ LOW         β”‚  Row 9
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Multi-Character Processing Flow

When processing a complete message like "HELLO", each character is processed sequentially. The following diagram shows how characters are placed in the grid:

                    GRID COLUMNS (0-79)
       0        1        2        3        4   ...
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
 12  β”‚   β–     β”‚        β”‚        β”‚        β”‚        β”‚ Row 12
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
 11  β”‚        β”‚        β”‚        β”‚        β”‚        β”‚ Row 11
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  0  β”‚        β”‚        β”‚        β”‚        β”‚   β–     β”‚ Row 0
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  1  β”‚   β–     β”‚        β”‚        β”‚        β”‚        β”‚ Row 1
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  2  β”‚        β”‚        β”‚        β”‚        β”‚        β”‚ Row 2
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  3  β”‚        β”‚        β”‚        β”‚        β”‚        β”‚ Row 3
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  4  β”‚        β”‚   β–     β”‚        β”‚        β”‚        β”‚ Row 4
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  5  β”‚        β”‚   β–     β”‚   β–     β”‚   β–     β”‚        β”‚ Row 5
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  6  β”‚        β”‚        β”‚        β”‚        β”‚        β”‚ Row 6
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  7  β”‚        β”‚        β”‚        β”‚        β”‚        β”‚ Row 7
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  8  β”‚        β”‚        β”‚        β”‚        β”‚   β–     β”‚ Row 8
     β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
  9  β”‚        β”‚        β”‚        β”‚        β”‚        β”‚ Row 9
     β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         H        E        L        L        O

Each character occupies one column in the grid. The project uses the Observer pattern to ensure that whenever a cell in this grid is updated, all displays (GUI, terminal, and hardware) are notified of the change.

Data Flow Sequence Diagram

This sequence diagram illustrates the flow of data when a user enters a message:

β”Œβ”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚User β”‚     β”‚GUI/Consoleβ”‚     β”‚Core Logicβ”‚      β”‚Data Modelβ”‚          β”‚GUI Viewβ”‚    β”‚Hardwareβ”‚
β””β”€β”€β”¬β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”¬β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚ Input "HELLO"β”‚                β”‚                 β”‚                     β”‚             β”‚
   │─────────────>β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚ process_messageβ”‚                 β”‚                     β”‚             β”‚
   β”‚              │───────────────>β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚ encode_message()β”‚                     β”‚             β”‚
   β”‚              β”‚                │────────────────>β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚ set_punch(12,0,true)β”‚             β”‚
   β”‚              β”‚                β”‚                 │────────────────────>β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚  set_punch(1,0,true)β”‚             β”‚ 
   β”‚              β”‚                β”‚                 │────────────────────>β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚   notify_observers()β”‚             β”‚
   β”‚              β”‚                β”‚                 │────────────────────>β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚ update_led()β”‚
   β”‚              β”‚                β”‚                 β”‚                     │────────────>β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚<────────────│
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚ Return          β”‚                     β”‚             β”‚
   β”‚              β”‚<───────────────│                 β”‚                     β”‚             β”‚
   β”‚              β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚ Show Result  β”‚                β”‚                 β”‚                     β”‚             β”‚
   β”‚<─────────────│                β”‚                 β”‚                     β”‚             β”‚
β”Œβ”€β”€β”΄β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”
β”‚User β”‚     β”‚GUI/Consoleβ”‚     β”‚Core Logicβ”‚      β”‚Data Modelβ”‚          β”‚GUI Viewβ”‚    β”‚Hardwareβ”‚
β””β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Simple Explanation: Letter-by-Letter Processing

In simple terms, here's what happens when you enter a message:

  1. Split Into Characters: Your message "HELLO" is broken into individual characters: 'H', 'E', 'L', 'L', 'O'.

  2. Look Up Punch Patterns: Each character is converted to its punch card pattern:

    • 'H' has punches in rows 12 and 8
    • 'E' has punches in rows 12 and 5
    • 'L' has punches in rows 11 and 3
    • 'L' has punches in rows 11 and 3
    • 'O' has punches in rows 11 and 6
  3. Update Central Grid: These patterns are placed into a central grid (like a spreadsheet), with each character taking one column.

  4. Notify All Displays: The central grid tells all displays (GUI, terminal, and hardware) what changed.

  5. Display-Specific Translation:

    • GUI: Converts grid positions to colored rectangles on screen
    • Terminal: Converts grid positions to ASCII characters
    • Hardware: Converts grid positions to electrical signals
  6. Synchronized Updates: All displays update at the same time, showing the same pattern.

  7. Timing Control: After a set time, all displays clear simultaneously.

This approach ensures that whether you're looking at the screen or the physical LED matrix, you see exactly the same pattern. It's like having multiple TV screens showing the same channel - the content is the same, just displayed on different devices.

Shared Data Model Observer Pattern

The Punch Card Project utilizes the Observer design pattern as a fundamental architectural component to ensure synchronization between the internal data representation and multiple display interfaces. This pattern is critical for maintaining consistency across GUI, terminal, and hardware displays.

Observer Pattern Implementation

In the Punch Card Project, the observer pattern follows this structure:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  PunchCardDataModel    β”‚
β”‚  (Subject/Observable)  β”‚
β”‚                        β”‚
β”‚  - grid: 2D array      β”‚
β”‚  - observers: list     β”‚
β”‚                        β”‚
β”‚  + register_observer() β”‚
β”‚  + remove_observer()   β”‚
β”‚  + notify_observers()  β”‚
β”‚  + set_punch()         β”‚
β”‚  + clear()             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
            β”‚ notifies
            β”‚
            β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Observers                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            β”‚
      β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
      β”‚           β”‚             β”‚             β”‚
      β–Ό           β–Ό             β–Ό             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ GUI      β”‚ β”‚ Terminal β”‚ β”‚ Hardware β”‚ β”‚ Other      β”‚
β”‚ Display  β”‚ β”‚ Display  β”‚ β”‚ Control  β”‚ β”‚ Observers  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Core Implementation Details

The implementation includes:

class PunchCardDataModel:
    """Central data model that manages the state of each LED in the grid."""
    
    # Singleton instance
    _instance = None
    
    @classmethod
    def get_instance(cls):
        """Get the singleton instance of the data model."""
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance
    
    def __init__(self):
        """Initialize the data model with an empty grid and no observers."""
        # Prevent multiple instantiation
        if PunchCardDataModel._instance is not None:
            raise RuntimeError("Use get_instance() to get the singleton instance")
            
        # 12 rows (IBM punch card standard) Γ— 80 columns
        self.rows = 12
        self.cols = 80
        
        # Initialize grid with all LEDs off (False)
        self.grid = [[False for _ in range(self.cols)] for _ in range(self.rows)]
        
        # List to store all observers that need to be notified of changes
        self.observers = []
        
        PunchCardDataModel._instance = self
    
    def register_observer(self, observer):
        """
        Register an observer to be notified of changes to the data model.
        
        Args:
            observer: An object implementing the update_display(row, col, state) method
        """
        if observer not in self.observers:
            self.observers.append(observer)
            
    def remove_observer(self, observer):
        """Remove an observer from the notification list."""
        if observer in self.observers:
            self.observers.remove(observer)
            
    def notify_observers(self, row, col, state):
        """
        Notify all registered observers about a state change.
        
        Args:
            row: The row index of the changed LED
            col: The column index of the changed LED
            state: Boolean indicating whether the LED is on (True) or off (False)
        """
        for observer in self.observers:
            # Call the observer's update method
            observer.update_display(row, col, state)
    
    def set_punch(self, row, col, state):
        """
        Set the state of a specific grid position and notify observers.
        
        Args:
            row: Row index (0-11)
            col: Column index (0-79)
            state: Boolean punch state (True = hole punched/LED on)
        """
        # Validate indices
        if 0 <= row < self.rows and 0 <= col < self.cols:
            # Only update and notify if there's an actual change
            if self.grid[row][col] != state:
                self.grid[row][col] = state
                self.notify_observers(row, col, state)
                
    def get_grid(self):
        """Return a copy of the current grid state."""
        return [row[:] for row in self.grid]  # Return a deep copy
    
    def clear(self):
        """Reset all positions to False (no punches/LEDs off)."""
        for row in range(self.rows):
            for col in range(self.cols):
                self.set_punch(row, col, False)

Observer Interface

Each observer implements an update method (named consistently) that processes changes:

# In GUI observer
def update_display(self, row, col, state):
    """Update the LED state in the GUI."""
    self.punch_card_grid.updateLED(row, col, state)
    
# In Terminal observer  
def update_display(self, row, col, state):
    """Update the LED state in the terminal display."""
    self.grid[row][col] = state
    self._redraw_display()
    
# In Hardware controller
def update_display(self, row, col, state):
    """Update physical LED state."""
    self.set_led(row, col, state)
    # Hardware-specific timing control
    if self.enforce_timing_constraints:
        time.sleep(self.led_update_delay)

Detailed Data Flow Diagram

The following diagram illustrates the complete flow of data through the observer pattern:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    User Interface   β”‚
β”‚  (GUI or Terminal)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”‚ User enters message
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Message Encoder   β”‚
β”‚  Converts to punch  β”‚
β”‚  card patterns      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”‚ Encoded message
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                                                             β”‚
β”‚                   PunchCardDataModel                        β”‚
β”‚                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  set_punch  β”‚  β”‚ notify_      β”‚   β”‚ State Grid        β”‚  β”‚
β”‚  β”‚  (row,col,  │─►│ observers    β”‚   β”‚ β”Œβ”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β” β”‚  β”‚
β”‚  β”‚   state)    β”‚  β”‚ (row,col,    β”‚   β”‚ β”‚ F β”‚ T β”‚ F β”‚ F β”‚ β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  state)      β”‚   β”‚ β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€ β”‚  β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚ β”‚ F β”‚ F β”‚ T β”‚ F β”‚ β”‚  β”‚
β”‚                          β”‚           β”‚ β”œβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”Όβ”€β”€β”€β”€ β”‚  β”‚
β”‚                          β”‚           β”‚ β”‚ T β”‚ F β”‚ F β”‚ T β”‚ β”‚  β”‚
β”‚                          β”‚           β”‚ β””β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”˜ β”‚  β”‚
β”‚                          β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β”‚ Broadcasts state changes
                          β”‚
                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                  β”‚               β”‚                  β”‚
                  β–Ό               β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   GUI Observer      β”‚ β”‚ Terminal         β”‚ β”‚ Hardware           β”‚
β”‚                     β”‚ β”‚ Observer         β”‚ β”‚ Observer           β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚ β”‚ update_display  β”‚ β”‚ β”‚ β”‚update_displayβ”‚ β”‚ β”‚ β”‚ update_display  β”‚β”‚
β”‚ β”‚ (row,col,state) β”‚ β”‚ β”‚ β”‚(row,col,stateβ”‚ β”‚ β”‚ β”‚ (row,col,state) β”‚β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚          β”‚          β”‚ β”‚        β”‚         β”‚ β”‚        β”‚           β”‚
β”‚          β–Ό          β”‚ β”‚        β–Ό         β”‚ β”‚        β–Ό           β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚ β”‚ Update UI       β”‚ β”‚ β”‚ β”‚Update       β”‚  β”‚ β”‚ β”‚ Send signal to  β”‚β”‚
β”‚ β”‚ - Change rect   β”‚ β”‚ β”‚ β”‚terminal     β”‚  β”‚ β”‚ β”‚ hardware:       β”‚β”‚
β”‚ β”‚   color         β”‚ β”‚ β”‚ β”‚- ASCII charsβ”‚  β”‚ β”‚ β”‚ - GPIO pin      β”‚β”‚
β”‚ β”‚ - Repaint regionβ”‚ β”‚ β”‚ β”‚- ASCII charsβ”‚  β”‚ β”‚ β”‚ - LED matrix    β”‚β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                     β”‚                     β”‚
          β–Ό                     β–Ό                     β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ User sees       β”‚    β”‚ User sees       β”‚   β”‚ Physical LEDs   β”‚
β”‚ GUI display     β”‚    β”‚ terminal        β”‚   β”‚ light up        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Advantages of the Observer Pattern

The observer pattern provides several critical benefits to the Punch Card Project:

  1. Decoupling: The data model is completely decoupled from the display implementations, making it easier to add new display types without changing the core logic.

  2. Consistency: All displays are guaranteed to show the same data because they all respond to the same change notifications.

  3. Synchronization: Updates to multiple displays occur in a coordinated manner, with each observer being notified of the same state change.

  4. Scalability: New observers (e.g., a web interface or network display) can be added by simply implementing the observer interface and registering with the data model.

  5. Selective Updates: Only changed cells trigger updates, reducing processing overhead compared to refreshing entire displays.

Real-Time Example

When a user enters "HELLO" as input:

  1. Initialization:

    • The data model is created with all grid positions set to False.
    • GUI, terminal, and hardware displays register as observers.
  2. Encoding Process:

    • Each character is encoded into its punch card pattern (specific rows punched).
    • For 'H', rows 12 and 8 need to be punched in column 0.
  3. Data Model Update:

    • Core logic calls data_model.set_punch(12, 0, True).
    • The data model updates its internal grid state.
    • The data model calls notify_observers(12, 0, True).
  4. Observer Notifications:

    • Each registered observer's update_display(12, 0, True) method is called.
    • The GUI observer updates the visual rectangle at position (12,0).
    • The terminal observer updates its ASCII representation at position (12,0).
    • The hardware observer sends a signal to turn on the physical LED at position (12,0).
  5. Consistent Display:

    • All three displays now show an active state at the same position (12,0).
    • The process repeats for each punched position in the message.

This synchronized update process ensures that all representations of the punch card - visual, terminal, and physical - remain perfectly aligned throughout the entire message display process.


Punch Card Encoding and Decoding

This section provides a detailed explanation of how the Punch Card Project implements encoding and decoding of characters using the IBM 80 column punch card format.

IBM 80 Column Punch Card Encoding

The IBM 80 column punch card system uses a specific encoding scheme where characters are represented by patterns of punches in a 12-row grid:

                             Rows in IBM 80 Column Punch Card
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Row 12 (Y): β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Zone
β”‚ Row 11 (X): β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Zone
β”‚ Row 0:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Zone
β”‚ Row 1:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 2:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 3:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 4:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 5:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 6:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 7:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 8:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚ Row 9:      β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ ... β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β—‹ β”‚ Digit
β”‚             1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 ... 6 7 8 9 0         β”‚
β”‚                                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                            Columns (1-80)

In this system, the rows are organized in a specific order:

  • Zone Rows: Rows 12, 11, and 0 are called "zone" rows
  • Digit Rows: Rows 1-9 are called "digit" rows

Characters are encoded using combinations of punches in these rows, following a systematic pattern:

Character Encoding Patterns

The encoding follows these general rules:

  1. Letters A-I: Zone punch in row 12 + digit punch in rows 1-9 respectively

    • A = 12 + 1
    • B = 12 + 2
    • ...
    • I = 12 + 9
  2. Letters J-R: Zone punch in row 11 + digit punch in rows 1-9 respectively

    • J = 11 + 1
    • K = 11 + 2
    • ...
    • R = 11 + 9
  3. Letters S-Z: Zone punch in row 0 + digit punch in rows 2-9 respectively

    • S = 0 + 2
    • T = 0 + 3
    • ...
    • Z = 0 + 9
  4. Numbers 0-9: Single punch in row 0-9 respectively

    • 0 = row 0 only
    • 1 = row 1 only
    • ...
    • 9 = row 9 only
  5. Special Characters: Various combinations of punches

    • Space = no punches
    • Period (.) = 12 + 3 + 8
    • Comma (,) = 0 + 3 + 8
    • Dash (-) = 11 only
    • etc.

Character Mapping Implementation

In the Punch Card Project, the character mapping is implemented using a dictionary that maps characters to their punch patterns. This is the core of the encoding system:

# Hollerith/EBCDIC encoding mapping implemented as a Python dictionary
CHAR_MAPPING = {
    # A-I: zone punch 12 + digit punches 1-9
    'A': [1,0,0,1,0,0,0,0,0,0,0,0],  # 12,1 (index 0=row12, index 3=row1)
    'B': [1,0,0,0,1,0,0,0,0,0,0,0],  # 12,2
    'C': [1,0,0,0,0,1,0,0,0,0,0,0],  # 12,3
    # ...more characters...
}

Each character maps to a 12-element array representing rows 12, 11, 0-9, where:

  • A 1 means a punch is present in that row
  • A 0 means no punch in that row

For example, the letter 'A' has punches in row 12 and row 1, so its array is [1,0,0,1,0,0,0,0,0,0,0,0].

Encoding Process

The encoding process involves converting a text message into a series of punch patterns, with each character occupying one column on the punch card. Here's the detailed process:

def encode_message(message: str, max_width: int = 80) -> List[List[bool]]:
    """
    Encode a message into punch card patterns.
    
    Args:
        message: Text message to encode
        max_width: Maximum columns to use (default 80)
        
    Returns:
        2D array of punch patterns, where each column is one character
    """
    # Convert to uppercase and truncate if needed
    message = message.upper()[:max_width]
    
    # Create empty grid (12 rows Γ— message length)
    encoded_grid = [[False for _ in range(len(message))] for _ in range(12)]
    
    # Process each character
    for col_idx, char in enumerate(message):
        # Get punch pattern for this character
        pattern = CHAR_MAPPING.get(char, [0] * 12)  # Default to no punches if character unknown
        
        # Apply the pattern to the grid column
        for row_idx, is_punched in enumerate(pattern):
            if is_punched:
                encoded_grid[row_idx][col_idx] = True
    
    return encoded_grid

The encoding process follows these steps:

  1. Prepare the message: Convert to uppercase and truncate if longer than 80 characters
  2. Create empty grid: Initialize a 12Γ—n grid where n is the message length
  3. Process each character:
    • Look up the punch pattern for the character
    • Apply the pattern to the corresponding column in the grid
  4. Return the encoded grid: A 2D array where True represents a punch

Visualization of Encoding

To visualize how the letter 'H' is encoded:

Character 'H' lookup: [1,0,0,0,0,0,0,0,1,0,0,0]  # Punches in rows 12 and 8

Applied to column:
Row 12: β–  (punched)
Row 11: β–‘ (not punched)
Row 0:  β–‘ (not punched)
Row 1:  β–‘ (not punched)
Row 2:  β–‘ (not punched)
Row 3:  β–‘ (not punched)
Row 4:  β–‘ (not punched)
Row 5:  β–‘ (not punched)
Row 6:  β–‘ (not punched)
Row 7:  β–‘ (not punched)
Row 8:  β–  (punched)
Row 9:  β–‘ (not punched)

Decoding Process

The decoding process is the reverse of encoding - it converts punch patterns back into characters:

def decode_message(punch_grid: List[List[bool]]) -> str:
    """
    Decode a punch card grid back into a text message.
    
    Args:
        punch_grid: 2D array of punch patterns (12 rows Γ— n columns)
        
    Returns:
        Decoded text message
    """
    # Create a reverse mapping from punch patterns to characters
    reverse_mapping = {tuple(pattern): char for char, pattern in CHAR_MAPPING.items()}
    
    # Decode each column into a character
    decoded_message = ""
    for col_idx in range(len(punch_grid[0])):
        # Extract this column's pattern
        column_pattern = [punch_grid[row_idx][col_idx] for row_idx in range(12)]
        
        # Convert booleans to binary values (1/0)
        binary_pattern = [1 if is_punched else 0 for is_punched in column_pattern]
        
        # Look up character in reverse mapping
        char = reverse_mapping.get(tuple(binary_pattern), ' ')  # Default to space if unknown
        decoded_message += char
    
    return decoded_message

The decoding process follows these steps:

  1. Create reverse mapping: Build a dictionary that maps punch patterns to characters
  2. Process each column:
    • Extract the punch pattern from that column
    • Convert to the binary format expected by the reverse mapping
    • Look up the character for this pattern
    • Append to the decoded message
  3. Return the decoded message: The original text is reconstructed

Translation to Display Forms

Once characters are encoded into punch patterns, they need to be translated into various forms for display:

1. GUI Visualization

For the GUI display, punch patterns are converted to colored rectangles:

def update_gui_from_encoding(encoded_grid, punch_card_grid):
    """Update GUI grid from encoded punch patterns"""
    for row in range(12):
        for col in range(len(encoded_grid[0])):
            # Set LED to on (True) or off (False)
            punch_card_grid.updateLED(row, col, encoded_grid[row][col])

2. Terminal Visualization

For terminal display, punches are represented as ASCII characters:

def format_for_terminal(encoded_grid):
    """Format punch patterns for terminal display"""
    terminal_rows = []
    for row in range(12):
        row_chars = []
        for col in range(len(encoded_grid[0])):
            # 'O' for punched holes, ' ' for no hole
            row_chars.append('O' if encoded_grid[row][col] else ' ')
        terminal_rows.append(''.join(row_chars))
    return '\n'.join(terminal_rows)

3. Hardware Control

For hardware LED control, punches are converted to electrical signals:

def send_to_hardware(encoded_grid, hardware_controller):
    """Send punch patterns to hardware controller"""
    for row in range(12):
        for col in range(len(encoded_grid[0])):
            # Send signal to turn LED on or off
            hardware_controller.set_led(row, col, encoded_grid[row][col])

Character Set Expansion and Custom Mappings

The Punch Card Project also supports expansion of the character set and custom mappings:

  1. Extended Character Set: Additional special characters can be defined using various punch combinations.

  2. Custom Encoding Maps: Users can define their own character mappings for special purposes.

  3. Multiple Encoding Standards: Different historical punch card standards (IBM 026, IBM 029, Bull, etc.) can be implemented with different mapping dictionaries.

Complete Encoding-Decoding-Display Pipeline

The entire process of encoding a message, displaying it, and optionally decoding it involves these key steps:

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ User Input    β”‚
   β”‚ "HELLO"       β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ char_to_punch │────►│ CHAR_MAPPING    β”‚
   β”‚ Conversion    │◄────│ Dictionary      β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”‚ Encoded message
           β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ Encoded Grid  β”‚
   β”‚ 12Γ—80 booleansβ”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”
    β”‚              β”‚
    β–Ό              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Display    β”‚  β”‚ Decode     β”‚
β”‚ Adapters   β”‚  β”‚ Function   β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
       β”‚              β”‚
 β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”       β–Ό
 β”‚            β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 β–Ό            β–Ό  β”‚ Original   β”‚
GUI    Hardware  β”‚ Text       β”‚
Display Control  β”‚ "HELLO"    β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Practical Example: Encoding the Message "HELLO"

To illustrate the process, here's how "HELLO" is encoded:

1. Character Lookup

'H' => [1,0,0,0,0,0,0,0,1,0,0,0]  # 12,8
'E' => [1,0,0,0,0,0,0,1,0,0,0,0]  # 12,5
'L' => [0,1,0,0,0,1,0,0,0,0,0,0]  # 11,3
'L' => [0,1,0,0,0,1,0,0,0,0,0,0]  # 11,3
'O' => [0,1,0,0,0,0,0,0,1,0,0,0]  # 11,6

2. Grid Construction

Row 12: [β– ,β– ,β–‘,β–‘,β–‘]  # 'H','E' have punches in row 12
Row 11: [β–‘,β–‘,β– ,β– ,β– ]  # 'L','L','O' have punches in row 11
Row 0:  [β–‘,β–‘,β–‘,β–‘,β–‘]
Row 1:  [β–‘,β–‘,β–‘,β–‘,β–‘]
Row 2:  [β–‘,β–‘,β–‘,β–‘,β–‘]
Row 3:  [β–‘,β–‘,β– ,β– ,β–‘]  # 'L','L' have punches in row 3
Row 4:  [β–‘,β–‘,β–‘,β–‘,β–‘]
Row 5:  [β–‘,β– ,β–‘,β–‘,β–‘]  # 'E' has punch in row 5
Row 6:  [β–‘,β–‘,β–‘,β–‘,β– ]  # 'O' has punch in row 6
Row 7:  [β–‘,β–‘,β–‘,β–‘,β–‘]
Row 8:  [β– ,β–‘,β–‘,β–‘,β–‘]  # 'H' has punch in row 8
Row 9:  [β–‘,β–‘,β–‘,β–‘,β–‘]

3. Display Conversion

GUI: LEDs at positions (12,0), (12,1), (11,2), (11,3), (11,4), (3,2), (3,3), (5,1), (6,4), (8,0) are ON.

Terminal:
β–  β–  β–‘ β–‘ β–‘  # Row 12
β–‘ β–‘ β–  β–  β–   # Row 11
β–‘ β–‘ β–‘ β–‘ β–‘  # Row 0
β–‘ β–‘ β–‘ β–‘ β–‘  # Row 1
β–‘ β–‘ β–‘ β–‘ β–‘  # Row 2
β–‘ β–‘ β–  β–  β–‘  # Row 3
β–‘ β–‘ β–‘ β–‘ β–‘  # Row 4
β–‘ β–  β–‘ β–‘ β–‘  # Row 5
β–‘ β–‘ β–‘ β–‘ β–   # Row 6
β–‘ β–‘ β–‘ β–‘ β–‘  # Row 7
β–  β–‘ β–‘ β–‘ β–‘  # Row 8
β–‘ β–‘ β–‘ β–‘ β–‘  # Row 9

Implementation Variations

The Punch Card Project implements several variations of this encoding system:

  1. Binary Array Format: For internal processing, punch patterns are stored as arrays of booleans or binary values (0/1).

  2. Row-index Format: For readability, punches are sometimes represented as lists of row indices where punches occur (e.g., 'A': [12, 1]).

  3. ASCII Visual Format: For terminal display, punches are represented as ASCII characters ('O' or 'β– ' for punched, ' ' or 'β–‘' for not punched).

  4. Compact Serialization: For storage in the database, punch patterns can be serialized to compact strings or binary formats.

The flexibility of these implementations allows the system to efficiently process punch card data while maintaining historical accuracy.

Decision-Making and Design Rationale

Modularity and Maintainability

The modular design allows independent development and testing of components, simplifying debugging and future enhancements.

Historical Accuracy

Adhering to IBM 80 column encoding and punch card standards provides educational value and historical authenticity.

Hardware Abstraction

Supporting both simulated and physical hardware enables flexible development and deployment scenarios.

GUI and Terminal Support

Providing both GUI and terminal interfaces ensures broad compatibility and accessibility.


Note:
This document provides a high-level yet detailed overview. For deeper dives into specific modules or code implementations, refer directly to the source code and inline documentation.