PyPI package: https://pypi.org/project/ubo-app/2.1.0
Changes:
Ubo App 2.1
Version 2.0 split the core from its UI and made every client a thin gRPC
renderer. 2.1 is what that architecture was for: the client stack now reaches
all the way down to microcontrollers, and the pod grew into a proper
Home Assistant citizen — Wyoming voice satellite, MQTT sensor bridge, and a
Compose add-on ecosystem.
These notes cover the changes on top of development after v2.0.0.
Headline: a C/LVGL client and ESP32 satellites
A second, native GUI client — written in C on LVGL — now speaks the same view
stream as the Kivy client. It runs on three targets:
- Desktop (SDL) for development,
- Raspberry Pi over an ST7789 SPI LCD, with backlight control tied to
display blanking, - ESP32 boards as standalone satellites — the Waveshare ESP32-C6 AMOLED
and the ESP32-S3-BOX-3.
Select it on the Pi with UBO_GUI_BACKEND=lvgl. It renders the full view
taxonomy: menus on the headed slot-grid model, menu items with L1/L2/L3 alignment, prompts, headings, QR and generic render widgets, the image
viewer, live frame streams, the chat overlay, and recording/progress/light
status-bar indicators — with local interaction and an honest disconnect
countdown when the core goes away. The ESP32 font carries the full icon set
the menus actually use, so no menu entry renders as a blank glyph.
Transports. Beyond native gRPC, the client added gRPC-Web (with event
subscriptions that reconnect after stream drops), tcp-lite — a raw-TCP
transport built for constrained clients, now the default — and
PPP-over-USB, so an ESP32 satellite can run tethered with no network at all.
The ESP32 satellites are full participants, not display heads: touch input
with interactive volume, ES8311 speaker playback and hold-to-talk mic
streaming, on-device wake word via esp-sr WakeNet with an AFE capture path
for the S3-BOX-3's two-mic array, WiFi captive-portal provisioning, and
low-res chunked frame streaming for the viewfinder and video playback. CI now
builds the firmware for both boards and publishes a web-flashable merged
.bin.
Details: the LVGL client README
for the renderer and transports, and the
ESP32 firmware README
for board support, flashing and provisioning.
Home Assistant
- Wyoming service — the pod is now a Wyoming voice satellite with
on-device wake word, nested under a Satellites settings container, with
combinable access policies rather than replace-on-add
(README). - MQTT service — a Home Assistant MQTT bridge with a bundled,
authenticated broker whose credentials the user owns
(README).
Home Assistant can also push text for the pod to read aloud, so an
automation can speak through the pod without going near the assistant. - I2C sensor platform — 15 sensor models (2 on-board, 13 STEMMA QT
parts you plug in and discover with a Refresh) exposing 35 entities, published
to Home Assistant withkey/device_classmetadata, rate-limited per sensor
with back-off on failure. The full list is in the
sensors service README. - Compose add-on ecosystem for running Home Assistant itself, moved from
macvlan to host networking so LAN discovery actually works.
Assistant and wake word
- Turn detection hardened — no listen/talk overlap, phrase and long-silence
end-of-turn, and push-to-talk finalizing on release. - New providers — Deepgram TTS, Mistral STT + TTS, ElevenLabs STT,
Moonshine local STT, Venice AI image generation, plus per-provider cloud
TTS voice selection. - Model management from the UI — download and delete local models,
one-step Vosk download from Voice Shortcuts. - System prompts the user can add and switch between.
- Screen reader routed through the assistant's TTS pipeline, with a
"Set Up Engine" shortcut in its menu. - Wake words — a new OpenWakeWord engine, a microWakeWord engine with a
model catalog and custom-model upload, and user-editable wake phrases from
the web UI. See the
speech-recognition README. - Voice shortcuts unified with the assistant, made location-aware, with
per-mode wake switches. - Memory: local TTS engines now load on selection and are freed on deselect.
- A provider that fails outright — an expired key, an exhausted balance — now
reports the error instead of returning an empty answer.
Apps, kiosk and remote access
- Browser kiosk service — multiple named dashboards on the pod's HDMI
outputs, driven by IR-bindable navigation keys, with every output selection
exposed as a bindable action
(README). - Tailscale app with QR sign-in.
- MCP extracted into a dedicated
090-mcpgateway service, provisioning
uv/uvxandnode/npxruntimes for stdio servers and reporting
per-server health
(README). - Docker health is now honest: a crash is told apart from a stop and said
so, tiles are tinted by actual state, the failing service in a stack is
named, and every client can show an app's logs. - Hermes setup gained OAuth provider configuration from the pod, offered
API keys, and a dashboard gated behind the sign-in it collects itself. - Bindable actions can take parameters.
Web UI
The forced main-menu redirect is gone — the web UI opens on a home
dashboard with date, seven-segment clock, and app-status tiles. It ships a
production bundle, stops re-rendering the whole dashboard on every update,
reconnects to the store after a core restart, and streams over fetch instead
of grpc-web XHR, which was retaining the whole stream and OOM-ing the kiosk
renderer. Browser file uploads travel the store rather than reaching across
services, which is what made them fail from the web UI specifically.
Networking, audio and hardware
- mDNS discovery — the gRPC control API is advertised as
_uborpc._tcp,
re-advertised when the LAN address appears or changes. This is what the
experimental iOS, watchOS, Android and Wear OS clients browse for; the README
now documents them and the ESP32 satellites. - Audio — playback output selection routed through PipeWire ports,
following the lineout jack; the WM8960 HAT pinned as WirePlumber's default. - WiFi — scan-based setup UX, an internet-sharing hotspot, and a
wifi-owned hotspot lifecycle. Band-split access points that advertise one
SSID under several profiles no longer stall the WiFi menu. - Camera — autofocus probed rather than inferred from camera type, Arducam
driver entries restored, and recovery from a read-only/boot/firmware. - Localization owns the device's clock and date, and a server-driven
Units of Measurement setting converts values on live readings. Under
load it no longer loses the clock, date, weather and location for the rest
of the session.
Quality
Fast unit coverage was expanded substantially — the full reducer decision
tables for keypad, camera, infrared, Docker, wifi, MCP, filesystem, chat,
settings, assistant and speech-recognition, plus gRPC serialization edges and
navigation event chains. Provider E2E tests now run on Pi pods with masked
secrets, and a closed-loop audio harness scores satellite microphones against a
reference control. Every core service gained a developer-internals README, and
CONTRIBUTING.md was added.
Shutdown is also cleaner: a service that was registered but never started no
longer strands every service after it in the stop sequence, turning what should
be a clean shutdown into a partial one.
Upgrade notes
- gRPC LAN access is now on by default, and Envoy starts at boot. A pod on
Ethernet is discoverable by the mobile clients with no setup step. This
does not add authentication — the exposed API stays unauthenticated, as
do tcp-lite (:50054), the web UI (:4321) and the MCP gateway (:4322),
which were already on by default. The shipped default is now open on the LAN;
turn it off under settings if that is not what you want. - The lite image grew from 6.5 GB to 8 GB to fit the heavier assistant
virtualenv. - The LVGL client's default transport is tcp-lite, not gRPC-Web.
Note than GitHub doesn't allow assets bigger than 2GB in a release. Due to this, the files bigger than 2GB have been split into 2GB chunks. You can join them using the following command:
cat [[filename]]_* > [[filename]]