The purpose of this exercise is to calculate the discreet x and y steps (like we might have from a stepper motor) needed to make a line of any slope. Note: this uses Bresenhan's Algorithm https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
The general equation of a line through two given points is: $${\frac {y-y_{0}}{y_{1}-y_{0}}}={\frac {x-x_{0}}{x_{1}-x_{0}}}$$
Let's use the example of the two points (0,0) and (20,10)
The equation for this line is then: $$ {\frac {y-0}{10-0}}={\frac {x-0}{20-0}}$$ or $$y={\frac {x}{2}}$$
So, for every time $x$ makes a step, $y$ moves ${\frac {1}{2}}$ step. Since there are no half steps we need to round and return the 'error' to be accumulated as we travel from $(x_{0}, y_{0})$ to $(x_{1}, y_{1})$. Note: we also need to return the current position.
In [1]:
    
# First do some imports, so we can see what we're doing:
%matplotlib notebook
import matplotlib.pyplot as plt
from math import sqrt
import numpy as np
    
In [2]:
    
def get_line(start, end):
    """Bresenham's Line Algorithm
    from: http://www.roguebasin.com/index.php?title=Bresenham%27s_Line_Algorithm
    Produces a list of tuples from start and end
 
    >>> points1 = get_line((0, 0), (3, 4))
    >>> points2 = get_line((3, 4), (0, 0))
    >>> assert(set(points1) == set(points2))
    >>> print points1
    [(0, 0), (1, 1), (1, 2), (2, 3), (3, 4)]
    >>> print points2
    [(3, 4), (2, 3), (1, 2), (1, 1), (0, 0)]
    """
    # Setup initial conditions
    x1, y1 = start
    x2, y2 = end
    dx = x2 - x1
    dy = y2 - y1
 
    # Determine how steep the line is
    is_steep = abs(dy) > abs(dx)
 
    # Rotate line
    if is_steep:
        x1, y1 = y1, x1
        x2, y2 = y2, x2
 
    # Swap start and end points if necessary and store swap state
    swapped = False
    if x1 > x2:
        x1, x2 = x2, x1
        y1, y2 = y2, y1
        swapped = True
 
    # Recalculate differentials
    dx = x2 - x1
    dy = y2 - y1
 
    # Calculate error
    error = int(dx / 2.0)
    ystep = 1 if y1 < y2 else -1
 
    # Iterate over bounding box generating points between start and end
    y = y1
    points = []
    for x in range(x1, x2 + 1):
        coord = (y, x) if is_steep else (x, y)
        points.append(coord)
        error -= abs(dy)
        if error < 0:
            y += ystep
            error += dx
 
    # Reverse the list if the coordinates were swapped
    if swapped:
        points.reverse()
    return points
    
In [3]:
    
points = [(20,0), (-20, 0), (20, 10), (10, 20), (0, 20), 
          (-10, 20), (-20, 10), (20, -10), (10, -20), 
          (0, -20), (-10, -20), (-20, -10),
          (20, 3), (-20, 3)]
for point in points:
    line = get_line((0,0), point)
    x, y = zip(*line)
    plt.plot(x, y)
    
    
    
           d
m1 o______________o m2
    \          __/
     \      __/
    a \  __/ b
       \/
       *
      (x,y)
Given the coordinates $(x,y)$, we need to calculate the lengths of the two cables which hold the pen, $a$ and $b$.
$a$ is the hypotenuse of a triangle with sides $x$ and $y$, so the length of a is: $$a = {\sqrt {x^2 + y^2}}$$ If the distance between the motors is $d$, then the length of $b$ is given by: $$b = {\sqrt {(d-x)^2 + y^2}}$$
So, if we're looking to draw a line between the "points" $(10, 10)$ and $(25, 50)$, let's first get our cable length targets. Here's a function to do that:
In [60]:
    
def cable_steps(x, y, d=100, steps_per_unit=1):
    a = int((sqrt(x**2 + y**2))*steps_per_unit)
    b = int((sqrt((d-x)**2 + y**2))*steps_per_unit)
    return a, b
    
