The AY-Robots try page showing three ways to start without owning a robot: drive a real SO-100 arm, compare models, rent a GPU
raspberry-pihardwaredeploymentusblerobot

Raspberry Pi as a Robot Host: What It Can and Cannot Do

AY-Robots ResearchAugust 23, 202625 min read

What a Raspberry Pi really does well in a robot: USB topology for the servo bus and cameras, power and encoder limits, and why the policy belongs on a GPU somewhere else.

A Raspberry Pi is a very good robot host and a very bad inference machine. Those two facts are not in tension, they are the whole design. The Pi sits next to the arm, owns the serial bus and the cameras, keeps a fixed-rate loop running, and talks to something else that owns the GPU. Get that split right and a Pi 5 will drive an SO-100 all day. Get it wrong and you spend a weekend discovering that a 3 B parameter model does not fit on a board with no CUDA device.

This page covers the boring half: which USB ports exist on which Pi, how much bandwidth two cameras need, where the power comes from, and what the published hardware requirements say when you read them literally. Every number here comes from a datasheet, a repo or a doc page linked at the bottom. Where the answer is do not run it here, it says so.

The short version

  • The Pi's job is I/O and timing: serial bus to the servos, USB video in, network out. A Raspberry Pi 5 does that comfortably.
  • USB topology decides whether two cameras work. On Pi 4 all four ports sit behind one VL805 controller on a single PCIe Gen 2 lane. On Pi 5 the RP1 southbridge gives every downstream port independent, uncontended bandwidth.
  • One 1080p30 uncompressed YUYV stream is about 995 Mbit/s. USB 2.0 signals at 480 Mbit/s. Force MJPEG, or the driver refuses to start the stream.
  • NVIDIA's Isaac-GR00T README lists inference hardware as one GPU with 16 GB or more of VRAM. LeRobot's docs put Pi0 at 14 GB at inference time and SmolVLA at about 2 GB. A Pi tops out at 16 GB of shared LPDDR4X and has no CUDA device.
  • An AI HAT+ is a 13 or 26 TOPS INT8 vision NPU, and Raspberry Pi's capability table marks LLM and VLM support as not supported. The AI HAT+ 2 does run VLMs on its own 8 GB, but a Hailo part is not a PyTorch device, so a fine-tuned VLA checkpoint is still not a drop-in.
  • The architecture that works is LeRobot's own: policy server on a GPU box, robot client on the Pi, action chunks over the wire. LeKiwi already does exactly this.
  • Remote inference is fine for slow pick and place and not fine for fast reactive motion. That is physics, not a product tier.

What the Pi is actually doing

Split the software into jobs and the placement decision answers itself. Some jobs need to be microseconds from a UART. Some need 40 GB of VRAM. Nothing needs both, which is lucky, because no single box at this price gives you both.

JobRuns well on a Pi?Why
Serial bus to the Feetech servosYesOne USB CDC-ACM port at 1 Mbaud. The wire is the bottleneck, not the CPU.
Reading 2 to 3 USB camerasYes, with careBandwidth and pixel format decide this, not CPU.
TeleoperationYesLeader-follower position streaming is a few hundred bytes per tick.
Recording a LeRobot datasetYes, with the encoder tunedVideo encoding is the cost. No hardware H.264 encoder on the Pi 5.
Running ACT (about 80 M params)UnprovenNo CUDA device. The 20 ms figure is a GPU number and no ARM-CPU rollout has been published.
Running SmolVLA, Pi0.5, GR00TNoVendor minimums start at a 16 GB VRAM GPU.
Fine-tuning a policyNoIsaac-GR00T recommends 40 GB or more of VRAM.

That table is the article. The rest turns each yes into a working setup and each no into an architecture instead of a disappointment. If the arm has never moved at all, start with the SO-100 getting started guide and come back when the servos respond.

USB topology is the whole design

Both boards advertise the same port count: two USB 3.0 and two USB 2.0. The topology behind those ports is completely different, and that difference decides whether your second camera streams.

Raspberry Pi 4 Model BRaspberry Pi 5
SoCBCM2711, quad-core Cortex-A72 at 1.8 GHzBCM2712, quad-core Cortex-A76 at 2.4 GHz
USB ports2 x USB 3.0, 2 x USB 2.02 x USB 3.0, 2 x USB 2.0
USB controllerVL805, four ports behind one PCIe Gen 2 x1 linkRP1 southbridge, two independent xHCI controllers
Per-port bandwidthShared across all four ports"every downstream port has independent and uncontended bandwidth" (RP1 datasheet)
USB 3.0 wording in the brief"2 x USB 3.0 ports""2 x USB 3.0 ports, supporting simultaneous 5Gbps operation"
Video encode in siliconH.264 1080p30 encodeNone listed; the brief lists a 4Kp60 HEVC decoder only
Max RAM8 GB LPDDR416 GB LPDDR4X-4267
Recommended PSU / max USB draw3.0 A / 1.2 A5.0 A / 1.6 A (600 mA on a 3 A supply)
Operating temperature0 to 50 C0 to 70 C
The RP1 sentence that matters

Chapter 5 of the RP1 peripherals datasheet describes two identical USB 3.0 xHCI host controllers, each with two downstream ports: "The controllers are configured with two USB2.0 HS/FS/LS Bus Instances and a dedicated SuperSpeed Bus Instance, so every downstream port has independent and uncontended bandwidth." On a Raspberry Pi 4 that sentence does not apply. The CM4 datasheet says that on Raspberry Pi 4 Model B the internal PCIe 2.0 x1 host controller "has been connected to a USB 3 host controller (using the Via Labs VLI805)", and Raspberry Pi's CM4 IO USB 3.0 application note adds that this is "exactly the same host controller as used on Raspberry Pi 4 Model B". The Raspberry Pi 4 Model B brief lists two USB 3.0 and two USB 2.0 ports, and they all hang off that one chip on one PCIe Gen 2 lane. Put two cameras and a servo adapter on a Pi 4 and assume they are competing.

Before debugging anything, look at the tree. One command tells you what the kernel thinks your topology is:

bash
# what is on which controller, at which speed
lsusb -t

# the shape to expect on a Pi 5 with one camera and the servo adapter
/:  Bus 04.Port 1: Dev 1, Class=root_hub, Driver=xhci-hcd/2p, 5000M
    |__ Port 1: Dev 2, If 0, Class=Video, Driver=uvcvideo, 5000M
/:  Bus 03.Port 1: Dev 1, Class=root_hub, Driver=xhci-hcd/2p, 480M
/:  Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci-hcd/2p, 5000M
/:  Bus 01.Port 1: Dev 1, Class=root_hub, Driver=xhci-hcd/2p, 480M
    |__ Port 2: Dev 3, If 0, Class=Comm, Driver=cdc_acm, 12M
    |__ Port 2: Dev 3, If 1, Class=CDC Data, Driver=cdc_acm, 12M
Four root hubs on a Pi 5, two per controller, is what independent controllers look like. The servo adapter as cdc_acm at 12M is fine: it is a serial port, not a video stream.

Camera bandwidth: do the arithmetic before you buy

USB video class cameras reserve a fixed slice of bus bandwidth when the stream starts. If the slice is not available, the stream does not start. This is the most common way a two-camera rig fails, and it is predictable from multiplication.

FormatResolutionfpsBytes per frameMB/sMbit/s
YUYV (uncompressed)640 x 48030614,40018.4147
YUYV (uncompressed)1280 x 720301,843,20055.3442
YUYV (uncompressed)1920 x 1080304,147,200124.4995
MJPEG (about 10:1)640 x 48030about 61,000about 1.8about 15
MJPEG (about 10:1)1920 x 108030about 415,000about 12.4about 100

USB 2.0 signals at 480 Mbit/s, and the periodic schedule only gives you part of that. A single uncompressed 1080p30 stream asks for roughly twice the whole bus. Two uncompressed 640x480 streams want about 294 Mbit/s on a bus that also carries servo traffic. The MJPEG rows are estimates, since the ratio depends on sensor and scene, but the order of magnitude is not in doubt: in-camera MJPEG is the difference between two cameras working and two cameras not working.

The failure that eats a day

