Satellites: One Brain, Many Speakers¶
In a nutshell
A satellite deployment puts one capable server in the middle and several small
devices around the house. The server runs the messagebus, ovos-core, and skills.
Each satellite only listens and speaks. This page is the build guide. It has two
paths: a raw shared bus for a trusted LAN, and HiveMind for anything that needs
auth or crosses an untrusted network. Terminology: this page says satellite.
HiveMind pages say client or node for the same thing: the device that
connects to the central server.
flowchart TD
subgraph Server
BUS[messagebus]
CORE[ovos-core<br/>+ skills]
end
subgraph Satellite
LIS[listener<br/>STT client]
AUD[audio<br/>TTS client]
end
LIS --> BUS
AUD --> BUS
CORE --> BUS
Diagram: the server runs the messagebus and ovos-core with skills. Each satellite runs a listener and an audio service, and both connect back to the server's messagebus.
Raw shared bus or HiveMind: pick one¶
| Raw shared bus | HiveMind | |
|---|---|---|
| Security | None. No login, no encryption. Anyone who reaches the port controls the assistant. | Access-key auth plus an encrypted protocol. |
| Isolation between clients | None. Every satellite sees every message on the bus. | Per-client credentials and a PolicyChain that can allow or block message types per client. |
| Session handling | One shared ovos-core. All satellites drive the same session and conversation state. |
Same shared agent by default, but HiveMind tracks each client's own connection and permissions. |
| Effort to set up | Low. Edit websocket.host in each service's config and start the services. |
Higher. Provision a client, run hivemind-core, set up identity on each satellite. |
Keep the raw shared bus on a trusted LAN only. Never forward its port to the internet. Use HiveMind for anything off your own network, or when you need to tell satellites apart.
Both paths on this page share one ovos-core brain across every satellite. If you instead
want each device to run its own independent ovos-core and only share the heavy STT/TTS
inference, see Thin clients + a shared speech backend
below.
Build walkthrough A: raw shared bus¶
Run this only on a network you fully trust. Every service that joins this bus gets full control of the assistant, with no login check.
Services and placement¶
| Host | Services |
|---|---|
| Server | ovos-messagebus, ovos-core |
| Each satellite | ovos-dinkum-listener, ovos-audio, ovos-PHAL |
Server config¶
On the server, bind the bus to all interfaces so satellites can reach it:
Satellite config¶
On each satellite, point websocket.host at the server's LAN address:
Replace 192.168.1.10 with the server's real address. Set this in every satellite's
mycroft.conf, and in the server's own file if any service other than the bus itself
also needs to find it.
Running this over containers instead of bare metal? See
Running OVOS in Containers: Server/satellite split
for matching server and satellite docker-compose.yml files.
One shared session, not one per satellite
ovos-core, ovos-audio, and ovos-dinkum-listener each assume they are the only
instance of that service on the bus. Two listeners on two satellites share the same
ovos-core and the same converse/session state. A conversation started in the kitchen
can carry into the bedroom's next utterance. This is not a bug to patch around with
session_id filtering in a skill. It is a property of the shared bus. See
Composable Deployments: services are implicit singletons per bus.
Language is the exception: it is not part of the shared state. Each satellite's own
listener/STT configuration decides the stt_lang that rides its utterances (see
Per-satellite language below), so two rooms can listen in two
languages while still sharing one ovos-core.
Per-satellite language¶
To make one satellite listen and answer in a different language, change that satellite's
config, not the server's. Set lang in the satellite's own mycroft.conf (its listener and
STT plugin follow it), or use a per-wake-word stt_lang override to bind a language to a
specific wake word: say "ok computer" for English, "olá computador" for Portuguese, on the
same box (see Wake Word Plugins). The language then rides each
utterance as stt_lang / request_lang, which sit above the server's configured default in
the resolution order (see Language Selection). So the server answers in
the language the satellite heard. A satellite that answers in the wrong language almost
always means its own config fell through to the server's default. For HiveMind mic-satellites
(hivemind-mic-satellite), the per-connection language knobs live in that project's own
documentation. This manual covers only the OVOS-side integration.
Defaults assume localhost
websocket.host defaults to 127.0.0.1 everywhere. A satellite left on the default
will start and look healthy, then never reach the server. Set the host explicitly on
every satellite and check it after any config reset. See
Composable Deployments: defaults assume localhost.
Build walkthrough B: HiveMind¶
Use HiveMind when satellites need auth, need to cross an untrusted network, or need to be told apart from one another. This manual covers only the OVOS side. HiveMind is a separate project with its own protocol and docs.
- Install and run the server:
pip install hivemind-core, thenhivemind-core listen. By default it bridges to a localovos-corethroughhivemind-ovos-agent-plugin. - Provision each satellite with its own access key:
hivemind-core add-client. A new client's message-type whitelist is empty and denies everything, so grant what the satellite sends and what it receives withhivemind-core allow-msg, or it will connect and stay mute. See HiveMind agents for the list. -
On each satellite, install the client and store the key. The distribution is
hivemind-bus-client. The repository ishivemind-websocket-client. The plain PyPI "stable" release (0.4.4) predates the current protocol, so--preis required.pip install --pre hivemind-bus-client hivemind-client set-identity --key <access_key> --password <password> --host <hostname-or-ip> --port 5678set-identitywith no arguments raises: it needs at least one of--key,--passwordor--siteid(--hostalone does not satisfy the check). -
Verify with
hivemind-client test-identitybefore trusting the link. -
For a mic-only satellite that leaves STT/TTS to the server, use
hivemind-mic-satelliteinstead of running a full listener/audio pair locally. Three things the package name does not tell you:- The server needs
pip install hivemind-audio-binary-protocol. Plainhivemind-coredoes no audio processing, so without it the satellite streams audio into a void. - The satellite's run command is
hivemind-mic-sat(after the sameset-identitystep as above, or pass--key/--password/--hostdirectly). - The package pulls in no microphone or VAD plugin, so install them yourself:
pip install --pre ovos-microphone-plugin-alsa ovos-vad-plugin-silero. Without them the satellite connects to the hub and only then fails, withTypeError: 'NoneType' object is not callablefrom the microphone and VAD factories once their default fallback chains find nothing installed. Silero runs on onnxruntime and needs no torch, so the pair stays light enough for a Pi Zero.
The device runs only a microphone and VAD plugin. Cheap hardware like a Pi Zero works for this shape, while wake word, STT and TTS all happen server-side. That is a satellite, not a full install: a Pi Zero cannot run the whole local stack, which Installing raspOVOS covers. The trade-off: with no local wake word, every VAD-detected voice segment streams upstream, costing bandwidth and putting the full STT load on the server. This is fine for a homelab with a handful of devices. For a local wake word on slightly stronger hardware, use HiveMind-voice-relay instead.
- The server needs
One firewall note for the OVOS host: hivemind-core listen binds websocket 5678 on
0.0.0.0, and its default config declares a second HTTP listener on 5679 that starts
only where hivemind-http-protocol is installed. Where it is, a firewall rule covering
only 5678 leaves the second one open. The mDNS/UPnP presence announcements need the
optional extra (pip install --pre "hivemind-core[presence]>=4.8.0a1"); with the plain install above, the
server silently makes no announcements. Changing hosts, ports, or those presence
announcements is HiveMind server configuration, covered by the upstream
HiveMind community docs, along
with the identity/credential setup on the satellite side.
Full steps, permission model, and satellite/client packages: see Remote Agents with HiveMind.
Thin clients + a shared speech backend¶
A common fleet topology is several low-power "thin" devices that each run a full
ovos-core (with the bus, listener and audio services), all pointed at one shared, more
capable machine that does the actual speech-to-text and text-to-speech work over HTTP (see
STT server and TTS server). Only the heavy STT/TTS
inference is centralized. Each device keeps its own core, its own session, and its own skills.
This is a sketch, based on the real container images published by
ovos-docker. For the client-side config keys
and a worked example on a single LAN IP, see
privacy-security: point a device at your own LAN servers.
Not the shared-brain pattern
This is not the shared-brain pattern described above. Every device here keeps its own
ovos-core, so each room is an independent assistant that shares only speech inference.
For one shared brain and session, use the raw shared bus or HiveMind walkthroughs above
instead.
These ports are unauthenticated plain HTTP
8080 and 9666 below serve unauthenticated plain HTTP by default. Never expose them
to untrusted networks. Add API keys and/or put a reverse proxy in front of them before
they leave localhost. See tts-server: Tips & Caveats for
how.
services:
ovos_stt_server:
image: docker.io/smartgic/ovos-stt-server-onnx-asr:${VERSION} # or your own build, see stt-server.md
ports: ["8080:8080"] # UNAUTHENTICATED — do not expose beyond localhost/VPN
ovos_tts_server:
image: docker.io/smartgic/ovos-tts-server-piper:${VERSION} # or your own build, see tts-server.md
ports: ["9666:9666"] # UNAUTHENTICATED — do not expose beyond localhost/VPN
The speech-server image name encodes the engine baked into it: ovos-stt-server-onnx-asr,
ovos-stt-server-onnx-asr-cuda, ovos-tts-server-piper, ovos-tts-server-kokoro,
ovos-tts-server-phoonnx and so on. Pick the variant carrying the plugin you want. There is
no generic image that loads an arbitrary engine at runtime.
services:
ovos_messagebus:
image: docker.io/smartgic/ovos-messagebus:${VERSION}
network_mode: host # shares host loopback AND LAN interfaces with every container on this host
ovos_listener:
image: docker.io/smartgic/ovos-listener:${VERSION}
network_mode: host
depends_on: [ovos_messagebus]
devices: ["/dev/snd"] # microphone passthrough
volumes:
- ${XDG_RUNTIME_DIR}/pulse:${XDG_RUNTIME_DIR}/pulse:ro
- ~/.config/pulse/cookie:/home/${OVOS_USER}/.config/pulse/cookie:ro
- ~/.config/mycroft:/home/${OVOS_USER}/.config/mycroft
# set stt.module = ovos-stt-plugin-server and stt.ovos-stt-plugin-server.urls to the
# central STT server above, in the mounted /home/${OVOS_USER}/.config/mycroft/mycroft.conf
ovos_audio:
image: docker.io/smartgic/ovos-audio:${VERSION}
network_mode: host
depends_on: [ovos_messagebus]
devices: ["/dev/snd"] # speaker passthrough
volumes:
- ${XDG_RUNTIME_DIR}/pulse:${XDG_RUNTIME_DIR}/pulse:ro
- ~/.config/pulse/cookie:/home/${OVOS_USER}/.config/pulse/cookie:ro
- ~/.config/mycroft:/home/${OVOS_USER}/.config/mycroft
- ovos_tts_cache:/home/${OVOS_USER}/.cache/mycroft/tts
# set tts.module = ovos-tts-plugin-server and tts.ovos-tts-plugin-server.host to the
# central TTS server above, in the mounted /home/${OVOS_USER}/.config/mycroft/mycroft.conf
ovos_core:
image: docker.io/smartgic/ovos-core:${VERSION}
network_mode: host
depends_on: [ovos_messagebus]
depends_on here only orders container start. It starts ovos_messagebus first, but does
not wait for it to actually accept WebSocket connections before starting the services listed
after it. Use the readiness probe
(or Compose's own depends_on: condition: service_healthy against a healthcheck that runs it)
as the real gate if a dependent service needs the bus to be live, not just the container to
exist.
Audio devices and sockets have to be handed to the containers that touch them: the listener
needs the microphone, the audio service needs the speaker, and both need the host's PulseAudio
or PipeWire socket. Anything you want to survive a container rebuild (downloaded models, the
TTS cache, listener recordings, local state) belongs in a named volume rather than the
container filesystem. The
reference compose file
in ovos-docker is the fuller version of the sketch above, with every volume, device,
resource limit and healthcheck spelled out.
network_mode: host shares the loopback across every container on that host
With network_mode: host, 127.0.0.1 is the host's loopback, not a container-private
one. Every container and process on that host shares it. A bus bound to 127.0.0.1 is
reachable by any of them, not just ovos_messagebus.
"Bound to localhost" no longer means "only reachable by this one process" once host networking is in play. Treat the whole host as the trust boundary, not the individual container.
Host networking also exposes any service that binds 0.0.0.0 straight onto the LAN, not
just the host's own loopback. gui_websocket.host ships as 127.0.0.1. If you widen
it to 0.0.0.0 for a remote display, or an older config still carries that value, then
with network_mode: host the GUI WebSocket lands on the LAN, not just the device. Keep
the loopback default unless a remote display client genuinely needs LAN access.
Each thin client still runs its own bus, listener, audio and core. Only the heavy STT/TTS inference is centralized. This is the same pattern as Wyoming bridges and HiveMind, just wired directly through the companion server plugins instead of a satellite protocol. See Composable Deployments for the general principle of splitting OVOS across machines.
Troubleshooting¶
Satellite can't connect to the server.
- Check the server's firewall allows inbound traffic on port
8181(raw bus) or port5678(the HiveMind websocket listener), plus5679where the HTTP listener is running, from the satellite's address. - Confirm
websocket.hoston the satellite points at the server's real LAN address, not127.0.0.1. - For HiveMind, run
hivemind-client test-identityon the satellite. A hang or error usually means the server is not running, the port is wrong, or the access key does not match one printed byhivemind-core add-client.
Audio plays on the wrong box.
- On the raw shared bus, every
ovos-audioinstance on the bus can react to the samespeakmessage. Check that only oneovos-audioprocess is running per satellite, and that satellites are not accidentally sharing oneovos-audioinstance across rooms. - Confirm each satellite's own
websocket.hostpoints at the intended server, not at another satellite left on a stale config.
Read next: HiveMind Agents Related: Composable Deployments · Production Operations · messagebus Service · Security Model