Build Sheets

01 · Brief

“COMPUTER… ALERT LOWER DECKS”. The LED ring wall mount glows red in the kid’s room. The computer responds “Priority one message sent”. And the best part? The kid appears downstairs.

It started as one Raspberry Pi taped to a monitor and grew into a small distributed system: a containerised home server, wall terminals in two rooms, and a soon to be fleet of microcontroller nodes designed, wired, and flashed from the laptop. I wanted a home smartboard that I didn’t have to pay a subscription for, and it slowly grew into a private home system. The whole thing has voice control, 100% private and hosted on the home server. Siri and Alexa will never hear a word! We can check the weather, share a calendar, get notified when the ISS flies over, track large family projects, and “Hail” each other from our terminal kiosks.

Privacy is the point, and the LCARS interface makes it so much fun. Star Trek audio files have been downloaded so the computer can actually talk back! Oh, and it’s got memes.

Each detail sheet below is one hardware node built for the system. The kiosk screens are the software half, and the equipment schedule lists every host and node in the set.

02 · Detail sheets

03 · Screens

04 · Equipment schedule

TAGROLEHARDWARELOCATION
COREserver · 4 containersMinisforum UM790 · Ubuntu 24.04 · Dockeroffice
BRIDGEkiosk · voice · commsRaspberry Pi 5 · 24" touchthe bridge
READY ROOMkiosk · commsRaspberry Pi 4 · 15.6" touchready room
LAMP ×1consumer · presenceESP32-DevKitC · 24-LED WS2812 · encoderkid's room
WX-01producer · T, RH, pressureXIAO ESP32S3 · BME280exterior outlet
SOIL-01producer · two-hopXIAO ESP32-C6 · capacitive probe · 18650at the tree
SOIL RELAYESP-NOW to HTTPESP32-DevKitCindoors, mains

05 · Integrations

SOURCECADENCEFEEDS
OpenWeatherMap One Call15 minHome, Conditions, Solar Dynamics
Radar tiles10 minConditions
Celestrak TLEs6 hISS Tracking, via SGP4 on the server
Sun and moon1 hSolar Dynamics
NASA APOD24 hAstronomy
iCloud shared calendar and album15 min (check)Home, Comms
Wyze camerason tapTactical
RSS feeds1 h (check)Library Access

06 · Source

event_bus.py

import asyncio
from typing import Any


class EventBus:
    """In-process pub-sub for server-sent events.

    One asyncio.Queue per subscriber. publish() fans out to all subscribers.
    A single dashboard client is expected; no external broker needed.

    This is intentionally feature-agnostic: voice, timers, alerts, and any
    future server-originated event all share one instance (see `event_bus`
    below) so the frontend can consume them on a single typed event stream.
    """

    def __init__(self) -> None:
        self._subscribers: list[asyncio.Queue[tuple[str, dict[str, Any]]]] = []

    def subscribe(self) -> asyncio.Queue[tuple[str, dict[str, Any]]]:
        q: asyncio.Queue[tuple[str, dict[str, Any]]] = asyncio.Queue(maxsize=64)
        self._subscribers.append(q)
        return q

    def unsubscribe(self, q: asyncio.Queue[tuple[str, dict[str, Any]]]) -> None:
        if q in self._subscribers:
            self._subscribers.remove(q)

    async def publish(self, event_type: str, payload: dict[str, Any]) -> None:
        for q in list(self._subscribers):
            try:
                q.put_nowait((event_type, payload))
            except asyncio.QueueFull:
                # Slow subscriber: drop oldest, then add. SSE clients reconnect.
                try:
                    q.get_nowait()
                except asyncio.QueueEmpty:
                    pass
                q.put_nowait((event_type, payload))


# Shared app-level singleton. Import this everywhere that publishes or
# subscribes — do NOT construct a second EventBus in app code, or publishers
# and the SSE endpoint will be wired to different fan-out lists.
event_bus = EventBus()

07 · Lessons

The frontend never talks to the internet. Every external call is proxied through the server and cached to SQLite with a per-endpoint TTL, so the wall keeps showing useful data when the connection hiccups.

One response envelope for every endpoint, data plus cached_at plus source, means the UI always knows what it is holding and how old it is.

One server-sent event stream carries voice, beacon, and comms events, and no feature flag may switch it off. Three subsystems depend on it.

The open source model used for wakeword capture misfires often, future retraining planned, current mitigation is to delay confirmation beep until a recognized command is confirmed (and not just wake word).

08 · Revisions

VERSIONDATENOTE
V12026-04-28One Pi. FastAPI, SQLite, and React on a 24" touchscreen.
V22026-06-20LCARS layout families, each with its own tokens and primitives.
V32026-06-23Hub migration. Backend and database move to the server; the Pi becomes a kiosk.
V42026-07-08Comms. Mumble intercom and Subspace Beacons on one event stream.
V52026-07-18The fleet. Terminal profiles, hardware nodes, one-command onboarding.