Plug in the second camera, start recording, and get VIDIOC_STREAMON: No space left on device. There is plenty of disk. ENOSPC here means USB bandwidth or usbfs buffer memory, not storage. Two fixes, in this order: force MJPEG instead of YUYV, since most UVC webcams produce MJPEG in the camera and bus traffic drops by roughly an order of magnitude; then, if your capture path goes through usbfs, raise usbcore.usbfs_memory_mb, which the Linux USB core (drivers/usb/core/devio.c) defaults to 16 MB. Check it with cat /sys/module/usbcore/parameters/usbfs_memory_mb. Moving a camera to the other port pair helps on a Pi 5 and barely helps on a Pi 4, where everything shares the same VL805 uplink.

bash
# what formats and frame rates does this camera really support?
v4l2-ctl --device /dev/video0 --list-formats-ext

# confirm what the driver actually negotiated
v4l2-ctl -d /dev/video0 --get-fmt-video

# usbfs buffer ceiling, in MB (16 by default)
cat /sys/module/usbcore/parameters/usbfs_memory_mb

# LeRobot's own discovery helper
lerobot-find-cameras opencv
Run all four before writing any robot code. No MJPG entry in --list-formats-ext means one stream per controller, forever.

Whatever you record is what the policy expects at inference time. A wrist camera at 15 fps during recording and 30 fps during a rollout is a changed input distribution. Fix format and rate once, write them down, and use the same values in the recording run and in the rollout. If a camera vanishes mid-session, the camera-not-detected page lists the usual causes.

The AY-Robots CLI page showing the install command and the run commands for driving an arm from a terminal
The /cli page. Anything you can do in the browser you can do from a terminal on the robot host, which is what you want on a headless Pi over SSH.

The servo bus: one serial port at 1 Mbaud

The arm is a daisy chain of Feetech STS3215 bus servos on a half-duplex TTL line, bridged to USB by a small adapter board. The servo spec lists a 1 Mbps bus and LeRobot's Feetech driver defaults to exactly that. At 1 Mbaud with 8N1 framing a byte is ten bit periods, so a byte costs 10 microseconds and reading six joints is a few hundred microseconds of signalling. That is not your problem. Everything around it is: USB scheduling, driver buffering, and the occasional corrupt packet.

LeRobot is explicit about the last one. The SO follower configuration carries num_read_retries: int = 2, with a comment that Feetech buses can return a corrupted status packet "especially when several joints move at once", which would otherwise abort the control loop (config_so_follower.py). Write your own loop instead and it will die on a fast move.

  1. 1
    Find the port

    Run the finder with the adapter plugged in, unplug when prompted, and it tells you which device node is yours. On Linux this is normally a /dev/ttyACM* node, because the adapter enumerates as USB CDC-ACM rather than as an FTDI part.

    bash
    lerobot-find-port
    # Finding all available ports for the MotorBus.
    # ['/dev/ttyACM0', '/dev/ttyACM1']
    # Remove the usb cable from your MotorsBus and press Enter when done.
    # The port of this MotorsBus is /dev/ttyACM1
  2. 2
    Give yourself access, then stop doing it by hand

    LeRobot's docs show sudo chmod 666 /dev/ttyACM0. That works once and is undone by the next replug. Add yourself to the dialout group instead and log out and in.

    bash
    sudo usermod -aG dialout $USER
    # log out and back in, then verify
    id -nG | tr ' ' '\n' | grep dialout
  3. 3
    Pin the device name so it survives a reboot

    With a leader and a follower plugged in, ACM0 and ACM1 swap depending on enumeration order. Use the stable symlink under /dev/serial/by-id/, or write a udev rule keyed on the adapter's serial number.

    bash
    ls -l /dev/serial/by-id/
    
    # find the attributes to key a rule on
    udevadm info -a -n /dev/ttyACM0 | grep -E 'idVendor|idProduct|serial' | head
    
    # /etc/udev/rules.d/99-lerobot.rules  (substitute your own values)
    # SUBSYSTEM=="tty", ATTRS{idVendor}=="XXXX", ATTRS{serial}=="YYYY", SYMLINK+="robot_follower"
    
    sudo udevadm control --reload-rules && sudo udevadm trigger
  4. 4
    Calibrate, on the Pi, over SSH

    Calibration writes a per-robot file that later runs load by id. Do it on the machine that will drive the arm, and give the arm a name you will still recognise in six months.

    bash
    lerobot-calibrate \
        --robot.type=so101_follower \
        --robot.port=/dev/serial/by-id/usb-...-if00 \
        --robot.id=pi_follower_01
  5. 5
    Prove the loop before you add cameras

    Teleoperate with no cameras configured first. If that is smooth, the bus and the power are fine and any later stutter is video or network.

    bash
    lerobot-teleoperate \
        --robot.type=so101_follower \
        --robot.port=/dev/serial/by-id/usb-...-if00 \
        --robot.id=pi_follower_01 \
        --teleop.type=so101_leader \
        --teleop.port=/dev/serial/by-id/usb-...-if01 \
        --teleop.id=pi_leader_01
