Pebble is a local-first MQTT communication stack for distributed components.
Robots at minimum have to implement the MQTT standard contract, but may be more complicated. Below is an example of a full-Linux architecture, the one present on Goob.
These platform directories are preserved as repository examples:
- Goob
- Fred
- MiniLAMP (Seeed XIAO ESP32-C6 firmware)
- BUMPERBOT
control/- local runtime services (launcher,av_daemon,mqtt_bridge,ros1_bridge,serial_mcu_bridge,soundboard_handler,autonomy_manager) plus service-specific support files such ascontrol/services/serial_standard.mdweb-interface/- web UI service and Docker runtime filesautonomy/apriltag-follow/- AprilTag/autonomy utilitiesaudio/- MQTT audio publisher/receiver scriptsvideo/- MQTT video publisher scriptssoundboard/- soundboard MQTT contract and usage docsfirmware/- MCU firmwaremqtt_standard.md- topic and payload standard
Pebble uses the topic format documented in mqtt_standard.md:
{system}/{type}/{id}/{incoming|outgoing}/{metric}
Infrastructure discovery remains reserved at:
{system}/infrastructure
This section is an example bring-up path for a Linux-hosted robot with:
- Raspberry Pi (or similar Linux host)
- serial-connected MCU
- camera/microphone (optional but expected for MQTT media)
sudo apt update
sudo apt install -y \
git \
mosquitto mosquitto-clients \
python3 python3-pip python3-venv \
python3-serial python3-paho-mqtt python3-flask python3-opencv python3-numpy \
gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
gstreamer1.0-alsa \
alsa-utils \
v4l-utilsIf control/configs/config.json sets services.av_daemon.video.backend to
libcamera, also install your distro's libcamera GStreamer plugin package
(for example on Raspberry Pi OS/Debian variants):
sudo apt install -y gstreamer1.0-libcamera libcamera-tools
gst-inspect-1.0 libcamerasrcsudo systemctl enable --now mosquitto
sudo systemctl status mosquitto --no-pagerUse any install path; /opt/pebble is only the systemd example default:
git clone <your-repo-url> <repo-path>
cd <repo-path>
cp control/configs/config.json.example control/configs/config.jsonEdit control/configs/config.json for this robot:
- robot identity (
robot.system,robot.type,robot.id) services.mqtt_bridge.remote_mqtt.*- serial port (
services.serial_mcu_bridge.serial.port) - serial safety controls (
services.serial_mcu_bridge.safety.*) - any camera/audio backend/device settings for
services.av_daemon
If you enable reboot control (services.mqtt_bridge.reboot_control.command uses sudo),
add this in sudo visudo so reboot can run without a password prompt:
username ALL=(root) NOPASSWD: /sbin/reboot
If you enable remote service-restart control (services.mqtt_bridge.service_restart_control.command),
add the matching systemctl line in sudo visudo so the runtime user can restart
pebble-control.service without a password prompt:
username ALL=(root) NOPASSWD: /usr/bin/systemctl restart pebble-control.service
If you enable remote git-pull control (services.mqtt_bridge.git_pull_control.command),
ensure the runtime service user can run git pull in /opt/pebble without prompts
(repo ownership + non-interactive auth keys/tokens as needed).
control/configs/config.json is gitignored and should stay local to the robot.
cp firmware/MINILAMP_seeed-xiao-c6/private_config.h.example \
firmware/MINILAMP_seeed-xiao-c6/private_config.hFill in private values in private_config.h. This file is gitignored.
Replace pebble with the Linux account that runs the service. This account
needs serial, audio, and video access:
sudo usermod -aG dialout pebble
sudo usermod -aG audio pebble
sudo usermod -aG video pebbleLog out and back in (or reboot) so the new group membership applies.
Discover stable camera device nodes:
ls /dev/v4l/by-id/
v4l2-ctl --device=/dev/video0 --list-formats-ext
v4l2-ctl --device=/dev/video1 --list-formats-extFor Logitech C922 specifically, use the stable ...video-index0 path for
capture and capture_format: "mjpeg" for high-resolution/high-fps modes.
For libcamera-based CSI camera setups, use services.av_daemon.video.backend
as libcamera (or alias arducam) and set
services.av_daemon.video.camera_controls as needed.
If the camera is exposed as V4L2 (like /dev/video0 with v4l2-ctl formats),
you can instead set services.av_daemon.video.backend to v4l2 and set
services.av_daemon.video.device to that node. If V4L2 allocation fails on
CSI cameras, use libcamera/arducam backend.
Discover capture/playback audio devices:
arecord -l
aplay -lIf needed, set a system-wide ALSA default playback card:
sudo tee /etc/asound.conf >/dev/null <<'EOF'
defaults.pcm.card 3
defaults.pcm.device 0
defaults.ctl.card 3
EOFcd <repo-path>
python3 control/launcher.py --config control/configs/config.jsonIf this starts cleanly, stop it with Ctrl+C and continue with systemd.
sudo cp systemd/pebble-control.service.example /etc/systemd/system/pebble-control.service
sudo nano /etc/systemd/system/pebble-control.serviceUpdate at least:
UserGroup(optional; remove this line if you do not use a dedicated group)WorkingDirectoryExecStart
Keep these example-unit lines unless you have a deliberate replacement:
PAMName=loginEnvironment=XDG_RUNTIME_DIR=/run/user/%UEnvironment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%U/bus
They provide a login-style user session for the service user, which keeps
the AV-daemon shared-memory audio path (shmsink -> shmsrc) working
reliably on robots like Goob. %U expands to the numeric UID for the
configured User=.
Then enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now pebble-control.servicesystemctl status pebble-control.service --no-pager
journalctl -u pebble-control.service -fYou can also sanity-check live MQTT traffic:
mosquitto_sub -h 127.0.0.1 -t 'pebble/#' -vThen run the on-robot smoke test:
python3 tests/on_robot_smoke.py --config control/configs/config.jsonAfter reboot/systemd validation:
python3 tests/on_robot_smoke.py --config control/configs/config.json --no-launchIf remote broker access is intentionally unavailable, run local-only validation:
python3 tests/on_robot_smoke.py --config control/configs/config.json --no-launch --skip-remoteRun from repo root:
python3 web-interface/web-control.pyWeb interface MQTT/robot defaults now come from control/configs/config.json
under the web_interface section.
For a dedicated web-only config file, set PEBBLE_CONFIG when starting
web-control.py.
Robots can also self-advertise features through retained
.../outgoing/capabilities, enabling dynamic UI capability discovery.
When capability + heartbeat telemetry is present, web_interface.robots can stay empty.
The UI includes soundboard and autonomy panels; autonomy start/stop publishes to
.../incoming/autonomy-command using scripts/config fields reported on
.../outgoing/autonomy-files.
If web_interface.mqtt_history.enabled=true, the UI can also persist all MQTT
message history to MongoDB (pymongo dependency).
The repo-root Dockerfile now builds a launcher-based Pebble image. The web UI
is started through services.web_handler, so the container still exposes the
same HTTP interface while also being able to run other Pebble services.
Typical server usage:
docker build -t pebble .
docker run --rm \
--name pebble \
-p 8080:8080 \
-v "$PWD/control/configs/config.json:/app/control/configs/config.json:ro" \
pebbleFor your server deployment, point both local_mqtt.host and
services.mqtt_bridge.remote_mqtt.host at the existing mosquitto_broker
container and enable only the services you want active on this machine.
- Expand
tests/with deeper subsystem + integration coverage:mqtt_bridgerouting/flags/loop-preventionserial_mcu_bridgecommand and telemetry behaviorsoundboard_handlercommand/files/status behaviorav_daemonlifecycle behavior- end-to-end local/remote broker scenarios
- Extend structured
{system}/{type}/{id}/outgoing/logsdiagnostics:- Include richer service metadata and optional media-source unavailability annotations.
- Evaluate replacing current delta-frame MQTT video payload with a more efficient encoded video transport.
- Standardize robot-wide soundboard event cues for scripts:
- Define stable cue names (for example
error,warning,info,success) mapped to consistent sounds. - Add helper utilities so autonomy/control scripts can publish soundboard commands with a shared contract.
- Define stable cue names (for example
- Add audio volume control integration:
- Extend the system audio script(s) so the web interface can adjust playback/capture volume via ALSA mixer commands (for example
amixer). - Expose bounded volume controls in the web UI and publish/apply updates safely on-robot.
- Extend the system audio script(s) so the web interface can adjust playback/capture volume via ALSA mixer commands (for example
- Add soundboard repeat playback modes:
- Support a repeat toggle that loops a selected sound until a manual stop command is received.
- Support timed repeat (for example "repeat for X seconds") for alert and status cues.
WARNING: erroneous pipeline: no element "alsasrc": installgstreamer1.0-alsa.WARNING: erroneous pipeline: no element "jpegparse": installgstreamer1.0-plugins-bad.WARNING: erroneous pipeline: no element "libcamerasrc": installgstreamer1.0-libcamera libcamera-tools, then verify withgst-inspect-1.0 libcamerasrc.Failed to allocate required memoryfromv4l2src: setservices.av_daemon.video.capture_formatexplicitly (for exampleyuyv) and tryservices.av_daemon.video.io_mode: "rw".arecord: ... no soundcards foundwhile/proc/asound/cardsshows devices: ensure the runtime user is inaudiogroup and re-login/reboot.- Webcam node confusion:
use
/dev/v4l/by-id/...video-index0as the capture device for C922. - If
arecordcapture requires stereo on C922: usechannels: 2inservices.av_daemon.audioandservices.mqtt_bridge.media.audio_publisher. - Video/audio stream shows "connecting" forever:
ensure
.../incoming/flags/mqtt-video/.../incoming/flags/mqtt-audioaretrue(retained). The web UI start actions set these automatically. front-camera control payload missing boolean: publish a boolean payload (true/false) or JSON withvalue/enabled(for example{"value": true}), not string commands like"start".- Robot keeps driving after control loss:
tune
services.serial_mcu_bridge.safety.drive_timeout_seconds(default0.75) and keepignore_retained_drive: true.