In [109]:
    
d = 1000
# Get cable lengths (measured in steps) for our two points (measured in inches):
x0, y0 = (850, 400)
x1, y1 = (220, 350)
a0, b0 = cable_steps(x0, y0, d=d)
a1, b1 = cable_steps(x1, y1, d=d)
    
In [110]:
    
a_move = a1-a0
b_move = b1-b0
print a0, b0
print a1, b1
print a_move, b_move
    
    
In [111]:
    
# plot what we know about a couple of points
fig = plt.figure(figsize=(6, 6))
# the motors
m1 = plt.Circle((0, 0), 4, color='black')
m2 = plt.Circle((d, 0), 4, color='black')
# the line between the motors
m_line = plt.Line2D([0, d], [0, 0], color='black')
# the radius of the cables and pen position - start
m1_circle0 = plt.Circle((0, 0), a0, color='gray', fill=False)
m2_circle0 = plt.Circle((d, 0), b0, color='gray', fill=False)
m1_line0 = plt.Line2D([0, x0], [0, -y0], color='red')
m2_line0 = plt.Line2D([d, x0], [0, -y0], color='red')
pen_pos0 = plt.Circle([x0, -y0], 1.0, color='red', fill=False)
# the radius of the cables and pen position - end
m1_circle1 = plt.Circle((0, 0), a1, color='blue', fill=False)
m2_circle1 = plt.Circle((d, 0), b1, color='blue', fill=False)
m1_line1 = plt.Line2D([0, x1], [0, -y1], color='green')
m2_line1 = plt.Line2D([d, x1], [0, -y1], color='green')
pen_pos1 = plt.Circle([x1, -y1], 1.0, color='green', fill=False)
# set a wide range, since mpl is acting goofy about this
plt.axis([-d, 2*d, -d, 2*d])
ax = fig.gca()
ax.set_autoscale_on(False)
# pile all this onto our figure axis
ax.add_artist(m1)
ax.add_artist(m2)
ax.add_artist(m_line)
ax.add_artist(m1_line0)
ax.add_artist(m2_line0)
ax.add_artist(m1_line1)
ax.add_artist(m2_line1)
ax.add_artist(m1_circle0)
ax.add_artist(m2_circle0)
ax.add_artist(pen_pos0)
ax.add_artist(m1_circle1)
ax.add_artist(m2_circle1)
ax.add_artist(pen_pos1)
    
    
    
    Out[111]:
In [ ]:
    
    
This is where we call our function to get our line movement:
In [112]:
    
path = get_line((x0, y0), (x1, y1))
    
In [113]:
    
path[:20]  # show the first 20 locations in path
    
    Out[113]:
In [ ]:
    
    
In [119]:
    