Two hardware traps, one voltage and one jumper

Voltage. The SO-100 and SO-101 use the 7.4 V Feetech STS3215, and 12 V destroys them. Waveshare's own ST3215 page advertises 30 kg.cm at 12 V, which is exactly the temptation that kills arms. Check the label on the servo, not on the power brick. Jumper. LeRobot's SO-101 troubleshooting says it plainly: "If you are using a Waveshare controller board, make sure that the two jumpers are set on the B channel (USB)." Position A is UART over the GPIO header. On the wrong position the board enumerates, the port appears, and nothing answers, which looks identical to a dead servo. See servo not responding and arm not detected.

A note on serial latency, since it gets misquoted

You will find advice telling you to write 1 into latency_timer to fix a slow servo bus. The knob is real: the Linux ftdi_sio driver exposes a read-write latency_timer sysfs attribute in units of milliseconds, and the kernel header records FTDI's device default as 16 ms (ftdi_sio.h). It exists only for FTDI parts, which appear as /dev/ttyUSB*. The board LeRobot documents for the SO-100 family appears as /dev/ttyACM* under cdc_acm, where the attribute does not exist. Measure your own loop instead of copying a fix for hardware you do not have.

python
import time, statistics
from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig

robot = SO101Follower(SO101FollowerConfig(port="/dev/serial/by-id/usb-...-if00", id="pi_follower_01"))
robot.connect()

samples = []
for _ in range(500):
    t0 = time.perf_counter()
    robot.get_observation()
    samples.append((time.perf_counter() - t0) * 1000)

samples.sort()
print(f"median {statistics.median(samples):.2f} ms  p99 {samples[494]:.2f} ms")
robot.disconnect()
Five hundred observations, sorted, median and p99. Run it with cameras off and then on. The delta is your video cost, and it is usually larger than anything the serial layer does.

Power: the Pi is not a power supply

The servo adapter board has its own DC barrel jack, and that is where servo current comes from. The USB cable carries data. People still try to skip the brick and run the arm off the Pi. The current budget says no, and it is not close.

RailNumberSource
Pi 5 downstream USB, 5 A PSU1.6 A total across all portsRaspberry Pi power supply documentation
Pi 5 downstream USB, 3 A PSU600 mA totalRaspberry Pi power supply documentation
Pi 4 downstream USB1.2 A totalRaspberry Pi power supply documentation
Pi 5 bare board, typical800 mARaspberry Pi power supply documentation
Waveshare ST3215 bus servo, no load200 mAWaveshare ST3215 specification (12 V variant)
Waveshare ST3215 bus servo, locked rotor2.7 AWaveshare ST3215 specification (12 V variant)

Waveshare publishes those currents for the 12 V ST3215, not for the 7.4 V STS3215 that the SO-100 actually uses, so take them as the order of magnitude rather than as your servo's datasheet. The conclusion survives either way: one stalled servo draws more than a Raspberry Pi 5 will hand to every USB peripheral combined, and an arm has six. Give the bus its own supply, sized for the servo voltage. Then check the Pi is on a real 5 V 5 A USB-C power delivery supply: on anything less it clamps downstream USB to 600 mA, and a camera browning out mid-episode looks exactly like a bad cable.

