Weeks 11 & 12

Making Sisyphus Kinetic Art Table.

Team Griffin

This week we worked together in Team Griffin: Ondřej Plšek, Vojtěch Mareček, Filip Švadlenka, Jan Doležil, Lukáš Trčka, Nastasja Samochvalova, and Anna Svobodová. Our task was to build a computer-controlled machine (assignment details).

Sand Art Concept

We came up with the idea to draw relaxing patterns and images in sand.

Video Demonstrations

Here are a few videos showcasing the machine's quiet, soothing movements:

My Contribution

I wrote process_scara_gcode.py to convert designs from Sandify directly into SCARA motor commands.

Copied!
import os
import sys
import math

def parse_gcode(file_path):
    alpha = []
    beta = []
    with open(file_path, 'r') as f:
        for line in f:
            line = line.strip()
            if line.startswith('G1') and 'X' in line and 'Y' in line:
                parts = line.split()
                x_val = None
                y_val = None
                for part in parts:
                    if part.startswith('X'):
                        try:
                            x_val = float(part[1:])
                        except ValueError:
                            pass
                    elif part.startswith('Y'):
                        try:
                            y_val = float(part[1:])
                        except ValueError:
                            pass
                if x_val is not None:
                    alpha.append(x_val)
                if y_val is not None:
                    beta.append(y_val)
    return alpha, beta

def main():
    # Absolute path to your .gcode file:
    file_path = r'path\to\your\file\your_file_name.gcode'
    if not os.path.isfile(file_path):
        print(f"Error: file not found:\n  {file_path}", file=sys.stderr)
        sys.exit(1)

    # parse the G-code file
    raw_alpha, raw_beta = parse_gcode(file_path)

    # number of decimal places
    n = 3

    # scaling factor k for angles in radians
    k = - (1/6) * (2 * math.pi)
    alpha_absolute = [round(x * k, n) for x in raw_alpha]
    beta_absolute  = [round(y * k, n) for y in raw_beta]
    
    # total number of steps
    n_steps = len(alpha_absolute)
    
    # rotate to always start at home position
    angle = alpha_absolute[0]
    alpha_absolute = [round(x - angle, n) for x in alpha_absolute]
    beta_absolute  = [round(y - angle, n) for y in beta_absolute]
    
    # convert to differences
    alpha_relative = [alpha_absolute[0]] + [
        round(alpha_absolute[i] - alpha_absolute[i-1], n)
        for i in range(1, n_steps)
    ]
    beta_relative = [beta_absolute[0]] + [
        round(beta_absolute[i] - beta_absolute[i-1], n)
        for i in range(1, n_steps)
    ]
    
    # end difference from home position
    home_alpha = -alpha_absolute[n_steps-1]
    home_beta = -beta_absolute[n_steps-1]
    
    # compute cartesian coordinates of all points in unit circle
    midpoints = [
        (
            math.cos(math.pi/2 + a) * 0.5,
            math.sin(math.pi/2 + a) * 0.5
        )
        for a in alpha_absolute
    ]
    
    points = []
    for (x1, y1), b in zip(midpoints, beta_absolute):
        angle2 = math.pi/2 + b
        x2 = x1 + math.cos(angle2) * 0.5
        y2 = y1 + math.sin(angle2) * 0.5
        points.append((x2, y2))

    # scaling factor k for absolute motor rotation
    k = 1 / (2 * math.pi) * (200 * 16) * (120 / 20)
    alpha_motor_absolute = [round(x * k, n) for x in alpha_absolute]
    beta_motor_absolute  = [round(y * k, n) for y in beta_absolute]

    # belt drift correction
    correction = 1 / 1
    beta_motor_absolute = [round(beta_motor_absolute[i] * correction, n) for i in range(n_steps)]
    
    # convert to differences
    alpha_motor_relative = [alpha_motor_absolute[0]] + [
        round(alpha_motor_absolute[i] - alpha_motor_absolute[i-1], n)
        for i in range(1, n_steps)
    ]
    beta_motor_relative = [beta_motor_absolute[0]] + [
        round(beta_motor_absolute[i] - beta_motor_absolute[i-1], n)
        for i in range(1, n_steps)
    ]
    
    # downsample to take every nth step
    downsample = 3
    alpha_motor_relative = alpha_motor_relative[::downsample]
    beta_motor_relative = beta_motor_relative[::downsample]
    
    # end difference from home position
    modulo_base = round((200 * 16) * (120 / 20)) # equals 19200
    home_alpha_motor = (-alpha_motor_absolute[n_steps - 1]) % modulo_base
    home_beta_motor = (-beta_motor_absolute[n_steps - 1]) % modulo_base

if __name__ == "__main__":
    main()

Design Experiments

At first we didn’t even use the SCARA robot. We tried a rotating arm with a carriage – selecting an angle and a radius. I made a prototype in Desmos.

In the end we went for the SCARA robot – effectively choosing two angles for two arms. Not knowing how to draw a predetermined image, we set constant rotations on both arms and visualized the resulting pattern in a web app, since that makes endless zen patterns possible. I made another prototype in Desmos.

After conducting research, I refined the script and successfully demonstrated its correct operation with a dove pattern in Desmos.

Soon, as the drawings became more complex, Desmos started to lag, so I wrote a new visualization script in Python. Here it is rendering a beautiful face:

Additional Work & Presentation

I also created a library of patterns for users to choose from. For full documentation, see our team site.

During presentations I saw other great projects, like this robotic croupier: