Skip to content

Field notes · Hardware

Stringman Hangs a Gripper From Four Ceiling Lines. Run the Cable Math Before You Buy the Kit

Stringman is an open source room-scale gripper that hangs from four motors in the ceiling corners. Before spending $655 on the kit, we wrote the cable math in numpy, built a four-cable robot in MuJoCo from scratch, and ran both on our box. Here is where the lines go slack, why the top of the room is expensive, and the first command to try yourself.

A simple room box drawn in thin lines with four anchors in the top corners and four lines meeting at a gripper. Two readouts compare 5.8 N per line at 1.0 m with 26.3 N per line at 2.2 m, with Amber on 26.3 N.

A field note on cable-driven robots. Every number in the test sections came from scripts we ran on our box on Nov 10, 2026. The scripts ship with this note.

The short version

Most home robots are vacuums on wheels, and wheels get stuck on socks, cords and chair legs. Stringman skips the floor. Four winch motors mount in the upper corners of a room, each one reels a line to a gripper, and changing the four line lengths moves the gripper anywhere in the space between them. It’s a claw machine the size of your living room.

The maker, Neufangled, published the designs, the parts list and the software. The Show HN post went up Nov 3 and sat at 844 points and 251 comments this morning. The firmwareLow-level software stored on a chip inside a device that controls the hardware directly and runs before any operating system loads. Read more → lives at nhnifong/cranebot3-firmware under Apache-2.0.

It isn’t free to build. The store lists $795 for the assembled system and $655 for the DIY hardware kit, and the published BOM totals $645.57 at the maker’s batch prices, before printed parts. So before you spend anything, run the physics. That’s what this note does, for $0.

Terms worth pinning down

Cable-driven parallel robot. A tool held by several cables at once. The cable lengths set the tool’s position. Stadium sky cameras that glide over a field on four cables work the same way.

Inverse kinematics. Working backward from “I want the gripper here” to “each line needs to be this long.” For a cable robot it’s just the straight-line distance from each anchor to the gripper.

Tension. How hard a line pulls, in newtons. One kilogram hanging still weighs about 9.81 N.

Slack. A line with no tension. A rope can pull, but it can’t push. If the math asks a line to push, that line goes limp and stops helping.

Redundancy. Stringman uses four lines to set three coordinates: x, y and height. That leaves one extra line. The extra line is what lets the controller keep every line taut, and it means there’s no single right answer for the tensions. The controller has to pick one.

What the maker says it is

Read the vendor’s own label before you plan a weekend around it. From the Stringman docs:

It is currently a public prototype. Is is launched, can self-calibrate in a new room and run policies, but is not yet reliable enough to be called a home appliance.

The typo is theirs. The same page’s safety section says the gripper sometimes crashes into things when moving fast and asks owners to keep the kill switch at hand. I’d rather read that than a glossy launch page. It tells you what you’re buying: a working research platform, not a finished appliance.

Specs from the homepage: rooms from 3 m to 10 m on the floor diagonal and 2.3 m to 4 m high, payload up to 2.2 kg near the floor and 1.9 kg on average, traverse speed around 0.4 m/s, and a 28 N per-line tension limit used for the payload figure. A host PC runs the brains. A Pi Zero 2W sits in the gripper and Pi 3A+ boards sit in the anchors.

License check. The downloads page puts the hardware designs under CERN-OHL-W 2.0 with an addendum that the user takes responsibility for injury or damage. The OSHWA certification US002788 says the license on everything is Apache-2.0, and the docs say the design is Apache 2.0. The firmware repo is Apache-2.0. The PCB repo has no LICENSE file. If you plan to fork the hardware or sell anything based on it, ask the maker which one governs. For tinkering at home it doesn’t change anything.

Test 1: the cable math in numpy

The geometry is simple enough to check by hand, so I wrote it out. The room is made up for teaching and is not Stringman’s spec: 4 m by 5 m, anchors in the four ceiling corners at 2.5 m, a 1 kg load, lines treated as weightless and non-stretching. The script works out each line length, then solves for the tensions that hold the load still. Full script: src/cable_math.py.

A = np.array([[0,0,2.5],[4,0,2.5],[4,5,2.5],[0,5,2.5]], float)  # anchors
v = A - p                      # anchor minus gripper position
L = np.linalg.norm(v, axis=1)  # line lengths: this is the whole inverse kinematics
u = v / L[:, None]             # unit vector along each line
t = np.linalg.lstsq(u.T, [0, 0, m*g], rcond=None)[0]  # tensions that balance gravity

Real output, Python 3.13.5, numpy 2.5.4:

pos (2, 2.5, 1.0): lines m = [3.536 3.536 3.536 3.536]  tension N = [5.8 5.8 5.8 5.8]  residual 5.03e-15
pos (2, 2.5, 2.2): lines m = [3.216 3.216 3.216 3.216]  tension N = [26.3 26.3 26.3 26.3]  residual 6.77e-15
pos (0.5, 0.5, 1.0): lines m = [1.658 3.841 5.895 4.77 ]  tension N = [8.7 2.5 1.  2.3]  residual 4.44e-15
pos (3.8, 4.8, 0.3): lines m = [6.505 5.284 2.218 4.395]  tension N = [-0.7  1.5  8.7  1.5]  residual 2.39e-15
1 mm error on line 1 at center, z=1.0 moves gripper (mm): [ 0.44  0.35 -0.59]

Three things fall out of five lines of output.

The top of the room is expensive. Same 1 kg, room center. At 1.0 m high each line carries 5.8 N. Lift it to 2.2 m, 30 cm under the anchors, and each line carries 26.3 N, about 4.5 times as much. The lines are nearly flat up there, so most of their pull goes sideways into the other lines and only a little goes up. Compare that against Stringman’s 28 N per-line limit and you can see why the payload spec is quoted near the floor.

One line wants to push. Low in the far corner at (3.8, 4.8, 0.3), the solver asks line 1 for -0.7 N. A rope can’t do that. This is where a naive controller fails, and it’s the question I wanted the second test to answer.

Small length errors stay small in the middle. Make line 1 one millimeter too long at room center and the gripper moves less than a millimeter on each axis. That estimate holds at the center only. Near the walls the line angles change and the same error moves the gripper further.

Test 2: the same robot in MuJoCo, built from scratch

MuJoCo is the free, open source physics engine from Google DeepMind. Stringman ships a MuJoCo sim of its own in the firmware repo’s simulator folder. I didn’t use theirs for this test. I wanted to check my own math, and a model small enough to read in one screen teaches more than a full robot.

The trick that makes this work is how MuJoCo handles a tendon with a length limit. A spatial tendon runs between two points. Give it limited="true" and a maximum length, and MuJoCo stops it from getting longer than that but does nothing when it’s shorter. That’s a rope. Four of those from the ceiling corners to a 1 kg ball gives a four-cable robot:

<spatial name="c0" limited="true" range="0 3.535534">
  <site site="a0"/><site site="g"/>
</spatial>

The script sets each tendon’s maximum length to the inverse-kinematics length for the target point, lets the ball settle for 5 seconds of sim time, then reads the force each tendon carries. It also solves the statics a second way: it uses the redundant fourth line to search for a tension set where every line is zero or positive. Full script: src/cable_sim.py. Install is one line in a virtualenv: pip install mujoco numpy.

Real output, MuJoCo 4.0.0 from PyPI, Python 3.13.5:

pos (2, 2.5, 1.0)
  min-norm static solve N : [5.8 5.8 5.8 5.8]
  all-positive solve N    : [5.8 5.8 5.8 5.8]
  MuJoCo cable forces N   : [5.8 5.8 5.8 5.8]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [ 0.   0.  -0.6]
pos (2, 2.5, 2.2)
  min-norm static solve N : [26.3 26.3 26.3 26.3]
  all-positive solve N    : [26.3 26.3 26.3 26.3]
  MuJoCo cable forces N   : [25.7 25.7 25.7 25.7]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [ 0.   0.  -6.9]
