Skip to content

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:

{
  "websocket": {
    "host": "0.0.0.0",
    "port": 8181,
    "route": "/core"
  }
}

Satellite config

On each satellite, point websocket.host at the server's LAN address:

{
  "websocket": {
    "host": "192.168.1.10",
    "port": 8181,
    "route": "/core"
  }
}

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.

  1. Install and run the server: pip install hivemind-core, then hivemind-core listen. By default it bridges to a local ovos-core through hivemind-ovos-agent-plugin.
  2. 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 with hivemind-core allow-msg, or it will connect and stay mute. See HiveMind agents for the list.
  3. On each satellite, install the client and store the key. The distribution is hivemind-bus-client. The repository is hivemind-websocket-client. The plain PyPI "stable" release (0.4.4) predates the current protocol, so --pre is required.

    pip install --pre hivemind-bus-client
    hivemind-client set-identity --key <access_key> --password <password> --host <hostname-or-ip> --port 5678
    

    set-identity with no arguments raises: it needs at least one of --key, --password or --siteid (--host alone does not satisfy the check).

  4. Verify with hivemind-client test-identity before trusting the link.

  5. For a mic-only satellite that leaves STT/TTS to the server, use hivemind-mic-satellite instead 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. Plain hivemind-core does no audio processing, so without it the satellite streams audio into a void.
    • The satellite's run command is hivemind-mic-sat (after the same set-identity step as above, or pass --key/--password/--host directly).
    • 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, with TypeError: 'NoneType' object is not callable from 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.

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.

docker-compose.yml — central speech backend
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.

docker-compose.yml — thin client (per device)
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 port 5678 (the HiveMind websocket listener), plus 5679 where the HTTP listener is running, from the satellite's address.
  • Confirm websocket.host on the satellite points at the server's real LAN address, not 127.0.0.1.
  • For HiveMind, run hivemind-client test-identity on 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 by hivemind-core add-client.

Audio plays on the wrong box.

  • On the raw shared bus, every ovos-audio instance on the bus can react to the same speak message. Check that only one ovos-audio process is running per satellite, and that satellites are not accidentally sharing one ovos-audio instance across rooms.
  • Confirm each satellite's own websocket.host points 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