x0, y0 = (850, 400)
x1, y1 = (500, 810)
a0, b0 = cable_steps(x0, y0, d=d)
a1, b1 = cable_steps(x1, y1, d=d)
path = get_line((x0, y0), (x1, y1))
fig2 = plt.figure(figsize=(6, 6))
# the motors
m1 = plt.Circle((0, 0), 4, color='black')
m2 = plt.Circle((d, 0), 4, color='black')
# the line between the motors
m_line = plt.Line2D([0,d], [0, 0], color='black')
# the radius of the cables and pen position - start
m1_circle0 = plt.Circle((0, 0), a0, color='gray', fill=False)
m2_circle0 = plt.Circle((d, 0), b0, color='gray', fill=False)
m1_line0 = plt.Line2D([0, x0], [0, -y0], color='red')
m2_line0 = plt.Line2D([d, x0], [0, -y0], color='red')
pen_pos0 = plt.Circle([x0, -y0], 1.0, color='red', fill=False)
# the radius of the cables and pen position - end
m1_circle1 = plt.Circle((0, 0), a1, color='blue', fill=False)
m2_circle1 = plt.Circle((d, 0), b1, color='blue', fill=False)
m1_line1 = plt.Line2D([0, x1], [0, -y1], color='green')
m2_line1 = plt.Line2D([d, x1], [0, -y1], color='green')
pen_pos1 = plt.Circle([x1, -y1], 1.0, color='green', fill=False)
# set a wide range, since mpl is acting goofy about this
plt.axis([-d, 2*d, -d, 2*d])
ax = fig2.gca()
ax.set_autoscale_on(False)
# pile all this onto our figure axis
ax.add_artist(m1)
ax.add_artist(m2)
ax.add_artist(m_line)
ax.add_artist(m1_line0)
ax.add_artist(m2_line0)
ax.add_artist(m1_line1)
ax.add_artist(m2_line1)
ax.add_artist(m1_circle0)
ax.add_artist(m2_circle0)
ax.add_artist(pen_pos0)
ax.add_artist(m1_circle1)
ax.add_artist(m2_circle1)
ax.add_artist(pen_pos1)
curr_point = path[0]
for point in path[1:]:
    x0, y0 = curr_point
    x1, y1 = point
    
    a0, b0 = cable_steps(x0, y0)
    a1, b1 = cable_steps(x1, y1)
    
    line_segx = plt.Line2D([x0, x1], [-y0, -y0], color='black')
    line_segy = plt.Line2D([x1, x1], [-y0, -y1], color='black')
    ax.add_artist(line_segx)
    ax.add_artist(line_segy)
    
    curr_point = point
    
    
    
In [ ]:
    
    
In [9]:
    
# Import GPIO Libraries
import RPi.GPIO as GPIO
import time
    
In [10]:
    
class Motor(object):
    
    def __init__(self, pins, delay=4, initial_position=25):
        """ pins are provided in the sequence A1, A2, B1, B2
        
        """
        
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(pins[0], GPIO.OUT)
        GPIO.setup(pins[1], GPIO.OUT)
        GPIO.setup(pins[2], GPIO.OUT)
        GPIO.setup(pins[3], GPIO.OUT)
        self.pins = pins
        self.state = ''
        self.delay = delay/1000.0
        self.initial_position = initial_position
        self.current_position = initial_position
        self.reverse_seq = ['1001', '1010', '0110', '0101']
        self.forward_seq = ['0101', '0110', '1010', '1001']  
        
    def _set_step(self, step):
        self.state = step
        GPIO.output(self.pins[0], step[0] == '1')
        GPIO.output(self.pins[1], step[1] == '1')
        GPIO.output(self.pins[2], step[2] == '1')
        GPIO.output(self.pins[3], step[3] == '1')
        
    def _move_forward(self, steps, delay=None):
        if not delay:
            delay = self.delay
        for i in range(steps):
            for step in self.forward_seq:
                self._set_step(step)
                time.sleep(delay)
    def _move_backward(self, steps, delay=None):
        if not delay:
            delay = self.delay
            
        for i in range(steps):
            for step in self.reverse_seq:
                self._set_step(step)
                time.sleep(delay)
        
        
    def move(self, steps):
        if steps > 0:
            self._move_forward(abs(steps))
        else:
            self._move_backward(abs(steps))
        self.current_position += steps
    
In [11]:
    
del motor_x
del motor_y
    
    
In [12]:
    
motor_x = Motor(pins=(18, 23, 24, 17))
motor_y = Motor(pins=(16, 21, 12, 20))
    
    
In [13]:
    
motor_x._set_step('0000')
motor_y._set_step('0000')
    
In [14]:
    
svg_file_name = 'img/geneva_1.svg'
    
In [ ]:
    
    
In [ ]:
    
    
In [ ]:
    
    
In [ ]:
    
    
In [114]:
    