pos (0.5, 0.5, 1.0)
  min-norm static solve N : [8.7 2.5 1.  2.3]
  all-positive solve N    : [8.4 3.1 0.  3.1]
  MuJoCo cable forces N   : [8.7 2.4 1.2 2.2]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [ 0.   0.  -0.4]
pos (3.8, 4.8, 0.3)
  min-norm static solve N : [-0.7  1.5  8.7  1.5]
  all-positive solve N    : [0.  0.9 9.  1. ]
  MuJoCo cable forces N   : [0.  0.9 9.  1. ]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [-0.1 -0.1 -0.3]

What I read from it

The “pushing” line just goes slack, and the robot still holds. At the far corner the physics put 0 N on line 1 and carried the load on the other three: 0.9, 9.0 and 1.0 N. That matches the all-positive solve exactly. My first read of Test 1 was “the robot loses control there.” That was wrong. The fourth line is spare, so three taut lines can still pin the point. The real lesson is narrower: the simple least-squares tension answer is not a valid command for a rope, and a controller has to solve for nonnegative tensions on purpose. When you see a negative number in your own math, that’s what it’s telling you.

Top-down room diagram with four anchors. The gripper sits near one corner. Line 1 is drawn dashed and labeled 0 N, the others solid and labeled 0.9 N, 9.0 N and 1.0 N. Amber on the dashed 0 N line. A side panel repeats those tensions. Footer: The simple solve asked L1 for -0.7 N. A rope can't push.

Redundancy means more than one right answer. At (0.5, 0.5, 1.0) the sim settled on 8.7, 2.4, 1.2 and 2.2 N. My all-positive solve picked 8.4, 3.1, 0 and 3.1 N. Both hold the load. With four lines and three coordinates, any blend along that spare direction works as long as nothing goes negative. Which one a real controller picks is a design choice: lowest peak tension, most even tension, or a minimum pretension on every line so none flaps around.

The sim sags near the ceiling, and that teaches the right thing for the wrong reason. At 2.2 m the ball settled 6.9 mm low and the forces dropped from 26.3 to 25.7 N. That sag comes from MuJoCo’s soft constraint model, not from rope stretch, so don’t read 6.9 mm as a Stringman number. The direction is still right. At shallow angles a tiny bit of give turns into a big vertical drop, and a slightly steeper angle sheds tension fast. Real braided line stretches under load, so on real hardware you’d expect the same effect near the top of the room.

Every run balanced. The vertical forces summed to 9.81 N in all four cases, the weight of 1 kg. When I build a sim from scratch, that check comes first. If the forces don’t add up to the load, the bug is in the model, not the physics.

What I didn’t test

I did not run Stringman’s own controller-in-the-loop sim (simulator/robot_simulator.py --mujoco) or the full pip install "nf_robot[host]" host stack. The firmware README warns the PyTorch and CUDA dependencies come to about 5.5 GB. I also have no hardware, so nothing here measures real line stretch, motor accuracy or anchor survey error. The room and the 1 kg load are my own example numbers.

How an operator would think about a ceiling robot

These are the checks I’d make before drilling a hole, in the order I’d make them.

  1. What holds the anchors. Each anchor sees the line tension plus the motor’s own weight, and the numbers above show tension climbing fast near the ceiling. Find studs or joists. Drywall anchors are the wrong answer for something that pulls sideways all day.
  2. Where the lines go slack in my room. Plug your real anchor coordinates into src/cable_math.py, sweep a grid of points, and flag every point with a negative tension in the simple solve. Those are the spots where the controller depends on the spare line, and the spots to test first.
  3. What happens on power loss. Read the maker’s docs on how the winches behave unpowered before anything heavy hangs over a couch.
  4. Who is in the room. It’s a prototype that can crash at speed, per the maker. Keep the kill switch where you can reach it, and keep kids and pets clear while you learn its habits.
  5. Local or cloud. The README’s local UI runs at http://localhost:8090/?robotid=lan. The web control panel at neufangled.com is optional. Decide which you want before you put a camera-carrying robot on your network.

Your first step

Copy the two scripts from this note and run them. On any Linux box with Python 3:

python3 -m venv venv && . venv/bin/activate
pip install mujoco numpy
python cable_sim.py

