Download
Get it
Pinion for macOS
A real .app: one Swift file and a bundle layout, no Xcode
project. It starts the control server, waits for it to answer rather
than guessing a delay, and shows it in a native window. Closing the
window takes the server with it — a stray process holding the servo bus
would leave the next launch unable to talk to the arm at all.
Honest requirements: this is a window onto the Pinion repository, not a self-contained program. It needs the repo, its virtual environment, and the arm on a FEETECH USB adapter. Without the arm it still opens — you get the 3D model, and it watches the bus and attaches by itself the moment a board appears.
First run: the app is signed ad-hoc, not
notarised, so macOS quarantines it on download and Gatekeeper refuses it.
Right-click → Open does not clear that — it only waives an
unknown developer, and an ad-hoc signature has no developer at all. Drag
it to Applications, then run
xattr -dr com.apple.quarantine /Applications/Pinion.app.
Geometry
What "derived" means
Placing the parts by hand produced a gripper that looked roughly right from one angle and obviously wrong from every other. So the assembly is solved from the things that actually locate the parts on the robot.
Three parts share one 32 × 20 mm four-bolt pattern. The solver finds the holes, matches them, and reports when two orientations fit equally rather than silently picking one — which is the sort of thing that quietly mirrors a part and is never noticed again.
The jaw claws did not exist. The printed fingers are rack carriers: each ends in a bare post with two M4 holes and nothing on them, so the model shut on a 59.5 mm gap between two posts and the arm appeared to have no hands. A generator measures the posts and the room around them and builds the pads that go there. The reach it derives — post face to one grip-gap short of centre — comes out identical on both sides without being made to, which is the check that the two fingers really are a mirrored pair.
The interesting bug
A rack and pinion that came apart as it worked
The first solve anchored each rack's innermost tooth at the pinion for the jaws-shut pose. That put the gear at the very end of the teeth when shut, and every millimetre of opening slid the rack further off the gear — by full travel the contact point was 13 mm past the last tooth.
Anchored on a tooth
Shut is decided by one rack. Looks correct in a still of the closed gripper, and comes apart the moment it moves.
Anchored on contact
Shut is where the two fingers meet. Both translate along one axis, so the clearance in any column of the cross-section is a constant plus twice the shift: measure the tightest column once — 31.019 mm — and the shift that opens it to 2 mm follows. No search, no tuning.
There is now a test that asserts the pinion contact point stays on each rack at every jaw position, because that is exactly the class of error a still image cannot show you.
The app
Why it is a native app and not a browser tab
The 3D view is raw WebGL over the arm's own STLs — meshes arrive as interleaved position and normal float32 that upload straight to a GPU buffer, so the page carries no mesh loader. STL stores one normal per facet, so shared vertices are welded and their normals averaged, but only across facets meeting below a crease angle: smoothing everything rounds off the box corners and makes printed parts look inflated.
None of that wanted rewriting in Swift. What was missing was a way to simply open it — so the app is the window, the menu, and the process supervisor, and nothing else.
It also fixed something no amount of Python could: the camera scan returned
nothing from inside the app while the identical probe from a terminal found it
every time. macOS gates local-network access behind an explicit grant
and denies an app that does not declare why — silently, with no error on either
side. Declaring it in Info.plist was the whole fix, and it
had been blocking the camera stream too.
Control
One symptom, four wrong answers
"The wrist stops moving when the arm is extended" took four attempts, and the first three are the useful part of the story.
Not torque
Everything past that joint is about 240 g at a 64 mm arm — 1.5 kg·cm against a 30 kg·cm rating. Five percent of what the servo has.
Not travel
196.6° of calibrated range. It was nowhere near a stop.
Partly the loop
Wrist pitch was the one gravity-loaded joint left on the base anti-windup allowance, while base yaw — which gravity does not load at all — sat above it.
Mostly the servo
Overload protection in EEPROM: exceed 80% load for 200 ms and the servo drops itself to 20% torque and latches there. Not enough to lift what the wrist carries. A servo protecting itself and a servo that is broken look identical from outside.
The same shape of mistake showed up in the coupled axes. Driving the claw up and down moved only the shoulder, so the arm swung about one pivot and the claw arced inward instead of descending. Adding a fixed elbow ratio was still wrong: holding the claw's attitude and its reach, that ratio is a property of the pose and runs from −4.2 to +0.7 across the working range. It is solved from the Jacobian every tick now, and measures 0.000 mm/s of drift off the line and 0.0000°/s of tilt.
Near a straight elbow the determinant vanishes, the ratio is genuinely undefined rather than merely large, and the solver refuses instead of flinging the arm. There is a test for that too.