Bridges the peripherals inside an RSC chassis (flipper servos, an addressable LED ring, an arcade button, audio, and the front-panel display) to clients on the local network, over an authenticated WebSocket.
Runs on the Raspberry Pi 4 inside the chassis. Developable on any laptop through a fake hardware backend implementing the same contract, so no robot is needed to work on the protocol, the peripherals or a client.
RSC is the Robot Study Companion, a stationary desktop robot for research use. The front panel is a CYD ("cheap yellow display"), an ESP32 board with a touchscreen, running its own firmware and talking to this daemon over a serial link.
Everything unfinished is listed in the documentation index rather than here.
docs/pi-setup.md is the real procedure. Several steps are
outside this repository: systemd units, group membership, an audio
configuration file, and optionally a polkit rule for shutdown. Skipping
them produces a daemon that starts and then cannot reach its hardware.
The Python part:
python3 -m venv --system-site-packages ~/rsc-env
~/rsc-env/bin/pip install -e ".[pi]"--system-site-packages matters: lgpio and gpiozero come from apt on a Pi
and are awkward to build from source. The [ring] extra is separate because
the ring helper runs as a different process, possibly under a different
interpreter.
One virtualenv only. Do not also pip install -e . on the robot. A package
installed into site-packages shadows the source tree, and the daemon will run
the installed copy while you edit the other.
git clone https://github.com/RobotStudyCompanion/host.git
cd host
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"The fake backend needs no hardware and no extras. Three packages are
mandatory: websockets, pydantic and numpy. Everything else is optional
and degrades with a log line rather than a crash.
pytesttests/test_config.py currently fails in five places. Four are tests that
predate a deliberate configuration change and need updating; one is a genuine
missing validation. Not yet done.
# Laptop: fake hardware, localhost only
RSC_HOST_TOKEN=dev python -m rsc_host
# Robot: real hardware, reachable on the local network
RSC_HOST_BACKEND=pi RSC_HOST_BIND=0.0.0.0 RSC_HOST_TOKEN=dev \
~/rsc-env/bin/python -m rsc_hostIn production it runs as two systemd services: rsc-host@rsc.service and
rsc-ring.service. Two, because the ring needs root privileges and the rest of
the daemon deliberately does not. See Architecture.
A one-line check with wscat:
wscat -c ws://127.0.0.1:8765 -s bearer -s dev
> {"type":"cmd","id":"1","verb":"ping","args":{}}
< {"type":"ack","id":"1","ok":true,"result":{"pong":true}}For anything more, use the web console:
RobotStudyCompanion/console,
published at rsc.ee/console.html. It is a single static HTML file and can be
opened from disk against any robot on the same network.
RSC_HOST_TOKEN is required and has no default. Everything else does, and the
defaults are values measured on the chassis rather than guessed. The reasoning
and the measurements behind each are recorded in
docs/configuration.md, which covers all 51
variables.
Settings changed from the console are written to /var/lib/rsc-host/ and layer
over the shipped defaults, so per-robot tuning and the shipped recipe coexist
without either editing the other. A robot with no configuration at all should
work.
rsc_host/
__main__.py Entry point: config, signals, lifecycle
protocol.py Wire schemas: Cmd, Ack, Event, ErrorCode
dispatch.py Verb registry and async dispatch
events.py Event bus: async fan-out to subscribers
auth.py Bearer-token check via WebSocket subprotocols
config.py Grouped settings read from the environment
errors.py Peripheral error types, importable from any layer
state.py Durable user settings, in a directory the daemon owns
power.py Shutdown and reboot, without root
server.py WebSocket server
audio_endpoints.py Binary audio handlers
discovery.py Network service advertisement
client.py Python client library
ring_helper.py Root-only LED ring daemon, its own service
hal/ Hardware abstraction layer
types.py Colour, Edge, GpioEdge
base.py Abstract peripheral bases
fake.py In-memory fakes with test hooks
pi.py Real backend for the Pi
dsp.py Capture chain: decimation, filters, echo-cancel hook
peripherals/ Semantic behaviour composed from the abstraction layer
registry.py Wires backends to peripherals to verbs
flipper.py Servo wrapper with ramping
ring.py Mode machine
button.py Edge debouncing
button_led.py Brightness modes
cyd.py Front-panel serial bridge
audio.py Playback and capture sessions
docs/ Setup, configuration, wire protocol, design notes
systemd/ Service unit files
scripts/ Install helpers
tests/ Behavioural tests, mostly against the fakes
Four layers, each depending only on the one below.
Contract: protocol.py, dispatch.py, events.py. Message shapes and
routing. No hardware, no input or output.
Hardware abstraction: hal/. One abstract class per peripheral type,
with the fake and Pi backends implementing all of them. Asynchronous
throughout. Domain logic lives above, so this layer stays a thin translation.
Peripherals: peripherals/. Behaviour composed from backends: ramping,
debouncing, mode machines, capture sessions.
Network: server.py, auth.py, audio_endpoints.py, discovery.py.
Three decisions look odd until you know the reason, and all three came from measurement rather than preference.
The general-purpose input/output layer uses lgpio, not pigpio. An earlier
investigation concluded the Pi could not drive servos, the ring and digital
audio concurrently, and recommended adding a co-processor. That was wrong. The
cause was a malformed pigpiod service file whose stop command killed its own
transaction, leaving hardware resources unrestored. With pigpio masked and
everything moved to lgpio, all peripherals run together. The full account, and
the two lgpio pitfalls it introduced, are in
docs/pi-setup.md. Do not reintroduce
pigpio.
The LED ring runs in a separate root process. It sits on a pin whose timing must be driven by direct memory access, which needs privileges the unprivileged memory interface does not grant. Rather than run the whole daemon as root, a small helper owns the strip behind a local socket. The daemon stays unprivileged and tolerates the helper being absent.
Audio uses arecord and aplay rather than a sound library. PortAudio
cannot address the ALSA plughw device and negotiates its own sample rate,
which on this codec produces audible warble. Subprocesses also give full duplex
for free and keep the mixer consistent with what the daemon believes. Measured
levels and the reasoning are in
docs/notes_on_audio.md.
| Document | Covers |
|---|---|
docs/README.md |
Index, and what is still unresolved |
docs/pi-setup.md |
Fresh Pi to running service |
docs/configuration.md |
Every environment variable |
docs/wire-protocol.md |
All 38 verbs, events, binary endpoints |
docs/cyd_state_note.md |
Front-panel state, and the firmware changes it needs |
docs/notes_on_audio.md |
Capture levels and known artefacts |
- console: the web console
- CYD: front-panel firmware
- robotstudycompanion.github.io (the site, and where the console is published)
Apache-2.0.