Then change the anchor coordinates in A to your own room, measured in meters from one corner, and add a few target points near your walls and ceiling. Watch which lines hit zero.

When you want the real robot in software, clone cranebot3-firmware and follow simulator/README.md. It loads stringman_arp_carbon270.xml, the maker’s model, and can run their controller against it. Their training data is public too. naavox/tidy_up on Hugging Face is one of the Stringman datasets you can browse without an account.

If the sim hooks you, the print guide, the BOM and the downloads page are the path to building one. No BNLC lesson covers cable robots yet.

Sources

Scripts

src/cable_math.py

# Cable-driven parallel robot: inverse kinematics (line lengths) and static tension.
# Example room: 4 m x 5 m, anchors in the ceiling corners at 2.5 m. Gripper hangs on 4 lines.
# Our own illustrative geometry, not Stringman's spec. Massless, inextensible lines assumed.
import numpy as np

A = np.array([[0,0,2.5],[4,0,2.5],[4,5,2.5],[0,5,2.5]], float)
m, g = 1.0, 9.81  # 1 kg gripper plus payload (example)
def solve(p):
    p = np.array(p, float)
    v = A - p
    L = np.linalg.norm(v, axis=1)
    u = v / L[:,None]
    # u^T t = [0,0,m g]; pick the min-norm nonnegative solution
    t = np.linalg.lstsq(u.T, np.array([0,0,m*g]), rcond=None)[0]; res = float(np.linalg.norm(u.T@t-np.array([0,0,m*g])))
    return L, t, res
for p in [(2,2.5,1.0),(2,2.5,2.2),(0.5,0.5,1.0),(3.8,4.8,0.3)]:
    L, t, res = solve(p)
    print(f"pos {p}: lines m = {np.round(L,3)}  tension N = {np.round(t,1)}  residual {res:.2e}")
# Sensitivity: how far does the gripper move if one line is 1 mm long?
p0 = np.array([2,2.5,1.0]); eps=1e-4
J = np.array([(np.linalg.norm(A-(p0+eps*e),axis=1)-np.linalg.norm(A-p0,axis=1))/eps for e in np.eye(3)]).T
dp = np.linalg.lstsq(J, np.array([0.001,0,0,0]), rcond=None)[0]
print("1 mm error on line 1 at center, z=1.0 moves gripper (mm):", np.round(dp*1000,2))

src/cable_math.out

pos (2, 2.5, 1.0): lines m = [3.536 3.536 3.536 3.536]  tension N = [5.8 5.8 5.8 5.8]  residual 5.03e-15
pos (2, 2.5, 2.2): lines m = [3.216 3.216 3.216 3.216]  tension N = [26.3 26.3 26.3 26.3]  residual 6.77e-15
pos (0.5, 0.5, 1.0): lines m = [1.658 3.841 5.895 4.77 ]  tension N = [8.7 2.5 1.  2.3]  residual 4.44e-15
pos (3.8, 4.8, 0.3): lines m = [6.505 5.284 2.218 4.395]  tension N = [-0.7  1.5  8.7  1.5]  residual 2.39e-15
1 mm error on line 1 at center, z=1.0 moves gripper (mm): [ 0.44  0.35 -0.59]

src/cable_sim.py

# BytesNation field note, Nov 10, 2026: a 4-cable parallel robot in MuJoCo, built from scratch.
# Same illustrative room as our numpy calculator: 4 m x 5 m, anchors in the ceiling corners at 2.5 m,
# 1 kg load. Each cable is a MuJoCo spatial tendon with an upper length limit, which behaves like a
# rope: it can pull when taut and does nothing when slack. Not Stringman's model or spec.
import numpy as np, mujoco

A = np.array([[0,0,2.5],[4,0,2.5],[4,5,2.5],[0,5,2.5]], float)
m, g = 1.0, 9.81

