leverage_the_display_bridge_on_your_Raspberry_Pi_5.md
The recommended architecture is a split-brain edge-display system: Muse performs high-level reasoning and engine execution on her host computer, while a dedicated Raspberry Pi 5 acts as the persistent physical display, telemetry receiver, and local edge-service node.
- Architecture Overview
- Compute Node: Muse Host
- Display Node: Raspberry Pi 5
- Neural Accelerator Role
- Ingestion Flow
- Complete Pi 5 HUD Engine
- Sending Updates from Muse
- Using the Neural Accelerator
- Recommended Production Improvements
- Final Runtime Topology
The most reliable design separates Muse's heavy reasoning environment from the physical display system.
flowchart LR
subgraph HOST["MUSE HOST - COMPUTE NODE"]
MUSE["Meta Muse Agent<br/>Reasoning / Dialogue"]
SAGNA["Sagnaskemma<br/>D&D / TTRPG Engine"]
ASTRO["Astrology & Divination Engine"]
BRIDGE["Telemetry Bridge<br/>Structured JSON Emitter"]
MUSE --> SAGNA
MUSE --> ASTRO
SAGNA --> BRIDGE
ASTRO --> BRIDGE
MUSE --> BRIDGE
end
BRIDGE -->|"LAN HTTP POST<br/>Port 8080"| API
subgraph PI["RASPBERRY PI 5 - DISPLAY NODE"]
API["HUD HTTP Receiver"]
STATE["Thread-Safe State Store"]
HUD["Pygame / SDL2<br/>60 FPS HUD"]
TTRPG["TTRPG View"]
DIV["Divination View"]
SPEECH["Muse Narrative Stream"]
NPU["Local Neural Accelerator"]
AUDIO["Local Audio"]
API --> STATE
STATE --> HUD
HUD --> TTRPG
HUD --> DIV
HUD --> SPEECH
STATE --> NPU
NPU --> AUDIO
end
classDef host fill:#21143d,stroke:#a78bfa,stroke-width:2px,color:#ffffff;
classDef edge fill:#132e2a,stroke:#34d399,stroke-width:2px,color:#ffffff;
classDef neural fill:#3b1d0b,stroke:#fb923c,stroke-width:2px,color:#ffffff;
class MUSE,SAGNA,ASTRO,BRIDGE host;
class API,STATE,HUD,TTRPG,DIV,SPEECH edge;
class NPU,AUDIO neural;
Muse's main computer remains responsible for the high-level cognitive workload.
Typical host-side responsibilities include:
- Meta Muse agent reasoning
- dialogue
- planning
- Sagnaskemma execution
- astrology calculations
- Tarot / rune logic
- long-context model operations
- tool orchestration
- structured telemetry generation
The host does not need to render the physical HUD.
Instead, it sends state changes over the LAN.
Muse
│
├── Sagnaskemma
│
├── Astrology Engine
│
├── Divination Engine
│
└── Reasoning / Speech
│
▼
JSON Payload
│
▼
Raspberry Pi
The Raspberry Pi 5 becomes a dedicated physical interface.
It handles:
- HTTP telemetry reception
- local state caching
- display-mode switching
- Pygame / SDL2 graphics
- TTRPG panels
- Tarot layouts
- astrology summaries
- Muse dialogue display
- keyboard controls
- local audio dispatch
The goal is to make the Pi operate like a persistent observation console rather than a general desktop application.
Displays:
- campaign
- location
- encounter
- party members
- HP
- maximum HP
- Armor Class
- character class
- recent dice rolls
- lore text
- Muse narration
Displays:
- astrology summary
- planetary hours
- Tarot spread
- card orientation
- symbolic keywords
- Elder Futhark glyphs
- Muse interpretation
Because the Raspberry Pi CPU and graphics subsystem handle networking and display composition, a neural accelerator can be reserved for supported local AI workloads.
Possible edge workloads include:
- neural text-to-speech
- semantic embeddings
- image enhancement
- image classification
- compact local models
- other accelerator-compatible inference graphs
Conceptually:
Raspberry Pi CPU
├── HTTP
├── State
├── Application Logic
└── HUD Rendering
Raspberry Pi GPU
└── Display Composition
Neural Accelerator
├── TTS
├── Embeddings
├── Image Processing
└── Future Edge AI
Muse executes a tool or engine on the host.
The resulting state is converted to JSON and transmitted to the Pi.
[Muse Host]
│
├── Sagnaskemma Engine
│ │
│ └── TTRPG state
│
├── Astrology Engine
│ │
│ └── Divination state
│
└── Muse Reasoning / Narration
│
▼
Structured JSON
│
▼
HTTP POST over LAN
│
▼
[Pi 5 Display Node :8080]
│
├── State Store
│
├── Pygame / SDL2 HUD
│
└── Local Neural Services
The reference implementation exposes:
| Endpoint | Purpose |
|---|---|
/api/ttrpg |
Update Sagnaskemma / TTRPG state |
/api/divination |
Update Tarot / astrology state |
/api/status |
Update Muse speech, status, or active view |
Base URL:
http://<PI_IP>:8080
File:
muse_display_node.py
Install Pygame:
sudo apt-get update
sudo apt-get install -y python3-pygameRun the display:
python3 muse_display_node.py#!/usr/bin/env python3
"""
muse_display_node.py
Dedicated real-time dual-engine HUD
for Raspberry Pi 5.
Receives live payloads over LAN from Meta Muse running:
1. Sagnaskemma
D&D / TTRPG lore and combat engine.
2. Astrology & Divination Engine
Astrology, Tarot, and rune state.
Renders:
- TTRPG state
- party vitals
- encounter lore
- dice logs
- divination cards
- astrology state
- Muse dialogue
"""
import sys
import json
import threading
from http.server import (
ThreadingHTTPServer,
BaseHTTPRequestHandler
)
import pygame
# =====================================================================
# Display Configuration
# =====================================================================
SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720
FPS = 60
# =====================================================================
# Palette
# =====================================================================
COLOR_BG = (
12,
14,
20
)
COLOR_PANEL_BG = (
22,
26,
38
)
COLOR_PANEL_BORDER = (
45,
52,
75
)
COLOR_TEXT_PRIMARY = (
235,
240,
250
)
COLOR_TEXT_MUTED = (
140,
148,
170
)
COLOR_ACCENT_GOLD = (
234,
179,
8
)
COLOR_ACCENT_BLUE = (
59,
130,
246
)
COLOR_ACCENT_PURPLE = (
168,
85,
247
)
COLOR_ACCENT_GREEN = (
34,
197,
94
)
COLOR_ACCENT_RED = (
239,
68,
68
)
COLOR_CARD_BG = (
30,
35,
52
)
# =====================================================================
# Global State Container
# =====================================================================
state_lock = threading.Lock()
app_state = {
"active_view":
"ttrpg",
"muse_status":
"ONLINE",
"muse_speech":
(
"Awaiting commands from "
"Sagnaskemma or Astrology Engine..."
),
"ttrpg": {
"campaign":
"Sagnaskemma: The Iron Forest",
"encounter":
"Ancient Barrow-Mound Entrance",
"party": [
{
"name":
"Volmarr",
"class":
"Skald 5",
"hp":
48,
"max_hp":
48,
"ac":
16
},
{
"name":
"Astrid",
"class":
"Shieldmaiden 5",
"hp":
52,
"max_hp":
55,
"ac":
18
},
{
"name":
"Torin",
"class":
"Rune Weaver 5",
"hp":
30,
"max_hp":
36,
"ac":
14
}
],
"lore_text":
(
"The runes carved into the lintel "
"glow with cold blue phosphorescence. "
"A bitter wind smells of iron and "
"old barrows."
),
"recent_rolls": [
{
"roller":
"Volmarr",
"check":
"History (Lore)",
"result":
"1d20+7 = 24 (Success)"
},
{
"roller":
"Muse",
"check":
"Perception",
"result":
"1d20+3 = 11"
}
]
},
"divination": {
"title":
"Elder Futhark & 3-Card Tarot Spread",
"spread_type":
(
"Three-Card Oracle "
"(Origin / Threshold / Outcome)"
),
"cards": [
{
"title":
"The High Priestess",
"orient":
"Upright",
"keyword":
(
"Intuition, Mystery, "
"Veiled Lore"
)
},
{
"title":
"The Tower",
"orient":
"Reversed",
"keyword":
(
"Averting Disaster, "
"Internal Shift"
)
},
{
"title":
"The Sun",
"orient":
"Upright",
"keyword":
(
"Clarity, Solar Radiance, "
"Triumph"
)
}
],
"astrology_summary":
(
"Sun in Libra 14° | "
"Moon in Scorpio 2° | "
"Mars conjunct Saturn | "
"Square Ascendant"
),
"planetary_hours":
(
"Current Hour: Mars | "
"Next: Sun (13:42)"
)
}
}
# =====================================================================
# HTTP Networking
# =====================================================================
class MuseRequestHandler(
BaseHTTPRequestHandler
):
def _send_response(
self,
code,
message
):
self.send_response(
code
)
self.send_header(
"Content-Type",
"application/json"
)
self.end_headers()
self.wfile.write(
json.dumps(
{
"status":
message
}
).encode(
"utf-8"
)
)
def do_POST(self):
content_length = int(
self.headers.get(
"Content-Length",
0
)
)
if content_length == 0:
self._send_response(
400,
"Empty payload"
)
return
body = self.rfile.read(
content_length
)
try:
payload = json.loads(
body.decode(
"utf-8"
)
)
except json.JSONDecodeError:
self._send_response(
400,
"Invalid JSON"
)
return
with state_lock:
# ---------------------------------------------------------
# TTRPG / Sagnaskemma
# ---------------------------------------------------------
if self.path == "/api/ttrpg":
app_state[
"active_view"
] = "ttrpg"
for key in [
"campaign",
"encounter",
"party",
"lore_text",
"recent_rolls"
]:
if key in payload:
app_state[
"ttrpg"
][key] = payload[key]
if "muse_speech" in payload:
app_state[
"muse_speech"
] = payload[
"muse_speech"
]
if "muse_status" in payload:
app_state[
"muse_status"
] = payload[
"muse_status"
]
self._send_response(
200,
"TTRPG state updated"
)
# ---------------------------------------------------------
# Astrology / Divination
# ---------------------------------------------------------
elif self.path == "/api/divination":
app_state[
"active_view"
] = "divination"
for key in [
"title",
"spread_type",
"cards",
"astrology_summary",
"planetary_hours"
]:
if key in payload:
app_state[
"divination"
][key] = payload[key]
if "muse_speech" in payload:
app_state[
"muse_speech"
] = payload[
"muse_speech"
]
if "muse_status" in payload:
app_state[
"muse_status"
] = payload[
"muse_status"
]
self._send_response(
200,
"Divination state updated"
)
# ---------------------------------------------------------
# Global Status
# ---------------------------------------------------------
elif self.path == "/api/status":
if "muse_speech" in payload:
app_state[
"muse_speech"
] = payload[
"muse_speech"
]
if "muse_status" in payload:
app_state[
"muse_status"
] = payload[
"muse_status"
]
if "active_view" in payload:
requested_view = payload[
"active_view"
]
if requested_view in (
"ttrpg",
"divination"
):
app_state[
"active_view"
] = requested_view
self._send_response(
200,
"Status updated"
)
else:
self._send_response(
404,
"Endpoint not found"
)
def log_message(
self,
format,
*args
):
return
def start_server(
host="0.0.0.0",
port=8080
):
server = ThreadingHTTPServer(
(
host,
port
),
MuseRequestHandler
)
server_thread = (
threading.Thread(
target=
server.serve_forever,
daemon=True
)
)
server_thread.start()
return server
# =====================================================================
# Text Rendering
# =====================================================================
def draw_text_wrapped(
surface,
text,
font,
color,
rect
):
words = text.split(
" "
)
lines = []
current_line = []
max_width = rect.width
for word in words:
test_line = " ".join(
current_line
+ [word]
)
if (
font.size(
test_line
)[0]
<= max_width
):
current_line.append(
word
)
else:
if current_line:
lines.append(
" ".join(
current_line
)
)
current_line = [
word
]
if current_line:
lines.append(
" ".join(
current_line
)
)
y = rect.top
line_height = (
font.get_linesize()
)
for line in lines:
if (
y + line_height
> rect.bottom
):
break
rendered = font.render(
line,
True,
color
)
surface.blit(
rendered,
(
rect.left,
y
)
)
y += line_height
# =====================================================================
# Main HUD
# =====================================================================
def render_hud(
screen,
fonts,
state
):
(
font_large,
font_med,
font_small,
font_bold
) = fonts
screen.fill(
COLOR_BG
)
# ---------------------------------------------------------
# Header
# ---------------------------------------------------------
pygame.draw.rect(
screen,
COLOR_PANEL_BG,
(
0,
0,
SCREEN_WIDTH,
56
)
)
pygame.draw.line(
screen,
COLOR_PANEL_BORDER,
(
0,
56
),
(
SCREEN_WIDTH,
56
),
2
)
if state[
"active_view"
] == "ttrpg":
header_title = (
"MUSE OMNI-DISPLAY :: "
"SAGNASKEMMA TTRPG"
)
header_color = (
COLOR_ACCENT_BLUE
)
else:
header_title = (
"MUSE OMNI-DISPLAY :: "
"DIVINATION & ASTROLOGY"
)
header_color = (
COLOR_ACCENT_PURPLE
)
title_surface = (
font_bold.render(
header_title,
True,
header_color
)
)
screen.blit(
title_surface,
(
24,
16
)
)
# ---------------------------------------------------------
# Status Badge
# ---------------------------------------------------------
status_string = (
"MUSE: "
f"{state['muse_status']}"
)
status_color = (
COLOR_ACCENT_GREEN
if state[
"muse_status"
].upper() == "ONLINE"
else COLOR_ACCENT_GOLD
)
pygame.draw.circle(
screen,
status_color,
(
SCREEN_WIDTH - 180,
28
),
7
)
screen.blit(
font_small.render(
status_string,
True,
COLOR_TEXT_PRIMARY
),
(
SCREEN_WIDTH - 165,
20
)
)
# ---------------------------------------------------------
# Toggle Hint
# ---------------------------------------------------------
hint_surface = (
font_small.render(
"[TAB / 1 / 2]",
True,
COLOR_TEXT_MUTED
)
)
screen.blit(
hint_surface,
(
SCREEN_WIDTH - 300,
20
)
)
# ---------------------------------------------------------
# Active View
# ---------------------------------------------------------
if state[
"active_view"
] == "ttrpg":
render_ttrpg_view(
screen,
fonts,
state["ttrpg"]
)
else:
render_divination_view(
screen,
fonts,
state["divination"]
)
# ---------------------------------------------------------
# Muse Narrative Stream
# ---------------------------------------------------------
panel_y = (
SCREEN_HEIGHT - 130
)
pygame.draw.rect(
screen,
COLOR_PANEL_BG,
(
20,
panel_y,
SCREEN_WIDTH - 40,
110
),
border_radius=8
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
(
20,
panel_y,
SCREEN_WIDTH - 40,
110
),
width=1,
border_radius=8
)
badge_surface = (
font_small.render(
"MUSE NARRATIVE STREAM",
True,
COLOR_ACCENT_GOLD
)
)
screen.blit(
badge_surface,
(
36,
panel_y + 12
)
)
speech_rect = pygame.Rect(
36,
panel_y + 36,
SCREEN_WIDTH - 72,
64
)
draw_text_wrapped(
screen,
(
f'"'
f'{state["muse_speech"]}'
f'"'
),
font_med,
COLOR_TEXT_PRIMARY,
speech_rect
)
# =====================================================================
# TTRPG View
# =====================================================================
def render_ttrpg_view(
screen,
fonts,
data
):
(
font_large,
font_med,
font_small,
font_bold
) = fonts
# ---------------------------------------------------------
# Party Column
# ---------------------------------------------------------
left_rect = pygame.Rect(
20,
72,
420,
500
)
pygame.draw.rect(
screen,
COLOR_PANEL_BG,
left_rect,
border_radius=8
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
left_rect,
width=1,
border_radius=8
)
screen.blit(
font_bold.render(
"PARTY ROSTER & STATS",
True,
COLOR_ACCENT_BLUE
),
(
36,
88
)
)
y_offset = 125
for member in data.get(
"party",
[]
):
card_rect = pygame.Rect(
36,
y_offset,
388,
76
)
pygame.draw.rect(
screen,
COLOR_CARD_BG,
card_rect,
border_radius=6
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
card_rect,
width=1,
border_radius=6
)
name_surface = (
font_bold.render(
member.get(
"name",
"Unknown"
),
True,
COLOR_TEXT_PRIMARY
)
)
class_surface = (
font_small.render(
member.get(
"class",
"Adventurer"
),
True,
COLOR_TEXT_MUTED
)
)
ac_surface = (
font_small.render(
f"AC "
f"{member.get('ac', 10)}",
True,
COLOR_ACCENT_GOLD
)
)
screen.blit(
name_surface,
(
48,
y_offset + 8
)
)
screen.blit(
class_surface,
(
48,
y_offset + 30
)
)
screen.blit(
ac_surface,
(
360,
y_offset + 8
)
)
# -----------------------------------------------------
# Health Bar
# -----------------------------------------------------
hp = member.get(
"hp",
0
)
max_hp = max(
1,
member.get(
"max_hp",
1
)
)
ratio = max(
0.0,
min(
1.0,
hp / max_hp
)
)
bar_rect = pygame.Rect(
48,
y_offset + 52,
364,
14
)
fill_rect = pygame.Rect(
48,
y_offset + 52,
int(
364 * ratio
),
14
)
bar_color = (
COLOR_ACCENT_GREEN
if ratio > 0.4
else COLOR_ACCENT_RED
)
pygame.draw.rect(
screen,
(
15,
18,
26
),
bar_rect,
border_radius=3
)
pygame.draw.rect(
screen,
bar_color,
fill_rect,
border_radius=3
)
hp_surface = (
font_small.render(
f"{hp}/{max_hp} HP",
True,
COLOR_TEXT_PRIMARY
)
)
screen.blit(
hp_surface,
(
200,
y_offset + 50
)
)
y_offset += 90
# ---------------------------------------------------------
# Encounter / Lore
# ---------------------------------------------------------
right_x = 460
right_width = (
SCREEN_WIDTH - 480
)
lore_box = pygame.Rect(
right_x,
72,
right_width,
240
)
pygame.draw.rect(
screen,
COLOR_PANEL_BG,
lore_box,
border_radius=8
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
lore_box,
width=1,
border_radius=8
)
screen.blit(
font_bold.render(
(
"LOCATION: "
f"{data.get('encounter', 'Unknown')}"
),
True,
COLOR_ACCENT_GOLD
),
(
right_x + 16,
88
)
)
screen.blit(
font_small.render(
(
"CAMPAIGN: "
f"{data.get('campaign', '')}"
),
True,
COLOR_TEXT_MUTED
),
(
right_x + 16,
112
)
)
lore_text_rect = pygame.Rect(
right_x + 16,
140,
right_width - 32,
160
)
draw_text_wrapped(
screen,
data.get(
"lore_text",
""
),
font_med,
COLOR_TEXT_PRIMARY,
lore_text_rect
)
# ---------------------------------------------------------
# Roll Log
# ---------------------------------------------------------
dice_box = pygame.Rect(
right_x,
328,
right_width,
244
)
pygame.draw.rect(
screen,
COLOR_PANEL_BG,
dice_box,
border_radius=8
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
dice_box,
width=1,
border_radius=8
)
screen.blit(
font_bold.render(
"RECENT DICE ROLLS & RESOLUTIONS",
True,
COLOR_ACCENT_BLUE
),
(
right_x + 16,
344
)
)
y_roll = 380
for roll in data.get(
"recent_rolls",
[]
):
roll_text = (
f"• [{roll.get('roller', 'Unknown')}] "
f"{roll.get('check', '')}: "
f"{roll.get('result', '')}"
)
screen.blit(
font_med.render(
roll_text,
True,
COLOR_TEXT_PRIMARY
),
(
right_x + 16,
y_roll
)
)
y_roll += 32
# =====================================================================
# Divination View
# =====================================================================
def render_divination_view(
screen,
fonts,
data
):
(
font_large,
font_med,
font_small,
font_bold
) = fonts
# ---------------------------------------------------------
# Astrology Summary
# ---------------------------------------------------------
astro_rect = pygame.Rect(
20,
72,
SCREEN_WIDTH - 40,
110
)
pygame.draw.rect(
screen,
COLOR_PANEL_BG,
astro_rect,
border_radius=8
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
astro_rect,
width=1,
border_radius=8
)
screen.blit(
font_bold.render(
"SWISS EPHEMERIS / TRANSIT MATRIX",
True,
COLOR_ACCENT_PURPLE
),
(
36,
88
)
)
screen.blit(
font_med.render(
data.get(
"astrology_summary",
""
),
True,
COLOR_TEXT_PRIMARY
),
(
36,
116
)
)
screen.blit(
font_small.render(
data.get(
"planetary_hours",
""
),
True,
COLOR_ACCENT_GOLD
),
(
36,
148
)
)
# ---------------------------------------------------------
# Tarot Spread
# ---------------------------------------------------------
cards_panel = pygame.Rect(
20,
198,
SCREEN_WIDTH - 40,
374
)
pygame.draw.rect(
screen,
COLOR_PANEL_BG,
cards_panel,
border_radius=8
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
cards_panel,
width=1,
border_radius=8
)
screen.blit(
font_bold.render(
(
"DIVINATION SPREAD: "
f"{data.get('spread_type', '')}"
),
True,
COLOR_ACCENT_GOLD
),
(
36,
214
)
)
cards = data.get(
"cards",
[]
)
if not cards:
return
card_width = (
cards_panel.width
- (len(cards) + 1) * 24
) // len(cards)
card_height = 300
for index, card in enumerate(
cards
):
cx = (
cards_panel.left
+ 24
+ index
* (
card_width + 24
)
)
cy = 250
card_rect = pygame.Rect(
cx,
cy,
card_width,
card_height
)
pygame.draw.rect(
screen,
COLOR_CARD_BG,
card_rect,
border_radius=8
)
pygame.draw.rect(
screen,
COLOR_ACCENT_PURPLE,
card_rect,
width=2,
border_radius=8
)
# -----------------------------------------------------
# Card Header
# -----------------------------------------------------
title_surface = (
font_bold.render(
card.get(
"title",
""
),
True,
COLOR_TEXT_PRIMARY
)
)
screen.blit(
title_surface,
(
cx + 14,
cy + 16
)
)
# -----------------------------------------------------
# Orientation
# -----------------------------------------------------
orientation = card.get(
"orient",
"Upright"
)
orientation_color = (
COLOR_ACCENT_GREEN
if orientation.lower()
== "upright"
else COLOR_ACCENT_RED
)
orientation_surface = (
font_small.render(
f"[{orientation.upper()}]",
True,
orientation_color
)
)
screen.blit(
orientation_surface,
(
cx + 14,
cy + 44
)
)
# -----------------------------------------------------
# Symbolic Glyph
# -----------------------------------------------------
glyph_rect = pygame.Rect(
cx + 14,
cy + 72,
card_width - 28,
120
)
pygame.draw.rect(
screen,
(
15,
18,
26
),
glyph_rect,
border_radius=4
)
pygame.draw.rect(
screen,
COLOR_PANEL_BORDER,
glyph_rect,
width=1,
border_radius=4
)
icon_text = (
font_large.render(
"ᛟ",
True,
COLOR_ACCENT_GOLD
)
)
screen.blit(
icon_text,
(
cx
+ (
card_width // 2
)
- 12,
cy + 115
)
)
# -----------------------------------------------------
# Keywords
# -----------------------------------------------------
keyword_rect = pygame.Rect(
cx + 14,
cy + 204,
card_width - 28,
80
)
draw_text_wrapped(
screen,
card.get(
"keyword",
""
),
font_small,
COLOR_TEXT_MUTED,
keyword_rect
)
# =====================================================================
# Main Runtime
# =====================================================================
def main():
pygame.init()
pygame.display.set_caption(
"Meta Muse HUD Display Node"
)
screen = pygame.display.set_mode(
(
SCREEN_WIDTH,
SCREEN_HEIGHT
)
)
clock = pygame.time.Clock()
# ---------------------------------------------------------
# Fonts
# ---------------------------------------------------------
font_large = pygame.font.SysFont(
"DejaVu Sans, Arial, Helvetica",
28,
bold=True
)
font_bold = pygame.font.SysFont(
"DejaVu Sans, Arial, Helvetica",
18,
bold=True
)
font_med = pygame.font.SysFont(
"DejaVu Sans, Arial, Helvetica",
16
)
font_small = pygame.font.SysFont(
"DejaVu Sans, Arial, Helvetica",
13
)
fonts = (
font_large,
font_med,
font_small,
font_bold
)
# ---------------------------------------------------------
# HTTP Receiver
# ---------------------------------------------------------
server = start_server(
port=8080
)
print(
"Muse Display Node listening on "
"http://0.0.0.0:8080"
)
# ---------------------------------------------------------
# Render Loop
# ---------------------------------------------------------
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key in (
pygame.K_ESCAPE,
pygame.K_q
):
running = False
elif event.key in (
pygame.K_TAB,
pygame.K_SPACE
):
with state_lock:
app_state[
"active_view"
] = (
"divination"
if app_state[
"active_view"
] == "ttrpg"
else "ttrpg"
)
elif event.key == pygame.K_1:
with state_lock:
app_state[
"active_view"
] = "ttrpg"
elif event.key == pygame.K_2:
with state_lock:
app_state[
"active_view"
] = "divination"
with state_lock:
current_render_state = (
json.loads(
json.dumps(
app_state
)
)
)
render_hud(
screen,
fonts,
current_render_state
)
pygame.display.flip()
clock.tick(
FPS
)
pygame.quit()
server.shutdown()
sys.exit(0)
if __name__ == "__main__":
main()Whenever Muse executes an engine or completes a headless task, the host can push the resulting state directly to:
http://<PI_IP>:8080
Replace:
<PI_IP>
with the Raspberry Pi's local network address.
Example:
192.168.1.150
would produce:
http://192.168.1.150:8080
When Muse resolves a TTRPG encounter:
curl -X POST http://<PI_IP>:8080/api/ttrpg \
-H "Content-Type: application/json" \
-d '{
"campaign": "Sagnaskemma - Volmarr'\''s Campaign",
"encounter": "Harrowed Barrow Entrance",
"lore_text": "The iron hinges groan as the tomb stone shifts back. Ancient warding runes hum in warning.",
"muse_speech": "Volmarr, your Skald roll of 24 pieced together the elder inscription. The way is open.",
"recent_rolls": [
{
"roller": "Volmarr",
"check": "Lore (History)",
"result": "1d20+7 = 24 (Success)"
}
]
}'The HUD will automatically switch to:
TTRPG VIEW
curl -X POST http://<PI_IP>:8080/api/ttrpg \
-H "Content-Type: application/json" \
-d '{
"party": [
{
"name": "Volmarr",
"class": "Skald 5",
"hp": 42,
"max_hp": 48,
"ac": 16
},
{
"name": "Astrid",
"class": "Shieldmaiden 5",
"hp": 49,
"max_hp": 55,
"ac": 18
},
{
"name": "Torin",
"class": "Rune Weaver 5",
"hp": 30,
"max_hp": 36,
"ac": 14
}
]
}'When Muse completes a Tarot reading or astrological calculation:
curl -X POST http://<PI_IP>:8080/api/divination \
-H "Content-Type: application/json" \
-d '{
"title": "Elder Futhark & 3-Card Reading",
"spread_type": "Past, Present, and Unfolding Fate",
"cards": [
{
"title": "The Magician",
"orient": "Upright",
"keyword": "Focused Will, Sovereign Creation"
},
{
"title": "Wheel of Fortune",
"orient": "Upright",
"keyword": "Turning Cycles, Wyrd Unfolding"
},
{
"title": "The Star",
"orient": "Upright",
"keyword": "Guiding Light, Hope Restored"
}
],
"astrology_summary": "Sun in Libra | Mars Sextile Jupiter | Moon Entering Scorpio",
"planetary_hours": "Planetary Hour: Sun (Peak Vitality)",
"muse_speech": "The cards align directly with your transit matrix. The Wheel turns in your favor."
}'The HUD automatically switches to:
DIVINATION VIEW
Update Muse without changing the underlying TTRPG or divination state:
curl -X POST http://<PI_IP>:8080/api/status \
-H "Content-Type: application/json" \
-d '{
"muse_status": "THINKING",
"muse_speech": "Reviewing the current world state before selecting the next action."
}'Force TTRPG mode:
curl -X POST http://<PI_IP>:8080/api/status \
-H "Content-Type: application/json" \
-d '{
"active_view": "ttrpg"
}'Force divination mode:
curl -X POST http://<PI_IP>:8080/api/status \
-H "Content-Type: application/json" \
-d '{
"active_view": "divination"
}'The display system can forward selected work to a local neural service.
Recommended flow:
flowchart LR
MUSE["Muse Speech Text"]
API["Pi HUD Receiver"]
QUEUE["Speech Queue"]
TTS["Local TTS Runtime"]
AUDIO["ALSA / PipeWire"]
SPEAKER["Speakers"]
MUSE --> API
API --> QUEUE
QUEUE --> TTS
TTS --> AUDIO
AUDIO --> SPEAKER
Muse can send:
{
"muse_speech": "The gate has opened."
}The Pi can then:
- display the sentence
- place it in the speech queue
- synthesize it locally
- play the generated audio
This avoids transmitting large audio streams from the host.
A future edge pipeline could create semantic embeddings for:
- lore
- character records
- campaign history
- divination logs
- system events
Conceptually:
Cosine similarity can then be used for retrieval:
Where supported by the chosen accelerator runtime and compiled models, possible local image operations include:
- thumbnail generation
- image classification
- card-art enhancement
- upscaling
- visual feature extraction
For larger generative-image workloads, model compatibility, memory requirements, accelerator support, and runtime limitations should be tested on the final hardware rather than assumed.
The reference implementation is intentionally simple.
A production version should add several additional layers.
The current reference server accepts LAN requests without authentication.
Production architecture:
Muse Host
│
▼
Authentication
│
▼
Schema Validation
│
▼
Rate Limiter
│
▼
HUD State
Possible options:
- bearer token
- API key
- mTLS
- signed messages
- private VLAN
- firewall rules
Instead of accepting arbitrary JSON, define explicit schemas.
Example TTRPG payload:
{
"campaign": "string",
"encounter": "string",
"party": [],
"lore_text": "string",
"recent_rolls": [],
"muse_speech": "string"
}Reject extremely large requests.
Conceptual maximum:
64 KB - 1 MB
depending on the selected protocol and content type.
Images should generally be transferred separately rather than embedded in giant JSON payloads.
Move hard-coded configuration into:
config.yaml
Example:
display:
width: 1280
height: 720
fps: 60
fullscreen: true
network:
host: 0.0.0.0
port: 8080
muse:
default_status: ONLINE
audio:
enabled: true
views:
default: ttrpgFor a dedicated physical terminal:
screen = pygame.display.set_mode(
(
SCREEN_WIDTH,
SCREEN_HEIGHT
),
pygame.FULLSCREEN
| pygame.DOUBLEBUF
)Development mode can remain windowed.
A dedicated Pi should launch the HUD automatically.
Example service:
[Unit]
Description=Muse Himinbjörg Display Node
After=network.target graphical.target
Wants=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/opt/muse-display
Environment=DISPLAY=:0
ExecStart=/usr/bin/python3 /opt/muse-display/muse_display_node.py
Restart=always
RestartSec=5
[Install]
WantedBy=graphical.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable muse-display.service
sudo systemctl start muse-display.serviceThe reference implementation currently uses:
HTTP POST
A future low-latency version can add:
WebSocket
for:
- streaming text
- rapidly changing telemetry
- live token output
- animation state
- continuous sensor data
Recommended separation:
HTTP
├── complete state updates
├── configuration
└── commands
WebSocket
├── streaming speech text
├── telemetry
├── rapid status changes
└── real-time events
flowchart TB
MUSE["META MUSE<br/>Primary Intelligence"]
subgraph HOST["HOST WORKSTATION"]
SAGNA["Sagnaskemma"]
ASTRO["Astrology / Divination"]
HARVEST["Telemetry Harvester"]
MUSE --> SAGNA
MUSE --> ASTRO
SAGNA --> HARVEST
ASTRO --> HARVEST
MUSE --> HARVEST
end
HARVEST -->|"LAN JSON / HTTP"| RECEIVER
subgraph EDGE["RASPBERRY PI 5"]
RECEIVER["Display Bridge Receiver"]
STATE["Shared State Store"]
HUD["Himinbjörg HUD"]
TTRPG["TTRPG Canvas"]
DIV["Divination Canvas"]
NARRATIVE["Muse Narrative"]
NEURAL["Neural Edge Services"]
AUDIO["Local Audio"]
RECEIVER --> STATE
STATE --> HUD
HUD --> TTRPG
HUD --> DIV
HUD --> NARRATIVE
STATE --> NEURAL
NEURAL --> AUDIO
end
classDef muse fill:#21143d,stroke:#a78bfa,stroke-width:2px,color:#ffffff;
classDef bridge fill:#172554,stroke:#60a5fa,stroke-width:2px,color:#ffffff;
classDef edge fill:#132e2a,stroke:#34d399,stroke-width:2px,color:#ffffff;
classDef neural fill:#3b1d0b,stroke:#fb923c,stroke-width:2px,color:#ffffff;
class MUSE,SAGNA,ASTRO muse;
class HARVEST,RECEIVER bridge;
class STATE,HUD,TTRPG,DIV,NARRATIVE edge;
class NEURAL,AUDIO neural;
The system should keep one clear division of labor:
Thinks.
reasoning
planning
engine execution
dialogue
orchestration
Translates.
structured state
network transport
validation
routing
Presents.
graphics
telemetry
input
local state
physical interface
Offloads.
supported neural inference
speech
embeddings
image processing
future edge models
Together:
MUSE
│
▼
STATE
│
▼
NETWORK
│
▼
RASPBERRY PI
│
├── VISUAL HUD
└── EDGE AI
│
▼
PHYSICAL PRESENCE
Muse remains the intelligence. The Raspberry Pi becomes her persistent physical window into the workspace.