Fast check for an undervolted Pi

Run vcgencmd get_throttled. Anything other than throttled=0x0 means the board has seen undervoltage or thermal throttling since boot, and you should fix that before trusting any timing you measured on it. Bit 0 is undervoltage now, bit 16 is undervoltage at some point since boot. Also watch vcgencmd measure_temp during a long recording: the Pi 5's rated ambient tops out at 70 C, and a passively cooled Pi in a printed enclosure with two cameras streaming gets there.

Why inference does not belong on the Pi

Here are the published requirements without commentary. NVIDIA's Isaac-GR00T README lists inference hardware as one GPU with 16 GB or more of VRAM, naming RTX 4090, L40, H100, Jetson AGX Thor and Orin, and DGX Spark; for fine-tuning it recommends 40 GB or more. LeRobot's async guide says Pi0 "occupies 14GB of memory at inference time, while SmolVLA requires only ~2GB". A Raspberry Pi 5 has at most 16 GB of LPDDR4X shared with the OS, the camera buffers and whatever is encoding video in software, and no CUDA device.

PolicyParamsInference per action stepGPU tier the platform uses
GR00T N1.7about 3 B, about 40 M trained during fine-tuning152 msA100 80 GB or H100 80 GB
GR00T N1.5about 3 B165 msA100 80 GB or H100 80 GB
Pi0.5about 3 B, PaliGemma backbone485 msA100 80 GB or H100 80 GB
SmolVLAabout 450 M245 msRTX 4090 or any 24 GB card
ACTabout 80 M20 msRTX 4090 or any 24 GB card

Read the ACT row carefully, because it is the one that tempts people. Twenty milliseconds per action step is a 50 Hz budget, and it is a GPU number. Moving the same 80 M parameter model to four Cortex-A76 cores with no accelerator is not shaving that budget, it is a different regime. Nobody has published a credible real-time ACT rollout on Pi-class CPU, so treat it as unproven rather than as a plan. Compare the five on the policies page if you are still choosing.

One genuine caveat. The paper behind SmolVLA says the model is "designed to be trained on a single GPU and deployed on consumer-grade GPUs or even CPUs". That is about x86 desktop CPUs, not a 2.4 GHz ARM board, and deployable is not the same as deployable at your control rate. Test it and publish numbers rather than assuming them. Its benchmark results sit next to 84 other models in the arena.

The software wall you hit before the compute wall

LeRobot's installation guide states that TorchCodec is not available on Linux ARM (aarch64, arm64, armv7l), and that LeRobot falls back to pyav there. Fine for a robot client, which decodes nothing. It matters the moment you try anything training-shaped on the Pi. The base install also wants Python 3.12, and on Linux the guide pins torch and torchvision to the CUDA 12.8 wheel index with a 570.86 driver floor, which is guidance for a machine that has an NVIDIA GPU. You will spend the day on wheels before measuring a single forward pass.

But what about an AI HAT?

Raspberry Pi documents the answer in a capability table. AI HAT+ is a Hailo-8L at 13 TOPS or a Hailo-8 at 26 TOPS, both INT8, and both use the Pi 5's own memory rather than carrying any. That table marks both large language model and vision-language model support as not supported for AI HAT+, and gives its use cases as object detection, camera post-processing, robotics and moderate neural workloads. The newer AI HAT+ 2 is a Hailo-10H at 40 TOPS, INT4, and the real difference is not TOPS: it carries its own 8 GB, which the docs say lets it run LLMs and VLMs "up to ~6 billion parameters".

Read that ceiling carefully. It is a real capability, and it is a capability for models Hailo's toolchain can compile and quantize. A vision-language-action model is a VLM with an action head trained on your robot's joint space, served through a runtime that expects a PyTorch device. That is not a supported path on a Hailo part today, and the checkpoint you produce by fine-tuning is not a drop-in for it. Memory stopped being the obvious blocker on that board; the toolchain became the blocker instead.