def xml(p, L):
    sites = "".join(f'<site name="a{i}" pos="{a[0]} {a[1]} {a[2]}" size="0.02"/>' for i, a in enumerate(A))
    tend = "".join(f'<spatial name="c{i}" limited="true" range="0 {L[i]:.6f}"><site site="a{i}"/><site site="g"/></spatial>' for i in range(4))
    return f'''<mujoco><option timestep="0.001" gravity="0 0 -{g}"/>
<worldbody>{sites}
<body name="grip" pos="{p[0]} {p[1]} {p[2]}"><freejoint/>
<geom type="sphere" size="0.05" mass="{m}" contype="0" conaffinity="0"/><site name="g"/></body>
</worldbody><tendon>{tend}</tendon></mujoco>'''

def nonneg_tensions(p):
    # Static balance with the 4th line's redundancy: t = t0 + s*n, pick s so every t >= 0 and spread is smallest.
    v = A - np.array(p, float); u = v / np.linalg.norm(v, axis=1)[:, None]
    b = np.array([0, 0, m*g]); t0 = np.linalg.lstsq(u.T, b, rcond=None)[0]
    n = np.linalg.svd(u.T)[2][-1]
    s = np.linspace(-50, 50, 200001); T = t0[None, :] + s[:, None]*n[None, :]
    ok = (T >= 0).all(axis=1)
    if not ok.any(): return t0, None
    Tk = T[ok]; return t0, Tk[np.argmin(Tk.max(axis=1))]

for p in [(2,2.5,1.0),(2,2.5,2.2),(0.5,0.5,1.0),(3.8,4.8,0.3)]:
    L = np.linalg.norm(A - np.array(p), axis=1)
    model = mujoco.MjModel.from_xml_string(xml(p, L)); data = mujoco.MjData(model)
    data.qvel[:] = 0
    for _ in range(5000):  # 5 s of sim time to settle
        data.qvel[:] *= 0.995
        mujoco.mj_step(model, data)
    F = np.zeros(4)
    for r in range(data.nefc):
        if data.efc_type[r] == mujoco.mjtConstraint.mjCNSTR_LIMIT_TENDON:
            F[data.efc_id[r]] += abs(data.efc_force[r])
    drift = (data.xpos[1] - np.array(p)) * 1000
    t0, tp = nonneg_tensions(p)
    print(f"pos {p}")
    print(f"  min-norm static solve N : {np.round(t0,1)}")
    print(f"  all-positive solve N    : {np.round(tp,1) if tp is not None else 'none exists'}")
    print(f"  MuJoCo cable forces N   : {np.round(F,1)}  sum of vertical check {np.round((F*((A-data.xpos[1])/np.linalg.norm(A-data.xpos[1],axis=1)[:,None])[:,2]).sum(),2)} vs {m*g}")
    print(f"  settled drift mm        : {np.round(drift,1)}")

src/cable_sim.out

pos (2, 2.5, 1.0)
  min-norm static solve N : [5.8 5.8 5.8 5.8]
  all-positive solve N    : [5.8 5.8 5.8 5.8]
  MuJoCo cable forces N   : [5.8 5.8 5.8 5.8]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [ 0.   0.  -0.6]
pos (2, 2.5, 2.2)
  min-norm static solve N : [26.3 26.3 26.3 26.3]
  all-positive solve N    : [26.3 26.3 26.3 26.3]
  MuJoCo cable forces N   : [25.7 25.7 25.7 25.7]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [ 0.   0.  -6.9]
pos (0.5, 0.5, 1.0)
  min-norm static solve N : [8.7 2.5 1.  2.3]
  all-positive solve N    : [8.4 3.1 0.  3.1]
  MuJoCo cable forces N   : [8.7 2.4 1.2 2.2]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [ 0.   0.  -0.4]
pos (3.8, 4.8, 0.3)
  min-norm static solve N : [-0.7  1.5  8.7  1.5]
  all-positive solve N    : [0.  0.9 9.  1. ]
  MuJoCo cable forces N   : [0.  0.9 9.  1. ]  sum of vertical check 9.81 vs 9.81
  settled drift mm        : [-0.1 -0.1 -0.3]
stringmancable-driven-robotroboticsmujocosimulationinverse-kinematicsopen-hardwarepythonnumpy

← All field notes