
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
| TAG | ROLE | HARDWARE | LOCATION |
|---|---|---|---|
| CORE | server · 4 containers | Minisforum UM790 · Ubuntu 24.04 · Docker | office |
| BRIDGE | kiosk · voice · comms | Raspberry Pi 5 · 24" touch | the bridge |
| READY ROOM | kiosk · comms | Raspberry Pi 4 · 15.6" touch | ready room |
| LAMP ×1 | consumer · presence | ESP32-DevKitC · 24-LED WS2812 · encoder | kid's room |
| WX-01 | producer · T, RH, pressure | XIAO ESP32S3 · BME280 | exterior outlet |
| SOIL-01 | producer · two-hop | XIAO ESP32-C6 · capacitive probe · 18650 | at the tree |
| SOIL RELAY | ESP-NOW to HTTP | ESP32-DevKitC | indoors, mains |
05 · Integrations
| SOURCE | CADENCE | FEEDS |
|---|---|---|
| OpenWeatherMap One Call | 15 min | Home, Conditions, Solar Dynamics |
| Radar tiles | 10 min | Conditions |
| Celestrak TLEs | 6 h | ISS Tracking, via SGP4 on the server |
| Sun and moon | 1 h | Solar Dynamics |
| NASA APOD | 24 h | Astronomy |
| iCloud shared calendar and album | 15 min (check) | Home, Comms |
| Wyze cameras | on tap | Tactical |
| RSS feeds | 1 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
| VERSION | DATE | NOTE |
|---|---|---|
| V1 | 2026-04-28 | One Pi. FastAPI, SQLite, and React on a 24" touchscreen. |
| V2 | 2026-06-20 | LCARS layout families, each with its own tokens and primitives. |
| V3 | 2026-06-23 | Hub migration. Backend and database move to the server; the Pi becomes a kiosk. |
| V4 | 2026-07-08 | Comms. Mumble intercom and Subspace Beacons on one event stream. |
| V5 | 2026-07-18 | The fleet. Terminal profiles, hardware nodes, one-command onboarding. |

