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

I then printed 2 copies of it which will be sandwiched together, and then added the fishing line.

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