← Back to Blog
AILangChainGradioVestaboardLocal LLMs

Part 3 — Words Made Visible: Simulating Vestaboard Output and Sharing the Project

The conclusion of the series — polishing the Gradio UI with a Vestaboard preview and publishing the repo

By Garry OsborneNovember 24, 202510 min read

Part 3 — Words Made Visible: Simulating Vestaboard Output and Sharing the Project

The conclusion of the three-part series — where digital language becomes kinetic art, and development becomes smoother with a live simulated display.

Introduction — Completing the Journey

In Part 1, we explored the spark behind this project: an AI model expressing itself through an analog medium — the Vestaboard.

In Part 2, we coded the essentials: a local Llama-3.2-1B-Instruct model orchestrated by LangChain, surfaced through a Gradio UI, and connected to the Vestaboard via the local API.

In this final installment, we polish the Gradio interface by adding a simulated Vestaboard display so you can see exactly what will be shown on the hardware, right from the browser. We wrap up by sharing the public GitHub repository so you can extend and enhance the project.

1 · Why Add a Simulated Vestaboard?

The Vestaboard is intentionally tactile and paced — which is wonderful in production, but slows iteration during development. A built-in simulated display lets you:

  • Preview the 6 × 22 (132-character) layout instantly
  • Verify truncation, line breaks, and sanitization
  • Test color tiles and patterns
  • Develop and debug even when the board isn't nearby

The simulation supplements the hardware — not replaces it — and dramatically speeds up UI/UX work.

2 · Reading the Board and Rendering a Live Preview (Actual Code)

The following function (added to app.py) reads what's currently on the Vestaboard and returns an HTML preview suitable for rendering inside the Gradio UI.

from typing import Tuple

def read_message_with_visual(self) -> Tuple[str, str]:
    """
    Read the current message from the Vestaboard and return both raw data 
    and visual HTML.

    Returns:
        Tuple of (raw board content, HTML visualization)
    """
    if not self.client:
        return "Error: Client not initialized", \
            "<div style='padding: 20px;'>Error: Client not initialized</div>"

    result = self.client.read_message()
    if result['status'] == 'success':
        board_data = result['data']
        if board_data:
            raw_text = f"Current board content:\n{self._format_board_data(board_data)}"

            # Extract the actual board array
            # The API may return {'message': [[...], [...]]} or just [[...], [...]]
            if isinstance(board_data, dict) and 'message' in board_data:
                board_array = board_data['message']
            elif isinstance(board_data, list):
                board_array = board_data
            else:
                board_array = None

            if board_array:
                html_visual = self.client.generate_board_html(board_array)
            else:
                html_visual = "<div style='padding: 20px; text-align: center;'>Unable to parse board data</div>"

            return raw_text, html_visual
        else:
            return "Board is empty", \
                "<div style='padding: 20px; text-align: center;'>Board is empty</div>"
    else:
        error_msg = result['message']
        return error_msg, \
            f"<div style='padding: 20px; text-align: center;'>{error_msg}</div>"

This powers a live HTML simulation right in the Gradio app, side-by-side with AI output.

3 · Simulating the Vestaboard Grid in the Browser (Actual Code)

Here's the simulation function added to vestaboard_client.py. It renders a faithful HTML/CSS grid of the Vestaboard, supporting both character tiles and color tiles.

def generate_board_html(self, board_data) -> str:
    """
    Generate HTML representation of the Vestaboard display.

    Args:
        board_data: 2D array of character codes (6 rows x 22 columns)

    Returns:
        HTML string with styled Vestaboard grid
    """
    if not board_data or not isinstance(board_data, list):
        return "<div style='padding: 20px; text-align: center;'>No board data available</div>"

    html = """
    <style>
        .vestaboard-container {
            background-color: #1a1a1a;
            padding: 12px;
            border-radius: 8px;
            display: block;
            margin: 0 auto;
            width: fit-content;
            max-width: 100%;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
        }
        .vestaboard-grid {
            display: grid;
            grid-template-columns: repeat(22, 27px);
            grid-template-rows: repeat(6, 34px);
            gap: 2px;
            background-color: #0a0a0a;
            padding: 5px;
            border-radius: 4px;
        }
        .vestaboard-tile {
            width: 27px;
            height: 34px;
            background-color: #000000;
            border: 1px solid #333;
            border-radius: 2px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: 'Courier New', monospace;
            font-size: 18px;
            font-weight: bold;
            color: #FF8800;
            text-align: center;
        }
        .vestaboard-color-tile {
            border: none;
        }
    </style>
    <div class="vestaboard-container">
        <div class="vestaboard-grid">
    """

    for row in board_data:
        for code in row:
            if code in COLOR_TILES:
                # Color tile
                color_name, color_hex = COLOR_TILES[code]
                html += (
                    f'<div class="vestaboard-tile vestaboard-color-tile" '
                    f'style="background-color: {color_hex};" '
                    f'title="{color_name}"></div>'
                )
            else:
                # Character tile
                char = CODE_TO_CHAR.get(code, ' ')
                # HTML escape special characters
                if char == '<': char = '<'
                elif char == '>': char = '>'
                elif char == '&': char = '&'
                elif char == ' ': char = ' '

                html += f'<div class="vestaboard-tile">{char}</div>'

    html += """
        </div>
    </div>
    """

    return html

This function mirrors the exact 6 × 22 grid, styling, and tile semantics you see on the hardware — making the browser preview a reliable stand-in during development.

4 · The Polished Gradio Experience

With these additions, the Gradio UI now provides:

  • AI Response (from LangChain)
  • Vestaboard Preview (HTML) — instant, accurate simulation
  • Hardware Status — result of posting to the local Vestaboard API
  • Model Controls — swap models, load, and test
  • Utilities — test color bits, display gold/silver prices, read board state

This turns the UI into a control center for both development and daily use.

5 · Explore and Extend the Project (Public Repo)

All source code — including the simulation and visualization — is available in the public repository:

👉 https://github.com/Garry-TI/vestaboard-vc

Inside you'll find:

  • app.py — Gradio UI & handlers (now with live preview and board readbacks)
  • llm_client.py — LangChain + Hugging Face local inference
  • vestaboard_client.py — Local API client and HTML simulator
  • config.py — Model and Vestaboard config
  • README.md — Setup & usage instructions

Contributions are welcome — feel free to open issues or PRs.

Conclusion — The Machine That Speaks Back

Across three parts, you built a loop where digital thought becomes analog art:

  • Part 1 — the vision
  • Part 2 — the engine
  • Part 3 — the polished experience with simulation and sharing

Your AI now thinks locally, speaks through Gradio, and performs on the Vestaboard — with a faithful preview that makes iteration fast and fun.

When the tiles finish flipping and the room falls quiet, what's left isn't just a message — it's presence.


Read the Series

  1. Part 1 → The Spark of Color
  2. Part 2 → From Code to Conversation
  3. Part 3 → Words Made Visible (you are here)

© 2025 Tomorrow's Innovations LLC · Written by Garry Osborne · GitHub Repository

This is Part 3 of the series

Enjoyed this series?

Stay updated with our latest insights on AI and machine learning