Putting an NPU on the robot host
What it does buy you
  • Real vision throughput for detection, segmentation and pose, next to the cameras, with no network hop.
  • Native support in rpicam-apps and Picamera2, so a perception preprocessing stage is easy to add.
  • Low power. The robot host stays on one 5 V supply.
  • Useful as a gate: run a cheap detector locally, wake the expensive policy only when the scene changes.
What it does not
  • TOPS is an integer-operations rating for compiled, quantized graphs. A Hailo part is not a PyTorch device and it does not run an arbitrary checkpoint.
  • Raspberry Pi's capability table marks LLM and VLM support as not supported for AI HAT+, both the 13 TOPS Hailo-8L and the 26 TOPS Hailo-8 variant.
  • AI HAT+ 2 does support VLMs up to about 6 B parameters on its own 8 GB, but a VLA is a VLM plus an action head served through a robot runtime, which is a different integration problem.
  • Every model has to go through the vendor's compiler and quantizer first. Your fine-tuned checkpoint is not a drop-in.

The architecture that actually works: Pi as robot client

LeRobot ships the answer, and their framing is worth reading literally. The RobotClient "streams observations to the PolicyServer, and receives action chunks obtained running inference on the server (which we assume to have better computational resources than the robot controller)". The Pi is the robot controller. Something with a GPU is the server. This is the supported path, not a workaround.

bash
# on the GPU machine
python -m lerobot.async_inference.policy_server \
     --host=0.0.0.0 \
     --port=8080

# on the Raspberry Pi, next to the arm
python -m lerobot.async_inference.robot_client \
    --server_address=<gpu-host>:8080 \
    --robot.type=so100_follower \
    --robot.port=/dev/serial/by-id/usb-...-if00 \
    --robot.id=pi_follower_01 \
    --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30} }" \
    --task="Pick up the cube and put it in the bin" \
    --policy_type=smolvla \
    --pretrained_name_or_path=<user>/<model> \
    --policy_device=cuda \
    --actions_per_chunk=50 \
    --chunk_size_threshold=0.5 \
    --aggregate_fn_name=weighted_average \
    --debug_visualize_queue_size=True
Install the extras first with pip install -e ".[async]". The two numbers to tune are actions_per_chunk and chunk_size_threshold; LeRobot documents 50 and 0.7 as defaults and suggests 0.5 to 0.6 for the threshold.

The mechanism that makes this survivable is action chunking. The server returns a batch of future actions, the Pi steps through them locally, and a new observation goes out when the queue drops below the threshold. You pay the round trip once per chunk instead of once per step, and you pay for it in open-loop blindness for the length of the chunk. Hugging Face's async inference post reports sub-100 ms round trips on a local network with SmolVLA on an RTX 4090, and about a 2x speedup in task completion time at comparable success rates.

Note the phrase local network. A GPU box on the same switch is a different animal from a rented GPU across the public internet, and that difference in inference latency is the whole design constraint. Slow, deliberate pick and place tolerates the hop. Fast reactive motion does not. If a policy stalls part way through a motion, the freeze page walks the causes.

LeKiwi is this pattern with wheels

The LeKiwi mobile base is the reference implementation of Pi-as-host inside LeRobot. The docs say the on-robot computer "is normally a Raspberry Pi, but can be any PC that can run on 5V and has enough usb ports (2 or more) for the cameras and motor control board". The Pi runs python -m lerobot.robots.lekiwi.lekiwi_host --robot.id=my_awesome_kiwi, the laptop runs the client, control on ZeroMQ port 5555 and video on 5556. Note the split: calibration on the Pi, teleoperation and policy evaluation from the laptop. See SO-100 against LeKiwi if you are choosing between a fixed arm and a base.

Recording datasets on the Pi

Recording is where a Pi is closest to its limit, because it is the only stage that is genuinely compute-bound on the host. LeRobot encodes each camera stream to MP4, and the default encoder is libsvtav1 at preset 12, crf 30, GOP 2. Software AV1 on four ARM cores while two cameras stream and a control loop runs is asking a lot, and there is no hardware encoder to fall back on: the Pi 4 brief lists H.264 1080p30 encode, the Pi 5 brief lists a 4Kp60 HEVC decoder and no encoder at all.

bash
lerobot-record \
    --robot.type=so101_follower \
    --robot.port=/dev/serial/by-id/usb-...-if00 \
    --robot.id=pi_follower_01 \
    --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30} }" \
    --teleop.type=so101_leader \
    --teleop.port=/dev/serial/by-id/usb-...-if01 \
    --teleop.id=pi_leader_01 \
    --dataset.repo_id=<user>/<dataset> \
    --dataset.num_episodes=50 \
    --dataset.single_task="Grab the cube" \
    --dataset.streaming_encoding=true \
    --dataset.encoder_threads=2 \
    --dataset.rgb_encoder.vcodec=h264 \
    --dataset.rgb_encoder.preset=fast \
    --display_data=false
Two changes matter on a Pi: swap libsvtav1 for h264, and turn the live viewer off. Changing encoder settings partway through a recording is not supported, so decide before episode one.
Encoder settings are baked into the dataset

LeRobot writes the encoder configuration into meta/info.json once, from the first episode, and assumes every later episode matches. Record 20 episodes, notice dropped frames, change vcodec, record 30 more, and the metadata describes the first 20 and lies about the rest. Merging makes it worse: video.codec, pix_fmt, height, width and fps must match across sources or FFmpeg's concat demuxer fails outright. Dropped frames also surface later as a dataset that trains to a low loss and yields a policy that does nothing, covered on the loss-falls page.

Whatever you record ends up as a LeRobot dataset, and the format version matters downstream: v3.0 for Pi0.5, SmolVLA and ACT, v2.0 or v2.1 for GR00T, which crashes on a v3.0 dataset and needs it converted down. A training-time problem rather than a Pi problem, but worth knowing before you record fifty episodes. Public examples live in the dataset directory.

The AY-Robots download page for the desktop client that records LeRobot-format datasets from a teleoperation session
The desktop client on /download is the platform's recording path. If your host is a headless Pi, check the supported platforms there rather than assuming an ARM Linux build exists.

Two ways to get a policy running next to your arm

Same goal, two paths: a trained policy driving your SO-100 with the Pi as host. One is entirely yours, one rents the GPU parts. The Pi's job is identical in both.

  1. Install LeRobot on the Pi from source with the core_scripts and async extras. Python 3.12, and expect the pyav fallback rather than TorchCodec on aarch64.
  2. Pin device names with udev, calibrate on the Pi, prove teleoperation with cameras off.
  3. Add cameras one at a time, forcing MJPEG, checking lsusb -t and the timing loop after each.
  4. Record 50 or more episodes with --dataset.rgb_encoder.vcodec=h264 and push to the Hub.
  5. Rent or own a GPU box, install LeRobot there with the training extras and a CUDA wheel matching your driver, and train.
  6. Run lerobot.async_inference.policy_server on that box and lerobot.async_inference.robot_client on the Pi, then tune chunk_size_threshold with --debug_visualize_queue_size.

Moving parts you own: two Python environments on two architectures, a udev ruleset, an encoder config, a GPU driver stack, a checkpoint store and a gRPC endpoint you keep reachable. All documented, none mysterious. It is simply a lot of surface, and the failures cluster on the ARM side where the wheels are thinnest.

The AY-Robots MCP server page listing the robot operations exposed to AI agents
/mcp exposes the same training and inference operations to an agent, useful when the robot host is headless and you want a script rather than a browser tab.

Which Pi, and when to stop using a Pi

Buying today for one SO-100 with two cameras: a Raspberry Pi 5 with 8 GB, a 5 V 5 A USB-C power delivery supply and active cooling. The independent USB controllers are the reason, not the CPU. A Pi 4 works with one camera and is painful with two.

  • Pi 4, one camera, MJPEG. Fine for teleoperation and a first dataset. Everything shares one VL805 uplink, so expect a fight for the second stream.
  • Pi 5, two or three cameras. The sensible default. Put each camera on a different root hub and confirm with lsusb -t.
  • Pi 5 plus AI HAT+. Worth it for local detection or segmentation as a preprocessing stage. Not a way to run a VLA.
  • Small x86 box. Higher resolution, more than three cameras, or a fully local policy: an x86 mini PC removes the ARM wheel problem and the encoder problem at once.
  • Jetson-class module. The only edge option with a CUDA device, and the one NVIDIA names in the Isaac-GR00T inference list. A different price bracket.