shelton_path = np.array([[ 1, 21],
       [ 4, 19],
       [ 6, 20],
       [ 7, 20],
       [ 8, 20],
       [ 9, 20],
       [10, 21],
       [13, 21],
       [14, 21],
       [15, 21],
       [15, 20],
       [18, 21],
       [19, 21],
       [20, 21],
       [21, 20],
       [20, 23],
       [20, 24],
       [21, 24],
       [21, 25],
       [22, 26],
       [22, 27],
       [23, 27],
       [24, 28],
       [24, 29],
       [25, 29],
       [25, 30],
       [26, 30],
       [26, 31],
       [27, 32],
       [27, 31],
       [26, 22],
       [28, 20],
       [23, 18],
       [26, 15],
       [28,  3],
       [27,  3],
       [26,  3],
       [26,  4],
       [25,  4],
       [25,  5],
       [24,  5],
       [24,  6],
       [23,  6],
       [23,  7],
       [23,  8],
       [22,  8],
       [22,  9],
       [22, 10],
       [23, 11],
       [24, 11],
       [24, 12],
       [23, 12],
       [24, 13],
       [25, 12],
       [26, 12],
       [26, 11],
       [25, 10],
       [26, 10],
       [27, 10],
       [28,  9],
       [27, 12],
       [28, 13],
       [27, 13],
       [27, 14],
       [25, 13],
       [24, 14],
       [23, 16],
       [21, 15],
       [21, 14],
       [21, 12],
       [20, 11],
       [21, 10],
       [20, 10],
       [19, 10],
       [19,  9],
       [18,  9],
       [17, 11],
       [17, 10],
       [17,  8],
       [16,  8],
       [16,  9],
       [15,  9],
       [15,  8],
       [14,  8],
       [14,  7],
       [13,  7],
       [12,  7],
       [12,  8],
       [12,  9],
       [11,  8],
       [11,  7],
       [10,  7],
       [11,  9],
       [10, 10],
       [10,  9],
       [ 9,  7],
       [ 8,  7],
       [ 7,  7],
       [ 7,  8],
       [ 6,  8],
       [ 5,  8],
       [ 5,  9],
       [ 4,  9],
       [ 3,  9],
       [ 3, 10],
       [ 2, 11],
       [ 2, 12],
       [ 2, 13],
       [ 2, 14],
       [ 2, 15],
       [ 2, 16],
       [ 2, 17],
       [ 3, 11],
       [ 4, 10],
       [10, 13],
       [11, 13],
       [12, 13],
       [12, 14],
       [10, 14],
       [10, 15],
       [12, 15],
       [21, 18],
       [17, 16],
       [16, 16],
       [17, 14],
       [16, 14],
       [16, 13],
       [16, 12],
       [15, 12],
       [14, 12],
       [14, 11],
       [14, 10],
       [13,  9],
       [13, 10],
       [11, 10],
       [11, 11],
       [12, 10],
       [12, 11],
       [13, 11],
       [14, 13],
       [14, 14],
       [14, 15],
       [14, 16],
       [12, 16],
       [11, 17],
       [ 6, 19],
       [ 5, 19],
       [ 4, 18],
       [ 3, 17],
       [ 3, 18]])
    
In [157]:
    
box_path = [(1, 1), (-20, 1), (-20, 20), (1, 20), (1, 1)]
work_path = shelton_path
    
In [159]:
    
current_point = work_path[0]
for point in work_path[1:]:
    x0, y0 = current_point
    x1, y1 = point
    
    # Get cable lengths (measured in steps) for our two points (measured in 'units'):
    a0, b0 = cable_steps(x0, y0, steps_per_unit=5)
    a1, b1 = cable_steps(x1, y1, steps_per_unit=5)
    
    #print "cable: (%s, %s) to (%s, %s)" % (a0, b0, a1, b1)
    path = get_line((a0, b0), (a1, b1))
    #print "path: %s" % path
    
    prev_position = list(path[0])
    for position in path[1:]:
        move_x = position[0]-prev_position[0]
        move_y = position[1]-prev_position[1]
        motor_x.move(move_x)
        motor_y.move(move_y)
        
        prev_position = position
    
    current_point = x1, y1
motor_x._set_step('0000')
motor_y._set_step('0000')
    
    
In [116]:
    
    
    Out[116]:
In [ ]: