Final Project: Spirobs
My motivation is to think about objects that are camouflaged but have a very interesting and surprising functional form. I originally thought about making a legged robot, but I decided I wanted to explore a different mechanical form rather than rigid legs which I do have prior knowledge in. I have always been curious about alternatives, such as tendon based robots. This lead me to explore the following videos / research.

https://www.youtube.com/watch?v=MxBeUQay8YM&t=38s

https://www.youtube.com/watch?v=Q2yYclPaEV0
I really likes the elegant design of the SpiRobs paper, https://arxiv.org/abs/2303.09861

Potential extensions I want to explore is how to add more DOF to it by having small solenoids on some of the segments that can clamp down on the wire. This video describes this idea https://www.youtube.com/watch?v=y9G-J1wP5O4&t=136s

Following spiral development I will first recreate SpiRobs, and if time persists, try to develop a locking mechanism to extend the DoF of SpiRobs.
I made some early attempts at understanding how to build a flexible core - this is 3d printed via PLA and it was too stiff and brittle. I ordered TPU filament as a result.
The TPU arrived and I first printed a long core of different thickness, but then I made their height just 1mm and it was too floppy. Then settled on a 3mm x 3mm x ? rod as the core. Then I went to designing the cad files of each segment.

I wanted to write a python program to find the vertex of each segment. I coded the basic logic first myself and then worked with ChatGPT to finish the code. Originally I was trying to have python save an STL file - but that didn’t work well and after chatting with Anthony he suggested I used SVG / DXF, and that worked perfectly. So the code below generates a DXF of each segment - there is still an unfixed bug which I will point out later.

# made in collaboration with ChatGPT
import math
import numpy as np
# coef for p = a e^{bθ}
a = 0.05
b = 0.2
def p(a, b, theta):
return a * math.exp(theta * b)
def polar_to_cartesian(p, theta):
x = p * math.cos(theta)
y = p * math.sin(theta)
return x, y
def reflect_points_across_line(points_to_reflect, line_points):
(x1, y1), (x2, y2) = points_to_reflect
(x3, y3), (x4, y4) = line_points
P1, P2 = np.array([x1, y1]), np.array([x2, y2])
A, B = np.array([x3, y3]), np.array([x4, y4])
d = B - A
d = d / np.linalg.norm(d)
def reflect_point(P):
AP = P - A
proj = A + np.dot(AP, d) * d
return 2 * proj - P
return tuple(reflect_point(P1)), tuple(reflect_point(P2))
def get_segment(a, b, d, i, p):
theta_1 = d * i
theta_2 = d * (i + 1)
# Radii
p1 = p(a, b, theta_1)
p2 = p(a, b, theta_2)
p3 = (p(a, b, theta_1 + 2 * math.pi) - p1) / 2 + p1
p4 = (p(a, b, theta_2 + 2 * math.pi) - p2) / 2 + p2
# Convert to Cartesian
x1, y1 = polar_to_cartesian(p1, theta_1)
x2, y2 = polar_to_cartesian(p2, theta_2)
x3, y3 = polar_to_cartesian(p3, theta_1)
x4, y4 = polar_to_cartesian(p4, theta_2)
# Reflect across line connecting (x3, y3) and (x4, y4)
(x5, y5), (x6, y6) = reflect_points_across_line(
[(x1, y1), (x2, y2)], [(x3, y3), (x4, y4)]
)
# --- Reorient segment so that (x3, y3) -> (0,0) and (x4, y4) -> (L,0) ---
# Translation
points = np.array([
[x1, y1],
[x2, y2],
[x3, y3],
[x4, y4],
[x5, y5],
[x6, y6],
])
translated = points - np.array([x3, y3])
# Rotation
vec = np.array([x4 - x3, y4 - y3])
angle = -math.atan2(vec[1], vec[0]) # rotate so line aligns with +x axis
rot = np.array([
[math.cos(angle), -math.sin(angle)],
[math.sin(angle), math.cos(angle)],
])
rotated = translated @ rot.T
# Unpack rotated points
x1, y1, x2, y2, x3, y3, x4, y4, x5, y5, x6, y6 = rotated.flatten()
# Build polygon (closing loop)
polygon = [
(float(x1), float(y1)),
(float(x2), float(y2)),
(float(x4), float(y4)),
(float(x6), float(y6)),
(float(x5), float(y5)),
(float(x3), float(y3)),
(float(x1), float(y1)),
]
return polygon
for i in range(10):
points = get_segment(a, b, math.pi / 6, i, p)
import ezdxf
from pathlib import Path
def points_to_dxf(points, filename="points.dxf", closed=False, scale=100):
"""
Converts a list of (x, y) points into a DXF file containing a polyline.
scale: multiply coordinates (so small float coords become visible in CAD)
"""
doc = ezdxf.new(dxfversion="R2010")
msp = doc.modelspace()
# scale and optionally close
pts = [(x * scale, y * scale) for x, y in points]
if closed and pts[0] != pts[-1]:
pts.append(pts[0])
# Add the polyline
msp.add_lwpolyline(pts, close=closed)
# Save
doc.saveas(filename)
print(f"✅ Saved {filename}")
points_to_dxf(points, filename=f"parts/segment_{i}.dxf", closed=True)ChatGPT Logs:
https://chatgpt.com/share/6913e0d8-05b8-8007-8ce4-7f0559265aff
https://chatgpt.com/share/6913e0fd-fab8-8007-827b-e628fe403dfc
https://chatgpt.com/share/6913e10c-d464-8007-a3f9-7174ef98012d
https://chatgpt.com/share/6913e121-7bd4-8007-b7df-f60ad04ce98b
https://chatgpt.com/share/6913e130-ccb4-8007-b2f5-3f8666a7d219
https://chatgpt.com/share/6913e151-2af4-8007-9358-8753e2e84de1
Here I manually added each part in OnShape, and moved it. I need to figure out how to automate this process. I then extruded some holes for the TPU core and the cable wire (I am using fishing line).
Next todo - see how the segments don’t align properly - there is a bug with my math. Then I noticed where the fishing line meets the glued segments, there is a lot of stress there and it snaps through the super glue - so I need to redesign it with that in mind.
Midterm Review

The black lines are the main cables to pull the segments, the red are for clamping the black cables. There will be 4 stepper motors.
The tasks left to do are,
Fix the segment generator bug
Make a larger version (around a foot in total length)
Make another motor driver
design and add the cable clamping mechanism
Timeline:
this week fix the generator bug and make a larger final size version and design the clamping mechanism
Next week is make the second motor driver
Then assemble a casing for all the steppers in the third week