Whatever you pick, the split stays the same. The host owns time and I/O, the GPU owns the policy, and the wire between them is the thing you measure. To feel the loop before buying anything, the live arm is a physical SO-100 you can drive from a browser with no signup, and the try page lays out the three starting points. The SO-100 complete guide covers the assembly and calibration this page assumes you have done.

Can a Raspberry Pi 5 run a VLA policy locally?

Not at a useful control rate. NVIDIA's Isaac-GR00T README lists inference hardware as one GPU with 16 GB or more of VRAM. LeRobot's async guide puts Pi0 at 14 GB at inference time and SmolVLA at about 2 GB. A Pi 5 has at most 16 GB of LPDDR4X shared with everything else and no CUDA device. SmolVLA's paper does claim CPU deployment, but that is about desktop CPUs, and no real-time ARM rollout has been published. Run the policy on a GPU and the robot client on the Pi.

Why do two USB cameras fail on the Pi when each works alone?

Because USB video class cameras reserve bandwidth when the stream starts, and uncompressed YUYV is enormous: a 1920x1080 30 fps YUYV stream is about 995 Mbit/s against a 480 Mbit/s USB 2.0 bus. Force MJPEG, which most webcams encode in hardware, and confirm with v4l2-ctl --list-formats-ext that the camera offers it. On a Pi 4 all four ports share one VL805 controller behind a single PCIe Gen 2 lane; on a Pi 5 the RP1 datasheet says every downstream port has independent and uncontended bandwidth, which is why the Pi 5 is the right board for two cameras.

Should I power the servos from the Raspberry Pi?

No. The servo adapter board has its own DC input for exactly this reason. A Pi 5 on a 5 A supply provides 1.6 A total to all downstream USB peripherals, and 600 mA on a 3 A supply. Waveshare quotes 2.7 A locked-rotor current for one ST3215 bus servo, and that is the 12 V variant rather than the 7.4 V STS3215 the arm uses, so treat it as the order of magnitude. Watch the voltage too: the SO-100 and SO-101 use the 7.4 V STS3215, and 12 V destroys them.

Which device path should I use, /dev/ttyACM0 or a udev symlink?

A symlink. With a leader and a follower plugged in, ACM0 and ACM1 swap depending on enumeration order, so a hard-coded path works until the first reboot. Use /dev/serial/by-id/ or a udev rule keyed on the adapter's serial number, then point --robot.port at the stable name.

Is recording a LeRobot dataset on a Pi realistic?

Yes, if you change the encoder. LeRobot defaults to libsvtav1 at preset 12, which is software AV1, and the Pi 5 has no hardware encoder to fall back on: its brief lists a 4Kp60 HEVC decoder and no encoder, while the Pi 4 brief lists H.264 1080p30 encode. Pass --dataset.rgb_encoder.vcodec=h264 and --dataset.rgb_encoder.preset=fast, turn the live viewer off, and watch for dropped frames. Decide before episode one, because LeRobot writes the encoder config into meta/info.json from the first episode and assumes the rest match.

Does an AI HAT+ change the answer on local inference?

Not today. Raspberry Pi's capability table lists AI HAT+ as a Hailo-8L at 13 TOPS or Hailo-8 at 26 TOPS, both INT8, using the Pi 5's memory, and marks LLM and VLM support as not supported. The AI HAT+ 2 is a Hailo-10H at 40 TOPS with its own 8 GB and does run LLMs and VLMs up to about 6 billion parameters. That is a real change, but a VLA is a VLM with an action head served through a runtime that expects a PyTorch device, and every model must pass through Hailo's compiler. Treat an AI HAT as a local perception stage, not a way to serve your fine-tuned policy.

Record the dataset, not the yak shave

The AY-Robots desktop client records LeRobot-format datasets straight from a teleoperation session: episodes, camera streams and joint states, in the format the trainers expect. Check the supported platforms and get it set up before you spend a weekend on encoder flags.

Get the desktop client

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started