A cable-driven parallel robot that watches a pet room and carries a claw to the treat bowl.
Four winches, four lines, one platform that can reach almost any point in the room. A Raspberry Pi sees the dog, an Arduino solves the geometry, an ESP32 works the claw.
Full delivery run at 8x speed. Claw unfortunately didn't close enough to pick up the treat. Click for the real-time video (2 min 49 s).
- What it does
- The platform
- How it works
- Architecture
- Repo layout
- Serial protocol
- Measured results
- Known limitations
- Bill of materials
- Wiring
- Quick start
- Things that cost the most time
- Future work
Type a coordinate in millimetres and the platform goes there. From a browser you can send it to a treat bowl, close the claw, lift, traverse to a crate and release, all while watching a live YOLO-annotated feed of the room.
- Room-scale workspace from four motors and four lengths of line. No rails, no gantry, no linear bearings.
- Exact inverse kinematics that fits on an Arduino Uno: four square roots.
- Straight-line motion from coordinated multi-axis stepping.
- Live pet detection on a Raspberry Pi 5 with YOLOv8n, streamed to any browser on the network.
- A one-click delivery sequence: bowl, grab, lift, crate, release. In the recorded run every move landed but the claw did not close far enough to pick up the treat, so the sequence is proven and the grip is not.
- Honest numbers: every measurement, every failure and every unresolved problem is written down below.
Built in three weeks, from first stepper on a breadboard to a full delivery run with an empty claw.
- Want to build one? Start with docs/build-guide.md.
- Want the maths and software explained from the ground up? Read docs/how-it-works.md.
Four lines meet at the corners of a printed box carrying the ESP32, the OLED face and the power bank. A stalk below it holds the SG90 servo and the two-jaw claw.
Four cables, four winches, three degrees of freedom. To move the platform toward one corner, that corner's winch reels in while the opposite one pays out. Cable lengths encode position.
winch 4 ┌──────────────────────┐ winch 1
(0, 4343) │ │ (3632, 4343)
│ ╲ ╱ │
│ ╲ ╱ │
│ ▣ │ ▣ = platform
│ ╱ ╲ │
│ ╱ ╲ │
(0, 0) │ │ (3632, 0)
winch 3 └──────────────────────┘ winch 2
pulleys at 2794 mm; winches at floor level, line runs up the corner
Inverse kinematics is one distance calculation per cable. For platform position P and anchor i at Aᵢ:
Lᵢ = ‖P − Aᵢ‖ = √((x−xᵢ)² + (y−yᵢ)² + (z−zᵢ)²)
stepsᵢ = Lᵢ × steps_per_mm
Forward kinematics is over-constrained and not solved here. MultiStepper
scales each motor's speed so all four arrive simultaneously, which is what
makes the platform travel in a straight line rather than an arc.
The fixed run from each winch up to its pulley never spools, so it drops out of the maths entirely. It folds into the homing offset.
Raspberry Pi 5 ── USB serial ──► Arduino Uno ──► 4× TMC2209 ──► 4× NEMA 17
ArduCam IMX708 IK + MultiStepper
YOLOv8n
Flask control UI
│
└────────── WiFi ──────────► ESP32 (on platform)
SG90 claw servo
SSD1306 face display
The Pi is the only component that makes decisions. The Arduino executes coordinates; the ESP32 executes claw commands. Neither knows the other exists.
| Path | Contents |
|---|---|
firmware/ |
Arduino sketch: IK, MultiStepper coordination, serial protocol |
esp32/ |
Platform node: WiFi server, claw servo, OLED face, OTA updates |
vision/ |
Camera capture, YOLO detection, homography tools |
control/ |
Serial wrapper and autonomous mission loop |
control.py |
Flask control page: live feed, moves, claw, delivery sequence |
platformio.ini |
Build and upload envs for both boards (uno, esp32, esp32_usb) |
cad/ |
Index of the printed parts; the files themselves are on cainpatel.com |
docs/ |
Build guide, how it works, kinematics, calibration, build log, results |
The Arduino accepts single-character commands at 115200 baud:
| Command | Effect |
|---|---|
M x y z |
Move to coordinate (blocking) |
R |
Report current position |
H x y z |
Declare the platform's actual position |
D / E |
Disable / enable motors |
H is the homing mechanism. There are no limit switches. The operator places
the platform at a known point and tells the firmware where it is.
Effective drum radius changes as line layers build, so steps_per_mm is not
constant. Measured on the original 15.6 mm core drum, 11 samples at one fill
level:
mm per revolution: 53.98, 60.33, 58.74, 58.74, 58.74, 57.15,
55.56, 53.98, 52.39, 58.74, 55.56
mean 56.7 mm/rev
range 52.4 – 60.3 mm/rev (±3.5%)
Across the full drum range the variation was far worse. Payout measured 100.3 mm/rev with the drum full against 56.7 mm/rev near empty, a 44% difference, because each added layer of 1.5 mm line increases effective diameter by 3 mm on a 15.6 mm core.
Redesigning to a 40 mm core cut per-layer diameter change from 19% to
7.5%. Post-redesign calibration: 16000 steps paid out 1524 mm, giving
steps_per_mm = 10.5.
Consequence: at 2 m of paid-out cable, the residual ±3.5% amounts to roughly ±70 mm of position uncertainty.
Cables pull; they cannot push. Every cable's tension must stay positive, which bounds the reachable volume well inside the anchor footprint.
Measured: with anchors spanning 3632 × 4343 mm, the cable from winch 2 went slack at y ≈ 3494 mm, about 80% of the way along that axis. Positions near the middle of a wall are unreachable at any useful height: the two near cables lie in the wall's plane and the far two pull away from it, so nothing pulls the platform toward the wall.
Working positions used in the demo, all verified taut:
| Waypoint | x | y | z |
|---|---|---|---|
| Park | 1500 | 1750 | 2400 |
| Treat bowl | 1500 | 1750 | 1800 |
| Dog crate | 2400 | 2200 | 1600 |
(z is the cable attachment plane; the claw hangs 140 mm below it.)
These are real, measured and unresolved. Listing them is more useful than pretending otherwise.
Open-loop steppers. Skipped steps are silent. The driver keeps counting pulses that produced no rotation, so commanded and actual cable length diverge permanently until re-homed. No encoder feedback is implemented.
Variable spool radius. Documented above. A single steps_per_mm constant
cannot represent a drum whose effective radius depends on how much line is
wound on it. The fix is either single-layer winding (needs a wider drum than a
NEMA 17 shaft comfortably cantilevers) or modelling radius as a function of
paid-out length.
Manual homing. No limit switches. The operator positions the platform and
issues H x y z. Every subsequent move inherits whatever error is in that
measurement.
Anchor coordinates measured by tape. Error here is systematic and propagates into position error everywhere in the workspace. It cannot be calibrated out without remeasuring.
Cable exit point migrates. The point where line leaves the pulley sheave shifts by up to ±15 mm as platform position changes. Not modelled.
Claw power. The SG90 completes its full range on a 3.7 V LiPo through a TP4056, but the current surge collapses the rail and resets the ESP32. Tested with 100 µF, 470 µF and 1470 µF of bulk capacitance, and with both step-ramped and single-write servo commands. Root cause is insufficient current capability in the LiPo/TP4056 output path. Resolved for the demo by powering the ESP32 from a regulated 5 V USB bank; the proper fix is a boost converter.
No mid-move abort. runSpeedToPosition() blocks, so a move cannot be
interrupted once started. Disabling the motors de-energises the drivers and
drops the platform. It is a last resort, not a brake.
Adhesive anchors. Corner pulleys are mounted with 3M VHB rather than mechanical fasteners, at the cost of a permanent installation. One pulley detached during testing. A slack safety tether is required at all times.
Prices are approximate US retail at the time of the build. The printer and laptop are not counted. Replacing the stepper driver and the servo that were destroyed along the way adds about $10.
| Item | Qty | Notes | Price |
|---|---|---|---|
| NEMA 17 stepper, 17HS19-2004S | 4 | one per winch | $60 |
| BigTreeTech TMC2209 v1.2 | 4 | standalone, 1/8 microstep, VREF ≈ 1.10 V | $25 |
| Arduino Uno | 1 | $25 | |
| 12 V PSU, 10 A | 1 | $20 | |
| Inline 5 A blade fuse and holder | 1 | added after a VM/GND reversal cooked a wire | $6 |
| 100 µF electrolytic capacitor | 4 | one per driver, at its VM/GND pins | $3 |
| U-groove 608 bearing | 4 | 30 mm OD, 8 mm bore, 10 mm wide | $10 |
| M8 × 45 bolt, nyloc nut, narrow washers | 4 | pulley axles | $6 |
| Braided Dyneema line | ~40 m | $12 | |
| 3M VHB 5952 tape | 1 roll | corner pulley mounting | $12 |
| Raspberry Pi 5, 8 GB | 1 | $80 | |
| ArduCam IMX708 | 1 | 22-pin to 15-pin Pi 5 ribbon required | $30 |
| Pi 5 camera ribbon, 22-pin to 15-pin | 1 | not included with either the Pi or the camera | $5 |
| ESP32 DevKit | 1 | $8 | |
| SG90 servo | 1 | rubber-band compliance on the jaws | $3 |
| SSD1306 OLED, 128×64 | 1 | I²C on GPIO 21/22 | $5 |
| USB power bank | 1 | platform power, regulated 5 V | $15 |
| PETG filament | ~500 g | spools, pulley housings, brackets, platform, claw | $15 |
| Wire, screw terminals, jumpers, spare fuses | $15 | ||
| Total | ≈ $350 |
The full list with the reasoning behind each part is in docs/hardware.md.
Arduino Uno, all four winches wired DIR-first:
| Winch | STEP | DIR |
|---|---|---|
| 1 | 3 | 2 |
| 2 | 5 | 4 |
| 3 | 7 | 6 |
| 4 | 9 | 11 |
Shared EN on D10. Winch 4 uses D11 for DIR because D8 stopped driving; its coil pairs are also swapped relative to the others, so its rotation direction is set in hardware rather than by the invert flag.
TMC2209 setup: MS1/MS2 unconnected gives 1/8 microstepping (1600 steps/rev).
I_RMS ≈ VREF × 0.71 on the v1.2 board's 0.11 Ω sense resistors, so 1.10 V
≈ 0.78 A RMS. Every driver needs its own 100 µF capacitor physically at its
VM and GND pins, and every ground (PSU, Arduino, all four drivers) must be one
net.
Pi setup
sudo apt install -y python3-picamera2 python3-opencv python3-venv
python3 -m venv --system-site-packages ~/cv
source ~/cv/bin/activate
pip install ultralytics pyserial flask requests--system-site-packages is required. picamera2 is an apt package with C
extensions bound to libcamera and is not reliably installable from PyPI.
Flash the boards
cp esp32/secrets.example.h esp32/secrets.h # WiFi details and an OTA password
cp platformio.local.example.ini platformio.local.ini # same OTA password, for uploads
pio run -e uno -t upload
pio run -e esp32_usb -t upload # first flash, over USB
pio run -e esp32 -t upload # OTA once the ESP32 is on the networkRun
cp vision/config.example.yaml vision/config.yaml # anchors, ports, waypoints
python control.py # then open http://<pi-ip>:5000python control.py --dry-run runs the page and the camera with serial and
the ESP32 faked, which is the way to check a sequence before the cables move.
Home the platform before the first move: measure its position and send
H x y z over serial, or use the H button on the page.
Recorded because they are the parts that are not obvious from the code.
Picamera2's RGB888 format returns BGR byte order. Both YOLO and
cv2.imwrite expect BGR, so no colour conversion is needed. Adding one
produces blue dogs.
AccelStepper(DRIVER, step, dir) takes STEP first. Wiring DIR to the
first pin silently produces a motor that runs but ignores direction.
Breadboard contacts cannot carry four motors. Three drivers would run and a fourth would drop out, with the victim changing between runs. Rated 1–2 A per contact, springy, and worse after repeated reseating. Screw terminals for the VM and GND distribution would have saved hours.
Wire colours do not identify stepper coil pairs. Measure with a multimeter: 1–5 Ω is one coil, open circuit is two different ones. One motor in the same batch was wired differently from the other three.
The Pi 5 camera ribbon goes gold-contacts-toward-the-USB-ports. Backwards gives no error, no warning and no camera.
A servo commanded past its mechanical limit stalls and destroys itself in
under a minute. Find the reachable angle range with the horn detached before
connecting it to anything, and detach() after every move.
- Encoder feedback (AS5600 on each drum) to detect step loss
- Limit switches or StallGuard for automatic homing
steps_per_mmas a function of paid-out length- ArUco marker on the platform for optical ground truth against commanded position, the measurement that would turn the accuracy section from estimated to measured
- 5 V boost converter for the claw so the platform runs on a LiPo again
- Mechanical pulley anchors
This is a LAN device with no login. The control page on the Pi and the HTTP
server on the ESP32 accept commands from anyone on the same WiFi, and either
one can move a 350 g platform around a room. Run it on a network you trust
and never expose port 5000 or the ESP32 to the internet. Over-the-air
firmware updates do require a password (set in esp32/secrets.h). WiFi
credentials never go in tracked files; the two gitignored copies are
esp32/secrets.h and platformio.local.ini.
- Claw design inspired by the SG90 servo gripper on GrabCAD.
- AccelStepper and MultiStepper by Mike McCauley. Ultralytics YOLO. OpenCV. Picamera2 by Raspberry Pi Ltd. U8g2 by olikraus. ESP32Servo by Kevin Harrington.
MIT. See LICENSE.

