Systemd — how TINY boots·
⏱ 4 min · what actually runs on the CM4
TINY is not one process. On the robot it is one system unit (Pollen's daemon), one system timer
(the daemon watchdog) and eight user units that survive reboots because pollen has linger enabled.
This page is the inventory as it stands on the robot on 2026-09-17 (systemctl --user list-units over
ssh reachy) — the Makefile installs the three-persona subset on any machine.
The boot chain·
flowchart TD
P["power on · Debian 13 · CM4"] --> DAE["reachy-mini-daemon.service (system, User=pollen)<br/>launcher.sh → :8000 · starts ASLEEP (--no-wake-up-on-start)"]
DAE --> WD["reachy-daemon-watchdog.timer (system)<br/>OnBootSec=180 · every 30 s → restart daemon after 3 missed /api/daemon/status"]
P --> L["linger=yes → default.target for pollen"]
L --> WAKE["tiny-wake · oneshot, sleep 8 → enable_motors + wake_up, os._exit"]
L --> TTS["tiny-tts · Piper on 127.0.0.1:5002"]
L --> V["tiny-voice · voice_listener.py"]
L --> T["tiny-telegram · telegram_listener.py"]
L --> K["tiny-thinker · thinker_loop.py"]
L --> DASH["reachy-dashboard · dashboard.server :8097"]
L --> TUN["reachy-tunnel · cloudflared → reachy.cagatay.my"]
L --> MHS["tiny-mhs · zenoh mount (gated on broker reachability)"]
DAE -. After= .-> WAKE & V & T & K & DASH
TTS -.-> V & DASH
DASH --> TUN
The units, as installed·
| unit | scope | state | ExecStart | why it exists |
|---|---|---|---|---|
reachy-mini-daemon |
system | active, enabled | …/reachy_mini/daemon/app/services/wireless/launcher.sh |
Pollen's daemon — owns motors, camera, mics, speaker; REST + WS on :8000. Drop-in nofile.conf raises LimitNOFILE to 65536 (it hit the 1024 default on 2026-09-17, see Perception → pressure). |
reachy-daemon-watchdog.timer |
system | active | /usr/local/bin/reachy-daemon-watchdog every 30 s, first run 180 s after boot |
Restarts the daemon after three consecutive failed GET /api/daemon/status. |
tiny-wake |
user | oneshot, RemainAfterExit |
python ~/tiny-wake.py after sleep 8 (source: scripts/systemd/robot/tiny-wake.py) |
The daemon boots with motors off; this energises them once and hard-exits so no SDK client socket lingers. It also enforces a boot volume floor: alsa-state persists the hardware mixer across power cycles, so a robot unplugged while muted (reachy_volume silent) would boot deaf — if the daemon reports volume < REACHY_BOOT_VOLUME_MIN (default 60) it is raised to that value. |
tiny-tts |
user | active | ~/tts-venv/bin/python ~/tiny-tts/tiny_tts_server.py |
Offline Piper TTS (en_US-lessac-medium, ~2.6 s per sentence) — what the text personas and the dashboard Say use. Own venv because /venvs/apps_venv is Pollen's. |
tiny-voice |
user | active | /venvs/apps_venv/bin/python voice_listener.py |
Voice persona — OpenAI Realtime, Restart=on-failure, RestartSec=8. |
tiny-telegram |
user | active | … telegram_listener.py |
Telegram persona. |
tiny-thinker |
user | active | … thinker_loop.py |
30 s heartbeat persona. The dashboard's demo mode stops/starts exactly this unit. |
reachy-dashboard |
user | active | /venvs/apps_venv/bin/python -m dashboard.server |
The cockpit on :8097. Restart=always, Nice=5, TimeoutStopSec=8, KillMode=mixed. Env: repo .env plus ~/.reachy-dashboard.env (token, passkey RP id, origin). |
reachy-tunnel |
user | active | cloudflared tunnel --config ~/.cloudflared/config.yml run reachy |
Named Cloudflare tunnel; ingress reachy.cagatay.my → http://127.0.0.1:8097, everything else 404. Restart=always. |
tiny-mhs |
user | gated | python -m demo.mounts.tiny_mount --broker zenoh://…:7447 |
Zenoh mount for the MHS demo. Drop-in broker-gate.conf runs nc -z first and refuses to start when the broker is unreachable — expected to sit in activating (auto-restart) off-site. |
Four of them (tiny-voice, tiny-telegram, tiny-thinker, reachy-dashboard) carry a tiny-mcp.conf
drop-in — EnvironmentFile=-%h/.tiny-mcp.env — that gives the persona a tiny.technology token for
use_device. Remove the file and the persona starts without fleet tools; nothing else changes.
Lessons the units encode
After=reachy-mini-daemon.serviceis not enough. The daemon answers HTTP a few seconds before the motors are ready;tiny-wakesleeps 8 s and retries 20 times for that reason.- Never
systemctl stop reachy-dashboardduring a demo —restartit. uvicorn waits on open MJPEG clients; beforetimeout_graceful_shutdown=2+TimeoutStopSec=8a stop took ~90 s and left the cockpit dark. Astopjob never auto-restarts; arestartdoes. - Every persona restart used to kill the camera. Any
ReachyMini(media_backend="no_media")client releases the daemon's media; the dashboard now re-acquires it (POST /api/media/acquire) within a second — see Perception. - A dead SDK client stays dead. After a daemon restart every persona said "Lost connection with
the server" for 12 minutes until
tools/_reachy_common.get_mini()learned to rebuild a client whose_is_aliveis false.
Day-to-day commands·
ssh reachy # pollen@reachy-mini.local, key auth
systemctl --user list-units 'tiny-*' 'reachy-*' # the eight, with state
systemctl --user restart tiny-voice # after editing .env or a prompt
systemctl --user restart reachy-dashboard # after dashboard/*.py changes (frontend: no restart)
journalctl _SYSTEMD_USER_UNIT=tiny-thinker.service -f # a persona's log (journalctl --user finds no files here)
curl -s localhost:8000/api/daemon/status # the daemon, unauthenticated, loopback only
curl -s localhost:8097/api/health | jq .pressure # fds / CLOSE-WAIT on the daemon, from the dashboard
sudo systemctl restart reachy-mini-daemon # last resort; personas reconnect on their own
Two things that are NOT units
The daemon's own reachy-mini-bluetooth.service (Pollen's GATT provisioning) is untouched by us.
And there is no Docker on the CM4 — docker-compose.yml in the repo is for a laptop next to a
Reachy Mini Lite (see Docker). Disk on the CM4 is at 89 % — do not create new venvs there.
The repo way·
On a fresh machine the Makefile installs the persona subset (the dashboard, tunnel and TTS are set up by hand — Deploying to the robot):
make venv # .venv + requirements (+ requirements-robot.txt, best effort)
make install-bare-services # copies scripts/systemd/tiny-{voice,telegram,thinker}.service
# → ~/.config/systemd/user, enable-linger, enable --now
make service-status # systemctl --user status tiny-*.service
The unit files in scripts/systemd/ point at .venv; the copies in scripts/systemd/robot/ are the
CM4 variants that use /venvs/apps_venv and /home/pollen/tiny-the-reachy. make install-compose-service
is the Docker alternative for a laptop.
Linger
Both install targets run loginctl enable-linger $USER. Verify with
loginctl show-user $USER -p Linger → Linger=yes. Without it the user units stop when the last
SSH session ends — which looks exactly like "the robot forgot everything overnight".